Skip to content

fix: direction-aware pnl_percent and trade cost decomposition - #5

Merged
stefan-jansen merged 5 commits into
mainfrom
fix/trade-cost-decomposition-and-short-pnl
Mar 4, 2026
Merged

fix: direction-aware pnl_percent and trade cost decomposition#5
stefan-jansen merged 5 commits into
mainfrom
fix/trade-cost-decomposition-and-short-pnl

Conversation

@stefan-jansen

Copy link
Copy Markdown
Contributor

Summary

  • Short P&L fix: pnl_percent now direction-aware — positive for profitable trades, negative for losing trades, regardless of long/short. Fixed in Position.pnl_percent(), FillExecutor close/flip, and Engine open-trade results.
  • Trade cost decomposition: New fields entry_slippage and multiplier on Position/Trade, plus computed properties gross_pnl, net_pnl, net_return, total_slippage_cost, and cost_drag.
  • Parquet schema: Updated with backward compatibility for old files (entry_slippage=0.0, multiplier=1.0 defaults).

Test plan

  • 37 new tests in test_trade_cost_decomposition.py
  • Full suite: 1116 passed, 14 skipped
  • ruff check, ruff format, ty check all clean
  • Validation scenarios 11-12 (short-specific) — verify parity preserved

Short P&L fix: pnl_percent now returns positive for profitable shorts
and negative for losing shorts, matching long-side convention. Fixed in
Position.pnl_percent(), FillExecutor close/flip, and Engine open-trade
results.

Trade cost decomposition: new fields entry_slippage and multiplier on
Position/Trade, plus computed properties gross_pnl, net_pnl, net_return,
total_slippage_cost, and cost_drag. Parquet schema updated with backward
compatibility for old files.

37 new tests in test_trade_cost_decomposition.py.
Copilot AI review requested due to automatic review settings March 4, 2026 12:56

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates backtest trade/position accounting to make pnl_percent direction-aware (profitable shorts yield positive returns) and adds trade cost decomposition fields/derived metrics (including entry slippage and futures multipliers), with Parquet backward compatibility.

Changes:

  • Fix direction-aware pnl_percent for shorts across Position, position close/flip execution, and open trade mark-to-market results.
  • Add entry_slippage and multiplier fields plus computed properties (gross_pnl, net_return, total_slippage_cost, cost_drag) for trade cost decomposition.
  • Extend Parquet schema + loader defaults for backward compatibility, and add extensive new tests.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/test_trade_cost_decomposition.py Adds comprehensive tests for short P&L sign correctness, MFE/MAE behavior, cost decomposition properties, futures multipliers, and Parquet roundtrips/back-compat.
tests/test_result.py Updates expected trades DataFrame columns to include entry_slippage and multiplier.
src/ml4t/backtest/types.py Adds Position.entry_slippage, Trade.entry_slippage/multiplier, and new computed cost decomposition properties; updates Position.pnl_percent() to be direction-aware.
src/ml4t/backtest/result.py Writes/reads new Parquet columns (entry_slippage, multiplier) and updates trades schema.
src/ml4t/backtest/execution/fill_executor.py Propagates entry slippage into positions/trades and fixes close/flip pnl_percent sign handling.
src/ml4t/backtest/engine.py Fixes open-trade mark-to-market PnL to include multiplier and applies direction-aware pnl_percent.
src/ml4t/backtest/analytics/trades.py Updates total_slippage aggregation to use dollar cost via total_slippage_cost.
src/ml4t/backtest/analytics/bridge.py Exposes new fields and computed decomposition metrics in diagnostic trade records.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/ml4t/backtest/execution/fill_executor.py
Comment thread src/ml4t/backtest/execution/fill_executor.py
Comment thread src/ml4t/backtest/types.py
Comment thread src/ml4t/backtest/result.py
Add comprehensive test infrastructure:
- Autouse accounting invariants (conftest.py) checked on every Engine.run()
- Exit-reason consistency checks (stop_loss→loss, take_profit→gain, etc.)
- Fill-level order-type bound checks (limit buy never overpays, etc.)
- Fill metadata enrichment: 3 optional fields on Fill dataclass
- Direction x order-type matrix (17 tests: limit/stop fills, gap-through, PnL)
- Bracket order lifecycle (16 tests: TP/SL cancel, gap-through, sequential)
- Property-based order-type invariants (5 Hypothesis tests, 900 examples)
- Independent oracle engine for differential testing
- Parametric scenario factory
- Direction matrix tests (long/short x multiple strategies)
- PnL property-based invariants

1367 passed, 14 skipped. All quality gates clean.
…, docs audit

- TradeAnalyzer: avg_win, avg_loss, largest_win, largest_loss, expectancy,
  avg_trade now all percentage-based (select from pnl_percent, not dollar PnL)
- Added payoff_ratio (avg_win / |avg_loss|)
- result.metrics includes cost decomposition: total_gross_pnl, total_costs,
  avg_cost_drag, gross_profit_factor
- to_trades_dataframe() includes 4 computed columns: gross_pnl, net_return,
  total_slippage_cost, cost_drag
- Rewrote docs/user-guide/results.md with Trade Analyzer section, cost
  decomposition section, Fill metadata section, percentage-based examples
- Fixed LIMITATIONS.md: Mode.REALISTIC -> BacktestConfig.from_preset()
- Audited and corrected all 10 AGENT.md files: fixed line counts, removed
  fictional class names (CashAccountPolicy, SignalStrategy etc.), updated
  to actual classes (UnifiedAccountPolicy, SignalFollowingStrategy etc.)
- Scale-in cost accumulation: entry_commission and entry_slippage now
  accumulated on position scale-up (was losing intermediate leg costs)
- Rebalancer sell-before-buy: INCREMENTAL/HYBRID modes now process
  reduces before increases, preventing cash-starved buy rejections
- Session alignment in to_portfolio_analysis(): auto-detects CME
  calendars and passes session_aligned=True to to_daily_pnl()
- MAE recovery sign: (pnl_percent - mae) / |mae| gives positive
  recovery ratio as documented
- Zero close price: use `is not None` instead of truthy check in
  datafeed and rebalancer price guards
- Minute break gaps: generate_trading_minutes() splits around
  break_start/break_end for exchanges like XTKS
- Intraday annualization: EquityCurve.years uses wall-clock time,
  new periods_per_year property, sharpe/sortino/volatility properties
  use inferred bar frequency
- Tearsheet slippage: fallback uses total_slippage_cost (dollar cost)
  instead of per-unit exit slippage

All 8 fixes include regression tests. 1375 passed, 14 skipped.
@stefan-jansen
stefan-jansen merged commit f25ca77 into main Mar 4, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants