Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2026-02-14 12:12:30 -06:00
parent 4c1c6f31d5
commit 9453036d8e
6 changed files with 673 additions and 14 deletions

View File

@ -12,6 +12,10 @@ Trend logic:
- Reversal requires 2 consecutive opposite real bars - Reversal requires 2 consecutive opposite real bars
- Fake bars do not reverse trend - Fake bars do not reverse trend
In-app help popup source:
- Primary: `web/src/web_core/help.html`
- Fallback: this `web/src/ONBOARDING.md`
## 2) Quick Start (Recommended) ## 2) Quick Start (Recommended)
From project root: From project root:
@ -78,6 +82,8 @@ Tip: sign and notarize before sharing broadly, so macOS trust prompts are reduce
- Real Bearish Bars - Real Bearish Bars
- Fake Bars - Fake Bars
7. Read `Trend Events` for starts and reversals. 7. Read `Trend Events` for starts and reversals.
8. Use `Live Decision Guide` to translate trend state into a practical bias/action/invalidation workflow.
9. Keep `Show past behavior examples` ON while learning to review historical entry/exit outcomes.
## 5) How To Read The Chart ## 5) How To Read The Chart
- Candle layer: full price action - Candle layer: full price action
@ -85,8 +91,85 @@ Tip: sign and notarize before sharing broadly, so macOS trust prompts are reduce
- Red triangle-down markers: `real_bear` - Red triangle-down markers: `real_bear`
- Gray candles (if enabled): visually de-emphasized fake bars - Gray candles (if enabled): visually de-emphasized fake bars
- Volume bars are colored by trend state - Volume bars are colored by trend state
- Optional training overlays:
- blue circle: example long entry
- orange diamond: example short entry
- green X: example winning exit
- red X: example loss/flat exit
## 6) Recommended Settings By Asset ## 6) Training Mode (New)
Use these toggles in sidebar `Training & Guidance`:
- `Show live decision guide`: summarizes current bias, signal status, and invalidation rule.
- `Show past behavior examples`: shows historical hypothetical trades and outcomes.
- `Overlay example entries/exits on chart`: draws example entry/exit markers on candles.
- `Focus chart on selected example`: zooms chart around the clicked training row.
- `Max training examples`: controls how many recent closed examples to include.
Example-trade method:
- Enter on a confirmed trend-change bar close.
- Exit on the opposite confirmed trend-change bar close.
- Direction is `LONG` in bullish trend and `SHORT` in bearish trend.
## 6.1) Tutorial: Use This Like a Training Coach
Goal: learn what the model is signaling and what would have happened if you followed it.
### Step A: Set up a clean training view
1. In sidebar, choose a liquid symbol (`AAPL` or `BTC-USD`).
2. Start with:
- `Timeframe = 1d` (less noise)
- `Period = 6mo`
- `Ignore potentially live last bar = ON`
3. Turn ON:
- `Show live decision guide`
- `Show past behavior examples`
- `Overlay example entries/exits on chart`
- `Focus chart on selected example` (optional but recommended while learning)
4. Set `Max training examples = 20`.
### Step B: Read current signal (what to do now)
1. Look at `Live Decision Guide`.
2. Use `Bias` as your directional filter:
- `Long Bias`: only evaluate long ideas.
- `Short Bias`: only evaluate short ideas.
- `Stand Aside / Neutral`: wait for clearer confirmation.
3. Read `Signal Status`:
- `Fresh Confirmation`: a new active trend was confirmed on the latest closed bar.
- `No New Confirmation`: no fresh reversal confirmation on the latest closed bar.
4. Use `Invalidation rule` as the condition that would cancel your current bias.
### Step C: Study historical behavior (what would have happened)
1. Open `Past Behavior Examples (Training)`.
2. Review summary metrics:
- `Closed Examples`
- `Wins / Losses`
- `Example Win Rate`
- `Avg P/L per Example`
3. Read the latest row first, then scan older examples.
4. On chart, compare markers to price structure:
- blue circle: long entry
- orange diamond: short entry
- green X: winning exit
- red X: loss/flat exit
5. Click any row in the examples table:
- app highlights that exact trade path on chart
- app shows a plain-English explanation of that row
### Step D: Practice loop (recommended)
1. Pick one symbol/timeframe.
2. Hide the table briefly and decide what you would do from `Live Decision Guide`.
3. Re-open table and compare your decision to recent example outcomes.
4. Repeat across different regimes:
- trending periods
- choppy/sideways periods
5. Keep notes on where the model performs best/worst.
### Step E: Move from training to live monitoring
1. Keep `Show live decision guide` ON.
2. Keep `Ignore potentially live last bar` ON to reduce unfinished-bar noise.
3. Enable `Auto-refresh` only when actively monitoring.
4. Treat this as a decision-support layer, not an execution signal by itself.
## 7) Recommended Settings By Asset
### Stocks (swing) ### Stocks (swing)
- Timeframe: `1d` - Timeframe: `1d`
- Period: `6mo` or `1y` - Period: `6mo` or `1y`
@ -97,7 +180,7 @@ Tip: sign and notarize before sharing broadly, so macOS trust prompts are reduce
- Period: `1mo-6mo` - Period: `1mo-6mo`
- Enable auto-refresh only when monitoring live - Enable auto-refresh only when monitoring live
## 7) Optional Filters ## 8) Optional Filters
### Ignore Wicks ### Ignore Wicks
Use when long wicks create false breakouts; compares close to previous body only. Use when long wicks create false breakouts; compares close to previous body only.
@ -115,13 +198,13 @@ Compresses non-trading time on stock charts:
Use OFF for 24/7 markets (for example many crypto workflows) when you want continuous time. Use OFF for 24/7 markets (for example many crypto workflows) when you want continuous time.
## 8) Exports ## 9) Exports
- CSV: `Download classified data (CSV)` - CSV: `Download classified data (CSV)`
- PDF chart: `Download chart (PDF)` - PDF chart: `Download chart (PDF)`
If PDF fails, ensure `kaleido` is installed (already in `requirements.txt`). If PDF fails, ensure `kaleido` is installed (already in `requirements.txt`).
## 9) Troubleshooting ## 10) Troubleshooting
### App wont start ### App wont start
```bash ```bash
./run.sh --setup-only ./run.sh --setup-only
@ -145,12 +228,12 @@ streamlit run app.py --server.port 8502
### Exports crash with timestamp errors ### Exports crash with timestamp errors
- Pull latest project changes (export logic now handles named index columns) - Pull latest project changes (export logic now handles named index columns)
## 10) Safety Notes ## 11) Safety Notes
- This app is analysis-only, no trade execution. - This app is analysis-only, no trade execution.
- Backtest snapshot is diagnostic and simplistic. - Backtest snapshot is diagnostic and simplistic.
- Not financial advice. - Not financial advice.
## 11) Useful Commands ## 12) Useful Commands
Setup only: Setup only:
```bash ```bash
./run.sh --setup-only ./run.sh --setup-only

View File

@ -12,7 +12,7 @@ Out of scope:
- macOS shell/wrapper architecture and packaging - macOS shell/wrapper architecture and packaging
## 2. Product Goal ## 2. Product Goal
Provide an analysis-only charting tool that classifies OHLC bars as real/fake, tracks trend state using only real bars, and exposes clear visual + exportable outputs. Provide an analysis-only charting tool that classifies OHLC bars as real/fake, tracks trend state using only real bars, and exposes clear visual, training-oriented guidance, and exportable outputs.
## 3. Inputs and Data Pipeline ## 3. Inputs and Data Pipeline
1. User configures: 1. User configures:
@ -40,6 +40,11 @@ Normalization constraints:
- `volume_sma_window`: `[2, 100]`, fallback `20` - `volume_sma_window`: `[2, 100]`, fallback `20`
- `volume_multiplier`: `[0.1, 3.0]`, rounded to 0.1, fallback `1.0` - `volume_multiplier`: `[0.1, 3.0]`, rounded to 0.1, fallback `1.0`
- `refresh_sec`: `[10, 600]`, fallback `60` - `refresh_sec`: `[10, 600]`, fallback `60`
- `show_live_guide`: boolean, fallback `true`
- `show_past_behavior`: boolean, fallback `true`
- `show_trade_markers`: boolean, fallback `true`
- `focus_chart_on_selected_example`: boolean, fallback `false`
- `max_training_examples`: `[5, 100]`, fallback `20`
- booleans normalized from common truthy/falsy strings and numbers - booleans normalized from common truthy/falsy strings and numbers
## 5. Classification Rules ## 5. Classification Rules
@ -86,6 +91,13 @@ Important:
- `real_bull`: green triangle-up - `real_bull`: green triangle-up
- `real_bear`: red triangle-down - `real_bear`: red triangle-down
- Optional fake-bar de-emphasis via gray candle layer (`gray_fake`). - Optional fake-bar de-emphasis via gray candle layer (`gray_fake`).
- Optional example-trade overlay markers (`show_trade_markers`):
- long entry marker
- short entry marker
- winning exit marker
- loss/flat exit marker
- If a past-behavior row is selected, chart highlights that trade window and path.
- Optional focused zoom around selected trade (`focus_chart_on_selected_example`).
- Volume subplot colored by trend state. - Volume subplot colored by trend state.
Gap handling (`hide_market_closed_gaps`): Gap handling (`hide_market_closed_gaps`):
@ -96,7 +108,7 @@ Gap handling (`hide_market_closed_gaps`):
## 8. Help and Onboarding Behavior ## 8. Help and Onboarding Behavior
- Web-only fallback help entry exists in sidebar: - Web-only fallback help entry exists in sidebar:
- `Help / Quick Start` - `Help / Quick Start`
- Content source: `web/src/ONBOARDING.md` - Content source: `web/src/web_core/help.html` (primary), `web/src/ONBOARDING.md` (fallback)
- Help appears in a dialog. - Help appears in a dialog.
## 9. Outputs ## 9. Outputs
@ -105,10 +117,21 @@ Gap handling (`hide_market_closed_gaps`):
- real bullish count - real bullish count
- real bearish count - real bearish count
- fake count - fake count
- Live decision guide (optional):
- bias (long/short/neutral)
- signal confirmation status
- latest bar interpretation
- action + invalidation guidance
- Trend events table (latest events) - Trend events table (latest events)
- Backtest snapshot: - Backtest snapshot:
- signal at trend-change rows to active bull/bear states - signal at trend-change rows to active bull/bear states
- next-bar close determines win/loss - next-bar close determines win/loss
- Past behavior examples (optional training panel):
- historical examples using trend-confirmation entries and opposite-confirmation exits
- per-example direction, entry/exit timestamps, bars held, P/L%, and outcome
- aggregate example metrics (count, win/loss, win rate, average P/L)
- selectable table rows that drive chart highlight of chosen example
- plain-language explanation for selected example
- Exports: - Exports:
- CSV always available - CSV always available
- PDF via Plotly image export (requires Kaleido runtime) - PDF via Plotly image export (requires Kaleido runtime)

View File

@ -5,13 +5,14 @@ from pathlib import Path
from typing import Any from typing import Any
import pandas as pd import pandas as pd
import plotly.graph_objects as go
import streamlit as st import streamlit as st
import yfinance as yf import yfinance as yf
from streamlit_autorefresh import st_autorefresh from streamlit_autorefresh import st_autorefresh
from web_core.analytics import backtest_signals from web_core.analytics import backtest_signals, simulate_trend_trades
from web_core.charting import build_figure from web_core.charting import build_figure
from web_core.constants import INTERVAL_OPTIONS, PERIOD_OPTIONS from web_core.constants import INTERVAL_OPTIONS, PERIOD_OPTIONS, TREND_BEAR, TREND_BULL, TREND_NEUTRAL
from web_core.data import fetch_ohlc, maybe_drop_live_bar from web_core.data import fetch_ohlc, maybe_drop_live_bar
from web_core.exporting import df_for_export from web_core.exporting import df_for_export
from web_core.strategy import classify_bars, detect_trends from web_core.strategy import classify_bars, detect_trends
@ -70,6 +71,11 @@ def normalize_web_settings(raw: dict[str, Any] | None) -> dict[str, Any]:
"volume_multiplier": 1.0, "volume_multiplier": 1.0,
"gray_fake": True, "gray_fake": True,
"hide_market_closed_gaps": True, "hide_market_closed_gaps": True,
"show_live_guide": True,
"show_past_behavior": True,
"show_trade_markers": True,
"focus_chart_on_selected_example": False,
"max_training_examples": 20,
"enable_auto_refresh": False, "enable_auto_refresh": False,
"refresh_sec": 60, "refresh_sec": 60,
} }
@ -112,6 +118,19 @@ def normalize_web_settings(raw: dict[str, Any] | None) -> dict[str, Any]:
raw.get("hide_market_closed_gaps"), raw.get("hide_market_closed_gaps"),
fallback=bool(defaults["hide_market_closed_gaps"]), fallback=bool(defaults["hide_market_closed_gaps"]),
) )
show_live_guide = _to_bool(raw.get("show_live_guide"), fallback=bool(defaults["show_live_guide"]))
show_past_behavior = _to_bool(raw.get("show_past_behavior"), fallback=bool(defaults["show_past_behavior"]))
show_trade_markers = _to_bool(raw.get("show_trade_markers"), fallback=bool(defaults["show_trade_markers"]))
focus_chart_on_selected_example = _to_bool(
raw.get("focus_chart_on_selected_example"),
fallback=bool(defaults["focus_chart_on_selected_example"]),
)
max_training_examples = _clamp_int(
raw.get("max_training_examples"),
fallback=int(defaults["max_training_examples"]),
minimum=5,
maximum=100,
)
enable_auto_refresh = _to_bool(raw.get("enable_auto_refresh"), fallback=bool(defaults["enable_auto_refresh"])) enable_auto_refresh = _to_bool(raw.get("enable_auto_refresh"), fallback=bool(defaults["enable_auto_refresh"]))
refresh_sec = _clamp_int( refresh_sec = _clamp_int(
raw.get("refresh_sec"), raw.get("refresh_sec"),
@ -132,6 +151,11 @@ def normalize_web_settings(raw: dict[str, Any] | None) -> dict[str, Any]:
"volume_multiplier": volume_multiplier, "volume_multiplier": volume_multiplier,
"gray_fake": gray_fake, "gray_fake": gray_fake,
"hide_market_closed_gaps": hide_market_closed_gaps, "hide_market_closed_gaps": hide_market_closed_gaps,
"show_live_guide": show_live_guide,
"show_past_behavior": show_past_behavior,
"show_trade_markers": show_trade_markers,
"focus_chart_on_selected_example": focus_chart_on_selected_example,
"max_training_examples": max_training_examples,
"enable_auto_refresh": enable_auto_refresh, "enable_auto_refresh": enable_auto_refresh,
"refresh_sec": refresh_sec, "refresh_sec": refresh_sec,
} }
@ -156,16 +180,25 @@ def save_web_settings(settings: dict[str, Any]) -> None:
@st.cache_data(show_spinner=False) @st.cache_data(show_spinner=False)
def load_help_markdown() -> str: def load_help_content() -> tuple[str, bool]:
help_html_paths = [
Path(__file__).with_name("web_core").joinpath("help.html"),
Path(__file__).with_name("help.html"),
]
for help_html_path in help_html_paths:
if help_html_path.exists():
return help_html_path.read_text(encoding="utf-8"), True
onboarding_path = Path(__file__).with_name("ONBOARDING.md") onboarding_path = Path(__file__).with_name("ONBOARDING.md")
if onboarding_path.exists(): if onboarding_path.exists():
return onboarding_path.read_text(encoding="utf-8") return onboarding_path.read_text(encoding="utf-8"), False
return "Help content not found." return "Help content not found.", False
@st.dialog("Help & Quick Start", width="large") @st.dialog("Help & Quick Start", width="large")
def help_dialog() -> None: def help_dialog() -> None:
st.markdown(load_help_markdown()) content, is_html = load_help_content()
st.markdown(content, unsafe_allow_html=is_html)
@st.cache_data(show_spinner=False, ttl=3600) @st.cache_data(show_spinner=False, ttl=3600)
@ -239,6 +272,48 @@ def resolve_symbol_identity(symbol: str) -> dict[str, str]:
return {"symbol": normalized_symbol, "name": "", "exchange": ""} return {"symbol": normalized_symbol, "name": "", "exchange": ""}
def build_live_decision_guide(
trend_now: str,
previous_trend: str,
latest_classification: str,
) -> dict[str, str]:
if trend_now == TREND_BULL:
bias = "Long Bias"
action = "Prefer pullback longs and avoid fresh shorts while the bullish trend remains active."
invalidation = "Bullish bias is invalidated only after 2 consecutive real bearish bars confirm a reversal."
elif trend_now == TREND_BEAR:
bias = "Short Bias"
action = "Prefer short setups on pops and avoid fresh longs while the bearish trend remains active."
invalidation = "Bearish bias is invalidated only after 2 consecutive real bullish bars confirm a reversal."
else:
bias = "Stand Aside / Neutral"
action = "Wait for 2 consecutive real bars in one direction before taking directional exposure."
invalidation = "No active trend yet; avoid forcing trades in noisy ranges."
if trend_now in {TREND_BULL, TREND_BEAR} and trend_now != previous_trend:
confirmation = "Fresh Confirmation"
confirmation_detail = "Latest closed bar confirmed a new active trend."
else:
confirmation = "No New Confirmation"
confirmation_detail = "Latest closed bar did not confirm a new trend reversal."
classification_hint = {
"real_bull": "Latest bar closed above the previous range.",
"real_bear": "Latest bar closed below the previous range.",
"fake": "Latest bar stayed inside the previous range (noise).",
"unclassified": "Latest bar is unclassified.",
}.get(latest_classification, "Latest bar classification unavailable.")
return {
"bias": bias,
"action": action,
"invalidation": invalidation,
"confirmation": confirmation,
"confirmation_detail": confirmation_detail,
"classification_hint": classification_hint,
}
def main() -> None: def main() -> None:
st.set_page_config(page_title="Real Bars vs Fake Bars Analyzer", layout="wide") st.set_page_config(page_title="Real Bars vs Fake Bars Analyzer", layout="wide")
st.title("Real Bars vs Fake Bars Trend Analyzer") st.title("Real Bars vs Fake Bars Trend Analyzer")
@ -354,6 +429,35 @@ def main() -> None:
value=bool(effective_defaults["hide_market_closed_gaps"]), value=bool(effective_defaults["hide_market_closed_gaps"]),
) )
st.header("Training & Guidance")
show_live_guide = st.checkbox(
"Show live decision guide",
value=bool(effective_defaults["show_live_guide"]),
help="Shows a plain-English interpretation of current trend state and confirmation status.",
)
show_past_behavior = st.checkbox(
"Show past behavior examples",
value=bool(effective_defaults["show_past_behavior"]),
help="Displays historical example trades based on trend confirmation and reversal signals.",
)
show_trade_markers = st.checkbox(
"Overlay example entries/exits on chart",
value=bool(effective_defaults["show_trade_markers"]),
help="Adds entry/exit markers for the training examples onto the main chart.",
)
focus_chart_on_selected_example = st.checkbox(
"Focus chart on selected example",
value=bool(effective_defaults["focus_chart_on_selected_example"]),
help="When a training row is selected, zooms chart around that trade window.",
)
max_training_examples = st.slider(
"Max training examples",
5,
100,
int(effective_defaults["max_training_examples"]),
5,
)
st.header("Monitoring") st.header("Monitoring")
enable_auto_refresh = st.checkbox("Auto-refresh", value=bool(effective_defaults["enable_auto_refresh"])) enable_auto_refresh = st.checkbox("Auto-refresh", value=bool(effective_defaults["enable_auto_refresh"]))
refresh_sec = st.slider("Refresh interval (seconds)", 10, 600, int(effective_defaults["refresh_sec"]), 10) refresh_sec = st.slider("Refresh interval (seconds)", 10, 600, int(effective_defaults["refresh_sec"]), 10)
@ -375,6 +479,11 @@ def main() -> None:
"volume_multiplier": float(volume_multiplier), "volume_multiplier": float(volume_multiplier),
"gray_fake": bool(gray_fake), "gray_fake": bool(gray_fake),
"hide_market_closed_gaps": bool(hide_market_closed_gaps), "hide_market_closed_gaps": bool(hide_market_closed_gaps),
"show_live_guide": bool(show_live_guide),
"show_past_behavior": bool(show_past_behavior),
"show_trade_markers": bool(show_trade_markers),
"focus_chart_on_selected_example": bool(focus_chart_on_selected_example),
"max_training_examples": int(max_training_examples),
"enable_auto_refresh": bool(enable_auto_refresh), "enable_auto_refresh": bool(enable_auto_refresh),
"refresh_sec": int(refresh_sec), "refresh_sec": int(refresh_sec),
} }
@ -433,6 +542,25 @@ def main() -> None:
c3.metric("Real Bearish Bars", bear_count) c3.metric("Real Bearish Bars", bear_count)
c4.metric("Fake Bars", fake_count) c4.metric("Fake Bars", fake_count)
previous_trend = str(analyzed.iloc[-2]["trend_state"]) if len(analyzed) > 1 else TREND_NEUTRAL
latest_classification = str(latest["classification"])
live_guide = build_live_decision_guide(
trend_now=trend_now,
previous_trend=previous_trend,
latest_classification=latest_classification,
)
if show_live_guide:
st.subheader("Live Decision Guide")
g1, g2, g3 = st.columns(3)
g1.metric("Bias", live_guide["bias"])
g2.metric("Signal Status", live_guide["confirmation"])
g3.metric("Latest Bar", latest_classification)
st.caption(live_guide["confirmation_detail"])
st.caption(live_guide["classification_hint"])
st.info(live_guide["action"])
st.caption(f"Invalidation rule: {live_guide['invalidation']}")
alert_key = f"{symbol}-{interval}-{period}" alert_key = f"{symbol}-{interval}-{period}"
newest_event = events[-1].event if events else "" newest_event = events[-1].event if events else ""
previous_event = st.session_state.get(f"last_event-{alert_key}", "") previous_event = st.session_state.get(f"last_event-{alert_key}", "")
@ -440,12 +568,184 @@ def main() -> None:
st.warning(f"Alert: {newest_event}") st.warning(f"Alert: {newest_event}")
st.session_state[f"last_event-{alert_key}"] = newest_event st.session_state[f"last_event-{alert_key}"] = newest_event
example_trades = simulate_trend_trades(analyzed, max_examples=int(max_training_examples))
selected_trade: pd.Series | None = None
if show_past_behavior:
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.")
else:
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]
st.caption(
"Latest closed example: "
f"{latest_example['direction']} from {latest_example['entry_timestamp']} "
f"to {latest_example['exit_timestamp']} "
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"].astype(str)
display_examples["exit_timestamp"] = display_examples["exit_timestamp"].astype(str)
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 = str(selected_trade["entry_timestamp"])
exit_ts = str(selected_trade["exit_timestamp"])
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}."
)
fig = build_figure( fig = build_figure(
analyzed, analyzed,
gray_fake=gray_fake, gray_fake=gray_fake,
interval=interval, interval=interval,
hide_market_closed_gaps=hide_market_closed_gaps, hide_market_closed_gaps=hide_market_closed_gaps,
) )
if show_trade_markers and not example_trades.empty:
long_entries = example_trades[example_trades["direction"] == "LONG"]
short_entries = example_trades[example_trades["direction"] == "SHORT"]
win_exits = example_trades[example_trades["outcome"] == "Win"]
non_win_exits = example_trades[example_trades["outcome"] != "Win"]
if not long_entries.empty:
fig.add_trace(
go.Scatter(
x=long_entries["entry_timestamp"],
y=long_entries["entry_price"],
mode="markers",
name="Example Entry (Long)",
marker=dict(color="#1565C0", size=9, symbol="circle"),
),
row=1,
col=1,
)
if not short_entries.empty:
fig.add_trace(
go.Scatter(
x=short_entries["entry_timestamp"],
y=short_entries["entry_price"],
mode="markers",
name="Example Entry (Short)",
marker=dict(color="#EF6C00", size=9, symbol="diamond"),
),
row=1,
col=1,
)
if not win_exits.empty:
fig.add_trace(
go.Scatter(
x=win_exits["exit_timestamp"],
y=win_exits["exit_price"],
mode="markers",
name="Example Exit (Win)",
marker=dict(color="#2E7D32", size=10, symbol="x"),
),
row=1,
col=1,
)
if not non_win_exits.empty:
fig.add_trace(
go.Scatter(
x=non_win_exits["exit_timestamp"],
y=non_win_exits["exit_price"],
mode="markers",
name="Example Exit (Loss/Flat)",
marker=dict(color="#C62828", size=10, symbol="x"),
),
row=1,
col=1,
)
if selected_trade is not None:
selected_entry_ts = pd.Timestamp(selected_trade["entry_timestamp"])
selected_exit_ts = pd.Timestamp(selected_trade["exit_timestamp"])
selected_entry_price = float(selected_trade["entry_price"])
selected_exit_price = float(selected_trade["exit_price"])
selected_direction = str(selected_trade["direction"])
selected_outcome = str(selected_trade["outcome"])
path_color = "#43A047" if selected_outcome == "Win" else ("#EF6C00" if selected_outcome == "Flat" else "#E53935")
window_fill = "#BBDEFB" if selected_direction == "LONG" else "#FFE0B2"
fig.add_vrect(
x0=selected_entry_ts,
x1=selected_exit_ts,
fillcolor=window_fill,
opacity=0.18,
line_width=0,
row=1,
col=1,
)
fig.add_trace(
go.Scatter(
x=[selected_entry_ts, selected_exit_ts],
y=[selected_entry_price, selected_exit_price],
mode="lines+markers",
name="Selected Example Path",
line=dict(color=path_color, width=3, dash="dot"),
marker=dict(color=path_color, size=11, symbol="star"),
),
row=1,
col=1,
)
if focus_chart_on_selected_example:
entry_pos = int(analyzed.index.get_indexer([selected_entry_ts], method="nearest")[0])
exit_pos = int(analyzed.index.get_indexer([selected_exit_ts], method="nearest")[0])
left_pos = max(0, min(entry_pos, exit_pos) - 4)
right_pos = min(len(analyzed) - 1, max(entry_pos, exit_pos) + 4)
fig.update_xaxes(range=[analyzed.index[left_pos], analyzed.index[right_pos]])
st.plotly_chart(fig, use_container_width=True) st.plotly_chart(fig, use_container_width=True)
bt = backtest_signals(analyzed) bt = backtest_signals(analyzed)

View File

@ -0,0 +1,81 @@
from __future__ import annotations
import pandas as pd
from web_core.analytics import simulate_trend_trades
from web_core.constants import TREND_BEAR, TREND_BULL, TREND_NEUTRAL
def test_simulate_trend_trades_returns_empty_when_insufficient_rows() -> None:
idx = pd.date_range("2025-01-01", periods=3, freq="D")
df = pd.DataFrame(
{
"Close": [100.0, 101.0, 102.0],
"trend_state": [TREND_NEUTRAL, TREND_BULL, TREND_BULL],
},
index=idx,
)
out = simulate_trend_trades(df)
assert out.empty
def test_simulate_trend_trades_closes_trade_on_opposite_signal() -> None:
idx = pd.date_range("2025-01-01", periods=7, freq="D")
df = pd.DataFrame(
{
"Close": [95.0, 98.0, 100.0, 102.0, 104.0, 110.0, 108.0],
"trend_state": [
TREND_NEUTRAL,
TREND_NEUTRAL,
TREND_BULL,
TREND_BULL,
TREND_BULL,
TREND_BEAR,
TREND_BEAR,
],
},
index=idx,
)
out = simulate_trend_trades(df)
assert len(out) == 1
trade = out.iloc[0]
assert trade["direction"] == "LONG"
assert trade["entry_timestamp"] == idx[2]
assert trade["exit_timestamp"] == idx[5]
assert trade["bars_held"] == 3
assert trade["pnl_pct"] == 10.0
assert trade["outcome"] == "Win"
def test_simulate_trend_trades_handles_multiple_flips_and_max_examples() -> None:
idx = pd.date_range("2025-01-01", periods=7, freq="D")
df = pd.DataFrame(
{
"Close": [90.0, 100.0, 103.0, 95.0, 92.0, 80.0, 82.0],
"trend_state": [
TREND_NEUTRAL,
TREND_BULL,
TREND_BULL,
TREND_BEAR,
TREND_BEAR,
TREND_BULL,
TREND_BULL,
],
},
index=idx,
)
out = simulate_trend_trades(df, max_examples=1)
assert len(out) == 1
trade = out.iloc[0]
assert trade["direction"] == "SHORT"
assert trade["entry_timestamp"] == idx[3]
assert trade["exit_timestamp"] == idx[5]
assert trade["bars_held"] == 2
assert trade["pnl_pct"] == 15.79
assert trade["outcome"] == "Win"

View File

@ -35,3 +35,95 @@ def backtest_signals(df: pd.DataFrame) -> dict[str, float | int]:
trades = wins + losses trades = wins + losses
win_rate = (wins / trades * 100.0) if trades else 0.0 win_rate = (wins / trades * 100.0) if trades else 0.0
return {"trades": trades, "wins": wins, "losses": losses, "win_rate": round(win_rate, 2)} return {"trades": trades, "wins": wins, "losses": losses, "win_rate": round(win_rate, 2)}
def simulate_trend_trades(df: pd.DataFrame, max_examples: int = 20) -> pd.DataFrame:
if len(df) < 4:
return pd.DataFrame(
columns=[
"direction",
"entry_timestamp",
"exit_timestamp",
"entry_price",
"exit_price",
"bars_held",
"pnl_pct",
"outcome",
]
)
trend_series = df["trend_state"]
trend_change = trend_series != trend_series.shift(1)
signal_idx = df.index[trend_change & trend_series.isin([TREND_BULL, TREND_BEAR])]
direction_by_trend = {
TREND_BULL: "LONG",
TREND_BEAR: "SHORT",
}
open_trade: dict[str, object] | None = None
closed_trades: list[dict[str, object]] = []
for idx in signal_idx:
pos = int(df.index.get_loc(idx))
trend_now = str(df.iloc[pos]["trend_state"])
direction = direction_by_trend[trend_now]
close_price = float(df.iloc[pos]["Close"])
if open_trade is None:
open_trade = {
"direction": direction,
"entry_timestamp": idx,
"entry_pos": pos,
"entry_price": close_price,
}
continue
if open_trade["direction"] == direction:
continue
entry_price = float(open_trade["entry_price"])
if str(open_trade["direction"]) == "LONG":
pnl_pct = ((close_price - entry_price) / entry_price) * 100.0
else:
pnl_pct = ((entry_price - close_price) / entry_price) * 100.0
closed_trades.append(
{
"direction": str(open_trade["direction"]),
"entry_timestamp": open_trade["entry_timestamp"],
"exit_timestamp": idx,
"entry_price": round(entry_price, 4),
"exit_price": round(close_price, 4),
"bars_held": pos - int(open_trade["entry_pos"]),
"pnl_pct": round(pnl_pct, 2),
"outcome": "Win" if pnl_pct > 0 else ("Loss" if pnl_pct < 0 else "Flat"),
}
)
# Flip into the new direction at the same confirmed reversal bar.
open_trade = {
"direction": direction,
"entry_timestamp": idx,
"entry_pos": pos,
"entry_price": close_price,
}
if not closed_trades:
return pd.DataFrame(
columns=[
"direction",
"entry_timestamp",
"exit_timestamp",
"entry_price",
"exit_price",
"bars_held",
"pnl_pct",
"outcome",
]
)
trades_df = pd.DataFrame(closed_trades)
if max_examples > 0 and len(trades_df) > max_examples:
trades_df = trades_df.iloc[-max_examples:].copy()
return trades_df

View File

@ -0,0 +1,80 @@
<h1>ManeshTrader Help</h1>
<h2>What This Tool Does</h2>
<p>
The app classifies each closed candle as <code>real_bull</code>, <code>real_bear</code>, or
<code>fake</code>, then tracks trend state from only the real bars.
</p>
<ul>
<li>2 consecutive real bullish bars: bullish trend active</li>
<li>2 consecutive real bearish bars: bearish trend active</li>
<li>Reversal also requires 2 consecutive opposite real bars</li>
<li>Fake bars do not reverse trend on their own</li>
</ul>
<h2>Quick Start</h2>
<ol>
<li>Choose <code>Symbol</code> (example: <code>AAPL</code> or <code>BTC-USD</code>).</li>
<li>Start with <code>Timeframe = 1d</code> and <code>Period = 6mo</code>.</li>
<li>Keep <code>Ignore potentially live last bar</code> ON.</li>
<li>Enable these in <code>Training &amp; Guidance</code>:
<ul>
<li><code>Show live decision guide</code></li>
<li><code>Show past behavior examples</code></li>
<li><code>Overlay example entries/exits on chart</code></li>
<li><code>Focus chart on selected example</code> (recommended for learning)</li>
</ul>
</li>
<li>Set <code>Max training examples</code> to 20.</li>
</ol>
<h2>How To Use The New Training Features</h2>
<h3>1) Live Decision Guide (what to do now)</h3>
<ul>
<li><strong>Bias</strong>: Long, Short, or Stand Aside / Neutral.</li>
<li><strong>Signal Status</strong>: Fresh confirmation or no new confirmation.</li>
<li><strong>Latest Bar</strong>: How the newest closed candle was classified.</li>
<li><strong>Action + Invalidation</strong>: Practical rule for what to prefer and when to stop.</li>
</ul>
<h3>2) Past Behavior Examples (what would have happened)</h3>
<p>
Examples are hypothetical and use one rule:
enter at trend-confirmation bar close, exit at opposite trend-confirmation bar close.
</p>
<ul>
<li><code>direction</code>: LONG (upside bias) or SHORT (downside bias)</li>
<li><code>entry_timestamp</code> / <code>exit_timestamp</code>: example start/end time</li>
<li><code>entry_price</code> / <code>exit_price</code>: example prices</li>
<li><code>bars_held</code>: number of candles held</li>
<li><code>pnl_pct</code>: percent gain/loss for that example</li>
<li><code>outcome</code>: Win, Loss, or Flat</li>
</ul>
<h3>3) Click A Row To Highlight It On Chart</h3>
<ol>
<li>Click any row in the <strong>Past Behavior Examples</strong> table.</li>
<li>The chart highlights that exact entry-to-exit window.</li>
<li>A path line shows the selected trade from entry price to exit price.</li>
<li>A plain-English explanation appears under the table for the selected row.</li>
<li>If <code>Focus chart on selected example</code> is ON, the chart zooms into that trade.</li>
</ol>
<h2>Marker Legend</h2>
<ul>
<li>Green triangle-up: real bullish bar</li>
<li>Red triangle-down: real bearish bar</li>
<li>Blue circle: example long entry</li>
<li>Orange diamond: example short entry</li>
<li>Green X: example winning exit</li>
<li>Red X: example loss/flat exit</li>
<li>Star path + highlighted window: currently selected example row</li>
</ul>
<h2>Important Notes</h2>
<ul>
<li>Analysis and training only. No trade execution.</li>
<li>Educational examples are simplified and not financial advice.</li>
<li>Use with risk controls before any real-money decision.</li>
</ul>