Sizing#
Position sizing models that convert a Signal
into absolute quantities for each leg.
PositionSizer#
Abstract base class for all position sizers.
- class spreadpy.sizing.positionSizer.PositionSizer[source]#
Abstract base class for position sizers.
Converts a
Signalinto absolute quantities(qty_y, qty_x)for each leg. Concrete subclasses implement different sizing models (linear z-score scaling, inverse volatility, Kelly criterion, etc.).The
fit()/size()discipline mirrors that ofSignalGenerator:fit()is called once on the combined train + evaluation spread to warm up any rolling estimators, thensize()is called bar-by-bar on the evaluation period.- fit(spread: SpreadSeries) PositionSizer[source]#
Calibrate the sizer on a spread series (no-op by default).
Subclasses that require a warm-up period (e.g.
InverseVolSizer) override this method to precompute rolling statistics.- Parameters:
spread (SpreadSeries) – Combined train + evaluation spread, used to warm up any rolling estimators before the first evaluation bar.
- Returns:
self.- Return type:
- abstractmethod size(signal: Signal, price_y: float, price_x: float, hedge_ratio: float, capital: float = 0.0) Tuple[float, float][source]#
Compute absolute quantities for each leg given a signal.
The direction of each leg is determined by the signal:
LONG spread: buy y (+qty_y), sell x (−qty_x)
SHORT spread: sell y (−qty_y), buy x (+qty_x)
FLAT: returns (0, 0)
Quantities are always returned as non-negative values; the engine applies the signed direction when executing fills.
- Parameters:
signal (Signal) – Signal at the current bar.
price_y (float) – Current price of the y leg.
price_x (float) – Current price of the x leg.
hedge_ratio (float) – Hedge ratio β_t at the current bar.
capital (float) – Current mark-to-market equity in monetary units. Used by capital-fraction sizers (e.g.
KellyTruncatedEntry); ignored by notional-based sizers.
- Returns:
(qty_y, qty_x)— absolute quantities for each leg.- Return type:
Tuple[float, float]
LinearSizer#
- class spreadpy.sizing.sizers.linearSizer.LinearSizer(max_notional: float = 100000.0, scale_fn: Callable[[float], float] | None = None)[source]#
Converts a Signal into (qty_y, qty_x) absolute quantities.
The position size is proportional to the z-score magnitude via a scaling function:
\[\begin{split}N_y &= N_{\max} \cdot \mathrm{scale}(|z|) \\ q_y &= N_y / p_y \\ q_x &= N_y \cdot |\beta| / p_x\end{split}\]where \(\beta\) is the hedge ratio at signal time. The x-leg quantity is adjusted so that the notional exposure is hedged: \(q_x \cdot p_x \approx |\beta| \cdot q_y \cdot p_y\).
The default scale function is a linear ramp:
\[\mathrm{scale}(|z|) = \mathrm{clip}\!\left(\frac{|z| - 1}{2},\; 0,\; 1\right)\]mapping \(|z| = 1 \to 0\%\) and \(|z| \geq 3 \to 100\%\) of
max_notional.- Parameters:
max_notional (float) – Maximum notional per leg in monetary units.
scale_fn (Optional[Callable[[float], float]]) – Maps \(|z|\) → [0, 1]. Defaults to the linear ramp described above.
- size(signal: Signal, price_y: float, price_x: float, hedge_ratio: float, capital: float = 0.0) Tuple[float, float][source]#
Compute absolute quantities proportional to the z-score magnitude.
Returns
(0, 0)whensignal.directionisFLATorsignal.zscoreis NaN. Thecapitalparameter is accepted for interface compatibility but is not used by this sizer.- Parameters:
signal (Signal) – Signal at the current bar.
price_y (float) – Current price of the y leg.
price_x (float) – Current price of the x leg.
hedge_ratio (float) – Hedge ratio β_t (absolute value used).
capital (float) – Ignored (notional-based sizer).
- Returns:
(qty_y, qty_x)— absolute quantities for each leg.- Return type:
Tuple[float, float]
InverseVolSizer#
- class spreadpy.sizing.sizers.inverseVolSizer.InverseVolSizer(window: int = 60, target_vol: float = 0.02, f_max: float = 0.5)[source]#
Markowitz inverse-volatility position sizer.
Sizes each position so that a 1-\(\sigma\) adverse move of the spread residual costs exactly
target_volof the current capital:\[f_t = \min\!\left(\frac{\texttt{target\_vol}}{\sigma_t},\; f_{\max}\right)\]where \(\sigma_t\) is the rolling standard deviation of spread residuals over the last
windowbars (no lookahead), consistent with the rolling z-score used byZScoreSignal. The y-leg notional is then:\[N_y = f_t \cdot \text{capital}_t\]fit()must be called beforesize(). Pass the combined train + evaluation spread so that σ_t is already warmed up at the first evaluation bar.- Parameters:
window (int) – Rolling window for spread volatility estimation (bars).
target_vol (float) – Target capital fraction at risk for a 1-σ adverse spread move (e.g. 0.02 means 2% of capital).
f_max (float) – Hard cap on the capital fraction (default 0.5).
- fit(spread: SpreadSeries) InverseVolSizer[source]#
Precompute the rolling volatility series from the spread residuals.
Must be called before
size(). Pass the combined train + evaluation spread so that the rolling window is warmed up before the first evaluation bar.- Parameters:
spread (SpreadSeries) – Combined train + evaluation spread series.
- Returns:
self.- Return type:
- size(signal: Signal, price_y: float, price_x: float, hedge_ratio: float, capital: float = 0.0) Tuple[float, float][source]#
Compute quantities using inverse-volatility sizing.
Returns
(0, 0)whensignal.directionisFLAT,signal.zscoreis NaN,capital≤ 0, or \(\sigma_t\) is unavailable or non-positive.- Parameters:
signal (Signal) – Signal at the current bar.
price_y (float) – Current price of the y leg.
price_x (float) – Current price of the x leg.
hedge_ratio (float) – Hedge ratio \(\beta_t\) (absolute value used).
capital (float) – Current mark-to-market equity in monetary units.
- Returns:
(qty_y, qty_x)— absolute quantities for each leg.- Return type:
Tuple[float, float]
- Raises:
RuntimeError – If
fit()has not been called.
Kelly sizers#
Three variants of the second-order Kelly criterion derived from truncated normal distributions of the entry and exit z-scores.
KellyTruncatedEntry#
- class spreadpy.sizing.sizers.kellySizers.KellyTruncatedEntry(z_entry: float, z_revert: float = 0.0, f_max: float = 0.5)[source]#
Kelly sizer where only the entry level \(z\) is random.
The entry z-score is modelled as a truncated standard normal:
\[z \sim \mathcal{N}(0,1) \text{ truncated to } [z_{\text{entry}}, +\infty), \quad p(z) = \frac{\varphi(z)}{1 - \Phi(z_{\text{entry}})}\]The reversion target \(z_{\text{revert}}\) is treated as a deterministic constant. The per-trade gain is \(G = z - z_{\text{revert}}\).
Moments (\(\lambda_+ = \lambda_+(z_{\text{entry}})\)):
\[\begin{split}\mathbb{E}[G] &= \lambda_+ - z_{\text{revert}} \\ \mathrm{Var}(z) &= 1 - \lambda_+(\lambda_+ - z_{\text{entry}}) \\ \mathbb{E}[G^2] &= \mathrm{Var}(z) + \mathbb{E}[G]^2\end{split}\]Second-order Kelly fraction:
\[f^* = \frac{\lambda_+ - z_{\text{revert}}} {1 - \lambda_+(\lambda_+ - z_{\text{entry}}) + (\lambda_+ - z_{\text{revert}})^2}\]The fraction is constant and computed once at construction.
- Parameters:
z_entry (float) – Entry threshold; positions are opened when \(|z_t| \geq z_{\text{entry}}\).
z_revert (float) – Deterministic reversion target (default 0.0, i.e. the mean).
f_max (float) – Hard cap on the Kelly fraction (default 0.5).
- size(signal: Signal, price_y: float, price_x: float, hedge_ratio: float, capital: float = 0.0) Tuple[float, float][source]#
Compute quantities using the constant Kelly fraction f*.
Returns
(0, 0)whensignal.directionisFLAT,signal.zscoreis NaN,capital≤ 0, or f* ≤ 0.- Parameters:
signal (Signal) – Signal at the current bar.
price_y (float) – Current price of the y leg.
price_x (float) – Current price of the x leg.
hedge_ratio (float) – Hedge ratio β_t (absolute value used).
capital (float) – Current mark-to-market equity in monetary units.
- Returns:
(qty_y, qty_x)— absolute quantities for each leg.- Return type:
Tuple[float, float]
KellyTruncatedExit#
- class spreadpy.sizing.sizers.kellySizers.KellyTruncatedExit(z_revert: float = 0.0, f_max: float = 0.5)[source]#
Kelly sizer where only the reversion level \(\tilde{z}\) is random.
The exit z-score is modelled as a truncated standard normal:
\[\tilde{z} \sim \mathcal{N}(0,1) \text{ truncated to } (-\infty, z_{\text{revert}}], \quad p(\tilde{z}) = \frac{\varphi(\tilde{z})}{\Phi(z_{\text{revert}})}\]The observed entry \(z_t\) is treated as a deterministic constant. The per-trade gain is \(G = z_t - \tilde{z}\).
Moments (\(\lambda_- = \lambda_-(z_{\text{revert}})\)):
\[\begin{split}\mathbb{E}[\tilde{z}] &= -\lambda_- \\ \mathbb{E}[G] &= z_t + \lambda_- \\ \mathrm{Var}(\tilde{z}) &= 1 - \lambda_-(\lambda_- + z_{\text{revert}}) \\ \mathbb{E}[G^2] &= \mathrm{Var}(\tilde{z}) + \mathbb{E}[G]^2\end{split}\]Second-order Kelly fraction (function of the observed \(z_t\)):
\[f^*(z_t) = \frac{z_t + \lambda_-} {1 - \lambda_-(\lambda_- + z_{\text{revert}}) + (z_t + \lambda_-)^2}\]By symmetry of \(\mathcal{N}(0,1)\), a LONG entry with \(z_t = -|z|\) is equivalent to a SHORT entry with the reflected \(|z|\), so
|signal.zscore|is used for both directions. The fraction is recomputed at each bar.- Parameters:
z_revert (float) – Right-truncation point for the exit distribution (default 0.0, i.e. exit at the mean).
f_max (float) – Hard cap on the Kelly fraction (default 0.5).
- size(signal: Signal, price_y: float, price_x: float, hedge_ratio: float, capital: float = 0.0) Tuple[float, float][source]#
Compute quantities using the bar-dependent Kelly fraction f*(\(|z_t|\)).
Returns
(0, 0)whensignal.directionisFLAT,signal.zscoreis NaN,capital≤ 0, or f*(\(|z_t|\)) ≤ 0.- Parameters:
signal (Signal) – Signal at the current bar.
price_y (float) – Current price of the y leg.
price_x (float) – Current price of the x leg.
hedge_ratio (float) – Hedge ratio β_t (absolute value used).
capital (float) – Current mark-to-market equity in monetary units.
- Returns:
(qty_y, qty_x)— absolute quantities for each leg.- Return type:
Tuple[float, float]
KellyTruncatedBoth#
- class spreadpy.sizing.sizers.kellySizers.KellyTruncatedBoth(z_entry: float, z_revert: float = 0.0, f_max: float = 0.5)[source]#
Kelly sizer where both entry \(z\) and exit \(\tilde{z}\) are random and independent.
Entry and exit z-scores are modelled as independent truncated normals:
\[\begin{split}z &\sim \mathcal{N}(0,1) \text{ truncated to } [z_{\text{entry}}, +\infty) && \text{(entry level)} \\ \tilde{z} &\sim \mathcal{N}(0,1) \text{ truncated to } (-\infty, z_{\text{revert}}] && \text{(exit level)} \\ z &\perp \tilde{z}\end{split}\]The per-trade gain is \(G = z - \tilde{z}\).
Moments (\(\lambda_+ = \lambda_+(z_{\text{entry}})\), \(\lambda_- = \lambda_-(z_{\text{revert}})\)):
\[\begin{split}\mathbb{E}[G] &= \lambda_+ + \lambda_- \\ \mathrm{Var}(z) &= 1 - \lambda_+(\lambda_+ - z_{\text{entry}}) \\ \mathrm{Var}(\tilde{z}) &= 1 - \lambda_-(\lambda_- + z_{\text{revert}}) \\ \mathrm{Var}(G) &= \mathrm{Var}(z) + \mathrm{Var}(\tilde{z}) && \text{(independence)} \\ \mathbb{E}[G^2] &= \mathrm{Var}(G) + \mathbb{E}[G]^2\end{split}\]Second-order Kelly fraction:
\[f^* = \frac{\lambda_+ + \lambda_-} {2 - \lambda_+(\lambda_+ - z_{\text{entry}}) - \lambda_-(\lambda_- + z_{\text{revert}}) + (\lambda_+ + \lambda_-)^2}\]The fraction is constant and computed once at construction.
- Parameters:
z_entry (float) – Entry threshold; positions are opened when \(|z_t| \geq z_{\text{entry}}\).
z_revert (float) – Right-truncation point for the exit distribution (default 0.0, i.e. exit at the mean).
f_max (float) – Hard cap on the Kelly fraction (default 0.5).
- size(signal: Signal, price_y: float, price_x: float, hedge_ratio: float, capital: float = 0.0) Tuple[float, float][source]#
Compute quantities using the constant Kelly fraction f*.
Returns
(0, 0)whensignal.directionisFLAT,signal.zscoreis NaN,capital≤ 0, or f* ≤ 0.- Parameters:
signal (Signal) – Signal at the current bar.
price_y (float) – Current price of the y leg.
price_x (float) – Current price of the x leg.
hedge_ratio (float) – Hedge ratio β_t (absolute value used).
capital (float) – Current mark-to-market equity in monetary units.
- Returns:
(qty_y, qty_x)— absolute quantities for each leg.- Return type:
Tuple[float, float]