File size: 10,996 Bytes
68c8e40
 
91942a9
c981557
 
 
 
 
68c8e40
 
 
 
 
 
 
 
91942a9
68c8e40
 
 
 
 
 
 
 
91942a9
68c8e40
 
 
c981557
 
 
 
68c8e40
 
91942a9
68c8e40
 
 
 
c981557
 
 
68c8e40
 
 
 
 
91942a9
 
 
 
c981557
91942a9
 
c981557
91942a9
 
 
c981557
 
 
 
 
91942a9
 
 
 
 
c981557
 
91942a9
 
 
 
 
 
 
61bccc2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68c8e40
 
 
91942a9
 
 
68c8e40
91942a9
 
68c8e40
91942a9
 
68c8e40
c981557
68c8e40
 
c981557
 
68c8e40
91942a9
68c8e40
 
91942a9
 
68c8e40
 
 
91942a9
68c8e40
 
 
 
 
91942a9
68c8e40
c981557
 
91942a9
68c8e40
91942a9
68c8e40
c981557
 
 
 
61bccc2
68c8e40
 
 
 
 
c981557
 
 
68c8e40
 
 
 
 
c981557
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
521114d
c981557
521114d
 
68c8e40
 
 
 
 
c981557
68c8e40
 
 
 
91942a9
 
c981557
91942a9
 
 
 
 
 
 
 
68c8e40
 
 
 
 
 
 
 
 
 
c981557
 
 
68c8e40
c981557
91942a9
 
 
c981557
 
 
 
91942a9
 
c981557
 
91942a9
 
 
 
 
 
 
 
68c8e40
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
"""
ui/components.py
Reusable Streamlit UI blocks.
Changes:
- Metrics row: Ann Return compared vs SPY (not T-bill)
- Max Daily DD: shows date it happened
- Conviction panel: compact single-line ETF list (no big bars)
- applymap β†’ map (deprecation fix)
"""

import streamlit as st
import pandas as pd
import numpy as np
from signals.conviction import conviction_color, conviction_icon


# ── Freshness status ──────────────────────────────────────────────────────────

def show_freshness_status(freshness: dict):
    if freshness.get("fresh"):
        st.success(freshness["message"])
    else:
        st.warning(freshness["message"])


# ── Winner signal banner ──────────────────────────────────────────────────────

def show_signal_banner(next_signal: str, next_date, approach_name: str):
    is_cash = next_signal == "CASH"
    bg = ("linear-gradient(135deg, #2d3436 0%, #1a1a2e 100%)" if is_cash
          else "linear-gradient(135deg, #00d1b2 0%, #00a896 100%)")
    label = ("⚠️ DRAWDOWN PROTECTION ACTIVE β€” CASH"
             if is_cash else f"🎯 {next_date.strftime('%Y-%m-%d')} β†’ {next_signal}")
    st.markdown(f"""
    <div style="background:{bg}; padding:25px; border-radius:15px;
                text-align:center; box-shadow:0 8px 16px rgba(0,0,0,0.3); margin:16px 0;">
      <div style="color:rgba(255,255,255,0.7); font-size:12px;
                  letter-spacing:3px; margin-bottom:6px;">
        {approach_name.upper()} Β· NEXT TRADING DAY SIGNAL
      </div>
      <h1 style="color:white; font-size:40px; margin:0; font-weight:800;
                 text-shadow:2px 2px 4px rgba(0,0,0,0.3);">
        {label}
      </h1>
    </div>
    """, unsafe_allow_html=True)


# ── All models signals panel ──────────────────────────────────────────────────

def show_all_signals_panel(all_signals: dict, target_etfs: list,
                            include_cash: bool, next_date, optimal_lookback: int):
    COLORS = {"Approach 1": "#00ffc8", "Approach 2": "#7c6aff", "Approach 3": "#ff6b6b"}

    st.subheader(f"πŸ—“οΈ All Models β€” {next_date.strftime('%Y-%m-%d')} Signals")
    st.caption(f"πŸ“ Lookback **{optimal_lookback}d** found optimal (auto-selected from 30 / 45 / 60d)")

    cols = st.columns(len(all_signals))
    for col, (name, info) in zip(cols, all_signals.items()):
        color    = COLORS.get(name, "#888")
        signal   = info["signal"]
        top_prob = float(np.max(info["proba"])) * 100
        badge    = " ⭐" if info["is_winner"] else ""
        sig_col  = "#aaa" if signal == "CASH" else "white"

        col.markdown(f"""
        <div style="border:2px solid {color}; border-radius:12px; padding:18px 16px;
                    background:#111118; text-align:center; margin-bottom:8px;">
            <div style="color:{color}; font-size:10px; font-weight:700;
                        letter-spacing:2px; margin-bottom:6px;">{name.upper()}{badge}</div>
            <div style="color:{sig_col}; font-size:30px; font-weight:800; margin:8px 0;">{signal}</div>
            <div style="color:#aaa; font-size:12px;">
                Confidence: <span style="color:{color}; font-weight:700;">{top_prob:.1f}%</span>
            </div>
        </div>
        """, unsafe_allow_html=True)


def _build_etf_badges(sorted_pairs: list, best_name: str, color: str) -> str:
    """Build compact ETF probability badges as HTML string."""
    badges = []
    for name, score in sorted_pairs:
        is_best = name == best_name
        bg      = "#e8fdf7" if is_best else "#f8f8f8"
        border  = color if is_best else "#ddd"
        txt_col = color if is_best else "#555"
        weight  = "700" if is_best else "400"
        star    = "β˜… " if is_best else ""
        badges.append(
            f'<span style="background:{bg}; border:1px solid {border}; '
            f'border-radius:6px; padding:4px 10px; font-size:13px; '
            f'color:{txt_col}; font-weight:{weight};">'
            f'{star}{name} {score:.3f}</span>'
        )
    return "".join(badges)


# ── Signal conviction panel ───────────────────────────────────────────────────

def show_conviction_panel(conviction: dict):
    label        = conviction["label"]
    z_score      = conviction["z_score"]
    best_name    = conviction["best_name"]
    sorted_pairs = conviction["sorted_pairs"]
    color        = conviction_color(label)
    icon         = conviction_icon(label)

    z_clipped = max(-3.0, min(3.0, z_score))
    bar_pct   = int((z_clipped + 3) / 6 * 100)

    # ── Header with Z-score gauge ─────────────────────────────────────────────
    st.markdown(f"""
    <div style="background:#ffffff; border:1px solid #ddd;
                border-left:5px solid {color}; border-radius:12px;
                padding:18px 24px 16px 24px; margin:12px 0;
                box-shadow:0 2px 8px rgba(0,0,0,0.07);">
      <div style="display:flex; align-items:center; gap:12px; margin-bottom:14px; flex-wrap:wrap;">
        <span style="font-size:20px;">{icon}</span>
        <span style="font-size:18px; font-weight:700; color:#1a1a1a;">Signal Conviction</span>
        <span style="background:#f0f0f0; border:1px solid {color}; color:{color};
                     font-weight:700; font-size:14px; padding:3px 12px; border-radius:8px;">
          Z = {z_score:.2f} &sigma;
        </span>
        <span style="margin-left:auto; background:{color}; color:#fff;
                     font-weight:700; padding:4px 16px; border-radius:20px; font-size:13px;">
          {label}
        </span>
      </div>
      <div style="display:flex; justify-content:space-between;
                  font-size:11px; color:#999; margin-bottom:4px;">
        <span>Weak &minus;3&sigma;</span><span>Neutral 0&sigma;</span><span>Strong +3&sigma;</span>
      </div>
      <div style="background:#f0f0f0; border-radius:8px; height:10px; overflow:hidden;
                  position:relative; border:1px solid #e0e0e0; margin-bottom:16px;">
        <div style="position:absolute; left:50%; top:0; width:2px; height:100%; background:#ccc;"></div>
        <div style="width:{bar_pct}%; height:100%;
                    background:linear-gradient(90deg,#fab1a0,{color}); border-radius:8px;"></div>
      </div>
      <div style="font-size:11px; color:#999; margin-bottom:8px; font-weight:600; letter-spacing:1px;">
        MODEL PROBABILITY BY ETF
      </div>
      <div style="display:flex; flex-wrap:wrap; gap:8px;">
        {_build_etf_badges(sorted_pairs, best_name, color)}
      </div>
    </div>
    """, unsafe_allow_html=True)

    st.caption(
        "Z-score = std deviations the top ETF's probability sits above the mean of all ETF probabilities. "
        "Higher β†’ model is more decisive.  "
        "⚠️ CASH override triggers if 2-day cumulative return ≀ βˆ’15%, exits when Z β‰₯ 1.0."
    )


# ── Metrics row ───────────────────────────────────────────────────────────────

def show_metrics_row(result: dict, tbill_rate: float, spy_ann_return: float = None):
    c1, c2, c3, c4, c5 = st.columns(5)

    # Ann return vs SPY
    if spy_ann_return is not None:
        diff = (result['ann_return'] - spy_ann_return) * 100
        sign = "+" if diff >= 0 else ""
        delta_str = f"vs SPY: {sign}{diff:.2f}%"
    else:
        delta_str = f"vs T-bill: {(result['ann_return'] - tbill_rate)*100:.2f}%"

    c1.metric("πŸ“ˆ Ann. Return", f"{result['ann_return']*100:.2f}%", delta=delta_str)
    c2.metric("πŸ“Š Sharpe",      f"{result['sharpe']:.2f}",
              delta="Strong" if result['sharpe'] > 1 else "Weak")
    c3.metric("🎯 Hit Ratio 15d", f"{result['hit_ratio']*100:.0f}%",
              delta="Good" if result['hit_ratio'] > 0.55 else "Weak")
    c4.metric("πŸ“‰ Max Drawdown", f"{result['max_dd']*100:.2f}%",
              delta="Peak to Trough")

    # Max daily DD with date (only show date if available)
    worst_date = result.get("max_daily_date", "N/A")
    dd_delta   = f"on {worst_date}" if worst_date != "N/A" else "Worst Single Day"
    c5.metric("⚠️ Max Daily DD", f"{result['max_daily_dd']*100:.2f}%", delta=dd_delta)


# ── Comparison table ──────────────────────────────────────────────────────────

def show_comparison_table(comparison_df: pd.DataFrame):
    def _highlight(row):
        if "WINNER" in str(row.get("Winner", "")):
            return ["background-color: rgba(0,200,150,0.15); font-weight:bold"] * len(row)
        return [""] * len(row)

    styled = (
        comparison_df.style
        .apply(_highlight, axis=1)
        .set_properties(**{"text-align": "center", "font-size": "14px"})
        .set_table_styles([
            {"selector": "th", "props": [("font-size", "14px"),
                                          ("font-weight", "bold"),
                                          ("text-align", "center")]},
            {"selector": "td", "props": [("padding", "10px")]},
        ])
    )
    st.dataframe(styled, use_container_width=True)


# ── Audit trail ───────────────────────────────────────────────────────────────

def show_audit_trail(audit_trail: list):
    if not audit_trail:
        st.info("No audit trail data available.")
        return

    df   = pd.DataFrame(audit_trail).tail(20)
    cols = [c for c in ["Date", "Signal", "Net_Return", "Z_Score"] if c in df.columns]
    df   = df[cols]

    def _color_ret(val):
        return ("color: #00c896; font-weight:bold" if val > 0
                else "color: #ff4b4b; font-weight:bold")

    fmt = {"Net_Return": "{:.2%}"}
    if "Z_Score" in df.columns:
        fmt["Z_Score"] = "{:.2f}"

    styled = (
        df.style
        .map(_color_ret, subset=["Net_Return"])
        .format(fmt)
        .set_properties(**{"font-size": "14px", "text-align": "center"})
        .set_table_styles([
            {"selector": "th", "props": [("font-size", "14px"),
                                          ("font-weight", "bold"),
                                          ("text-align", "center")]},
            {"selector": "td", "props": [("padding", "10px")]},
        ])
    )
    st.dataframe(styled, use_container_width=True, height=500)