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:
1 parent
e816448e30
commit
0157409f0d
2 files changed
+46
-5
No files matched your search
@@ -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)
|
||||
Reference in new issue
Block a user