maneshtrader/app.py

362 lines
16 KiB
Python

from __future__ import annotations
import time
import pandas as pd
import streamlit as st
from web_core.analytics import backtest_signals, simulate_trend_trades
from web_core.chart_overlays import add_example_trade_markers, highlight_selected_trade
from web_core.charting import build_figure
from web_core.constants import TREND_BEAR, TREND_BULL, TREND_NEUTRAL
from web_core.data import fetch_ohlc, maybe_drop_live_bar
from web_core.exporting import df_for_export
from web_core.insights import (
compare_symbols,
compute_session_stats,
compute_signal_quality,
detect_regime,
multi_timeframe_confirmation,
send_webhook_alert,
)
from web_core.ui.login_ui import render_profile_login
from web_core.live_guide import build_live_decision_guide
from web_core.auth.profile_store import (
find_existing_profile_id,
first_query_param_value,
is_truthy_flag,
is_profile_session_expired,
list_web_profiles,
mark_profile_login,
normalize_profile_id,
normalize_web_settings,
profile_requires_pin,
save_web_settings,
)
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_beginner_training_panel, render_training_panel
from web_core.time_display import format_timestamp
def main() -> None:
st.set_page_config(page_title="Real Bars vs Fake Bars Analyzer", layout="wide")
st.title("Real Bars vs Fake Bars Trend Analyzer")
st.caption(
"Price-action tool that classifies closed bars, filters fake bars, and tracks trend persistence using only real bars."
)
st.caption("If you refresh, a brief placeholder screen may appear while data and UI load.")
query_params = st.query_params
now_epoch = time.time()
active_profile = st.session_state.get("active_profile")
last_active = st.session_state.get("profile_last_active_at")
remember_requested = is_truthy_flag(first_query_param_value(query_params, "remember"))
session_expired = False
if active_profile and is_profile_session_expired(last_active, now_epoch):
if remember_requested and not profile_requires_pin(str(active_profile)):
st.session_state["profile_last_active_at"] = now_epoch
else:
session_expired = True
st.session_state.pop("active_profile", None)
st.session_state.pop("profile_last_active_at", None)
active_profile = None
if "profile" in query_params:
del query_params["profile"]
if not active_profile:
remembered_profile = first_query_param_value(query_params, "profile")
if remember_requested and remembered_profile:
matched_profile = find_existing_profile_id(remembered_profile, set(list_web_profiles()))
if matched_profile and not profile_requires_pin(matched_profile):
active_profile = matched_profile
st.session_state["active_profile"] = matched_profile
st.session_state["profile_last_active_at"] = now_epoch
mark_profile_login(matched_profile, now_epoch=int(now_epoch))
if not active_profile:
render_profile_login(now_epoch=now_epoch, query_params=query_params, session_expired=session_expired)
st.stop()
active_profile = normalize_profile_id(active_profile)
previous_session_profile = st.session_state.get("session_profile_id")
if previous_session_profile != active_profile or "session_started_at" not in st.session_state:
st.session_state["session_started_at"] = now_epoch
st.session_state["session_profile_id"] = active_profile
st.session_state["active_profile"] = active_profile
st.session_state["profile_last_active_at"] = now_epoch
sidebar_settings = normalize_web_settings(
render_sidebar(active_profile=active_profile, query_params=query_params)
)
try:
save_web_settings(sidebar_settings, profile_id=active_profile)
except Exception:
pass
symbol = str(sidebar_settings["symbol"])
interval = str(sidebar_settings["interval"])
period = str(sidebar_settings["period"])
max_bars = int(sidebar_settings["max_bars"])
display_timezone = str(sidebar_settings.get("display_timezone", "America/Chicago"))
use_24h_time = bool(sidebar_settings.get("use_24h_time", False))
if not symbol:
st.error("Please enter a symbol.")
st.stop()
symbol_identity = resolve_symbol_identity(symbol)
identity_name = symbol_identity["name"]
identity_exchange = symbol_identity["exchange"]
if identity_name:
st.markdown(f"### {symbol} - {identity_name}")
if identity_exchange:
st.caption(f"Exchange: {identity_exchange}")
else:
st.markdown(f"### {symbol}")
try:
raw = fetch_ohlc(symbol=symbol, interval=interval, period=period)
raw = maybe_drop_live_bar(raw, interval=interval, enabled=bool(sidebar_settings["drop_live"]))
if len(raw) > max_bars:
raw = raw.iloc[-max_bars:].copy()
if len(raw) < 3:
st.error("Not enough bars to classify. Increase period or use a broader timeframe.")
st.stop()
classified = classify_bars(
raw,
use_body_range=bool(sidebar_settings["use_body_range"]),
volume_filter_enabled=bool(sidebar_settings["volume_filter_enabled"]),
volume_sma_window=int(sidebar_settings["volume_sma_window"]),
volume_multiplier=float(sidebar_settings["volume_multiplier"]),
)
analyzed, events = detect_trends(classified)
except Exception as exc:
st.error(f"Data error: {exc}")
st.stop()
replay_mode_enabled = bool(sidebar_settings.get("replay_mode_enabled", False))
replay_bars = int(sidebar_settings.get("replay_bars", 120))
if replay_mode_enabled:
replay_len = max(3, min(len(analyzed), replay_bars))
analyzed_view = analyzed.iloc[:replay_len].copy()
events_view = [e for e in events if e.timestamp <= analyzed_view.index[-1]]
st.caption(f"Replay mode active: showing first {replay_len} bars only.")
else:
analyzed_view = analyzed
events_view = events
regime_label = detect_regime(analyzed_view)
quality = compute_signal_quality(
analyzed_view,
trend_now=str(analyzed_view.iloc[-1]["trend_state"]),
volume_filter_enabled=bool(sidebar_settings["volume_filter_enabled"]),
)
if bool(sidebar_settings["enable_regime_filter"]) and regime_label == "Choppy":
st.warning("Regime filter active: market is choppy. Directional bias is suppressed.")
latest = analyzed_view.iloc[-1]
trend_now = str(latest["trend_state"])
if bool(sidebar_settings["enable_regime_filter"]) and regime_label == "Choppy":
trend_now = TREND_NEUTRAL
c1, c2, c3, c4 = st.columns(4)
c1.metric("Current Trend", trend_now)
c2.metric("Real Bullish Bars", int((analyzed_view["classification"] == "real_bull").sum()))
c3.metric("Real Bearish Bars", int((analyzed_view["classification"] == "real_bear").sum()))
c4.metric("Fake Bars", int((analyzed_view["classification"] == "fake").sum()))
previous_trend = str(analyzed_view.iloc[-2]["trend_state"]) if len(analyzed_view) > 1 else TREND_NEUTRAL
latest_classification = str(latest["classification"])
live_guide = build_live_decision_guide(trend_now, previous_trend, latest_classification)
q1, q2, q3 = st.columns(3)
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:
st.caption("Advanced panels paused to keep UI responsive. Click 'Run advanced panels now' in sidebar.")
if bool(sidebar_settings["enable_multi_tf_confirmation"]) and run_advanced_panels:
mtf = multi_timeframe_confirmation(
symbol=symbol,
period=period,
use_body_range=bool(sidebar_settings["use_body_range"]),
volume_filter_enabled=bool(sidebar_settings["volume_filter_enabled"]),
volume_sma_window=int(sidebar_settings["volume_sma_window"]),
volume_multiplier=float(sidebar_settings["volume_multiplier"]),
)
st.subheader("Multi-Timeframe Confirmation")
st.caption(f"Consensus: {mtf['consensus']} | Agreement: {mtf['agreement_pct']}%")
st.dataframe(pd.DataFrame(mtf["states"]), use_container_width=True)
if bool(sidebar_settings["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"{active_profile}-{symbol}-{interval}-{period}"
newest_event = events_view[-1].event if events_view else ""
previous_event = st.session_state.get(f"last_event-{alert_key}", "")
if newest_event and newest_event != previous_event:
st.warning(f"Alert: {newest_event}")
st.session_state[f"last_event-{alert_key}"] = newest_event
if bool(sidebar_settings["enable_alert_rules"]):
should_alert = (
(bool(sidebar_settings["alert_on_bull"]) and "Bullish" in newest_event)
or (bool(sidebar_settings["alert_on_bear"]) and "Bearish" in newest_event)
)
if should_alert:
payload = {
"symbol": symbol,
"interval": interval,
"event": newest_event,
"trend_now": trend_now,
"timestamp": str(analyzed_view.index[-1]),
}
ok, detail = send_webhook_alert(str(sidebar_settings["alert_webhook_url"]), payload)
if str(sidebar_settings["alert_webhook_url"]).strip():
st.caption(f"Webhook alert status: {'sent' if ok else 'failed'} ({detail})")
example_trades = simulate_trend_trades(analyzed_view, max_examples=int(sidebar_settings["max_training_examples"]))
selected_trade = render_training_panel(
show_past_behavior=bool(sidebar_settings["show_past_behavior"]),
example_trades=example_trades,
alert_key=alert_key,
display_timezone=display_timezone,
use_24h_time=use_24h_time,
)
fig = build_figure(
analyzed_view,
gray_fake=bool(sidebar_settings["gray_fake"]),
interval=interval,
hide_market_closed_gaps=bool(sidebar_settings["hide_market_closed_gaps"]),
display_timezone=display_timezone,
use_24h_time=use_24h_time,
)
if bool(sidebar_settings["show_trade_markers"]):
add_example_trade_markers(fig, example_trades)
highlight_selected_trade(
fig,
analyzed_view,
selected_trade,
focus_chart_on_selected_example=bool(sidebar_settings["focus_chart_on_selected_example"]),
)
st.plotly_chart(fig, use_container_width=True)
try:
bt = backtest_signals(
analyzed_view,
slippage_bps=float(sidebar_settings["backtest_slippage_bps"]),
fee_bps=float(sidebar_settings["backtest_fee_bps"]),
stop_loss_pct=(
float(sidebar_settings["backtest_stop_loss_pct"])
if float(sidebar_settings["backtest_stop_loss_pct"]) > 0
else None
),
take_profit_pct=(
float(sidebar_settings["backtest_take_profit_pct"])
if float(sidebar_settings["backtest_take_profit_pct"]) > 0
else None
),
min_hold_bars=int(sidebar_settings["backtest_min_hold_bars"]),
max_hold_bars=int(sidebar_settings["backtest_max_hold_bars"]),
)
except Exception as exc:
st.error(f"Backtest error: {exc}")
bt = {"trades": 0, "wins": 0, "losses": 0, "win_rate": 0.0, "avg_pnl_pct": 0.0}
st.subheader("Backtest Snapshot")
b1, b2, b3, b4, b5 = st.columns(5)
b1.metric("Signals", int(bt["trades"]))
b2.metric("Wins", int(bt["wins"]))
b3.metric("Losses", int(bt["losses"]))
b4.metric("Win Rate", f"{bt['win_rate']}%")
b5.metric("Avg P/L", f"{bt['avg_pnl_pct']}%")
st.caption("Method: trend-change signal, scored by next-bar direction. Educational only; not a trading recommendation.")
st.subheader("Session Stats")
session_stats = compute_session_stats(analyzed_view, session_started_at=float(st.session_state["session_started_at"]))
s1, s2, s3, s4 = st.columns(4)
s1.metric("Since Login Wins", int(session_stats["wins"]))
s2.metric("Since Login Losses", int(session_stats["losses"]))
s3.metric("Since Login Avg Move", f"{session_stats['avg_move_pct']}%")
s4.metric("Since Login Fake Rate", f"{session_stats['fake_ratio_pct']}%")
if bool(sidebar_settings["enable_compare_symbols"]) and sidebar_settings["compare_symbols"] and run_advanced_panels:
st.subheader("Compare Symbols")
compare_df = compare_symbols(
symbols=list(sidebar_settings["compare_symbols"]),
interval=interval,
period=period,
use_body_range=bool(sidebar_settings["use_body_range"]),
volume_filter_enabled=bool(sidebar_settings["volume_filter_enabled"]),
volume_sma_window=int(sidebar_settings["volume_sma_window"]),
volume_multiplier=float(sidebar_settings["volume_multiplier"]),
)
st.dataframe(compare_df, use_container_width=True)
st.subheader("Trend Events")
if events_view:
event_df = pd.DataFrame(
{
"timestamp": [
format_timestamp(e.timestamp, display_timezone=display_timezone, use_24h_time=use_24h_time)
for e in events_view[-25:]
][::-1],
"event": [e.event for e in events_view[-25:]][::-1],
"trend_after": [e.trend_after for e in events_view[-25:]][::-1],
}
)
st.dataframe(event_df, use_container_width=True)
else:
st.info("No trend start/reversal events detected in the selected data window.")
st.subheader("Export")
export_df = df_for_export(analyzed_view)
csv_bytes = export_df.to_csv(index=False).encode("utf-8")
st.download_button(
"Download classified data (CSV)",
data=csv_bytes,
file_name=f"{symbol}_{interval}_classified.csv",
mime="text/csv",
)
try:
pdf_bytes = fig.to_image(format="pdf")
st.download_button(
"Download chart (PDF)",
data=pdf_bytes,
file_name=f"{symbol}_{interval}_chart.pdf",
mime="application/pdf",
)
except Exception:
st.caption("PDF export unavailable. Install `kaleido` and rerun to enable chart PDF downloads.")
with st.expander("Latest classified bars"):
st.dataframe(export_df.tail(30), use_container_width=True)
if __name__ == "__main__":
main()