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
- 01You are testing futures, FX, crypto or mixed portfolios.
- 02You need PnL to respect multipliers, lot sizes, margins and inverse contracts.
- 03You 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
functioncompute_position_pnl(positions, prices, specs=None) Computes daily mark-to-market PnL per instrument.
Returns
pd.DataFrame.
compute_portfolio_pnl
functioncompute_portfolio_pnl(positions, prices, specs=None) Sums instrument PnL into portfolio daily PnL.
Returns
pd.Series.
compute_margin_usage / compute_total_margin
functioncompute_total_margin(positions, prices, specs=None) Computes per-instrument and total margin requirements.
Returns
pd.DataFrame or pd.Series.
compute_notional_exposure
functioncompute_notional_exposure(positions, prices, specs=None) Computes absolute notional exposure per instrument.
Returns
pd.DataFrame.
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