maneshtrader/web_core/ui/training_ui.py

234 lines
9.4 KiB
Python

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