- display_names.py: the SINGLE internal-id -> human-name map every view uses (absorbs app.py's interim _SIM_BOOK_LABELS local map + a per-asset "what it is" description for the holdings table). - exec_reconcile.py: exec_vs_sim() — THE shared "how closely real trades follow the plan" signal (executed vs modeled cumulative forward return + median slippage + fees), reused by later D3/D4 work. - dashboard_service.detail() attaches holdings/recent_trades/account_nav/recon for the IBKR real-account books (multistrat/vrp), sourced from IbkrAccountRepo (Task 2) + ForwardNavRepo's new cum_return(). - strategy.html: "Following the plan" card + plan_pill, a real-vs-plan value-over-time curve (new charts.overlay_curve), a "what it's holding right now" table, and a "recent trades" table with honest cost-to-trade as a % (never a fabricated non-zero) — all plain-language, no raw ids/bps/"vs sim". - Fixed a pre-existing raw-id leak (exec_twin_sid rendered as literal link text) via a new `display_name` Jinja global, used project-wide. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
66 lines
2.8 KiB
Python
66 lines
2.8 KiB
Python
"""D2.2/2.3 (Task 3) — exec_vs_sim: THE one shared "how closely real trades follow the plan" signal, reused
|
|
by the holdings detail page (this task) and D3/D4 overview surfacing later. Compares a strategy's EXECUTED
|
|
forward track's cumulative return (`<sid>_exec`) against its MODELED/sim forward cumulative return (`sid`) —
|
|
both already-persisted `forward_summary` numbers (no new compute), plus a median of the strategy-tagged
|
|
`exec_fills.shortfall_bps` and a fee total. Fakes stand in for `IbkrAccountRepo` (`fills_for`) and
|
|
`ForwardNavRepo` (`cum_return`) — sid-agnostic (matched only by the `_exec` suffix), so the same Fake works
|
|
for any strategy_id."""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from fxhnt.application.exec_reconcile import exec_vs_sim
|
|
|
|
|
|
@dataclass
|
|
class _FakeFill:
|
|
shortfall_bps: float
|
|
fee: float
|
|
|
|
|
|
class FakeExec:
|
|
"""Stands in for `IbkrAccountRepo.fills_for`. `cum` is accepted (mirrors the real repo's shape from the
|
|
caller's POV) but unused here — the executed cumulative return comes from the forward repo, not the
|
|
fills table."""
|
|
def __init__(self, cum: float | None, fills_bps: list[float], fees: float) -> None:
|
|
self.cum = cum
|
|
n = len(fills_bps)
|
|
per_fill_fee = (fees / n) if n else 0.0
|
|
self._fills = [_FakeFill(shortfall_bps=b, fee=per_fill_fee) for b in fills_bps]
|
|
|
|
def fills_for(self, strategy_id: str, limit: int = 20) -> list[_FakeFill]:
|
|
return self._fills
|
|
|
|
|
|
class FakeFwd:
|
|
"""Stands in for `ForwardNavRepo.cum_return`. Sid-agnostic: any id ending in `_exec` gets `exec_cum`,
|
|
everything else gets `sim_cum` — so one Fake works regardless of the base strategy_id under test."""
|
|
def __init__(self, exec_cum: float | None, sim_cum: float | None) -> None:
|
|
self._exec_cum = exec_cum
|
|
self._sim_cum = sim_cum
|
|
|
|
def cum_return(self, strategy_id: str) -> float | None:
|
|
return self._exec_cum if strategy_id.endswith("_exec") else self._sim_cum
|
|
|
|
|
|
def test_divergence_and_slippage():
|
|
r = exec_vs_sim("multistrat", exec_repo=FakeExec(cum=0.031, fills_bps=[3.0, 5.0, 4.0], fees=12.0),
|
|
forward_repo=FakeFwd(exec_cum=0.031, sim_cum=0.038))
|
|
assert round(r.divergence, 4) == -0.007 # exec 3.1% vs sim 3.8%
|
|
assert r.median_slippage_bps == 4.0
|
|
assert r.has_exec is True
|
|
assert round(r.cum_exec_return, 4) == 0.031
|
|
assert round(r.cum_sim_return, 4) == 0.038
|
|
assert round(r.total_fees, 4) == 12.0
|
|
|
|
|
|
def test_no_exec_data():
|
|
r = exec_vs_sim("sixtyforty", exec_repo=FakeExec(cum=None, fills_bps=[], fees=0.0),
|
|
forward_repo=FakeFwd(exec_cum=None, sim_cum=0.05))
|
|
assert r.has_exec is False
|
|
assert r.cum_exec_return == 0.0
|
|
assert r.cum_sim_return == 0.0
|
|
assert r.divergence == 0.0
|
|
assert r.median_slippage_bps == 0.0
|
|
assert r.total_fees == 0.0
|