"""P/L overlays for M2D backtesting. Two overlays computed from the same trade outcome: - ``pl_marius``: real overlay used by the trader. 50% closed at TP0 (+0.2 R), BE move on the remaining half, then close 50% of that at ~TP1 (+0.3 R total contribution) or at SL/BE depending on outcome. TP1 is treated as the final exit even when the chart subsequently reaches TP2. - ``pl_theoretical``: reference 1/3-1/3-1/3 overlay that holds to TP2. Used as an opportunity-cost benchmark vs. ``pl_marius``. Returns are expressed in multiples of R (risk per trade). ``None`` from ``pl_marius`` denotes a still-pending trade. """ from __future__ import annotations __all__ = [ "PL_MARIUS_TABLE", "PL_THEORETICAL_TABLE", "pl_marius", "pl_theoretical", ] PL_MARIUS_TABLE: dict[tuple[str, bool], float | None] = { ("SL", True): -1.0, ("SL", False): -1.0, ("TP0->SL", True): 0.20, ("TP0->SL", False): -0.30, ("TP0->TP1", True): 0.50, ("TP0->TP1", False): 0.50, ("TP0->TP2", True): 0.50, ("TP0->TP2", False): 0.50, ("TP0->pending", True): None, ("TP0->pending", False): None, ("pending", True): None, ("pending", False): None, } PL_THEORETICAL_TABLE: dict[str, float] = { "SL_first": -1.0, "TP0": 0.133, "TP1": 0.333, "TP2": 0.667, } _VALID_OUTCOME_PATHS: frozenset[str] = frozenset( {"SL", "TP0->SL", "TP0->TP1", "TP0->TP2", "TP0->pending", "pending"} ) def _normalize_outcome_path(outcome_path: str) -> str: return outcome_path.replace("→", "->").replace("→", "->") def pl_marius(outcome_path: str, be_moved: bool) -> float | None: """Return the P/L (in R) for the real Marius overlay. Accepts both ASCII arrow ``"TP0->TP1"`` and unicode arrow ``"TP0→TP1"``. Returns ``None`` for pending outcomes. """ normalized = _normalize_outcome_path(outcome_path) if normalized not in _VALID_OUTCOME_PATHS: raise ValueError(f"invalid outcome_path: {outcome_path!r}") return PL_MARIUS_TABLE[(normalized, be_moved)] def pl_theoretical(max_reached: str) -> float: """Return the P/L (in R) for the theoretical 1/3-1/3-1/3 hold-to-TP2 overlay.""" if max_reached not in PL_THEORETICAL_TABLE: raise ValueError(f"invalid max_reached: {max_reached!r}") return PL_THEORETICAL_TABLE[max_reached]