QuantJourney Backtester

QuantJourney Backtester

Share product feedback

Thank you.

Your note is now in the QuantJourney inbox.

backtester/portfolio/calc/pnl_multi_asset.py

pnl_multi_asset.py:
Asset-class-aware PnL, margin and notional exposure.

This file computes mark-to-market PnL for mixed portfolios. It understands equities, futures, FX, crypto and inverse contracts through ContractSpec rather than assuming one share equals one dollar of exposure.

Import from backtester.portfolio.calc import pnl_multi_asset

When To Read This

  • 01
    You are testing futures, FX, crypto or mixed portfolios.
  • 02
    You need PnL to respect multipliers, lot sizes, margins and inverse contracts.
  • 03
    You are explaining why multi-asset returns should come from PnL / capital, not naive price pct-change.

File Anatomy

  • Position PnL: lagged position x price change x multiplier x lot size.
  • Inverse contract branch: uses reciprocal price difference.
  • Margin: per-instrument margin usage and total margin.
  • Notional exposure and returns: converts PnL to returns and NAV.

Data Contract

Inputs

  • positions: DataFrame dates x instruments, quantities/contracts held.
  • prices: DataFrame dates x instruments.
  • specs: optional dict symbol -> ContractSpec; fallback through get_contract_spec.

Outputs

  • Instrument-level daily PnL DataFrame.
  • Portfolio-level PnL, returns and NAV Series.
  • Margin and notional exposure DataFrames.

Invariants

  • PnL uses lagged positions, so today position earns tomorrow price move.
  • Instrument specs control multipliers, lot sizes, margin and inverse behavior.
  • Returns from PnL are based on capital, which is correct for margin instruments.

Public API And Key Internals

compute_position_pnl

function
compute_position_pnl(positions, prices, specs=None)

Computes daily mark-to-market PnL per instrument.

Returns

pd.DataFrame.

compute_portfolio_pnl

function
compute_portfolio_pnl(positions, prices, specs=None)

Sums instrument PnL into portfolio daily PnL.

Returns

pd.Series.

compute_margin_usage / compute_total_margin

function
compute_total_margin(positions, prices, specs=None)

Computes per-instrument and total margin requirements.

Returns

pd.DataFrame or pd.Series.

compute_notional_exposure

function
compute_notional_exposure(positions, prices, specs=None)

Computes absolute notional exposure per instrument.

Returns

pd.DataFrame.

compute_returns_from_pnl / compute_nav_from_pnl

function
compute_nav_from_pnl(pnl, initial_capital)

Converts PnL into return and NAV series.

Returns

pd.Series.

Implementation Notes

  • This module is one of the main reasons the backtester can be more than an equity-only toy.
  • ContractSpec keeps asset-class mechanics out of strategy code.
  • The inverse branch is explicit because crypto derivatives can invert normal price-change intuition.

Code Walkthrough

Compute PnL for a mixed portfolio

Specs make the same PnL path work for equities, futures, FX and crypto.

pnl_multi_asset_usage.py Python
from backtester.execution.contract_spec import ContractSpec
from backtester.portfolio.calc import pnl_multi_asset

specs = {
    "ES": ContractSpec.future("ES", multiplier=50, tick_size=0.25),
    "EURUSD": ContractSpec.fx("EURUSD", pip_size=0.0001, margin=0.02),
    "BTCUSD": ContractSpec.crypto("BTCUSD", tick_size=0.01),
}

instrument_pnl = pnl_multi_asset.compute_position_pnl(positions, prices, specs)
portfolio_pnl = pnl_multi_asset.compute_portfolio_pnl(positions, prices, specs)
returns = pnl_multi_asset.compute_returns_from_pnl(portfolio_pnl, capital=100_000)
nav = pnl_multi_asset.compute_nav_from_pnl(portfolio_pnl, initial_capital=100_000)

Key implementation: lagged position mark-to-market

The lag prevents look-ahead PnL: yesterday position earns today price move.

backtester/portfolio/calc/pnl_multi_asset.py Python
price_change = prices.diff().fillna(0.0)
lagged_pos = positions.shift(1).fillna(0.0)

for col in positions.columns:
    spec = specs.get(col, get_contract_spec(col))

    if spec.inverse:
        p_prev = prices[col].shift(1)
        p_curr = prices[col]
        inv_diff = (1.0 / p_prev - 1.0 / p_curr).fillna(0.0)
        pnl[col] = lagged_pos[col] * spec.multiplier * inv_diff
    else:
        pnl[col] = lagged_pos[col] * price_change[col] * spec.multiplier * spec.lot_size