diff --git a/src/fxhnt/adapters/web/app.py b/src/fxhnt/adapters/web/app.py index d59dd91..97a518e 100644 --- a/src/fxhnt/adapters/web/app.py +++ b/src/fxhnt/adapters/web/app.py @@ -4,6 +4,7 @@ from __future__ import annotations import datetime as dt import logging +import time from pathlib import Path from typing import TYPE_CHECKING, Any @@ -165,13 +166,27 @@ def create_app(repo: ForwardNavRepo, svc: DashboardReadModel | None = None, _SIM_BOOKS = ("binance_combined", "bybit_4edge") _DEFAULT_BOOK = "binance_combined" + # In-process TTL cache for the per-sleeve return tables. These are PRECOMPUTED nightly (the sim never + # recomputes an edge on the request path), so re-reading the whole history from the DB on every request + # is pure waste. A short TTL keeps the page from going stale-forever: the nightly snapshot lands within + # one TTL window, and a manual book-switch is at most TTL seconds behind the DB. Keyed by book. + _SIM_RETURNS_TTL_S = 300.0 + _sim_returns_cache: dict[str, tuple[float, dict[str, dict[int, float]]]] = {} + def _sim_returns(book: str = _DEFAULT_BOOK) -> dict[str, dict[int, float]]: """All cached per-sleeve returns for `book`. binance_combined reads paper_sleeve_ret (a far-future cutoff = full history); bybit_4edge reads the precomputed bybit_sleeve_ret table — NEITHER recomputes - any edge on the request path.""" + any edge on the request path. Memoized in-process with a 300s TTL (returns change only nightly).""" + now = time.monotonic() + hit = _sim_returns_cache.get(book) + if hit is not None and (now - hit[0]) < _SIM_RETURNS_TTL_S: + return hit[1] if book == "bybit_4edge": - return _paper_repo().bybit_sleeve_returns() - return _paper_repo().sleeve_returns(before="2999-01-01") + returns = _paper_repo().bybit_sleeve_returns() + else: + returns = _paper_repo().sleeve_returns(before="2999-01-01") + _sim_returns_cache[book] = (now, returns) + return returns def _forward_track(strategy_id: str) -> dict[str, Any] | None: """The live forward track view for `strategy_id` (T0, gate status/days, the NAV curve) — read from @@ -250,14 +265,23 @@ def create_app(repo: ForwardNavRepo, svc: DashboardReadModel | None = None, @app.get("/paper/sim", response_class=HTMLResponse) def paper_sim(request: Request, book: str = _DEFAULT_BOOK) -> HTMLResponse: + """Render the config form + controls INSTANTLY, then let HTMX fetch the (expensive) sim result from + /paper/sim/run on load. The page no longer runs the full book overlay before first paint — that was + the ~7s cost. The default sim (all sleeves) matches what /paper/sim/run computes for an unconfigured + page, so the lazy-loaded result is identical to what the eager render produced.""" from fxhnt.config import get_settings book = book if book in _SIM_BOOKS else _DEFAULT_BOOK returns = _sim_returns(book) avail = sorted(returns.keys()) - # bybit_4edge defaults to all 4 sleeves (the whole book); binance defaults to all available too. - ctx = _sim_context(returns, capital=get_settings().paper_capital, sleeves=avail, - start=None, end=None, kelly=1.0, controller="killswitch", cost_bps=0.0, - book=book) + capital = get_settings().paper_capital + # The form's echoed config + the URL HTMX loads on first paint. Defaults mirror /paper/sim/run's + # whole-book default: all available sleeves, kelly=1.0, killswitch, cost 0. + cfg = {"capital": capital, "start": "", "end": "", "sleeves": avail, "kelly": 1.0, + "controller": "killswitch", "cost_bps": 0.0, "book": book} + result_url = (f"/paper/sim/run?capital={capital:g}&sleeves={','.join(avail)}" + f"&kelly=1.0&controller=killswitch&cost_bps=0&book={book}") + ctx = {"cfg": cfg, "available_sleeves": avail, "presets": _SIM_PRESETS, + "books": _SIM_BOOKS, "result_url": result_url, "now": _now()} return templates.TemplateResponse(request, "sim.html", ctx) @app.get("/paper/sim/run", response_class=HTMLResponse) diff --git a/src/fxhnt/adapters/web/templates/sim.html b/src/fxhnt/adapters/web/templates/sim.html index dca4b60..aa89e93 100644 --- a/src/fxhnt/adapters/web/templates/sim.html +++ b/src/fxhnt/adapters/web/templates/sim.html @@ -70,8 +70,11 @@ style="background:#193b1e; color:#3fb950; border:1px solid #2ea043; border-radius:6px; padding:6px 16px; cursor:pointer">Run -
- {% include "_sim_result.html" %} +{# The result is lazy-loaded: the form above paints INSTANTLY; HTMX fetches the (expensive) book sim from + /paper/sim/run on load and swaps it in here. Using htmx (not fetch+innerHTML) means the result fragment's + inline scripts — the timeline scrubber / ▶Play wiring — actually run on every swap. #} +
+

running sim…