from __future__ import annotations import pandas as pd import streamlit as st from web_core.time_display import format_timestamp def build_learning_window_rows(analyzed: pd.DataFrame) -> pd.DataFrame: if analyzed.empty: return pd.DataFrame( columns=[ "Window", "Bars", "Price Change %", "Real Bull Bars", "Real Bear Bars", "Fake Bars", "Trend Flips", "What this says", ] ) indexed = analyzed.sort_index() def _all_data_row(message: str) -> pd.DataFrame: first_close = float(indexed.iloc[0]["Close"]) if len(indexed) else 0.0 last_close = float(indexed.iloc[-1]["Close"]) if len(indexed) else 0.0 change_pct = round(((last_close - first_close) / first_close) * 100.0, 2) if first_close else 0.0 return pd.DataFrame( [ { "Window": "All data", "Bars": int(len(indexed)), "Price Change %": change_pct, "Real Bull Bars": int((indexed["classification"] == "real_bull").sum()), "Real Bear Bars": int((indexed["classification"] == "real_bear").sum()), "Fake Bars": int((indexed["classification"] == "fake").sum()), "Trend Flips": int(max(0, (indexed["trend_state"] != indexed["trend_state"].shift(1)).sum() - 1)), "What this says": message, } ] ) if len(indexed) < 2: return _all_data_row("Not enough history yet. Load a longer period.") latest_ts = indexed.index[-1] earliest_ts = indexed.index[0] if latest_ts - earliest_ts < pd.Timedelta(days=30): return _all_data_row("Short history loaded. Increase Period for 1M/3M/6M/1Y views.") windows = [("1M", 30), ("3M", 90), ("6M", 180), ("1Y", 365)] rows: list[dict[str, object]] = [] for label, days in windows: start_ts = latest_ts - pd.Timedelta(days=days) window = indexed[indexed.index >= start_ts].copy() if len(window) < 2: continue first_close = float(window.iloc[0]["Close"]) last_close = float(window.iloc[-1]["Close"]) change_pct = round(((last_close - first_close) / first_close) * 100.0, 2) if first_close else 0.0 bull_count = int((window["classification"] == "real_bull").sum()) bear_count = int((window["classification"] == "real_bear").sum()) fake_count = int((window["classification"] == "fake").sum()) trend_flips = int((window["trend_state"] != window["trend_state"].shift(1)).sum() - 1) trend_flips = max(0, trend_flips) if fake_count > max(bull_count, bear_count): interpretation = "Mostly noisy/choppy. Keep risk small or stand aside." elif bull_count > bear_count and change_pct > 0: interpretation = "Buy-side pressure dominated this window." elif bear_count > bull_count and change_pct < 0: interpretation = "Sell-side pressure dominated this window." else: interpretation = "Mixed behavior. Wait for cleaner confirmation." rows.append( { "Window": label, "Bars": int(len(window)), "Price Change %": change_pct, "Real Bull Bars": bull_count, "Real Bear Bars": bear_count, "Fake Bars": fake_count, "Trend Flips": trend_flips, "What this says": interpretation, } ) if not rows: return _all_data_row("Short history loaded. Increase Period for 1M/3M/6M/1Y views.") return pd.DataFrame(rows) def render_beginner_training_panel( analyzed: pd.DataFrame, trend_now: str, signal_quality: dict[str, float | str], regime_label: str, ) -> None: st.subheader("What This Tool Means (Beginner Training)") st.caption("This app is a chart interpreter. It labels candles, tracks trend state, and shows how similar signals behaved in the past.") with st.expander("Plain-English metric guide", expanded=False): st.markdown( f""" - `Current Trend`: **{trend_now}**. This is the app's directional state right now. - `Real Bullish Bars`: candles that closed above the prior range (upside pressure). - `Real Bearish Bars`: candles that closed below the prior range (downside pressure). - `Fake Bars`: candles that stayed inside the prior range (noise, indecision). - `Signal Quality`: **{signal_quality['score']} ({signal_quality['label']})**. Higher means cleaner recent structure. - `Regime`: **{regime_label}**. `Trending` means directional movement; `Choppy` means frequent whipsaws. - `Recent Fake Ratio`: **{signal_quality['fake_ratio']}%**. High values usually mean harder trading conditions. """ ) window_rows = build_learning_window_rows(analyzed) st.caption("Historical training snapshots from the loaded data window (1M/3M/6M/1Y).") st.dataframe(window_rows, use_container_width=True) st.info( "How to use this: if most windows show high fake bars and many trend flips, treat signals as low-confidence. " "If windows show consistent real-bar dominance with fewer flips, conditions are usually cleaner." ) def render_training_panel( show_past_behavior: bool, example_trades: pd.DataFrame, alert_key: str, display_timezone: str, use_24h_time: bool, ) -> pd.Series | None: selected_trade: pd.Series | None = None if not show_past_behavior: return selected_trade st.subheader("Past Behavior Examples (Training)") if example_trades.empty: st.info("No closed example trades yet. Expand the period/timeframe to include more trend reversals.") return selected_trade wins = int((example_trades["outcome"] == "Win").sum()) losses = int((example_trades["outcome"] == "Loss").sum()) total_examples = int(len(example_trades)) win_rate = round((wins / total_examples) * 100.0, 2) if total_examples else 0.0 avg_pnl = round(float(example_trades["pnl_pct"].mean()), 2) t1, t2, t3, t4 = st.columns(4) t1.metric("Closed Examples", total_examples) t2.metric("Wins / Losses", f"{wins} / {losses}") t3.metric("Example Win Rate", f"{win_rate}%") t4.metric("Avg P/L per Example", f"{avg_pnl}%") latest_example = example_trades.iloc[-1] latest_entry_ts = format_timestamp( latest_example["entry_timestamp"], display_timezone=display_timezone, use_24h_time=use_24h_time, ) latest_exit_ts = format_timestamp( latest_example["exit_timestamp"], display_timezone=display_timezone, use_24h_time=use_24h_time, ) st.caption( "Latest closed example: " f"{latest_example['direction']} from {latest_entry_ts} " f"to {latest_exit_ts} " f"({latest_example['bars_held']} bars, {latest_example['pnl_pct']}%)." ) st.caption("Click a row below to highlight that specific example on the chart.") st.caption("Example method: enter on trend confirmation bar close and exit on opposite trend confirmation.") display_examples_raw = example_trades.iloc[::-1].reset_index(drop=True) display_examples = display_examples_raw.copy() display_examples["entry_timestamp"] = display_examples["entry_timestamp"].map( lambda value: format_timestamp(value, display_timezone=display_timezone, use_24h_time=use_24h_time) ) display_examples["exit_timestamp"] = display_examples["exit_timestamp"].map( lambda value: format_timestamp(value, display_timezone=display_timezone, use_24h_time=use_24h_time) ) table_event = st.dataframe( display_examples, use_container_width=True, on_select="rerun", selection_mode="single-row", key=f"training-examples-{alert_key}", ) selected_rows: list[int] = [] try: selected_rows = list(table_event.selection.rows) except Exception: selected_rows = [] selected_row_state_key = f"selected-training-row-{alert_key}" if selected_rows: selected_row_idx = int(selected_rows[0]) st.session_state[selected_row_state_key] = selected_row_idx else: selected_row_idx = int(st.session_state.get(selected_row_state_key, 0)) if selected_row_idx < 0 or selected_row_idx >= len(display_examples_raw): selected_row_idx = 0 selected_trade = display_examples_raw.iloc[selected_row_idx] direction = str(selected_trade["direction"]) entry_ts = format_timestamp( selected_trade["entry_timestamp"], display_timezone=display_timezone, use_24h_time=use_24h_time, ) exit_ts = format_timestamp( selected_trade["exit_timestamp"], display_timezone=display_timezone, use_24h_time=use_24h_time, ) entry_price = float(selected_trade["entry_price"]) exit_price = float(selected_trade["exit_price"]) bars_held = int(selected_trade["bars_held"]) pnl_pct = float(selected_trade["pnl_pct"]) outcome = str(selected_trade["outcome"]) pnl_text = f"+{pnl_pct}%" if pnl_pct > 0 else f"{pnl_pct}%" result_text = "profit" if pnl_pct > 0 else ("loss" if pnl_pct < 0 else "flat result") st.info( f"Selected example explained: {direction} entry at {entry_ts} ({entry_price}), " f"exit at {exit_ts} ({exit_price}), held {bars_held} bars, {result_text} {pnl_text}, outcome: {outcome}." ) return selected_trade