CoolFace
Apppublic

hwdevelops/equitylens

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
app.py774 linesDownload Raw Back to root
1"""
2app.py โ€” Streamlit web dashboard for EquityLens.
3
4A professional stock analysis web app combining technical screening,
5fundamental analysis, DCF valuation, Monte Carlo simulation,
6and FinBERT news sentiment analysis.
7"""
8
9import streamlit as st
10import plotly.graph_objects as go
11import pandas as pd
12import numpy as np
13
14from modules.screener import screen_ticker
15from modules.fundamentals import analyze_ticker, calculate_analyst_rating
16from modules.dcf import run_dcf_analysis, run_monte_carlo
17from modules.sentiment import analyze_sentiment
18
19# Page Configuration
20st.set_page_config(
21    page_title="EquityLens",
22    page_icon="๐Ÿ“ˆ",
23    layout="wide",
24    initial_sidebar_state="collapsed"
25)
26
27# Custom CSS
28st.markdown("""
29<style>
30    .stApp { background-color: #0a0e1a; color: #ffffff; }
31    .metric-card {
32        background: linear-gradient(135deg, #1a1f35 0%, #0d1225 100%);
33        border: 1px solid #2a3050; border-radius: 12px;
34        padding: 20px; text-align: center; margin: 5px;
35    }
36    .metric-label {
37        color: #8892b0; font-size: 12px; font-weight: 600;
38        text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px;
39    }
40    .metric-value { color: #ffffff; font-size: 24px; font-weight: 700; }
41    .metric-positive { color: #00d4aa; }
42    .metric-negative { color: #ff4757; }
43    .metric-neutral  { color: #ffd700; }
44    .signal-badge {
45        display: inline-block; padding: 8px 24px;
46        border-radius: 50px; font-size: 18px; font-weight: 800;
47        letter-spacing: 2px; text-transform: uppercase;
48    }
49    .signal-strong-buy  { background: #00d4aa22; color: #00d4aa; border: 2px solid #00d4aa; }
50    .signal-buy         { background: #00ff8822; color: #00ff88; border: 2px solid #00ff88; }
51    .signal-hold        { background: #ffd70022; color: #ffd700; border: 2px solid #ffd700; }
52    .signal-sell        { background: #ff475722; color: #ff4757; border: 2px solid #ff4757; }
53    .signal-strong-sell { background: #ff000022; color: #ff0000; border: 2px solid #ff0000; }
54    .section-header {
55        color: #64ffda; font-size: 14px; font-weight: 700;
56        text-transform: uppercase; letter-spacing: 2px;
57        border-bottom: 1px solid #2a3050; padding-bottom: 8px;
58        margin-bottom: 16px;
59    }
60    #MainMenu { visibility: hidden; }
61    footer    { visibility: hidden; }
62    header    { visibility: hidden; }
63</style>
64""", unsafe_allow_html=True)
65
66
67def get_signal_class(signal: str) -> str:
68    return "signal-" + signal.lower().replace(" ", "-")
69
70
71def get_overall_signal(screener, fundamentals, dcf):
72    score = 0
73    tech_signal = screener.get("overall_signal", "neutral")
74    if tech_signal == "strong_buy":   score += 30
75    elif tech_signal == "buy":        score += 15
76    elif tech_signal == "sell":       score -= 15
77    elif tech_signal == "strong_sell": score -= 30
78
79    fund_score = fundamentals.get("fundamental_score", 3)
80    score += (fund_score - 3) * 10
81
82    base_mos = dcf.get("base", {}).get("margin_of_safety", 0)
83    if base_mos is not None:
84        if base_mos > 20:    score += 40
85        elif base_mos > 0:   score += 20
86        elif base_mos > -20: score -= 20
87        else:                score -= 40
88
89    if score >= 30:   return "STRONG BUY"
90    elif score >= 10: return "BUY"
91    elif score >= -10: return "HOLD"
92    elif score >= -30: return "SELL"
93    else:             return "STRONG SELL"
94
95
96def render_header():
97    col1, col2, col3 = st.columns([1, 2, 1])
98    with col2:
99        st.markdown("""
100        <div style='text-align: center; padding: 40px 0 20px 0;'>
101            <h1 style='color: #64ffda; font-size: 48px; font-weight: 900;
102                       letter-spacing: 4px; margin: 0;'>
103                EQUITY<span style='color: #ffffff;'>LENS</span>
104            </h1>
105            <p style='color: #8892b0; font-size: 14px; letter-spacing: 2px;
106                      margin-top: 8px;'>
107                QUANTITATIVE STOCK ANALYSIS PLATFORM
108            </p>
109        </div>
110        """, unsafe_allow_html=True)
111
112        ticker = st.text_input(
113            "",
114            placeholder="Enter a stock ticker (e.g. AAPL, MSFT, NVDA)",
115            key="ticker_input"
116        ).upper().strip()
117
118        analyze = st.button("ANALYZE", use_container_width=True, type="primary")
119
120    return ticker, analyze
121
122
123def render_signal_banner(signal, ticker, price):
124    signal_class = get_signal_class(signal)
125    color = {
126        "STRONG BUY":  "#00d4aa",
127        "BUY":         "#00ff88",
128        "HOLD":        "#ffd700",
129        "SELL":        "#ff4757",
130        "STRONG SELL": "#ff0000"
131    }.get(signal, "#ffffff")
132
133    st.markdown(f"""
134    <div style='background: linear-gradient(135deg, #1a1f35, #0d1225);
135                border: 1px solid {color}33; border-left: 4px solid {color};
136                border-radius: 12px; padding: 24px 32px;
137                display: flex; align-items: center;
138                justify-content: space-between; margin: 20px 0;'>
139        <div>
140            <div style='color: #8892b0; font-size: 12px;
141                        letter-spacing: 2px;'>ANALYZING</div>
142            <div style='color: #ffffff; font-size: 32px;
143                        font-weight: 900;'>{ticker}</div>
144        </div>
145        <div style='text-align: center;'>
146            <div style='color: #8892b0; font-size: 12px;
147                        letter-spacing: 2px;'>CURRENT PRICE</div>
148            <div style='color: #ffd700; font-size: 32px;
149                        font-weight: 700;'>${price}</div>
150        </div>
151        <div style='text-align: right;'>
152            <div style='color: #8892b0; font-size: 12px;
153                        letter-spacing: 2px; margin-bottom: 8px;'>
154                OVERALL SIGNAL</div>
155            <span class='signal-badge {signal_class}'>{signal}</span>
156        </div>
157    </div>
158    """, unsafe_allow_html=True)
159
160
161def render_price_chart(ticker: str):
162    """Render candlestick chart with MA and RSI panels."""
163    import yfinance as yf
164    from plotly.subplots import make_subplots
165
166    st.markdown(
167        "<div class='section-header'>Price History and Technical Chart</div>",
168        unsafe_allow_html=True
169    )
170
171    df = yf.Ticker(ticker).history(period="1y")
172    if df.empty:
173        st.warning("No price data available.")
174        return
175
176    df["MA50"]  = df["Close"].rolling(50).mean()
177    df["MA200"] = df["Close"].rolling(200).mean()
178    delta = df["Close"].diff()
179    gain  = delta.where(delta > 0, 0).rolling(14).mean()
180    loss  = (-delta.where(delta < 0, 0)).rolling(14).mean()
181    rs    = gain / loss
182    df["RSI"] = 100 - (100 / (1 + rs))
183
184    fig = make_subplots(
185        rows=3, cols=1, shared_xaxes=True,
186        vertical_spacing=0.03, row_heights=[0.6, 0.2, 0.2]
187    )
188
189    fig.add_trace(go.Candlestick(
190        x=df.index, open=df["Open"], high=df["High"],
191        low=df["Low"], close=df["Close"], name=ticker,
192        increasing_line_color="#00d4aa",
193        decreasing_line_color="#ff4757"
194    ), row=1, col=1)
195
196    fig.add_trace(go.Scatter(
197        x=df.index, y=df["MA50"], name="50-Day MA",
198        line=dict(color="#ffd700", width=1.5)
199    ), row=1, col=1)
200
201    fig.add_trace(go.Scatter(
202        x=df.index, y=df["MA200"], name="200-Day MA",
203        line=dict(color="#00bfff", width=1.5)
204    ), row=1, col=1)
205
206    colors = ["#00d4aa" if c >= o else "#ff4757"
207              for c, o in zip(df["Close"], df["Open"])]
208    fig.add_trace(go.Bar(
209        x=df.index, y=df["Volume"], name="Volume",
210        marker_color=colors, opacity=0.7
211    ), row=2, col=1)
212
213    fig.add_trace(go.Scatter(
214        x=df.index, y=df["RSI"], name="RSI (14)",
215        line=dict(color="#a78bfa", width=1.5)
216    ), row=3, col=1)
217
218    fig.add_hline(y=70, line_dash="dash", line_color="#ff4757",
219                  opacity=0.5, row=3, col=1)
220    fig.add_hline(y=30, line_dash="dash", line_color="#00d4aa",
221                  opacity=0.5, row=3, col=1)
222
223    fig.update_layout(
224        paper_bgcolor="#0a0e1a", plot_bgcolor="#0a0e1a",
225        font_color="#ffffff", height=700, showlegend=True,
226        legend=dict(bgcolor="#1a1f35", bordercolor="#2a3050"),
227        xaxis_rangeslider_visible=False,
228        margin=dict(l=0, r=0, t=20, b=0)
229    )
230    fig.update_xaxes(gridcolor="#1a1f35", zerolinecolor="#2a3050")
231    fig.update_yaxes(gridcolor="#1a1f35", zerolinecolor="#2a3050")
232    st.plotly_chart(fig, use_container_width=True)
233
234
235def render_analyst_ratings(ticker: str):
236    """Display analyst consensus and price targets."""
237    import yfinance as yf
238
239    st.markdown(
240        "<div class='section-header'>Analyst Consensus</div>",
241        unsafe_allow_html=True
242    )
243
244    try:
245        info           = yf.Ticker(ticker).info
246        current_price  = info.get("currentPrice", 0)
247        target_mean    = info.get("targetMeanPrice")
248        target_high    = info.get("targetHighPrice")
249        target_low     = info.get("targetLowPrice")
250        recommendation = info.get("recommendationKey", "N/A").upper()
251        num_analysts   = info.get("numberOfAnalystOpinions", "N/A")
252
253        upside = ((target_mean - current_price) / current_price * 100
254                  if target_mean else None)
255
256        rec_color = (
257            "#00d4aa" if recommendation in ["STRONG_BUY", "BUY"]
258            else "#ffd700" if recommendation == "HOLD"
259            else "#ff4757"
260        )
261
262        col1, col2, col3, col4 = st.columns(4)
263
264        with col1:
265            st.markdown(
266                f"<div class='metric-card'>"
267                f"<div class='metric-label'>Consensus</div>"
268                f"<div class='metric-value' style='color: {rec_color}; "
269                f"font-size: 18px;'>{recommendation}</div>"
270                f"<div style='color: #8892b0; font-size: 12px; "
271                f"margin-top: 4px;'>{num_analysts} analysts</div>"
272                f"</div>",
273                unsafe_allow_html=True
274            )
275
276        with col2:
277            st.markdown(
278                f"<div class='metric-card'>"
279                f"<div class='metric-label'>Price Target</div>"
280                f"<div class='metric-value'>${target_mean:.2f}</div>"
281                f"<div style='color: #8892b0; font-size: 12px; "
282                f"margin-top: 4px;'>Consensus mean</div>"
283                f"</div>",
284                unsafe_allow_html=True
285            )
286
287        with col3:
288            upside_color = "#00d4aa" if upside and upside > 0 else "#ff4757"
289            upside_text  = f"{upside:+.1f}%" if upside else "N/A"
290            st.markdown(
291                f"<div class='metric-card'>"
292                f"<div class='metric-label'>Upside to Target</div>"
293                f"<div class='metric-value' style='color: {upside_color};'>"
294                f"{upside_text}</div>"
295                f"<div style='color: #8892b0; font-size: 12px; "
296                f"margin-top: 4px;'>vs current price</div>"
297                f"</div>",
298                unsafe_allow_html=True
299            )
300
301        with col4:
302            low  = f"${target_low:.0f}"  if target_low  else "N/A"
303            high = f"${target_high:.0f}" if target_high else "N/A"
304            st.markdown(
305                f"<div class='metric-card'>"
306                f"<div class='metric-label'>Target Range</div>"
307                f"<div class='metric-value' style='font-size: 16px;'>"
308                f"{low} - {high}</div>"
309                f"<div style='color: #8892b0; font-size: 12px; "
310                f"margin-top: 4px;'>Low / High</div>"
311                f"</div>",
312                unsafe_allow_html=True
313            )
314
315    except Exception as e:
316        st.warning(f"Analyst data unavailable for {ticker}")
317
318
319def render_technical(screener: dict):
320    """Render the technical signals section."""
321    st.markdown(
322        "<div class='section-header'>Technical Signals</div>",
323        unsafe_allow_html=True
324    )
325
326    col1, col2, col3 = st.columns(3)
327
328    rsi        = screener.get("rsi", "N/A")
329    rsi_signal = screener.get("rsi_signal", "neutral")
330    rsi_color  = (
331        "metric-negative" if rsi_signal == "overbought"
332        else "metric-positive" if rsi_signal == "oversold"
333        else "metric-neutral"
334    )
335
336    ma_signal = screener.get("ma_signal", "neutral")
337    ma_color  = (
338        "metric-positive" if ma_signal == "golden_cross"
339        else "metric-negative" if ma_signal == "death_cross"
340        else "metric-neutral"
341    )
342
343    vol_spike = screener.get("volume_spike", False)
344    vol_color = "metric-positive" if vol_spike else "metric-neutral"
345
346    with col1:
347        st.markdown(
348            f"<div class='metric-card'>"
349            f"<div class='metric-label'>RSI (14 Day)</div>"
350            f"<div class='metric-value {rsi_color}'>{rsi}</div>"
351            f"<div style='color: #8892b0; font-size: 12px; "
352            f"margin-top: 4px;'>{rsi_signal.upper()}</div>"
353            f"</div>",
354            unsafe_allow_html=True
355        )
356
357    with col2:
358        ma_label = (
359            "GOLDEN CROSS" if ma_signal == "golden_cross"
360            else "DEATH CROSS" if ma_signal == "death_cross"
361            else "NEUTRAL"
362        )
363        st.markdown(
364            f"<div class='metric-card'>"
365            f"<div class='metric-label'>Moving Average</div>"
366            f"<div class='metric-value {ma_color}' "
367            f"style='font-size: 16px;'>{ma_label}</div>"
368            f"<div style='color: #8892b0; font-size: 12px; "
369            f"margin-top: 4px;'>50d: {screener.get('ma_50')} / "
370            f"200d: {screener.get('ma_200')}</div>"
371            f"</div>",
372            unsafe_allow_html=True
373        )
374
375    with col3:
376        st.markdown(
377            f"<div class='metric-card'>"
378            f"<div class='metric-label'>Volume Spike</div>"
379            f"<div class='metric-value {vol_color}'>"
380            f"{'YES' if vol_spike else 'NO'}</div>"
381            f"<div style='color: #8892b0; font-size: 12px; "
382            f"margin-top: 4px;'>"
383            f"{int(screener.get('current_volume', 0)):,} shares</div>"
384            f"</div>",
385            unsafe_allow_html=True
386        )
387
388
389def render_fundamentals(fundamentals: dict):
390    """Render the fundamental analysis section."""
391    st.markdown(
392        "<div class='section-header'>Fundamental Analysis</div>",
393        unsafe_allow_html=True
394    )
395
396    metrics = [
397        ("P/E Ratio",     fundamentals.get("pe_ratio"),      ""),
398        ("EV/EBITDA",     fundamentals.get("ev_ebitda"),      ""),
399        ("Debt/Equity",   fundamentals.get("debt_equity"),    ""),
400        ("Current Ratio", fundamentals.get("current_ratio"),  ""),
401        ("Gross Margin",  fundamentals.get("gross_margin"),   "%"),
402        ("Revenue CAGR",  fundamentals.get("revenue_cagr"),   "%"),
403        ("Beta",          fundamentals.get("beta"),           ""),
404    ]
405
406    cols = st.columns(7)
407    for i, (label, value, suffix) in enumerate(metrics):
408        with cols[i]:
409            display = f"{value}{suffix}" if value is not None else "N/A"
410            st.markdown(
411                f"<div class='metric-card'>"
412                f"<div class='metric-label'>{label}</div>"
413                f"<div class='metric-value' style='font-size: 20px;'>"
414                f"{display}</div>"
415                f"</div>",
416                unsafe_allow_html=True
417            )
418
419    score       = fundamentals.get("fundamental_score", "N/A")
420    score_color = (
421        "#00d4aa" if isinstance(score, int) and score >= 4
422        else "#ffd700" if isinstance(score, int) and score == 3
423        else "#ff4757"
424    )
425    st.markdown(
426        f"<div style='text-align: center; margin-top: 16px;'>"
427        f"<span style='color: #8892b0; font-size: 12px; "
428        f"letter-spacing: 2px;'>FUNDAMENTAL SCORE  </span>"
429        f"<span style='color: {score_color}; font-size: 24px; "
430        f"font-weight: 700;'>{score} / 5</span>"
431        f"</div>",
432        unsafe_allow_html=True
433    )
434
435
436def render_analyst_rating(rating: dict):
437    """Display the 0-10 analyst rating with category breakdown."""
438    if not rating:
439        return
440
441    score     = rating.get("score", 0)
442    label     = rating.get("label", "N/A")
443    breakdown = rating.get("breakdown", {})
444
445    color = (
446        "#00d4aa" if label in ["STRONG BUY", "BUY"]
447        else "#ffd700" if label == "HOLD"
448        else "#ff4757"
449    )
450
451    st.markdown(
452        "<div class='section-header'>EquityLens Analyst Rating</div>",
453        unsafe_allow_html=True
454    )
455
456    col1, col2 = st.columns([1, 2])
457
458    with col1:
459        st.markdown(
460            f"<div class='metric-card' style='border-top: 3px solid {color}; "
461            f"text-align: center; padding: 30px;'>"
462            f"<div class='metric-label'>ANALYST RATING</div>"
463            f"<div style='color: {color}; font-size: 64px; "
464            f"font-weight: 900; line-height: 1;'>{score}</div>"
465            f"<div style='color: #8892b0; font-size: 12px; "
466            f"margin: 4px 0;'>OUT OF 10</div>"
467            f"<div style='color: {color}; font-size: 18px; "
468            f"font-weight: 700; margin-top: 8px;'>{label}</div>"
469            f"</div>",
470            unsafe_allow_html=True
471        )
472
473    with col2:
474        categories = [
475            ("Valuation",        breakdown.get("valuation", 0), 3),
476            ("Business Quality", breakdown.get("quality",   0), 3),
477            ("Momentum",         breakdown.get("momentum",  0), 2),
478            ("Risk",             breakdown.get("risk",      0), 2),
479        ]
480
481        for cat_name, cat_score, cat_max in categories:
482            fill      = (cat_score / cat_max) * 100
483            bar_color = (
484                "#00d4aa" if fill >= 66
485                else "#ffd700" if fill >= 33
486                else "#ff4757"
487            )
488            st.markdown(
489                f"<div style='margin-bottom: 16px;'>"
490                f"<div style='display: flex; justify-content: space-between; "
491                f"margin-bottom: 4px;'>"
492                f"<span style='color: #8892b0; font-size: 12px; "
493                f"font-weight: 600; text-transform: uppercase; "
494                f"letter-spacing: 1px;'>{cat_name}</span>"
495                f"<span style='color: {bar_color}; font-size: 12px; "
496                f"font-weight: 700;'>{cat_score}/{cat_max}</span>"
497                f"</div>"
498                f"<div style='background: #1a1f35; border-radius: 4px; "
499                f"height: 8px; overflow: hidden;'>"
500                f"<div style='background: {bar_color}; width: {fill}%; "
501                f"height: 100%; border-radius: 4px;'></div>"
502                f"</div></div>",
503                unsafe_allow_html=True
504            )
505
506
507def render_dcf(dcf: dict):
508    """Render the DCF valuation section."""
509    st.markdown(
510        "<div class='section-header'>DCF Valuation</div>",
511        unsafe_allow_html=True
512    )
513
514    current_price = dcf.get("current_price", 0)
515    scenarios     = ["bear", "base", "bull"]
516    colors        = ["#ff4757", "#ffd700", "#00d4aa"]
517    labels        = ["BEAR", "BASE", "BULL"]
518
519    cols = st.columns(3)
520    for i, scenario in enumerate(scenarios):
521        data = dcf.get(scenario, {})
522        iv   = data.get("intrinsic_value", "N/A")
523        mos  = data.get("margin_of_safety", "N/A")
524
525        mos_color = (
526            "#00d4aa" if isinstance(mos, float) and mos > 0
527            else "#ff4757"
528        )
529
530        with cols[i]:
531            st.markdown(
532                f"<div class='metric-card' "
533                f"style='border-color: {colors[i]}44; "
534                f"border-top: 3px solid {colors[i]};'>"
535                f"<div class='metric-label'>{labels[i]} CASE</div>"
536                f"<div class='metric-value' "
537                f"style='color: {colors[i]};'>${iv}</div>"
538                f"<div style='color: {mos_color}; font-size: 14px; "
539                f"margin-top: 8px; font-weight: 600;'>"
540                f"{mos}% vs current</div>"
541                f"</div>",
542                unsafe_allow_html=True
543            )
544
545
546def render_monte_carlo_chart(results: dict):
547    """Render the Monte Carlo histogram using Plotly."""
548    if "error" in results:
549        st.warning(f"Monte Carlo: {results['error']}")
550        return
551
552    st.markdown(
553        "<div class='section-header'>Monte Carlo Simulation "
554        "(10,000 Scenarios)</div>",
555        unsafe_allow_html=True
556    )
557
558    values        = results["intrinsic_values"]
559    current_price = results["current_price"]
560    p10           = results["p10"]
561    p50           = results["p50"]
562    p90           = results["p90"]
563    prob          = results["prob_undervalued"]
564
565    fig = go.Figure()
566
567    fig.add_trace(go.Histogram(
568        x=values[values < current_price], nbinsx=80,
569        marker_color="#ff4757", opacity=0.8,
570        name="Overvalued Scenarios"
571    ))
572    fig.add_trace(go.Histogram(
573        x=values[values >= current_price], nbinsx=80,
574        marker_color="#00d4aa", opacity=0.8,
575        name="Undervalued Scenarios"
576    ))
577
578    for val, color, label in [
579        (current_price, "#ffffff", f"Current Price ${current_price}"),
580        (p50,           "#ffd700", f"Median ${p50}"),
581        (p10,           "#ff8c00", f"P10 ${p10}"),
582        (p90,           "#00bfff", f"P90 ${p90}"),
583    ]:
584        fig.add_vline(
585            x=val, line_color=color, line_width=2, line_dash="dash",
586            annotation_text=label, annotation_position="top",
587            annotation_font_color=color
588        )
589
590    fig.update_layout(
591        barmode="overlay",
592        paper_bgcolor="#0a0e1a", plot_bgcolor="#0a0e1a",
593        font_color="#ffffff",
594        title=dict(
595            text=f"Probability Undervalued: {prob}%",
596            font_size=16, font_color="#64ffda"
597        ),
598        xaxis=dict(
599            title="Intrinsic Value Per Share ($)",
600            gridcolor="#1a1f35", zerolinecolor="#2a3050"
601        ),
602        yaxis=dict(title="Number of Scenarios", gridcolor="#1a1f35"),
603        legend=dict(bgcolor="#1a1f35", bordercolor="#2a3050"),
604        height=400
605    )
606
607    st.plotly_chart(fig, use_container_width=True)
608
609    col1, col2, col3, col4, col5 = st.columns(5)
610    stats = [
611        ("P10 Deep Bear", f"${results['p10']}"),
612        ("P25 Bear",      f"${results['p25']}"),
613        ("P50 Median",    f"${results['p50']}"),
614        ("P75 Bull",      f"${results['p75']}"),
615        ("P90 Deep Bull", f"${results['p90']}"),
616    ]
617    for col, (label, value) in zip(
618        [col1, col2, col3, col4, col5], stats
619    ):
620        with col:
621            st.markdown(
622                f"<div class='metric-card'>"
623                f"<div class='metric-label'>{label}</div>"
624                f"<div class='metric-value' style='font-size: 18px;'>"
625                f"{value}</div>"
626                f"</div>",
627                unsafe_allow_html=True
628            )
629
630
631def render_sentiment(sentiment: dict):
632    """Display FinBERT news sentiment analysis."""
633    if not sentiment or "error" in sentiment:
634        st.warning("Sentiment data unavailable.")
635        return
636
637    st.markdown(
638        "<div class='section-header'>News Sentiment Analysis</div>",
639        unsafe_allow_html=True
640    )
641
642    label          = sentiment.get("sentiment_label", "neutral")
643    display_score  = sentiment.get("display_score", 50)
644    headline_count = sentiment.get("headline_count", 0)
645    headlines      = sentiment.get("headlines", [])
646
647    color = (
648        "#00d4aa" if label == "positive"
649        else "#ff4757" if label == "negative"
650        else "#ffd700"
651    )
652
653    col1, col2 = st.columns([1, 2])
654
655    with col1:
656        st.markdown(
657            f"<div class='metric-card' style='text-align: center; "
658            f"padding: 30px; border-top: 3px solid {color};'>"
659            f"<div class='metric-label'>SENTIMENT SCORE</div>"
660            f"<div style='color: {color}; font-size: 56px; "
661            f"font-weight: 900; line-height: 1;'>{display_score:.0f}</div>"
662            f"<div style='color: #8892b0; font-size: 12px; "
663            f"margin: 4px 0;'>OUT OF 100</div>"
664            f"<div style='color: {color}; font-size: 18px; "
665            f"font-weight: 700; margin-top: 8px;'>{label.upper()}</div>"
666            f"<div style='color: #8892b0; font-size: 11px; "
667            f"margin-top: 8px;'>Based on {headline_count} "
668            f"recent headlines</div></div>",
669            unsafe_allow_html=True
670        )
671
672    with col2:
673        st.markdown(
674            "<div style='color: #64ffda; font-size: 12px; "
675            "font-weight: 700; text-transform: uppercase; "
676            "letter-spacing: 1px; margin-bottom: 12px;'>"
677            "Recent Headlines</div>",
678            unsafe_allow_html=True
679        )
680
681        for item in headlines[:6]:
682            h_label  = item.get("label", "neutral")
683            h_score  = item.get("score", 0)
684            headline = item.get("headline", "")
685
686            h_color   = (
687                "#00d4aa" if h_label == "positive"
688                else "#ff4757" if h_label == "negative"
689                else "#8892b0"
690            )
691            indicator = (
692                "+" if h_label == "positive"
693                else "-" if h_label == "negative"
694                else "o"
695            )
696
697            st.markdown(
698                f"<div style='display: flex; align-items: flex-start; "
699                f"margin-bottom: 10px; padding: 8px 12px; "
700                f"background: #1a1f35; border-radius: 8px; "
701                f"border-left: 3px solid {h_color};'>"
702                f"<span style='color: {h_color}; font-size: 14px; "
703                f"margin-right: 8px; flex-shrink: 0;'>{indicator}</span>"
704                f"<div><div style='color: #ffffff; font-size: 12px; "
705                f"line-height: 1.4;'>{headline}</div>"
706                f"<div style='color: {h_color}; font-size: 11px; "
707                f"margin-top: 2px;'>{h_label.upper()} "
708                f"confidence {h_score:.0%}</div></div></div>",
709                unsafe_allow_html=True
710            )
711
712
713def main():
714    ticker, analyze = render_header()
715
716    if analyze and ticker:
717        with st.spinner(f"Analyzing {ticker}..."):
718            screener     = screen_ticker(ticker)
719            fundamentals = analyze_ticker(ticker)
720            dcf          = run_dcf_analysis(ticker)
721            mc_results   = run_monte_carlo(ticker)
722            sentiment    = analyze_sentiment(ticker)
723
724        if "error" in screener:
725            st.error(f"Could not find data for {ticker}.")
726            return
727
728        import yfinance as yf
729        rec = yf.Ticker(ticker).info.get(
730            "recommendationKey", "N/A"
731        ).upper()
732
733        rating = calculate_analyst_rating(
734            gross_margin      = fundamentals.get("gross_margin"),
735            revenue_cagr      = fundamentals.get("revenue_cagr"),
736            fundamental_score = fundamentals.get("fundamental_score"),
737            rsi               = screener.get("rsi"),
738            ma_signal         = screener.get("ma_signal"),
739            base_mos          = dcf.get("base", {}).get("margin_of_safety"),
740            prob_undervalued  = mc_results.get("prob_undervalued"),
741            recommendation    = rec,
742            beta              = fundamentals.get("beta"),
743            current_ratio     = fundamentals.get("current_ratio")
744        )
745
746        signal = get_overall_signal(screener, fundamentals, dcf)
747        price  = fundamentals.get("price", "N/A")
748
749        render_signal_banner(signal, ticker, price)
750        render_analyst_rating(rating)
751        render_price_chart(ticker)
752        render_analyst_ratings(ticker)
753
754        st.markdown("<br>", unsafe_allow_html=True)
755        render_technical(screener)
756
757        st.markdown("<br>", unsafe_allow_html=True)
758        render_fundamentals(fundamentals)
759
760        st.markdown("<br>", unsafe_allow_html=True)
761        render_dcf(dcf)
762
763        st.markdown("<br>", unsafe_allow_html=True)
764        render_monte_carlo_chart(mc_results)
765
766        st.markdown("<br>", unsafe_allow_html=True)
767        render_sentiment(sentiment)
768
769    elif analyze and not ticker:
770        st.warning("Please enter a stock ticker.")
771
772
773if __name__ == "__main__":
774    main()