fix(options-engine): tolerate malformed trade PnL and disclose exclusions (#907)

Trade records carrying pnl=None or a non-numeric value raised TypeError
while computing trade statistics. Skip those records instead, and — since
trade_count still reports every record — append a warning naming how many
were excluded so win_rate and profit_loss_ratio are never silently
derived from a subset.
This commit is contained in:
santhreal authored and Haozhe Wu committed 2026-07-30 13:01:53 +08:00
1 parent e816448e30
commit 0157409f0d
2 files changed
+46 -5

No files matched your search

+23 -5
View File
@@ -732,11 +732,29 @@ def _calc_options_metrics(
warnings.append("Sortino ratio requires finite returns.")
# Trade statistics
closed_pnl = [
float(t["pnl"])
for t in trades
if t.get("pnl", 0) != 0 and np.isfinite(float(t["pnl"]))
]
closed_pnl: List[float] = []
ignored_pnl_records = 0
for t in trades:
raw_pnl = t.get("pnl")
if raw_pnl is None:
ignored_pnl_records += 1
continue
try:
val = float(raw_pnl)
except (TypeError, ValueError):
ignored_pnl_records += 1
continue
if not np.isfinite(val):
ignored_pnl_records += 1
continue
if val != 0:
closed_pnl.append(val)
if ignored_pnl_records:
warnings.append(
f"Ignored PnL for {ignored_pnl_records} trade records "
"(missing or non-numeric pnl); win rate and profit/loss ratio "
"are computed from the remaining trades only."
)
wins = [p for p in closed_pnl if p > 0]
losses = [p for p in closed_pnl if p < 0]
win_rate = len(wins) / len(closed_pnl) if closed_pnl else 0.0
@@ -109,3 +109,26 @@ def test_daily_bar_annualization_still_finite() -> None:
assert metrics["annual_return"] is not None
assert np.isfinite(metrics["annual_return"])
json.dumps(metrics, allow_nan=False)
def test_null_and_non_numeric_trade_pnl_handled_safely() -> None:
"""Trades containing pnl: None or non-numeric strings must not raise TypeError."""
trades = [
{"pnl": None},
{"pnl": "invalid"},
{"pnl": 100.0},
{"pnl": -50.0},
]
metrics = _calc_options_metrics(pd.Series([100.0, 150.0]), 100.0, trades)
assert metrics["trade_count"] == 4
assert metrics["win_rate"] == 0.5
assert metrics["profit_loss_ratio"] == 2.0
ignored_warnings = [w for w in metrics["warnings"] if "Ignored PnL" in w]
assert ignored_warnings and "2 trade records" in ignored_warnings[0]
json.dumps(metrics, allow_nan=False)
def test_all_numeric_trade_pnl_emits_no_ignored_warning() -> None:
"""Clean numeric trades must not trigger the ignored-PnL warning."""
trades = [{"pnl": 100.0}, {"pnl": -50.0}, {"pnl": 0.0}]
metrics = _calc_options_metrics(pd.Series([100.0, 150.0]), 100.0, trades)
assert not any("Ignored PnL" in w for w in metrics["warnings"])
json.dumps(metrics, allow_nan=False)