Utilities

The utils module provides utility functions and classes used throughout the keeks package.

keeks.utils.crra_utility(wealth: _Buffer | _SupportsArray[dtype[Any]] | _NestedSequence[_SupportsArray[dtype[Any]]] | bool | int | float | complex | str | bytes | _NestedSequence[bool | int | float | complex | str | bytes], risk_aversion: float = 1.0) → float | ndarray[source]

Calculate CRRA (Constant Relative Risk Aversion) utility.

For γ=1 (risk_aversion=1.0), uses log utility. For γ≠1, uses power utility: U(W) = W^(1-γ) / (1-γ)

Parameters:
  • wealth (float or array-like) – The wealth level(s) to evaluate

  • risk_aversion (float, default=1.0) – Coefficient of relative risk aversion (γ) - γ=1.0: Log utility (Kelly Criterion) - γ=1.5-2.0: Moderate risk aversion - γ>2.0: High risk aversion

Returns:

The utility value(s)

Return type:

float or array-like

Notes

CRRA utility exhibits constant relative risk aversion, meaning the fraction of wealth an agent is willing to risk remains constant as wealth changes.

keeks.utils.expected_utility(outcomes: _Buffer | _SupportsArray[dtype[Any]] | _NestedSequence[_SupportsArray[dtype[Any]]] | bool | int | float | complex | str | bytes | _NestedSequence[bool | int | float | complex | str | bytes], probabilities: _Buffer | _SupportsArray[dtype[Any]] | _NestedSequence[_SupportsArray[dtype[Any]]] | bool | int | float | complex | str | bytes | _NestedSequence[bool | int | float | complex | str | bytes], current_wealth: float, entry_price: float, risk_aversion: float = 1.0) → float[source]

Calculate expected utility of a gamble.

Parameters:
  • outcomes (array-like) – The possible payoffs from the gamble

  • probabilities (array-like) – The probability of each outcome (must sum to no more than 1, within PROBABILITY_SUM_TOLERANCE). Any omitted mass is treated as a zero-payout outcome.

  • current_wealth (float) – Current wealth before the gamble. Must be finite and greater than 0.

  • entry_price (float) – Price to pay to participate in the gamble. Must be finite; its sign is unconstrained, because a negative price models being paid to take the gamble.

  • risk_aversion (float, default=1.0) – Coefficient of relative risk aversion (γ). Must be finite and greater than 0.

Returns:

The expected utility from participating in the gamble

Return type:

float

Raises:

ValueError – If the gamble arrays are malformed, or if any scalar argument falls outside the ranges documented above.

keeks.utils.find_indifference_price(outcomes: _Buffer | _SupportsArray[dtype[Any]] | _NestedSequence[_SupportsArray[dtype[Any]]] | bool | int | float | complex | str | bytes | _NestedSequence[bool | int | float | complex | str | bytes], probabilities: _Buffer | _SupportsArray[dtype[Any]] | _NestedSequence[_SupportsArray[dtype[Any]]] | bool | int | float | complex | str | bytes | _NestedSequence[bool | int | float | complex | str | bytes], current_wealth: float, risk_aversion: float = 1.0, tolerance: float = 0.01, max_search_fraction: float = 0.5) → float[source]

Find maximum price willing to pay for a gamble using binary search.

This function finds the entry price where the expected utility from participating equals the utility of not participating (indifference price).

Parameters:
  • outcomes (array-like) – The possible payoffs from the gamble

  • probabilities (array-like) – The probability of each outcome (must sum to no more than 1, within PROBABILITY_SUM_TOLERANCE). Any omitted mass is treated as a zero-payout outcome.

  • current_wealth (float) – Current wealth before the gamble. Must be finite and greater than 0.

  • risk_aversion (float, default=1.0) – Coefficient of relative risk aversion (γ). Must be finite and greater than 0.

  • tolerance (float, default=0.01) – Convergence tolerance for binary search. Must be finite and greater than 0.

  • max_search_fraction (float, default=0.5) – Maximum fraction of wealth to consider as upper bound. Must be finite and non-negative; values above 1.0 are allowed and search beyond current wealth.

Returns:

Maximum price willing to pay for the gamble

Return type:

float

Raises:

ValueError – If the gamble arrays are malformed, or if any scalar control falls outside the ranges documented above.

Warns:

RuntimeWarning – If the gamble is still worth buying at the top of the search range, current_wealth * max_search_fraction. The search cannot look past that bound, so the returned price is the bound itself rather than a solved indifference price, and the true price is at or above it. Raise max_search_fraction to search further.

Examples

>>> # A favourable gamble: 60% chance to win $200, 40% chance to lose $100
>>> outcomes = [200, -100]
>>> probabilities = [0.6, 0.4]
>>> max_price = find_indifference_price(outcomes, probabilities,
...                                      current_wealth=1000, risk_aversion=2.0)
>>> print(f"Willing to pay: ${max_price:.2f}")
Willing to pay: $57.56
keeks.utils.validate_probabilities(probabilities: _Buffer | _SupportsArray[dtype[Any]] | _NestedSequence[_SupportsArray[dtype[Any]]] | bool | int | float | complex | str | bytes | _NestedSequence[bool | int | float | complex | str | bytes]) → ndarray[source]

Validate a probability vector and return it as a float array.

A vector is valid when every probability is finite and nonnegative and the probabilities sum to no more than one within PROBABILITY_SUM_TOLERANCE. Mass below one is left as-is: completing a partial vector (padding it with an implicit outcome, or rescaling a within-tolerance excess) is a gamble-modeling decision, so expected_utility() and find_indifference_price() make it themselves and treat the omitted mass as a zero-payout outcome.

Parameters:

probabilities (array-like) – The probability of each outcome. Must be a non-empty one-dimensional sequence of finite, nonnegative numbers whose sum is at most 1 + PROBABILITY_SUM_TOLERANCE.

Returns:

The validated probabilities as a one-dimensional float array.

Return type:

numpy.ndarray

Raises:

ValueError – If the probabilities are not a non-empty one-dimensional sequence of finite nonnegative numbers, or if they sum to more than 1 + PROBABILITY_SUM_TOLERANCE.

Examples

>>> validate_probabilities([0.25, 0.75])
array([0.25, 0.75])

A sum above one by more than the tolerance is rejected:

>>> validate_probabilities([0.5, 0.6])
Traceback (most recent call last):
    ...
ValueError: Probabilities must sum to no more than one

Exceptions

exception keeks.utils.RuinError[source]

Bases: Exception

Exception raised when a bankroll safeguard refuses a removal.

Raised by the BankRoll class in two cases: a removal would take the funds below zero (bankruptcy), or a removal exceeds the configured max_transaction_loss fraction of current funds. The message names the attempted amount, the configured limit, and the current funds; the simulators re-report it as a UserWarning when a refusal stops a run.

Checking stated probabilities

Every strategy sizes from a stated probability, so before replaying a recorded log it is worth asking whether those probabilities were calibrated. keeks.calibration.calibration_report() scores them with the Brier score and log loss and compares stated and observed frequencies in equal-width bins (empty bins are omitted). It is an educational diagnostic, not advice. Inputs are validated first: probabilities must be finite in [0, 1], outcomes must be bool or 0/1 (not 1.0), and at least one observation is required.

from keeks import calibration_report

report = calibration_report([0.2, 0.2, 0.8, 0.8], [0, 1, 1, 1])
print(round(report.brier_score, 6))  # 0.19
for b in report.bins:
    print(b.lower, b.mean_predicted, b.observed_frequency)
keeks.calibration.calibration_report(probabilities: Sequence[float], outcomes: Sequence[bool | int], n_bins: int = 10) → CalibrationReport[source]

Check whether recorded win probabilities were calibrated.

Strategies size from a stated probability, so a miscalibrated one is the main way Kelly-family sizing loses money. This is an educational diagnostic of past data, not advice: a good score on a short record does not show the probabilities will stay calibrated.

Parameters:
  • probabilities (sequence of float) – Stated win probability of each event, each finite and in [0, 1].

  • outcomes (sequence of bool or int) – Whether each event won: bool or 0/1 only (floats such as 1.0 are rejected), same length as probabilities.

  • n_bins (int, default 10) – Number of equal-width bins over [0, 1].

Returns:

Frozen dataclass of Brier score, log loss, base rate and per-bin stated-versus-observed frequencies.

Return type:

CalibrationReport

Raises:

ValueError – If an input is invalid, the lengths differ, there are no observations, or n_bins is not a positive integer.

Examples

>>> report = calibration_report([0.2, 0.2, 0.8, 0.8], [0, 1, 1, 1])
>>> report.n, round(report.brier_score, 6), report.base_rate
(4, 0.19, 0.75)
>>> [(b.lower, b.count, b.observed_frequency) for b in report.bins]
[(0.2, 2, 0.5), (0.8, 2, 1.0)]
class keeks.calibration.CalibrationReport(n: int, brier_score: float, log_loss: float, min_clipped: bool, max_clipped: bool, base_rate: float, bins: tuple[CalibrationBin, ...])[source]

Bases: object

Calibration diagnostics for recorded probabilities and outcomes.

n

Number of observations.

Type:

int

brier_score

Mean squared error between probability and outcome.

Type:

float

log_loss

Mean negative log-likelihood, with probabilities clipped to [1e-12, 1 - 1e-12].

Type:

float

min_clipped, max_clipped

Whether any probability fell below / above the clip bounds, so the log loss reflects the bound rather than the stated value.

Type:

bool

base_rate

Fraction of outcomes that were wins.

Type:

float

bins

Non-empty bins in ascending order; empty bins are omitted.

Type:

tuple of CalibrationBin

class keeks.calibration.CalibrationBin(lower: float, upper: float, count: int, mean_predicted: float, observed_frequency: float)[source]

Bases: object

One non-empty equal-width probability bin.

lower, upper

Bin edges. Bins include their lower edge; only the last bin also includes 1.0.

Type:

float

count

Observations in the bin.

Type:

int

mean_predicted

Mean stated probability in the bin.

Type:

float

observed_frequency

Fraction of the bin’s outcomes that were wins.

Type:

float

Loading a recorded bet log

keeks.bet_log.load_bet_log() reads a CSV with a header row and returns the parallel (probabilities, outcomes) lists that calibration_report, HistoricalBinarySimulator and compare_strategies take. Rows keep their file order. Probabilities must be finite and in [0, 1]; outcomes may be 0, 1, true or false (case-insensitive). Errors are ValueErrors naming the column and the 1-based data row.

from keeks import calibration_report, load_bet_log

probabilities, outcomes = load_bet_log("my_bets.csv")
report = calibration_report(probabilities, outcomes)
keeks.bet_log.load_bet_log(path: str | PathLike[str], *, probability_column: str = 'probability', outcome_column: str = 'outcome') → tuple[list[float], list[int]][source]

Read chronological probabilities and 0/1 outcomes from a CSV file.

Row order is preserved. Probabilities must be finite and in [0, 1]; outcomes may be 0, 1, true or false (case-insensitive). Any problem raises ValueError naming the file column and the 1-based data row (the header is not counted).

Parameters:
  • path (str or os.PathLike) – CSV file with a header row.

  • probability_column (str) – Header names of the two columns to read.

  • outcome_column (str) – Header names of the two columns to read.

Returns:

(probabilities, outcomes), ready for calibration_report, HistoricalBinarySimulator or compare_strategies.

Return type:

tuple[list[float], list[int]]

Examples

>>> import tempfile
>>> from pathlib import Path
>>> with tempfile.TemporaryDirectory() as folder:
...     log = Path(folder) / "log.csv"
...     _ = log.write_text("probability,outcome\n0.6,1\n0.4,false\n")
...     load_bet_log(log)
([0.6, 0.4], [1, 0])