Download app.py from 3VVM/Lk: direct link, hf CLI and curl.
- Browser
- Download file 27.7 kB
-
https://huggingface.co/spaces/3VVM/Lk/resolve/main/app.py
- Command line
-
hf download hf://spaces/3VVM/Lk/app.py
-
curl -L -o app.py https://huggingface.co/spaces/3VVM/Lk/resolve/main/app.py
27.7 kB
| """ | |
| Gradio UI for Hugging Face Spaces (spec sections 49-53). Wires together | |
| data_layer -> features -> moirai_model -> decision_engine -> backtest_engine. | |
| Errors are surfaced with their real cause (section 98), never hidden | |
| behind a generic "something went wrong". | |
| """ | |
| from __future__ import annotations | |
| import traceback | |
| import gradio as gr | |
| import pandas as pd | |
| import config as cfg | |
| import walk_forward as wf | |
| from arima_model import AutoArimaForecaster | |
| from backtest_engine import InsufficientDataError, WalkForwardBacktester | |
| from chronos_model import Chronos2Forecaster | |
| from data_layer import ( | |
| DataSourceError, causal_resample, data_freshness, fetch_ohlcv, | |
| resolve_history_window, validate_ohlcv, | |
| ) | |
| from decision_engine import decide | |
| from features import compute_indicators, warm_up_valid_from | |
| from forecast_types import ModelInferenceError, ModelInputError, ModelLoadError | |
| from metrics import trading_summary | |
| from moirai_model import MoiraiMoEForecaster | |
| # Each model is fully independent (own module, own class, own | |
| # load()/predict()) -- this dict is the ONLY place they're gathered | |
| # together, and only so the UI's model picker can look one up by key. | |
| # Nothing here blends or falls back between them; a request uses | |
| # exactly one entry, chosen by the person, for its entire lifetime. | |
| _forecasters = { | |
| "moirai": MoiraiMoEForecaster(), | |
| "autoarima": AutoArimaForecaster(), | |
| "chronos2": Chronos2Forecaster(), | |
| } | |
| _model_status = {key: {"loaded": False, "error": None} for key in _forecasters} | |
| _MODEL_LABEL_TO_KEY = {v["display_name"]: k for k, v in cfg.AVAILABLE_MODELS.items()} | |
| _MODEL_CHOICES = [cfg.AVAILABLE_MODELS[k]["display_name"] for k in _forecasters] | |
| _DEFAULT_MODEL_LABEL = cfg.AVAILABLE_MODELS[cfg.DEFAULT_MODEL_KEY]["display_name"] | |
| # The one piece of cross-tab state this app keeps: the most recently | |
| # ACCEPTED walk-forward result (see run_walk_forward_validation below). | |
| # Live Forecast and Backtest both read this (never write it) when their | |
| # "use frozen calibration" checkbox is on -- this is the concrete | |
| # mechanism that makes them share one decision pipeline (spec item 15) | |
| # instead of maintaining calibration state independently. A REJECTED | |
| # validation run clears this rather than leaving a stale accepted | |
| # result in place from a previous, unrelated symbol/timeframe. Keyed by | |
| # model too, since a calibration fit for one model's forecasts has no | |
| # meaning applied to a different model's. | |
| _frozen_wf = {"calibrator": None, "dc": None, "model_key": None, | |
| "symbol": None, "timeframe": None, "asset_class": None} | |
| def _ensure_model(model_key: str) -> bool: | |
| status = _model_status[model_key] | |
| if status["loaded"]: | |
| return True | |
| try: | |
| _forecasters[model_key].load() | |
| status["loaded"] = True | |
| return True | |
| except ModelLoadError as e: | |
| status["error"] = str(e) | |
| return False | |
| def _get_ohlcv(symbol: str, requested_tf: str, history_label: str): | |
| """Returns (df, native_or_derived, source_tf, validation_report).""" | |
| kind, value = resolve_history_window(history_label) | |
| period = value if kind == "period" else None | |
| start = value if kind == "start" else None | |
| if requested_tf in cfg.DERIVED_MAP: | |
| source_tf, _ = cfg.DERIVED_MAP[requested_tf] | |
| raw = fetch_ohlcv(symbol, source_tf, start=start, period=period) | |
| clean, report = validate_ohlcv(raw) | |
| derived = causal_resample(clean, source_tf, requested_tf) | |
| return derived, "derived", source_tf, report | |
| raw = fetch_ohlcv(symbol, requested_tf, start=start, period=period) | |
| clean, report = validate_ohlcv(raw) | |
| return clean, "native", requested_tf, report | |
| def run_live_forecast(symbol, timeframe, horizon, history_label, model_label=_DEFAULT_MODEL_LABEL, use_frozen_wf=False): | |
| model_key = _MODEL_LABEL_TO_KEY[model_label] | |
| forecaster = _forecasters[model_key] | |
| if not _ensure_model(model_key): | |
| return f"**MODEL ERROR ({model_label})**\n\n{_model_status[model_key]['error']}", None, None, None | |
| try: | |
| df, kind, source_tf, val_report = _get_ohlcv(symbol, timeframe, history_label) | |
| except DataSourceError as e: | |
| return f"**SOURCE UNAVAILABLE**\n\n{e}", None, None, None | |
| except Exception as e: | |
| return f"**DATA ERROR**\n\n{e}", None, None, None | |
| feat = compute_indicators(df) | |
| try: | |
| warm = warm_up_valid_from(feat) | |
| except ValueError as e: | |
| return f"**INSUFFICIENT HISTORY**\n\n{e}", None, None, None | |
| feat = feat.iloc[warm:] | |
| fresh = data_freshness(feat.index[-1], timeframe) | |
| try: | |
| forecast = forecaster.predict( | |
| context_df=feat, target_col=cfg.TARGET_FEATURE, | |
| past_covariate_cols=[c for c in cfg.PAST_DYNAMIC_FEATURES if c in feat.columns], | |
| prediction_length=int(horizon), freq=cfg.TIMEFRAME_TO_PANDAS_FREQ[timeframe], | |
| ) | |
| except (ModelInputError, ModelInferenceError) as e: | |
| return f"**MODEL ERROR ({model_label})**\n\n{e}", df.tail(200), val_report.__dict__, fresh | |
| price_now = float(feat["close"].iloc[-1]) | |
| # Unlike the Backtest tab (which has an explicit asset-class radio), | |
| # the Live tab only has a symbol -- infer forex vs. crypto from it so | |
| # decide() gets a real, asset-appropriate cost estimate instead of | |
| # silently falling back to DecisionConfig's flat generic default | |
| # (previously every Live symbol used the same cost regardless of | |
| # whether it was forex or crypto, cf. WalkForwardBacktester which | |
| # always has a real cost_estimate_frac -- see config.round_trip_cost_frac). | |
| asset_class = cfg.infer_asset_class(symbol) | |
| cost_frac = cfg.round_trip_cost_frac(cfg.COST_PROFILES[asset_class]) | |
| calibrator, dc, calib_note = None, None, "raw (uncalibrated) probabilities" | |
| if use_frozen_wf: | |
| matches = (_frozen_wf["calibrator"] is not None and _frozen_wf["model_key"] == model_key | |
| and _frozen_wf["symbol"] == symbol and _frozen_wf["timeframe"] == timeframe | |
| and _frozen_wf["asset_class"] == asset_class) | |
| if not matches: | |
| return ( | |
| f"**NO FROZEN CALIBRATION AVAILABLE FOR {model_label} on {symbol} @ {timeframe} ({asset_class})**\n\n" | |
| f"Run Walk-Forward Validation for this exact model/symbol/timeframe/asset-class " | |
| f"combination first, and it must reach ACCEPTED β a rejected or not-yet-run " | |
| f"validation leaves nothing here to reuse, on purpose (spec: no forced trading " | |
| f"on unproven configs).", | |
| None, None, None, | |
| ) | |
| calibrator, dc = _frozen_wf["calibrator"], _frozen_wf["dc"] | |
| calib_note = f"frozen walk-forward calibration (min_probability={dc.min_probability}, buy_threshold={dc.buy_threshold:.4%})" | |
| decision = decide(forecast, price_now, dc=dc, cost_estimate_frac=cost_frac, calibrator=calibrator) | |
| duration_min = cfg.TIMEFRAME_MINUTES[timeframe] * int(horizon) | |
| summary = ( | |
| f"### {symbol} @ {timeframe} _( {kind} from {source_tf} )_\n\n" | |
| f"**Model:** {model_label}\n\n" | |
| f"**Data:** {fresh['status'].upper()} β last bar `{fresh['latest_market_ts']}`\n\n" | |
| f"**Horizon:** {horizon} bars β {duration_min} min\n\n" | |
| f"**Cost profile:** {asset_class} (inferred from symbol) β round-trip β {cost_frac:.3%}\n\n" | |
| f"**Probability basis:** {calib_note}\n\n" | |
| f"**Last close:** {price_now:.5f}\n\n" | |
| f"**Forecast return (P50):** {decision.expected_return:+.4%} " | |
| f" [P10 {decision.p10_return:+.4%}, P90 {decision.p90_return:+.4%}]\n\n" | |
| f"### Decision: {decision.action}\n\n{decision.rationale}\n\n" | |
| + (f"_HOLD reason: {decision.hold_reason}_\n\n" if decision.hold_reason else "") | |
| + f"_Forecasts are probabilistic estimates, not guaranteed future prices. " | |
| f"Not financial advice._" | |
| ) | |
| return summary, df.tail(200), val_report.__dict__, fresh | |
| def run_backtest(symbol, timeframe, horizon, history_label, asset_class, context_length=256, | |
| stop_loss_pct=0.0, take_profit_pct=0.0, model_label=_DEFAULT_MODEL_LABEL, use_frozen_wf=False): | |
| model_key = _MODEL_LABEL_TO_KEY[model_label] | |
| forecaster = _forecasters[model_key] | |
| if not _ensure_model(model_key): | |
| return f"**MODEL ERROR ({model_label})**\n\n{_model_status[model_key]['error']}", None, None | |
| try: | |
| df, kind, source_tf, val_report = _get_ohlcv(symbol, timeframe, history_label) | |
| except DataSourceError as e: | |
| return f"**SOURCE UNAVAILABLE**\n\n{e}", None, None | |
| feat = compute_indicators(df) | |
| # 0 on either slider means "disabled" (a UI sentinel, not a real 0% | |
| # stop) -- keeps the default identical to every pre-existing backtest | |
| # unless someone explicitly opens Advanced and picks a value. | |
| risk = cfg.RiskManagement( | |
| stop_loss_frac=(stop_loss_pct / 100.0) or None, | |
| take_profit_frac=(take_profit_pct / 100.0) or None, | |
| ) | |
| calibrator, decision_cfg = None, None | |
| if use_frozen_wf: | |
| matches = (_frozen_wf["calibrator"] is not None and _frozen_wf["model_key"] == model_key | |
| and _frozen_wf["symbol"] == symbol and _frozen_wf["timeframe"] == timeframe | |
| and _frozen_wf["asset_class"] == asset_class) | |
| if not matches: | |
| return ( | |
| f"**NO FROZEN CALIBRATION AVAILABLE FOR {model_label} on {symbol} @ {timeframe} ({asset_class})**\n\n" | |
| f"Run Walk-Forward Validation for this exact model/symbol/timeframe/asset-class " | |
| f"combination first, and it must reach ACCEPTED.", None, None, | |
| ) | |
| calibrator, decision_cfg = _frozen_wf["calibrator"], _frozen_wf["dc"] | |
| bt = WalkForwardBacktester(forecaster, timeframe=timeframe, asset_class=asset_class, | |
| horizon=int(horizon), context_length=int(context_length), risk=risk, | |
| decision_cfg=decision_cfg, calibrator=calibrator) | |
| try: | |
| result = bt.run(feat) | |
| except (ValueError, InsufficientDataError) as e: | |
| # warm_up_valid_from (ValueError) or the context-length/origin check | |
| # (InsufficientDataError) -- both mean "your History window / | |
| # context length don't have enough bars for this configuration", | |
| # a real and common condition, not an unexpected code bug. The | |
| # message is already specific and actionable (bars available vs. | |
| # needed), so show it cleanly instead of a scary raw traceback -- | |
| # matching how the Live tab already handles the same ValueError | |
| # from warm_up_valid_from above. | |
| return f"**INSUFFICIENT HISTORY**\n\n{e}", None, None | |
| except Exception as e: | |
| return f"**BACKTEST ERROR**\n\n{e}\n\n```\n{traceback.format_exc(limit=3)}\n```", None, None | |
| trades_df = pd.DataFrame([t.__dict__ for t in result["trades"]]) | |
| summary = trading_summary(trades_df, result["equity_curve"], cfg.TIMEFRAME_MINUTES[timeframe]) | |
| diag = result["diagnostics"] | |
| n = result["num_origins"] | |
| lines = [ | |
| f"### {symbol} @ {timeframe} ({kind} from {source_tf}) β walk-forward backtest", | |
| f"Model: **{model_label}**", | |
| f"Origins evaluated: {n} | Model errors: {len(result['errors'])} | " | |
| f"Probabilities: {'frozen walk-forward calibration' if calibrator else 'raw (uncalibrated)'}", | |
| "", | |
| ] | |
| for k, v in summary.items(): | |
| lines.append(f"- **{k}**: {v}") | |
| # UI -> app -> engine propagation, stated explicitly rather than | |
| # assumed (audit item 17/20): requested is exactly what this | |
| # function received as its stop_loss_pct/take_profit_pct arguments; | |
| # actual is read back from the RiskManagement object the | |
| # WalkForwardBacktester instance actually used, so any conversion | |
| # bug between the two would show up here as a mismatch. | |
| req = result["requested_risk"] | |
| req_sl_pct, req_tp_pct = stop_loss_pct, take_profit_pct | |
| actual_sl = "off" if req["stop_loss_frac"] is None else f"{req['stop_loss_frac']*100:.2f}%" | |
| actual_tp = "off" if req["take_profit_frac"] is None else f"{req['take_profit_frac']*100:.2f}%" | |
| lines.append( | |
| f"- **risk params β requested**: SL={req_sl_pct:.2f}%, TP={req_tp_pct:.2f}% | " | |
| f"**backend received**: SL={actual_sl}, TP={actual_tp}" | |
| ) | |
| if trades_df.empty: | |
| exit_counts = {"horizon": 0, "stop_loss": 0, "take_profit": 0} | |
| else: | |
| exit_counts = trades_df["exit_reason"].value_counts().to_dict() | |
| lines.append( | |
| f"- **exit reasons**: horizon={exit_counts.get('horizon', 0)}, " | |
| f"stop_loss={exit_counts.get('stop_loss', 0)}, take_profit={exit_counts.get('take_profit', 0)}" | |
| ) | |
| diag = dict(diag) # copy before mutating -- result["diagnostics"] should stay pristine | |
| diag["num_origins"] = n | |
| diag["model_errors_detail_count"] = len(result["errors"]) | |
| if result["errors"]: | |
| # Every origin failing the same way means a real, systematic bug -- | |
| # showing it is far more useful than a bare count (section 98: | |
| # errors must say what failed, not just that something did). | |
| unique_msgs = {} | |
| for err in result["errors"]: | |
| unique_msgs.setdefault(err["error"], []).append(err["origin_ts"]) | |
| lines.append("\n### Model error detail (first 3 distinct messages)") | |
| for msg, origins in list(unique_msgs.items())[:3]: | |
| lines.append(f"- `{msg}` β seen at {len(origins)} origin(s), e.g. `{origins[0]}`") | |
| return "\n".join(lines), trades_df, diag | |
| def run_walk_forward_validation(symbol, timeframe, horizon, history_label, asset_class, | |
| context_length=256, model_label=_DEFAULT_MODEL_LABEL): | |
| """The one function in this app that CHOOSES calibration/threshold | |
| parameters -- everywhere else takes them as an already-frozen given | |
| (spec item 15/22). Splits chronologically, fits + selects on | |
| train/validation only, evaluates the frozen result on the test | |
| split exactly once, compares against two simple causal baselines | |
| through the identical cost/execution machinery, and returns an | |
| explicit ACCEPT/REJECT verdict. Only an ACCEPT updates the | |
| module-level _frozen_wf state Live Forecast and Backtest can opt | |
| into reusing -- a REJECT clears it rather than leaving a stale | |
| accepted result from a different, unrelated run in place.""" | |
| model_key = _MODEL_LABEL_TO_KEY[model_label] | |
| forecaster = _forecasters[model_key] | |
| if not _ensure_model(model_key): | |
| return f"**MODEL ERROR ({model_label})**\n\n{_model_status[model_key]['error']}", None | |
| try: | |
| df, kind, source_tf, val_report = _get_ohlcv(symbol, timeframe, history_label) | |
| except DataSourceError as e: | |
| return f"**SOURCE UNAVAILABLE**\n\n{e}", None | |
| feat = compute_indicators(df) | |
| wf_cfg = cfg.WalkForwardConfig() | |
| try: | |
| train_df, val_df, test_df = wf.chronological_split(feat, wf_cfg) | |
| except ValueError as e: | |
| return f"**INSUFFICIENT HISTORY FOR A TRAIN/VALIDATION/TEST SPLIT**\n\n{e}", None | |
| try: | |
| calibrator, frozen_dc, selection_report = wf.select_frozen_config( | |
| forecaster, train_df, val_df, timeframe=timeframe, asset_class=asset_class, | |
| horizon=int(horizon), context_length=int(context_length), wf=wf_cfg, | |
| ) | |
| except ValueError as e: | |
| _frozen_wf.update(calibrator=None, dc=None, model_key=None, symbol=None, timeframe=None, asset_class=None) | |
| return ( | |
| f"**NO ACCEPTABLE CONFIGURATION ON VALIDATION DATA ({model_label})**\n\n{e}\n\n" | |
| f"This is a legitimate outcome, not an error to work around: it means no tested " | |
| f"combination of confidence/threshold showed enough signal on validation data to be " | |
| f"worth evaluating further. **Verdict: NO TRADE.** Nothing was frozen for reuse.", | |
| None, | |
| ) | |
| bt = WalkForwardBacktester(forecaster, timeframe=timeframe, asset_class=asset_class, | |
| horizon=int(horizon), context_length=int(context_length), | |
| decision_cfg=frozen_dc, calibrator=calibrator) | |
| try: | |
| test_result = bt.run(test_df) | |
| except (ValueError, InsufficientDataError) as e: | |
| return f"**INSUFFICIENT TEST-WINDOW HISTORY**\n\n{e}", None | |
| test_trades_df = pd.DataFrame([t.__dict__ for t in test_result["trades"]]) | |
| test_summary = trading_summary(test_trades_df, test_result["equity_curve"], cfg.TIMEFRAME_MINUTES[timeframe]) \ | |
| if not test_trades_df.empty else {"num_trades": 0, "note": "no trades on the frozen test window"} | |
| baselines = {} | |
| for name, cls in [("zero_skill", wf.ZeroSkillForecaster), ("naive_momentum", wf.NaiveMomentumForecaster)]: | |
| baselines[name] = wf.run_baseline(cls, test_df, timeframe, asset_class, int(horizon), | |
| int(context_length), frozen_dc, calibrator) | |
| acceptance = wf.check_acceptance(test_summary, baselines, test_result["equity_curve"], wf_cfg) | |
| if acceptance["accepted"]: | |
| _frozen_wf.update(calibrator=calibrator, dc=frozen_dc, model_key=model_key, symbol=symbol, | |
| timeframe=timeframe, asset_class=asset_class) | |
| else: | |
| _frozen_wf.update(calibrator=None, dc=None, model_key=None, symbol=None, timeframe=None, asset_class=None) | |
| verdict = "β ACCEPTED" if acceptance["accepted"] else "β REJECTED β NO TRADE" | |
| lines = [ | |
| f"### Walk-forward validation β {model_label} on {symbol} @ {timeframe} ({asset_class})", | |
| f"Train: {len(train_df)} bars | Validation: {len(val_df)} bars | " | |
| f"Test (frozen, evaluated once): {len(test_df)} bars", | |
| f"Selected on validation only: `min_probability={selection_report['chosen_min_probability']}`, " | |
| f"`buy_threshold={selection_report['chosen_buy_threshold']:.4%}` " | |
| f"(from {len(selection_report['trials'])} validation-only grid trials, " | |
| f"{selection_report['n_calibration_pairs']} calibration pairs from the training window)", | |
| "", | |
| f"## Verdict: {verdict}", | |
| "", | |
| "**Test-window result (frozen config, evaluated exactly once):**", | |
| ] | |
| for k, v in test_summary.items(): | |
| lines.append(f"- **{k}**: {v}") | |
| lines.append("\n**Baseline comparison β same test window, same costs, same execution:**") | |
| for name, s in baselines.items(): | |
| lines.append(f"- **{name}**: net_pnl={s.get('net_pnl')}, num_trades={s.get('num_trades', 0)}") | |
| lines.append("\n**Acceptance checks:**") | |
| for k, v in acceptance["checks"].items(): | |
| lines.append(f"- {'β ' if v else 'β'} {k}") | |
| if acceptance["accepted"]: | |
| lines.append( | |
| "\n_Live Forecast and Backtest can now use this frozen calibration for this exact " | |
| "symbol/timeframe/asset-class via their 'Use frozen walk-forward calibration' checkbox._" | |
| ) | |
| else: | |
| lines.append( | |
| "\n_No credible edge survived unseen test data β the honest result is NO TRADE, not a " | |
| "forced strategy. Nothing was frozen for reuse; any previously-frozen result for this " | |
| "symbol/timeframe was cleared._" | |
| ) | |
| return "\n".join(lines), acceptance | |
| with gr.Blocks(title="Forex & Crypto Forecaster (Moirai-MoE / AutoARIMA / Chronos-2)") as demo: | |
| gr.Markdown( | |
| "# Forex & Crypto Forecasting\n" | |
| "Three independent, selectable models β never blended into one signal: " | |
| f"**Moirai-MoE** (`{cfg.MODEL_CHECKPOINT}`), **AutoARIMA** (classical, fit fresh per " | |
| f"forecast, no pretrained checkpoint), and **Chronos-2** (`amazon/chronos-2`). " | |
| "Pick one per tab; each goes through the identical decision/cost/backtest pipeline. " | |
| "Forecasts are probabilistic estimates, not guaranteed future prices β not financial advice." | |
| ) | |
| with gr.Tab("Live Forecast"): | |
| with gr.Row(): | |
| symbol_in = gr.Dropdown(choices=cfg.ALL_SYMBOLS, value="EURUSD=X", | |
| allow_custom_value=True, | |
| label="Symbol (pick one, or type any Yahoo ticker)") | |
| tf_in = gr.Dropdown(choices=cfg.ALL_TIMEFRAMES, value="1h", label="Timeframe") | |
| horizon_in = gr.Slider(1, 20, value=5, step=1, label="Horizon (bars)") | |
| period_in = gr.Dropdown(cfg.HISTORY_WINDOW_CHOICES, value="30 days", label="History window") | |
| model_in = gr.Radio(_MODEL_CHOICES, value=_DEFAULT_MODEL_LABEL, label="Model") | |
| wf_check_live = gr.Checkbox( | |
| value=False, | |
| label="Use frozen walk-forward calibration (run Walk-Forward Validation tab first β must ACCEPT, for this exact model)") | |
| run_btn = gr.Button("Run Forecast", variant="primary") | |
| out_md = gr.Markdown() | |
| out_table = gr.Dataframe(label="Recent data") | |
| with gr.Accordion("Data validation & freshness", open=False): | |
| out_val = gr.JSON(label="Validation report") | |
| out_fresh = gr.JSON(label="Freshness") | |
| run_btn.click(run_live_forecast, [symbol_in, tf_in, horizon_in, period_in, model_in, wf_check_live], | |
| [out_md, out_table, out_val, out_fresh]) | |
| with gr.Tab("Walk-Forward Backtest"): | |
| with gr.Row(): | |
| b_symbol = gr.Dropdown(choices=cfg.ALL_SYMBOLS, value="BTC-USD", | |
| allow_custom_value=True, | |
| label="Symbol (pick one, or type any Yahoo ticker)") | |
| b_tf = gr.Dropdown(choices=cfg.ALL_TIMEFRAMES, value="1d", label="Timeframe") | |
| b_horizon = gr.Slider(1, 20, value=5, step=1, label="Horizon (bars)") | |
| b_period = gr.Dropdown(cfg.HISTORY_WINDOW_CHOICES, value="1 year", label="History window") | |
| b_class = gr.Radio(["forex", "crypto"], value="crypto", label="Asset class (cost profile)") | |
| b_model = gr.Radio(_MODEL_CHOICES, value=_DEFAULT_MODEL_LABEL, label="Model") | |
| with gr.Accordion("Advanced (model context & risk management)", open=False): | |
| gr.Markdown( | |
| "**Model context**: bars of lookback the model sees at each " | |
| "backtest origin, on top of indicator warm-up. A short " | |
| "History window needs a smaller context length here, or " | |
| "the backtest has no room left for even one origin β the " | |
| "error message will say exactly how many bars are missing " | |
| "either way.\n\n" | |
| "**Risk management**: 0 = off (the original behavior β " | |
| "every trade holds the full horizon no matter what " | |
| "happens in between). A stop-loss/take-profit is checked " | |
| "against each bar's real intrabar high/low between entry " | |
| "and the horizon exit, and closes the trade early β at " | |
| "that bar's close, not the exact stop level β the first " | |
| "time either is crossed. Type an exact value here β a " | |
| "slider this precise (0.05% steps) is very easy to " | |
| "overshoot with a drag on a small screen, which is exactly " | |
| "how a previous run ended up at 0.5% instead of an " | |
| "intended 0.05%." | |
| ) | |
| b_context = gr.Slider(32, 512, value=256, step=32, label="Model context length (bars)") | |
| b_stop = gr.Number(value=0, minimum=0, maximum=5, step=0.01, precision=2, | |
| label="Stop-loss (%, 0 = off)") | |
| b_tp = gr.Number(value=0, minimum=0, maximum=5, step=0.01, precision=2, | |
| label="Take-profit (%, 0 = off)") | |
| b_btn = gr.Button("Run Backtest", variant="primary") | |
| b_wf_check = gr.Checkbox( | |
| value=False, | |
| label="Use frozen walk-forward calibration (run Walk-Forward Validation tab first β must ACCEPT)") | |
| b_out = gr.Markdown() | |
| b_trades = gr.Dataframe(label="Trade ledger") | |
| with gr.Accordion("Diagnostics (audit trail: signals, rejections, exits)", open=False): | |
| b_diag = gr.JSON(label="Every origin's outcome, broken down") | |
| b_btn.click(run_backtest, [b_symbol, b_tf, b_horizon, b_period, b_class, b_context, b_stop, b_tp, b_model, b_wf_check], | |
| [b_out, b_trades, b_diag]) | |
| with gr.Tab("Walk-Forward Validation"): | |
| gr.Markdown( | |
| "Splits your History window chronologically into " | |
| f"train ({int(cfg.WalkForwardConfig().train_frac*100)}%) / " | |
| f"validation ({int(cfg.WalkForwardConfig().val_frac*100)}%) / " | |
| f"test ({int((1-cfg.WalkForwardConfig().train_frac-cfg.WalkForwardConfig().val_frac)*100)}%). " | |
| "Probability calibration is fit and the confidence/threshold combination is chosen using " | |
| "**train+validation only**, then evaluated on the **test window exactly once** β that " | |
| "result is compared against two simple baselines (always-flat, naive momentum) run " | |
| "through the identical cost/execution model. A REJECT is a real, honest result: it means " | |
| "no credible edge survived unseen data, and the system will not force a trade.\n\n" | |
| "**AutoARIMA note:** unlike Moirai-MoE/Chronos-2 (one forward pass per origin), AutoARIMA " | |
| "re-fits a fresh model at every origin, and this tab evaluates many origins across a grid " | |
| "of threshold combinations β measured at ~6s for a single combination on ~135 bars, so " | |
| "the default grid on a realistic History window can take a long time. Prefer a shorter " | |
| "History window or a smaller model context when using AutoARIMA here." | |
| ) | |
| with gr.Row(): | |
| w_symbol = gr.Dropdown(choices=cfg.ALL_SYMBOLS, value="EURUSD=X", | |
| allow_custom_value=True, | |
| label="Symbol (pick one, or type any Yahoo ticker)") | |
| w_tf = gr.Dropdown(choices=cfg.ALL_TIMEFRAMES, value="1h", label="Timeframe") | |
| w_horizon = gr.Slider(1, 20, value=5, step=1, label="Horizon (bars)") | |
| w_period = gr.Dropdown(cfg.HISTORY_WINDOW_CHOICES, value="1 year", label="History window") | |
| w_class = gr.Radio(["forex", "crypto"], value="forex", label="Asset class (cost profile)") | |
| w_model = gr.Radio(_MODEL_CHOICES, value=_DEFAULT_MODEL_LABEL, label="Model") | |
| w_context = gr.Slider(32, 512, value=256, step=32, label="Model context length (bars)") | |
| w_btn = gr.Button("Run Walk-Forward Validation", variant="primary") | |
| w_out = gr.Markdown() | |
| with gr.Accordion("Full acceptance report (JSON)", open=False): | |
| w_report = gr.JSON(label="Checks, trials, and verdict") | |
| w_btn.click(run_walk_forward_validation, [w_symbol, w_tf, w_horizon, w_period, w_class, w_context, w_model], | |
| [w_out, w_report]) | |
| gr.Markdown( | |
| "---\n**Modes implemented in this build:** Live inference (Mode D) and walk-forward " | |
| "historical evaluation with trade simulation (Mode C). Fine-tuning (Mode B) has real " | |
| "training-loop code in `moirai_model.py` but no UI tab yet in this build β see README " | |
| "limitations." | |
| ) | |
| if __name__ == "__main__": | |
| # Gradio 6.26 on HF Spaces appears to auto-detect the container | |
| # environment even without this, but binding explicitly is cheap | |
| # insurance against any platform that doesn't auto-detect it. | |
| demo.launch(server_name="0.0.0.0", server_port=7860) | |