diff --git a/app_lib.py b/app_lib.py index 55702bc..de3c3cd 100644 --- a/app_lib.py +++ b/app_lib.py @@ -23,7 +23,7 @@ from osl.backtest.rules import IronCondor1Sigma, LongStraddle, ShortPut16Delta from osl.config import Settings, get_settings from osl.data.providers import Freshness, build_provider, freshness_badge -from osl.report.playbook import PlaybookData, StrategyRow +from osl.report.playbook import LegRow, PlaybookData, StrategyRow from osl.strategy.enumerate import View, enumerate_candidates from osl.strategy.liquidity import strategy_liquidity from osl.strategy.metrics import compute_metrics @@ -639,6 +639,18 @@ def build_playbook_data( break best = rank(candidates, obj)[0] m = best.metrics + legs = tuple( + LegRow( + side="Sell" if leg.sign < 0 else "Buy", + quantity=int(leg.quantity), + right=leg.right, + strike=float(leg.strike), + expiry=leg.expiration.isoformat(), + premium=float(leg.premium), + iv=float(leg.iv), + ) + for leg in best.strategy.legs + ) rows.append( StrategyRow( name=best.strategy.name, @@ -651,6 +663,8 @@ def build_playbook_data( loss_unbounded=m.loss_unbounded, liquidity=best.liquidity.score, breakevens=m.breakevens, + net_debit=float(best.strategy.net_debit), + legs=legs, ) ) diff --git a/osl/report/playbook.py b/osl/report/playbook.py index b81223f..510e874 100644 --- a/osl/report/playbook.py +++ b/osl/report/playbook.py @@ -18,6 +18,10 @@ _COLUMN_GLOSSARY: tuple[tuple[str, str], ...] = ( ("Objective", "The ranking goal this strategy topped (e.g. EV per risk, theta per risk)."), ("Strategy", "The option structure (vertical, iron condor, strangle, ...)."), + ( + "Net", + "Net cash at entry, in dollars: debit (paid) is positive, credit (received) is negative.", + ), ("Expiry", "Expiration date of the nearest leg."), ( "POP (RN)", @@ -30,6 +34,29 @@ ("Liquidity", "0-1 tradability score from spread, open interest, volume and ATM distance."), ) +_TICKET_HEADERS: tuple[str, ...] = ( + "Action", + "Qty", + "Right", + "Strike", + "Expiry", + "Premium (mid)", + "IV", +) + + +@dataclass(frozen=True) +class LegRow: + """One leg of a strategy, in the form needed to actually place the trade.""" + + side: str # "Buy" or "Sell" + quantity: int + right: str # "C" or "P" + strike: float + expiry: str # ISO date + premium: float # per-share mid + iv: float + @dataclass(frozen=True) class StrategyRow: @@ -43,6 +70,10 @@ class StrategyRow: loss_unbounded: bool liquidity: float breakevens: tuple[float, ...] + # Execution detail. Defaults preserve back-compat with older callers/tests + # that did not carry leg-level data. + net_debit: float = 0.0 + legs: tuple[LegRow, ...] = () @dataclass(frozen=True) @@ -90,9 +121,18 @@ def _fmt_money(x: float) -> str: return "∞ (uncovered)" if x == float("-inf") else f"{x:,.2f}" +def _fmt_net(x: float) -> str: + """Format net entry cash: positive = debit, negative = credit.""" + if x > 0: + return f"${x:,.2f} debit" + if x < 0: + return f"${abs(x):,.2f} credit" + return "$0.00" + + def _strategy_rows_html(rows: Sequence[StrategyRow]) -> str: if not rows: - return "
Net: {escape(_fmt_net(r.net_debit))} · " + f"Breakevens: {escape(bes)}
" + "Exact legs for each top strategy — side, quantity, right (C/P), strike and expiry — so the trade can be entered directly.
+{_trade_tickets_html(data.strategies)} +