QuantJourney Backtester

QuantJourney Backtester

Share product feedback

Thank you.

Your note is now in the QuantJourney inbox.

backtester/portfolio/calc/round_trips.py

round_trips.py:
Canonical FIFO trade matching for trade-level analytics.

This file is the single source of truth for completed round trips. It converts a blotter into matched trades, PnL, holding periods, win/loss statistics, turnover and consistency checks.

Import from backtester.portfolio.calc.round_trips import RoundTripAnalyzer

When To Read This

  • 01
    You need to explain why trade stats, turnover and round-trip counts agree.
  • 02
    You are debugging order-based strategies and want the exact FIFO matching behavior.
  • 03
    You are adding a trading analytics section to a tear-sheet.

File Anatomy

  • RoundTrip dataclass: immutable record of one completed long or short round trip.
  • FIFO matcher: internal signed-quantity engine that handles position reductions and zero-crossing trades.
  • RoundTripAnalyzer: report-facing API built from one raw blotter and one returns series.
  • Consistency checks: trade-to-round-trip ratio, volume consistency and position overlap.

Data Contract

Inputs

  • trades_df with Timestamp, Instrument, Side, Quantity, Price and optional TransactionCost / TradeValue.
  • returns: Series used to reconstruct NAV-based trade analytics context.
  • initial_capital for NAV and percentage calculations.

Outputs

  • round_trips DataFrame with entry/exit, direction, costs, gross/net PnL and holding days.
  • summary dict consumed by portfolio_perf.py dot-path metrics.
  • holding period lists and PnL series for plots.

Invariants

  • Signed quantity: buy is positive, sell is negative.
  • FIFO closes oldest lots first.
  • Costs are allocated proportionally when a fill partially closes a lot.

Public API And Key Internals

RoundTrip

dataclass
RoundTrip(instrument, direction, quantity, entry_price, exit_price, entry_time, exit_time, entry_cost, exit_cost, pnl_gross, pnl_net, holding_days, return_pct)

Frozen record for one completed round trip.

_fifo_match

helper
_fifo_match(trades_df) -> list[RoundTrip]

Internal matching engine using signed quantities and FIFO open lots.

Returns

list[RoundTrip].

RoundTripAnalyzer

class
RoundTripAnalyzer(trades_df, returns, initial_capital=100_000.0)

Main report-facing class that owns raw trades, NAV and matched round trips.

round_trips

method
@property round_trips -> pd.DataFrame

Lazily materializes completed round trips as a DataFrame.

summary

method
summary() -> dict[str, Any]

Combines NAV metrics, trade counts, volume, commissions, round trips, holding periods and checks.

holding_periods_list / pnl_series / pnl_with_timestamps

method
pnl_with_timestamps() -> pd.DataFrame

Plot-friendly accessors for distribution and time-series visualizations.

Implementation Notes

  • This is the right place for order-based analytics because it starts from actual fills, not target weights.
  • The analyzer writes keys that portfolio_perf.py reads through paths like compute_trade_analytics.net_profit.
  • Cross-checks intentionally expose suspicious output rather than hiding it with formatting.

Code Walkthrough

Analyze an order-based strategy blotter

The result dict feeds report tables; round_trips feeds detailed trade review.

round_trips_usage.py Python
from backtester.portfolio.calc.round_trips import RoundTripAnalyzer

analyzer = RoundTripAnalyzer(
    trades_df=trades,
    returns=strategy_returns,
    initial_capital=100_000,
)

trade_summary = analyzer.summary()
round_trips = analyzer.round_trips
pnl_by_exit = analyzer.pnl_with_timestamps()

Key implementation: signed FIFO matching

Positive lots are long, negative lots are short. A trade with opposite sign closes the oldest lot first.

backtester/portfolio/calc/round_trips.py Python
signed_qty = raw_qty if side == "buy" else -raw_qty
remaining = signed_qty

while remaining != 0 and open_lots:
    lot = open_lots[0]
    lot_qty, lot_price, lot_ts, lot_cost = lot

    if (lot_qty > 0 and remaining > 0) or (lot_qty < 0 and remaining < 0):
        break

    close_qty = min(abs(remaining), abs(lot_qty))
    direction = "long" if lot_qty > 0 else "short"
    direction_sign = 1.0 if lot_qty > 0 else -1.0
    pnl_gross = close_qty * (price - lot_price) * direction_sign

Key implementation: summary is the report contract

Every report field comes from the same matched trade set.

backtester/portfolio/calc/round_trips.py Python
def summary(self) -> Dict[str, Any]:
    result: Dict[str, Any] = {}
    result.update(self._nav_metrics())
    result.update(self._trade_counts())
    result.update(self._volume_and_turnover())
    result.update(self._commission_stats())
    result.update(self._round_trip_stats())
    result.update(self._holding_period_stats())
    result.update(self._cross_checks())
    return result