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 Signal into 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 of SignalGenerator: fit() is called once on the combined train + evaluation spread to warm up any rolling estimators, then size() 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:

PositionSizer

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) when signal.direction is FLAT or signal.zscore is NaN. The capital parameter 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_vol of 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 window bars (no lookahead), consistent with the rolling z-score used by ZScoreSignal. The y-leg notional is then:

\[N_y = f_t \cdot \text{capital}_t\]

fit() must be called before size(). 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:

InverseVolSizer

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) when signal.direction is FLAT, signal.zscore is 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) when signal.direction is FLAT, signal.zscore is 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) when signal.direction is FLAT, signal.zscore is 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) when signal.direction is FLAT, signal.zscore is 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]