From a5f620063624e2c7c7f3e05ef24b09481a576de6 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Mon, 23 Feb 2026 19:43:30 -0600 Subject: [PATCH] Signed-off-by: Matt Bruce --- web/src/ONBOARDING.md | 19 +++-- web/src/PRD.md | 7 ++ web/src/app.py | 8 +- web/src/tests/test_training_ui.py | 42 ++++++++++ web/src/web_core/ui/training_ui.py | 120 +++++++++++++++++++++++++++++ 5 files changed, 190 insertions(+), 6 deletions(-) create mode 100644 web/src/tests/test_training_ui.py diff --git a/web/src/ONBOARDING.md b/web/src/ONBOARDING.md index 55f96ab..d7f9140 100644 --- a/web/src/ONBOARDING.md +++ b/web/src/ONBOARDING.md @@ -17,6 +17,11 @@ In-app help popup: - Uses a multi-screen, child-friendly guide implemented in `web/src/web_core/ui/help_content.py` - Includes dedicated screens for core setup, filters, training mode, backtest controls, and advanced panels +Beginner interpretation panel: +- Main page always shows `What This Tool Means (Beginner Training)` +- Includes plain-English definitions of top metrics and a historical snapshot table for `1M`, `3M`, `6M`, `1Y` +- Each row includes a simple interpretation so non-traders can quickly understand whether a window was directional or noisy + ## 2) Quick Start (Recommended) From project root: @@ -93,11 +98,15 @@ Tip: sign and notarize before sharing broadly, so macOS trust prompts are reduce - Real Bullish Bars - Real Bearish Bars - Fake Bars -13. Read `Trend Events` for starts and reversals. -14. Use `Live Decision Guide` to translate trend state into a practical bias/action/invalidation workflow. -15. Keep `Show past behavior examples` ON while learning to review historical entry/exit outcomes. -16. Set `Display Timezone (US)` to your preferred timezone (default is `America/Chicago`, CST/CDT). -17. Choose `Use 24-hour time` ON for `13:00` style, or OFF for `1:00 PM` style. +13. Read `What This Tool Means (Beginner Training)`: + - Open `Plain-English metric guide` for quick definitions. + - Review `1M / 3M / 6M / 1Y` snapshot rows to see how this symbol behaved recently. + - Use `What this says` to understand if conditions were trend-friendly or choppy. +14. Read `Trend Events` for starts and reversals. +15. Use `Live Decision Guide` to translate trend state into a practical bias/action/invalidation workflow. +16. Keep `Show past behavior examples` ON while learning to review historical entry/exit outcomes. +17. Set `Display Timezone (US)` to your preferred timezone (default is `America/Chicago`, CST/CDT). +18. Choose `Use 24-hour time` ON for `13:00` style, or OFF for `1:00 PM` style. ## 4.1) Advanced Features (Optional) - `Advanced Signals`: diff --git a/web/src/PRD.md b/web/src/PRD.md index 9b81aea..12dc95e 100644 --- a/web/src/PRD.md +++ b/web/src/PRD.md @@ -170,6 +170,11 @@ Gap handling (`hide_market_closed_gaps`): - `Help / Quick Start` - Help appears in a dialog with multiple navigable screens (screen picker + previous/next). - Help copy is intentionally beginner-friendly and explains each major sidebar control group, including detailed backtest controls and why each setting matters. +- Main page includes a beginner training block: + - `What This Tool Means (Beginner Training)` + - Plain-English definitions for top metrics (`Current Trend`, real/fake bars, `Signal Quality`, `Regime`, `Recent Fake Ratio`). + - Historical learning table with trailing windows (`1M`, `3M`, `6M`, `1Y`) computed from loaded data. + - Per-window interpretation text that summarizes whether behavior was trend-dominant, bearish-dominant, or choppy/noisy. - The onboarding markdown remains project documentation; in-app help content is rendered from `web/src/web_core/ui/help_content.py`. ## 9. Outputs @@ -178,6 +183,8 @@ Gap handling (`hide_market_closed_gaps`): - real bullish count - real bearish count - fake count + - beginner training guide (plain-English metric glossary) + - historical learning snapshots (`1M`, `3M`, `6M`, `1Y`) including price change, bar-type counts, trend flips, and interpretation - Live decision guide (optional): - bias (long/short/neutral) - signal confirmation status diff --git a/web/src/app.py b/web/src/app.py index bd3cfc3..9257109 100644 --- a/web/src/app.py +++ b/web/src/app.py @@ -36,7 +36,7 @@ from web_core.auth.profile_store import ( from web_core.ui.sidebar_ui import render_sidebar from web_core.strategy import classify_bars, detect_trends from web_core.market.symbols import resolve_symbol_identity -from web_core.ui.training_ui import render_training_panel +from web_core.ui.training_ui import render_beginner_training_panel, render_training_panel from web_core.time_display import format_timestamp @@ -180,6 +180,12 @@ def main() -> None: q1.metric("Signal Quality", f"{quality['score']} ({quality['label']})") q2.metric("Regime", regime_label) q3.metric("Recent Fake Ratio", f"{quality['fake_ratio']}%") + render_beginner_training_panel( + analyzed=analyzed_view, + trend_now=trend_now, + signal_quality=quality, + regime_label=regime_label, + ) run_advanced_panels = bool(sidebar_settings.get("advanced_auto_run", False) or sidebar_settings.get("run_advanced_now", False)) if (bool(sidebar_settings["enable_multi_tf_confirmation"]) or bool(sidebar_settings["enable_compare_symbols"])) and not run_advanced_panels: diff --git a/web/src/tests/test_training_ui.py b/web/src/tests/test_training_ui.py new file mode 100644 index 0000000..c99712e --- /dev/null +++ b/web/src/tests/test_training_ui.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +import pandas as pd + +from web_core.constants import TREND_BEAR, TREND_BULL, TREND_NEUTRAL +from web_core.ui.training_ui import build_learning_window_rows + + +def _make_analyzed(start: str = "2025-01-01", periods: int = 420) -> pd.DataFrame: + idx = pd.date_range(start, periods=periods, freq="D", tz="UTC") + closes = [100.0 + (i * 0.2) for i in range(periods)] + classifications_cycle = ["real_bull", "fake", "real_bear", "fake", "real_bull"] + trend_cycle = [TREND_BULL, TREND_BULL, TREND_BEAR, TREND_BEAR, TREND_NEUTRAL] + classifications = [classifications_cycle[i % len(classifications_cycle)] for i in range(periods)] + trends = [trend_cycle[i % len(trend_cycle)] for i in range(periods)] + return pd.DataFrame({"Close": closes, "classification": classifications, "trend_state": trends}, index=idx) + + +def test_build_learning_window_rows_includes_standard_windows() -> None: + analyzed = _make_analyzed() + rows = build_learning_window_rows(analyzed) + + assert list(rows["Window"]) == ["1M", "3M", "6M", "1Y"] + assert set(rows.columns) == { + "Window", + "Bars", + "Price Change %", + "Real Bull Bars", + "Real Bear Bars", + "Fake Bars", + "Trend Flips", + "What this says", + } + + +def test_build_learning_window_rows_fallbacks_with_short_history() -> None: + analyzed = _make_analyzed(periods=10) + rows = build_learning_window_rows(analyzed) + + assert len(rows) == 1 + assert rows.iloc[0]["Window"] == "All data" + assert int(rows.iloc[0]["Bars"]) == 10 diff --git a/web/src/web_core/ui/training_ui.py b/web/src/web_core/ui/training_ui.py index 7b7690e..5873b38 100644 --- a/web/src/web_core/ui/training_ui.py +++ b/web/src/web_core/ui/training_ui.py @@ -6,6 +6,126 @@ 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,