Files
fxhnt/tests/unit/test_exec_reconcile.py
jgrusewski d25ff9659a feat(D2.2/2.3): IBKR paper-account section on the strategy detail + shared exec-vs-sim divergence signal
- 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>
2026-07-15 00:02:25 +02:00

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