from __future__ import annotations from typing import Any import streamlit as st from streamlit_autorefresh import st_autorefresh from web_core.constants import INTERVAL_OPTIONS, PERIOD_OPTIONS from web_core.ui.help_content import help_dialog from web_core.market.presets import MARKET_PRESET_OPTIONS, apply_market_preset from web_core.market.symbols import lookup_symbol_candidates from web_core.time_display import US_TIMEZONE_OPTIONS from web_core.auth.profile_store import ( first_query_param_value, format_epoch, get_profile_audit, list_web_profiles, load_web_settings, normalize_web_settings, ) def _parse_watchlist(raw: str) -> list[str]: seen: set[str] = set() symbols: list[str] = [] normalized_raw = str(raw or "").replace("\\n", "\n").replace("\\N", "\n").replace(",", "\n") for token in normalized_raw.splitlines(): normalized = token.strip().upper() if not normalized or normalized in seen: continue seen.add(normalized) symbols.append(normalized) return symbols[:40] def render_sidebar(active_profile: str, query_params: Any) -> dict[str, Any]: with st.sidebar: persisted_settings = load_web_settings(profile_id=active_profile) query_overrides: dict[str, Any] = {} for key in persisted_settings: candidate = first_query_param_value(query_params, key) if candidate is not None: query_overrides[key] = candidate effective_defaults = normalize_web_settings({**persisted_settings, **query_overrides}) display_timezone = st.selectbox( "Display Timezone (US)", US_TIMEZONE_OPTIONS, index=US_TIMEZONE_OPTIONS.index(str(effective_defaults["display_timezone"])), help="Controls how timestamps are shown in the sidebar and training/event tables.", ) use_24h_time = st.checkbox( "Use 24-hour time", value=bool(effective_defaults["use_24h_time"]), help="On: 13:00 format. Off: 1:00 PM format.", ) st.header("Profile") st.success(f"Logged in as: {active_profile}") if st.button("Switch profile", use_container_width=True): st.session_state.pop("active_profile", None) if "profile" in query_params: del query_params["profile"] st.rerun() st.divider() available_profiles = list_web_profiles() profile_audit = get_profile_audit(active_profile) st.caption( "Profiles found: " + ", ".join(available_profiles + ([active_profile] if active_profile not in available_profiles else [])) ) st.caption( "Audit: created " f"{format_epoch(profile_audit.get('created_at'), display_timezone=display_timezone, use_24h_time=use_24h_time)}, " f"last login {format_epoch(profile_audit.get('last_login_at'), display_timezone=display_timezone, use_24h_time=use_24h_time)}, " f"updated {format_epoch(profile_audit.get('updated_at'), display_timezone=display_timezone, use_24h_time=use_24h_time)}, " f"last symbol {str(profile_audit.get('last_symbol') or 'n/a')}" ) st.divider() st.header("Data Settings") if st.button("Help / Quick Start", use_container_width=True): help_dialog() st.divider() st.subheader("Market Preset") preset_default = str(effective_defaults["market_preset"]) preset_index = MARKET_PRESET_OPTIONS.index(preset_default) if preset_default in MARKET_PRESET_OPTIONS else 0 market_preset = st.selectbox( "Preset", MARKET_PRESET_OPTIONS, index=preset_index, help="Select a template, then click Apply Preset to populate defaults.", ) if market_preset != "Custom" and st.button("Apply Preset", use_container_width=True): effective_defaults = apply_market_preset(effective_defaults, market_preset) st.caption(f"Preset applied: {market_preset}") st.subheader("Watchlist") watchlist_raw = st.text_input( "Watchlist symbols (comma separated)", value=", ".join(effective_defaults["watchlist"]), help="Saved per profile. Example: AAPL, MSFT, NVDA, BTC-USD", ) watchlist = _parse_watchlist(watchlist_raw) selected_from_watchlist: str | None = None if watchlist: st.caption("Quick select:") for idx, watch_symbol in enumerate(watchlist[:12]): if st.button(watch_symbol, use_container_width=True, key=f"watchlist-{watch_symbol}-{idx}"): selected_from_watchlist = watch_symbol st.session_state["watchlist_symbol_choice"] = watch_symbol selected_from_watchlist = st.session_state.get("watchlist_symbol_choice", selected_from_watchlist) st.subheader("Find Symbol") symbol_search_query = st.text_input( "Search by company or ticker", value="", placeholder="Apple, Tesla, Bitcoin...", help="Type a name (e.g. Apple) and select a result to prefill Symbol.", ) symbol_from_search: str | None = None if symbol_search_query.strip(): candidates = lookup_symbol_candidates(symbol_search_query) if candidates: result_placeholder = "Select a result..." labels = [result_placeholder] + [ " | ".join( [ candidate["symbol"], candidate["name"] or "No name", candidate["exchange"] or "Unknown exchange", ] ) for candidate in candidates ] selected_label = st.selectbox("Matches", labels, index=0) if selected_label != result_placeholder: selected_index = labels.index(selected_label) - 1 symbol_from_search = candidates[selected_index]["symbol"] else: st.caption("No matches found. Try another company name.") symbol = st.text_input( "Symbol", value=symbol_from_search or selected_from_watchlist or str(effective_defaults["symbol"]), help="Ticker or pair to analyze, e.g. AAPL, MSFT, BTC-USD.", ).strip().upper() interval = st.selectbox( "Timeframe", INTERVAL_OPTIONS, index=INTERVAL_OPTIONS.index(str(effective_defaults["interval"])), help="Bar size for each candle. Shorter intervals are noisier; 1d is a good default.", ) period = st.selectbox( "Period", PERIOD_OPTIONS, index=PERIOD_OPTIONS.index(str(effective_defaults["period"])), help="How much history to load for trend analysis.", ) max_bars = st.number_input( "Max bars", min_value=20, max_value=5000, value=int(effective_defaults["max_bars"]), step=10, help="Limits loaded candles to keep charting responsive. 500 is a solid starting point.", ) drop_live = st.checkbox("Ignore potentially live last bar", value=bool(effective_defaults["drop_live"])) with st.expander("Classification Filters", expanded=False): use_body_range = st.checkbox( "Use previous body range (ignore wicks)", value=bool(effective_defaults["use_body_range"]), ) volume_filter_enabled = st.checkbox( "Enable volume filter", value=bool(effective_defaults["volume_filter_enabled"]), ) volume_sma_window = st.slider("Volume SMA window", 2, 100, int(effective_defaults["volume_sma_window"])) volume_multiplier = st.slider( "Min volume / SMA multiplier", 0.1, 3.0, float(effective_defaults["volume_multiplier"]), 0.1, ) gray_fake = st.checkbox("Gray out fake bars", value=bool(effective_defaults["gray_fake"])) hide_market_closed_gaps = st.checkbox( "Hide market-closed gaps (stocks)", value=bool(effective_defaults["hide_market_closed_gaps"]), ) with st.expander("Training & Guidance", expanded=False): 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, ) replay_mode_enabled = st.checkbox( "Replay mode (hide future bars)", value=bool(effective_defaults["replay_mode_enabled"]), ) replay_bars = st.slider( "Replay bars shown", 20, 1000, int(effective_defaults["replay_bars"]), 5, ) with st.expander("Monitoring", expanded=False): 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) if enable_auto_refresh: st_autorefresh(interval=refresh_sec * 1000, key="data_refresh") with st.expander("Advanced Signals", expanded=False): advanced_auto_run = st.checkbox( "Auto-run advanced panels (slower)", value=bool(effective_defaults["advanced_auto_run"]), ) run_advanced_now = st.button("Run advanced panels now", use_container_width=True) enable_multi_tf_confirmation = st.checkbox( "Show multi-timeframe confirmation (1h/4h/1d)", value=bool(effective_defaults["enable_multi_tf_confirmation"]), ) enable_regime_filter = st.checkbox( "Regime filter (stand aside in choppy periods)", value=bool(effective_defaults["enable_regime_filter"]), ) with st.expander("Compare Symbols", expanded=False): enable_compare_symbols = st.checkbox( "Enable compare symbols panel", value=bool(effective_defaults["enable_compare_symbols"]), ) compare_symbols_raw = st.text_input( "Compare symbols (comma separated)", value=", ".join(effective_defaults["compare_symbols"]), help="Example: AAPL, MSFT, NVDA", ) compare_symbols = _parse_watchlist(compare_symbols_raw)[:12] with st.expander("Alerts", expanded=False): enable_alert_rules = st.checkbox( "Enable alert rules", value=bool(effective_defaults["enable_alert_rules"]), ) alert_on_bull = st.checkbox("Alert on bullish confirmations", value=bool(effective_defaults["alert_on_bull"])) alert_on_bear = st.checkbox("Alert on bearish confirmations", value=bool(effective_defaults["alert_on_bear"])) alert_webhook_url = st.text_input( "Webhook URL (optional)", value=str(effective_defaults["alert_webhook_url"]), help="Use Zapier/Make/Telegram bot webhook for push delivery.", ) with st.expander("Backtest Controls", expanded=False): backtest_slippage_bps = st.slider( "Slippage (bps per side)", 0.0, 100.0, float(effective_defaults["backtest_slippage_bps"]), 0.5, ) backtest_fee_bps = st.slider( "Fee (bps per side)", 0.0, 100.0, float(effective_defaults["backtest_fee_bps"]), 0.5, ) backtest_stop_loss_pct = st.slider( "Stop loss (%)", 0.0, 25.0, float(effective_defaults["backtest_stop_loss_pct"]), 0.25, ) backtest_take_profit_pct = st.slider( "Take profit (%)", 0.0, 25.0, float(effective_defaults["backtest_take_profit_pct"]), 0.25, ) backtest_min_hold_bars = st.slider( "Min hold bars", 1, 20, int(effective_defaults["backtest_min_hold_bars"]), 1, ) backtest_max_hold_bars = st.slider( "Max hold bars", backtest_min_hold_bars, 40, int(max(effective_defaults["backtest_max_hold_bars"], backtest_min_hold_bars)), 1, ) return { "symbol": symbol, "interval": interval, "period": period, "max_bars": int(max_bars), "drop_live": bool(drop_live), "use_body_range": bool(use_body_range), "volume_filter_enabled": bool(volume_filter_enabled), "volume_sma_window": int(volume_sma_window), "volume_multiplier": float(volume_multiplier), "gray_fake": bool(gray_fake), "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), "refresh_sec": int(refresh_sec), "watchlist": watchlist, "market_preset": market_preset, "compare_symbols": compare_symbols, "enable_compare_symbols": bool(enable_compare_symbols), "enable_multi_tf_confirmation": bool(enable_multi_tf_confirmation), "advanced_auto_run": bool(advanced_auto_run), "run_advanced_now": bool(run_advanced_now), "enable_regime_filter": bool(enable_regime_filter), "replay_mode_enabled": bool(replay_mode_enabled), "replay_bars": int(replay_bars), "enable_alert_rules": bool(enable_alert_rules), "alert_on_bull": bool(alert_on_bull), "alert_on_bear": bool(alert_on_bear), "alert_webhook_url": str(alert_webhook_url).strip(), "backtest_slippage_bps": float(backtest_slippage_bps), "backtest_fee_bps": float(backtest_fee_bps), "backtest_stop_loss_pct": float(backtest_stop_loss_pct), "backtest_take_profit_pct": float(backtest_take_profit_pct), "backtest_min_hold_bars": int(backtest_min_hold_bars), "backtest_max_hold_bars": int(backtest_max_hold_bars), "display_timezone": str(display_timezone), "use_24h_time": bool(use_24h_time), }