234 lines
9.4 KiB
Python
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
|