77 lines
2.3 KiB
Python
77 lines
2.3 KiB
Python
"""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]
|