ZenQuant.Sizing (zen_quant v0.7.0)

Copy Markdown View Source

Position sizing calculations for trading systems.

Pure functions for calculating position sizes and their compounding growth based on risk parameters, account size, and volatility.

Example

# Fixed fractional sizing (risk 1% of account)
ZenQuant.Sizing.fixed_fractional(100_000, 0.01, 500)
# => 2.0  (2 units where each unit has $500 max loss)

# Kelly criterion
ZenQuant.Sizing.kelly(0.55, 1.5)
# => 0.183  (18.3% of bankroll)

API Functions

FunctionArityDescriptionParam Kinds
optimal_f1Calculate Ralph Vince's optimal fixed fraction from trade history.trades: value
terminal_wealth_relative2Evaluate observed geometric-mean holding-period returns at a fraction.trades: value, fraction: value
anti_martingale4Calculate anti-martingale position adjustment.base_size: value, consecutive_wins: value
volatility_scaled4Calculate position size scaled by volatility.account_size: value, risk_percent: value, current_volatility: value, target_volatility: value
simulate_kelly1Simulate seeded terminal wealth and drawdown for discrete Kelly bets.inputs: value
median_terminal_wealth_multiple4Project the median terminal-wealth multiple after a number of bets.win_rate: value, win_loss_ratio: value, fraction: value, bet_count: value
growth_curve4Sample expected log-growth over a caller-supplied fraction range.win_rate: value, win_loss_ratio: value, fraction_range: value, step: value
log_growth3Calculate expected logarithmic bankroll growth per bet.win_rate: value, win_loss_ratio: value, fraction: value
kelly3Calculate optimal position size using Kelly criterion.win_rate: value, win_loss_ratio: value
max_loss2Calculate position size based on maximum loss amount.max_loss_amount: value, stop_distance: value
fixed_fractional3Calculate position size using fixed fractional method.account_size: value, risk_percent: value, stop_distance: value

Summary

Types

Fraction of bankroll (0.0 to 1.0)

One simulated path's terminal wealth and maximum peak-to-trough drawdown

Named input error returned by simulate_kelly/1

Caller-supplied inputs for a seeded discrete-Kelly simulation

Position size in units or currency

Functions

Calculate position size using fixed fractional method.

Sample expected log-growth over a caller-supplied fraction range.

Calculate optimal position size using Kelly criterion.

Calculate expected logarithmic bankroll growth per bet.

Calculate position size based on maximum loss amount.

Project the median terminal-wealth multiple after a number of bets.

Calculate Ralph Vince's optimal fixed fraction from trade history.

Simulate seeded terminal wealth and drawdown for discrete Kelly bets.

Evaluate observed geometric-mean holding-period returns at a fraction.

Types

fraction()

@type fraction() :: float()

Fraction of bankroll (0.0 to 1.0)

kelly_path()

@type kelly_path() :: %{
  terminal_wealth_multiple: float(),
  max_drawdown: float(),
  log_growth_per_bet: float()
}

One simulated path's terminal wealth and maximum peak-to-trough drawdown

kelly_simulation_error()

@type kelly_simulation_error() ::
  :invalid_inputs
  | :invalid_win_rate
  | :invalid_win_loss_ratio
  | :invalid_fraction
  | :invalid_bet_count
  | :invalid_path_count
  | :invalid_seed
  | :invalid_quantiles
  | :invalid_thresholds

Named input error returned by simulate_kelly/1

kelly_simulation_inputs()

@type kelly_simulation_inputs() :: %{
  win_rate: fraction(),
  win_loss_ratio: float(),
  fraction: fraction(),
  bet_count: pos_integer(),
  path_count: pos_integer(),
  seed: integer(),
  quantiles: [number()],
  thresholds: [number()]
}

Caller-supplied inputs for a seeded discrete-Kelly simulation

position_size()

@type position_size() :: float()

Position size in units or currency

Functions

anti_martingale(base_size, consecutive_wins, scale_factor \\ 0.25, max_scale \\ 2.0)

@spec anti_martingale(number(), integer(), fraction(), float()) :: position_size()

Calculate anti-martingale position adjustment.

Parameters

  • base_size - Starting position size (value)
  • consecutive_wins - Number of consecutive wins (negative for losses) (value)

Options

  • scale_factor - Adjustment per win/loss (0.25 = 25%) (default: 0.25)
  • max_scale - Maximum multiplier cap (default: 2.0)

Returns

Adjusted position size (float)

Example

0.1095
# descripex:contract
%{
  opts: %{
    scale_factor: %{
      default: 0.25,
      type: :float,
      description: "Adjustment per win/loss (0.25 = 25%)"
    },
    max_scale: %{
      default: 2.0,
      type: :float,
      description: "Maximum multiplier cap"
    }
  },
  params: %{
    base_size: %{description: "Starting position size", kind: :value},
    consecutive_wins: %{
      description: "Number of consecutive wins (negative for losses)",
      kind: :value
    }
  },
  returns: %{type: :float, description: "Adjusted position size"},
  returns_example: 0.1095
}

fixed_fractional(account_size, risk_percent, stop_distance)

@spec fixed_fractional(number(), fraction(), number()) :: position_size()

Calculate position size using fixed fractional method.

Parameters

  • account_size - Total account equity (value)
  • risk_percent - Risk per trade as decimal (e.g., 0.01 = 1%) (value)
  • stop_distance - Distance to stop loss in account currency per unit (value)

Returns

Position size in units (e.g., contracts) (float)

Example

0.1095
# descripex:contract
%{
  params: %{
    account_size: %{description: "Total account equity", kind: :value},
    risk_percent: %{
      description: "Risk per trade as decimal (e.g., 0.01 = 1%)",
      kind: :value
    },
    stop_distance: %{
      description: "Distance to stop loss in account currency per unit",
      kind: :value
    }
  },
  returns: %{
    type: :float,
    description: "Position size in units (e.g., contracts)"
  },
  returns_example: 0.1095
}

growth_curve(win_rate, win_loss_ratio, arg3, step)

@spec growth_curve(fraction(), float(), {fraction(), fraction()}, float()) ::
  [{fraction(), float()}]
  | {:error, :invalid_fraction | :invalid_fraction_range | :invalid_step}

Sample expected log-growth over a caller-supplied fraction range.

Parameters

  • win_rate - Probability of winning (0.0 to 1.0) (value)
  • win_loss_ratio - Average win divided by average loss (value)
  • fraction_range - Inclusive {first_fraction, last_fraction} range (value)
  • step - Positive interval between sampled fractions (value)

Returns

Inclusive [{fraction, expected_log_growth}] samples or an error tuple (list)

Example

[{0.1, 0.015}, {0.2, 0.0201}]

Errors

  • :invalid_fraction
  • :invalid_fraction_range
  • :invalid_step
# descripex:contract
%{
  params: %{
    step: %{
      description: "Positive interval between sampled fractions",
      kind: :value
    },
    win_rate: %{
      description: "Probability of winning (0.0 to 1.0)",
      kind: :value
    },
    win_loss_ratio: %{
      description: "Average win divided by average loss",
      kind: :value
    },
    fraction_range: %{
      description: "Inclusive {first_fraction, last_fraction} range",
      kind: :value
    }
  },
  errors: [:invalid_fraction, :invalid_fraction_range, :invalid_step],
  returns: %{
    type: :list,
    description: "Inclusive [{fraction, expected_log_growth}] samples or an error tuple"
  },
  returns_example: [{0.1, 0.015}, {0.2, 0.0201}]
}

kelly(win_rate, win_loss_ratio, kelly_fraction \\ 0.5)

@spec kelly(fraction(), float(), fraction()) :: fraction()

Calculate optimal position size using Kelly criterion.

Parameters

  • win_rate - Probability of winning (0.0 to 1.0) (value)
  • win_loss_ratio - Average win divided by average loss (value)

Options

  • kelly_fraction - Fraction of Kelly to use (0.5 = half Kelly) (default: 0.5)

Returns

Optimal bet size as fraction of bankroll, 0.0 if negative EV (float)

Example

0.1095
# descripex:contract
%{
  opts: %{
    kelly_fraction: %{
      default: 0.5,
      type: :float,
      description: "Fraction of Kelly to use (0.5 = half Kelly)"
    }
  },
  params: %{
    win_rate: %{
      description: "Probability of winning (0.0 to 1.0)",
      kind: :value
    },
    win_loss_ratio: %{
      description: "Average win divided by average loss",
      kind: :value
    }
  },
  returns: %{
    type: :float,
    description: "Optimal bet size as fraction of bankroll, 0.0 if negative EV"
  },
  returns_example: 0.1095
}

log_growth(win_rate, win_loss_ratio, fraction)

@spec log_growth(fraction(), float(), fraction()) ::
  float() | {:error, :invalid_fraction}

Calculate expected logarithmic bankroll growth per bet.

Parameters

  • win_rate - Probability of winning (0.0 to 1.0) (value)
  • win_loss_ratio - Average win divided by average loss (value)
  • fraction - Fraction of bankroll bet, greater than 0.0 and less than 1.0 (value)

Returns

Expected natural-log growth per bet or {:error, :invalid_fraction} (float)

Example

0.0201

Errors

  • :invalid_fraction
# descripex:contract
%{
  params: %{
    fraction: %{
      description: "Fraction of bankroll bet, greater than 0.0 and less than 1.0",
      kind: :value
    },
    win_rate: %{
      description: "Probability of winning (0.0 to 1.0)",
      kind: :value
    },
    win_loss_ratio: %{
      description: "Average win divided by average loss",
      kind: :value
    }
  },
  errors: [:invalid_fraction],
  returns: %{
    type: :float,
    description: "Expected natural-log growth per bet or {:error, :invalid_fraction}"
  },
  returns_example: 0.0201
}

max_loss(max_loss_amount, stop_distance)

@spec max_loss(number(), number()) :: position_size()

Calculate position size based on maximum loss amount.

Parameters

  • max_loss_amount - Maximum acceptable loss in account currency (value)
  • stop_distance - Distance to stop loss per unit (value)

Returns

Position size in units (float)

Example

0.1095
# descripex:contract
%{
  params: %{
    stop_distance: %{
      description: "Distance to stop loss per unit",
      kind: :value
    },
    max_loss_amount: %{
      description: "Maximum acceptable loss in account currency",
      kind: :value
    }
  },
  returns: %{type: :float, description: "Position size in units"},
  returns_example: 0.1095
}

median_terminal_wealth_multiple(win_rate, win_loss_ratio, fraction, bet_count)

@spec median_terminal_wealth_multiple(
  fraction(),
  float(),
  fraction(),
  non_neg_integer()
) ::
  float() | {:error, :invalid_fraction | :invalid_bet_count}

Project the median terminal-wealth multiple after a number of bets.

Parameters

  • win_rate - Probability of winning (0.0 to 1.0) (value)
  • win_loss_ratio - Average win divided by average loss (value)
  • fraction - Fraction of bankroll bet, greater than 0.0 and less than 1.0 (value)
  • bet_count - Non-negative number of bets (value)

Returns

Median terminal bankroll divided by starting bankroll or an error tuple (float)

Example

1.7227

Errors

  • :invalid_fraction
  • :invalid_bet_count
# descripex:contract
%{
  params: %{
    fraction: %{
      description: "Fraction of bankroll bet, greater than 0.0 and less than 1.0",
      kind: :value
    },
    win_rate: %{
      description: "Probability of winning (0.0 to 1.0)",
      kind: :value
    },
    win_loss_ratio: %{
      description: "Average win divided by average loss",
      kind: :value
    },
    bet_count: %{description: "Non-negative number of bets", kind: :value}
  },
  errors: [:invalid_fraction, :invalid_bet_count],
  returns: %{
    type: :float,
    description: "Median terminal bankroll divided by starting bankroll or an error tuple"
  },
  returns_example: 1.7227
}

optimal_f(trades)

@spec optimal_f([number()]) :: fraction() | nil

Calculate Ralph Vince's optimal fixed fraction from trade history.

Parameters

  • trades - List of trade results (positive for wins, negative for losses) (value)

Returns

Optimal fraction of account to risk per trade, or nil if insufficient data (float)

Example

0.1095
# descripex:contract
%{
  params: %{
    trades: %{
      description: "List of trade results (positive for wins, negative for losses)",
      kind: :value
    }
  },
  returns: %{
    type: :float,
    description: "Optimal fraction of account to risk per trade, or nil if insufficient data"
  },
  returns_example: 0.1095
}

simulate_kelly(arg1)

@spec simulate_kelly(kelly_simulation_inputs()) ::
  {:ok,
   %{
     seed: integer(),
     rng_algorithm: :exsss,
     bet_count: pos_integer(),
     path_count: pos_integer(),
     paths: [kelly_path()],
     terminal_wealth: %{
       mean: float(),
       standard_error: float() | nil,
       quantiles: [%{quantile: number(), value: float()}]
     },
     log_growth_per_bet: %{mean: float(), standard_error: float() | nil},
     fraction_below_thresholds: [
       %{threshold: number(), fraction_below: float()}
     ]
   }}
  | {:error, kelly_simulation_error()}

Simulate seeded terminal wealth and drawdown for discrete Kelly bets.

Parameters

  • inputs - Required map with :win_rate, :win_loss_ratio, :fraction, positive :bet_count/:path_count, integer :seed, :quantiles, and :thresholds (value)

Returns

{:ok, result} with per-path outcomes, terminal-wealth quantiles/mean/standard error, log-growth uncertainty, and fractions below thresholds (tuple)

Example

{:ok,
 %{
   seed: 42,
   log_growth_per_bet: %{standard_error: 0.002, mean: 0.008},
   terminal_wealth: %{
     standard_error: 0.04,
     quantiles: [%{value: 1.09, quantile: 0.5}],
     mean: 1.18
   },
   fraction_below_thresholds: [%{threshold: 1.0, fraction_below: 0.31}]
 }}

Errors

  • :invalid_inputs
  • :invalid_win_rate
  • :invalid_win_loss_ratio
  • :invalid_fraction
  • :invalid_bet_count
  • :invalid_path_count
  • :invalid_seed
  • :invalid_quantiles
  • :invalid_thresholds
# descripex:contract
%{
  params: %{
    inputs: %{
      description: "Required map with :win_rate, :win_loss_ratio, :fraction, positive :bet_count/:path_count, integer :seed, :quantiles, and :thresholds",
      kind: :value
    }
  },
  errors: [:invalid_inputs, :invalid_win_rate, :invalid_win_loss_ratio,
   :invalid_fraction, :invalid_bet_count, :invalid_path_count, :invalid_seed,
   :invalid_quantiles, :invalid_thresholds],
  returns: %{
    type: :tuple,
    description: "{:ok, result} with per-path outcomes, terminal-wealth quantiles/mean/standard error, log-growth uncertainty, and fractions below thresholds"
  },
  returns_example: {:ok,
   %{
     seed: 42,
     log_growth_per_bet: %{standard_error: 0.002, mean: 0.008},
     terminal_wealth: %{
       standard_error: 0.04,
       quantiles: [%{value: 1.09, quantile: 0.5}],
       mean: 1.18
     },
     fraction_below_thresholds: [%{threshold: 1.0, fraction_below: 0.31}]
   }}
}

terminal_wealth_relative(trades, fraction)

@spec terminal_wealth_relative([number()], fraction()) ::
  float() | {:error, :invalid_fraction | :invalid_trades | :no_losing_trades}

Evaluate observed geometric-mean holding-period returns at a fraction.

Parameters

  • trades - Observed numeric trade results with at least one loss (value)
  • fraction - Fraction of bankroll risked, greater than 0.0 and less than 1.0 (value)

Returns

Geometric mean of normalized holding-period returns or an error tuple (float)

Example

1.0607

Errors

  • :invalid_fraction
  • :invalid_trades
  • :no_losing_trades
# descripex:contract
%{
  params: %{
    fraction: %{
      description: "Fraction of bankroll risked, greater than 0.0 and less than 1.0",
      kind: :value
    },
    trades: %{
      description: "Observed numeric trade results with at least one loss",
      kind: :value
    }
  },
  errors: [:invalid_fraction, :invalid_trades, :no_losing_trades],
  returns: %{
    type: :float,
    description: "Geometric mean of normalized holding-period returns or an error tuple"
  },
  returns_example: 1.0607
}

volatility_scaled(account_size, risk_percent, current_volatility, target_volatility)

@spec volatility_scaled(number(), fraction(), number(), number()) :: position_size()

Calculate position size scaled by volatility.

Parameters

  • account_size - Total account equity (value)
  • risk_percent - Base risk per trade as decimal (value)
  • current_volatility - Current volatility measure (e.g., ATR, std dev) (value)
  • target_volatility - Target/baseline volatility for normal sizing (value)

Returns

Volatility-adjusted risk amount in account currency (float)

Example

0.1095
# descripex:contract
%{
  params: %{
    account_size: %{description: "Total account equity", kind: :value},
    risk_percent: %{description: "Base risk per trade as decimal", kind: :value},
    current_volatility: %{
      description: "Current volatility measure (e.g., ATR, std dev)",
      kind: :value
    },
    target_volatility: %{
      description: "Target/baseline volatility for normal sizing",
      kind: :value
    }
  },
  returns: %{
    type: :float,
    description: "Volatility-adjusted risk amount in account currency"
  },
  returns_example: 0.1095
}