365 lines
16 KiB
Python
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),
|
|
}
|