Published on

Bet Sizing

Bet Sizing

This chapter argues that, much like in poker, bet sizing is a critical component of a profitable investment strategy. An ML algorithm with high predictive accuracy can still lose money if its bets are sized improperly. The chapter introduces several methods for determining position size, moving from simple heuristics to dynamic, probability-based models.


Strategy-Independent Bet Sizing Approaches

The text introduces three preliminary methods for bet sizing:

  1. Gaussian CDF: This method computes the net number of concurrent long and short bets (ctc_t) active at time tt. A mixture of two Gaussians is fit to the distribution of {ct}\{c_t\}. The bet size mtm_t is then determined by the CDF FF of this mixture, which scales the bet based on how extreme the current signal concurrency is.

    • Equation:
      mt={F(ct)F[0]1F[0] if ct0F(ct)F[0]F[0] if ct<0m_{t}= \begin{cases}\frac{F(c_{t})-F[0]}{1-F[0]} & \text { if } c_{t} \geq 0 \\ \frac{F(c_{t})-F[0]}{F[0]} & \text { if } c_{t}<0\end{cases}
  2. Budgeting Approach: The bet size is a simple fraction of the maximum number of concurrent bets ever observed, scaling the position to ensure the full size is not reached prematurely.

    • Equation: mt=ct,l1maxi(ci,l)ct,s1maxi(ci,s)m_{t}=c_{t, l} \frac{1}{\max_{i}(c_{i, l})}-c_{t, s} \frac{1}{\max_{i}(c_{i, s})}
  3. Meta-Labeling: Use the probability of a false positive (derived from a meta-model, as discussed in Chapter 3) to determine the bet size. This leads to the more general method of using predicted probabilities.


Bet Sizing from Predicted Probabilities

This is the core method, which converts a model's predicted probability (confidence) into a bet size.

  • Binary Case (Labels x{1,1}x \in \{-1, 1\}): We test the prediction's confidence against the null hypothesis H0:p=0.5H_0: p = 0.5. The bet size is the CDF of the resulting zz-score.

    1. Test Statistic: z=p[x=1]0.5p[x=1](1p[x=1])z=\frac{p[x=1]-0.5}{\sqrt{p[x=1](1-p[x=1])}}
    2. Bet Size: m=2Z[z]1m=2 Z[z]-1, where ZZ is the Standard Normal CDF.
  • Multi-Class Case (e.g., x{1,0,1}x \in \{-1, 0, 1\}): We test the probability of the most likely outcome (p~\tilde{p}) against the null hypothesis of pure chance (H0:p~=1/XH_0: \tilde{p} = 1/|X|).

    1. Test Statistic: z=p~1/Xp~(1p~)z=\frac{\tilde{p}-1/|X|}{\sqrt{\tilde{p}(1-\tilde{p})}}
    2. Bet Size: m=x(2Z[z]1)m=x (2 Z[z]-1). Here, xx is the side (e.g., -1 or 1) and the term (2Z[z]1)(2Z[z]-1) is the magnitude of the bet, scaled between [0,1][0, 1].

Bet Sizing Refinements

These methods are used to reduce excess turnover and trading costs.

  1. Averaging Active Bets: Instead of overriding a position when a new signal arrives, this method averages the bet size across all signals that are still active (i.e., have not yet hit a barrier from the triple-barrier method).
  2. Size Discretization: To prevent "jitter" (many small, inefficient trades) from the averaging process, the final bet size is discretized to a specific step size dd.
    • Equation: m=round(m/d)dm^{*} = \text{round}(m/d) \cdot d

Dynamic Bet Sizes and Limit Prices

This is an advanced method that dynamically adjusts the bet size as the market price ptp_t fluctuates relative to the forecasted price fif_i.

  • Sizing Function (Sigmoid): The target position q^i,t\hat{q}_{i,t} is determined by a sigmoid function, where the input xx is the divergence between the forecast and the current price (x=fiptx = f_i - p_t).

    • Equation: m[ω,x]=xω+x2m[\omega, x] = \frac{x}{\sqrt{\omega+x^{2}}}
    • Property: As the market price approaches the forecast (ptfip_t \to f_i), the divergence x0x \to 0, and the bet size m0m \to 0. This automatically "realizes gains."
    • Calibration: The width ω\omega can be calibrated: ω=x2(m21)\omega=x^{2}\left(m^{*-2}-1\right)
  • Limit Price: This method also allows for the calculation of a breakeven limit price pˉ\bar{p} for the order, preventing the algorithm from realizing losses as it dynamically adjusts.

    • Inverse Function: L(fi,ω,m)=fimω1m2L\left(f_{i}, \omega, m\right)=f_{i}-m \sqrt{\frac{\omega}{1-m^{2}}}
  • Alternative (Power Function): A power function can also be used, which offers different curvature properties.

    • Equation: m~[ω,x]=sgn[x]xω\tilde{m}[\omega, x]=\operatorname{sgn}[x]|x|^{\omega}

API reference

RiskLabAI implements these in Python and Julia (signatures auto-generated from the package source):

PythonJulia
def average_bet_sizes(
    price_dates: np.ndarray,
    start_dates: np.ndarray,
    end_dates: np.ndarray,
    bet_sizes: np.ndarray,
) -> np.ndarray:
function average_bet_sizes(
    price_dates,
    start_dates,
    end_dates,
    bet_sizes::AbstractVector{<:Real},
)
def strategy_bet_sizing(
    price_timestamps: pd.Index,
    times: pd.Series,
    sides: pd.Series,
    probabilities: pd.Series,
) -> pd.Series:
function strategy_bet_sizing(
    price_timestamps,
    start_times,
    end_times,
    sides::AbstractVector{<:Real},
    probabilities::AbstractVector{<:Real},
)
def avg_active_signals(signals: pd.DataFrame, n_threads: int) -> pd.DataFrame:
function avg_active_signals(start_times, end_times, signal_values::AbstractVector{<:Real})
def discrete_signal(signal: pd.Series, step_size: float) -> pd.Series:
function discrete_signal(signal::AbstractVector{<:Real}, step_size::Real)
def generate_signal(
    events: pd.DataFrame,
    step_size: float,
    probability: pd.Series,
    prediction: pd.Series,
    n_classes: int,
    n_threads: int,
) -> pd.Series:
function generate_signal(
    start_times,
    end_times,
    sides::Union{Nothing,AbstractVector{<:Real}},
    probability::AbstractVector{<:Real},
    prediction::AbstractVector{<:Real},
    n_classes::Integer,
    step_size::Real,
)
def inverse_price(f: float, w: float, m: float) -> float:
function inverse_price(f::Real, w::Real, m::Real)
def limit_price(
    target_position_size: int,
    current_position: int,
    f: float,
    w: float,
    maximum_position_size: int,
) -> float:
function limit_price(
    target_position_size::Integer,
    current_position::Integer,
    f::Real,
    w::Real,
    maximum_position_size::Integer,
)
def compute_sigmoid_width(x: float, m: float) -> float:
function compute_sigmoid_width(x::Real, m::Real)

Full source: Python · Julia