maneshtrader/web_core/ui/sidebar_ui.py

365 lines
16 KiB
Python

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),
}