Bankroll

The bankroll object simulates the financial side of simulations. It takes into account things like max drawdown limits and in the future may include risk free rate investments, interest and concepts like that.

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.

Summarizing a run

evaluate_strategy mutates the bankroll and returns nothing, so keeks.metrics.summarize_history() reduces bankroll.history to the numbers that distinguish strategies: bets recorded, total return, geometric growth per bet, maximum drawdown against the running peak, and whether the run ended ruined. Values that cannot be defined (growth with no recorded bets or nonpositive end funds, return from a zero start) are None rather than 0.0; an empty, non-finite or negative history raises ValueError.

from keeks import summarize_history

summary = summarize_history(bankroll.history)
print(summary.max_drawdown, summary.geometric_growth_per_period)
keeks.metrics.summarize_history(history: Sequence[float]) → HistorySummary[source]

Summarize a BankRoll.history.

Parameters:

history (sequence of float) – Bankroll values in time order, starting with the initial funds.

Returns:

Frozen dataclass of drawdown, growth and ruin statistics. Values that cannot be defined are None rather than 0.0.

Return type:

HistorySummary

Raises:

ValueError – If the history is empty, or holds a non-finite or negative value.

Examples

>>> summary = summarize_history([1000.0, 1250.0, 1000.0])
>>> summary.bets, summary.total_return, summary.max_drawdown
(2, 0.0, 0.2)
class keeks.metrics.HistorySummary(bets: int, start: float, end: float, total_return: float | None, geometric_growth_per_period: float | None, max_drawdown: float | None, ruined: bool)[source]

Bases: object

End-of-run statistics for a bankroll history.

bets

Periods recorded after the starting entry (len(history) - 1).

Type:

int

start, end

The first and last history entries.

Type:

float

total_return

end / start - 1; None when start is not positive.

Type:

float or None

geometric_growth_per_period

(end / start) ** (1 / bets) - 1; None when undefined (no recorded periods, or a nonpositive start or end).

Type:

float or None

max_drawdown

Largest peak-to-trough fractional loss, in [0, 1], measured against the running peak; None when no entry is ever positive.

Type:

float or None

ruined

Whether the final funds are at or below zero.

Type:

bool

Comparing strategies on a recorded log

keeks.comparison.compare_strategies() replays one recorded log through several strategies, each from a fresh strategy and a fresh bankroll, and returns one StrategyComparison per strategy (name plus its HistorySummary), ranked by sort_by with undefined (None) values last.

from keeks import FixedFractionStrategy, KellyCriterion, compare_strategies

odds = {"payoff": 1.0, "loss": 1.0, "transaction_cost_rate": 0.0}
rows = compare_strategies(
    {
        "Kelly": lambda: KellyCriterion(**odds),
        "Fixed 5%": lambda: FixedFractionStrategy(**odds, fraction=0.05),
    },
    probabilities,
    outcomes,
    payoff=1.0,
    loss=1.0,
    fee_per_bet=0.0,
    initial_funds=1000.0,
)
for row in rows:
    print(row.name, row.summary.max_drawdown)
keeks.comparison.compare_strategies(strategies: Mapping[str, Callable[[], BaseStrategy]], probabilities: Sequence[float], outcomes: Sequence[bool | int], *, payoff: float, loss: float, fee_per_bet: float, initial_funds: float, percent_bettable: float = 1.0, max_transaction_loss: float | None = None, sort_by: str = 'geometric_growth_per_period') → tuple[StrategyComparison, ...][source]

Replay one recorded bet log through several strategies and rank them.

Each strategy is built by calling its factory and runs against a fresh BankRoll, so no state carries from one run to the next. Replay is by HistoricalBinarySimulator; a strategy whose odds differ from payoff/loss raises its ValueError unchanged. This is an educational look at past data, not advice.

Parameters:
  • strategies (mapping of str to callable) – Strategy name to a zero-argument factory returning a fresh strategy. Must be non-empty.

  • probabilities (sequence) – The recorded log, as for HistoricalBinarySimulator.

  • outcomes (sequence) – The recorded log, as for HistoricalBinarySimulator.

  • payoff (float) – The odds and flat fee passed to the simulator.

  • loss (float) – The odds and flat fee passed to the simulator.

  • fee_per_bet (float) – The odds and flat fee passed to the simulator.

  • initial_funds (float) – Passed to each fresh BankRoll.

  • percent_bettable (float) – Passed to each fresh BankRoll.

  • max_transaction_loss (float) – Passed to each fresh BankRoll.

  • sort_by (str, default="geometric_growth_per_period") – A numeric or boolean HistorySummary field: total_return, geometric_growth_per_period, end, max_drawdown or ruined. max_drawdown and ruined rank smaller first, the rest larger first; None values always rank last, and ties keep the mapping’s order.

Returns:

One row per strategy, best first.

Return type:

tuple of StrategyComparison

Raises:

ValueError – If the arguments are invalid; checked before any strategy runs.

Examples

>>> from keeks import FixedFractionStrategy
>>> odds = {"payoff": 1.0, "loss": 1.0, "transaction_cost_rate": 0.0}
>>> rows = compare_strategies(
...     {
...         "5%": lambda: FixedFractionStrategy(**odds, fraction=0.05),
...         "20%": lambda: FixedFractionStrategy(**odds, fraction=0.2),
...     },
...     [0.6, 0.6, 0.6],
...     [1, 0, 1],
...     payoff=1.0, loss=1.0, fee_per_bet=0.0, initial_funds=100.0,
... )
>>> [(row.name, round(row.summary.end, 2)) for row in rows]
[('20%', 115.2), ('5%', 104.74)]
class keeks.comparison.StrategyComparison(name: str, summary: HistorySummary)[source]

Bases: object

One strategy’s replay result: its name plus the run’s summary.

name

The key the strategy was supplied under.

Type:

str

summary

summarize_history() of the replayed bankroll.

Type:

HistorySummary

How sure is the ranking?

A single replay is one draw. keeks.comparison.bootstrap_comparison() resamples the log’s bets (block_size > 1 resamples moving blocks), replays every strategy on the same resample so differences are paired, and returns a BootstrapRow per strategy: the full-log point value, percentile lower/upper bounds, and rank_counts (how often it ranked first, second, …). seed is required and uses a private generator. Resamples where the metric is undefined are excluded from the bounds and counted in undefined.

>>> from keeks import FixedFractionStrategy, bootstrap_comparison
>>> odds = {"payoff": 1.0, "loss": 1.0, "transaction_cost_rate": 0.0}
>>> rows = bootstrap_comparison(
...     {
...         "Fixed 1%": lambda: FixedFractionStrategy(**odds, fraction=0.01),
...         "Fixed 10%": lambda: FixedFractionStrategy(**odds, fraction=0.1),
...     },
...     [0.7] * 8,
...     [1] * 8,
...     payoff=1.0, loss=1.0, fee_per_bet=0.0, initial_funds=100.0,
...     n_resamples=20, seed=1,
... )
>>> [(row.name, round(row.point, 3), row.rank_counts) for row in rows]
[('Fixed 1%', 0.01, (0, 20)), ('Fixed 10%', 0.1, (20, 0))]

Note

The intervals reflect sampling of the recorded log only. They say nothing about model error, a changed edge, or a different future. Runtime is n_resamples times one compare_strategies() call.

keeks.comparison.bootstrap_comparison(strategies: Mapping[str, Callable[[], BaseStrategy]], probabilities: Sequence[float], outcomes: Sequence[bool | int], *, payoff: float, loss: float, fee_per_bet: float, initial_funds: float, percent_bettable: float = 1.0, max_transaction_loss: float | None = None, n_resamples: int = 200, block_size: int = 1, seed: int | None = None, confidence: float = 0.9, sort_by: str = 'geometric_growth_per_period') → tuple[BootstrapRow, ...][source]

Put percentile intervals and rank counts on a strategy comparison.

The log’s bets are resampled with replacement (or in moving blocks of block_size), kept in chronological order, and every strategy is replayed on the same resample via compare_strategies(), so differences between strategies are paired. Intervals reflect sampling of the recorded log only, not model error or a different future. Runtime is n_resamples times the cost of one compare_strategies() call.

Parameters:
Returns:

One row per strategy in the mapping’s order.

Return type:

tuple of BootstrapRow

Raises:

ValueError – If n_resamples, block_size, confidence or seed is invalid, or the log or strategies are; checked before any replay.

Examples

>>> from keeks import FixedFractionStrategy
>>> odds = {"payoff": 1.0, "loss": 1.0, "transaction_cost_rate": 0.0}
>>> rows = bootstrap_comparison(
...     {
...         "none": lambda: FixedFractionStrategy(**odds, fraction=0.0),
...         "10%": lambda: FixedFractionStrategy(**odds, fraction=0.1),
...     },
...     [0.7] * 8,
...     [1] * 8,
...     payoff=1.0, loss=1.0, fee_per_bet=0.0, initial_funds=100.0,
...     n_resamples=20, seed=1,
... )
>>> [(row.name, row.rank_counts) for row in rows]
[('none', (0, 20)), ('10%', (20, 0))]
class keeks.comparison.BootstrapRow(name: str, point: float | None, lower: float | None, upper: float | None, undefined: int, rank_counts: tuple[int, ...])[source]

Bases: object

One strategy’s bootstrap result for the sort_by metric.

name

The key the strategy was supplied under.

Type:

str

point

The metric on the full log (None if undefined there).

Type:

float or None

lower, upper

Percentile bounds over the resamples where the metric was defined (None if it was never defined).

Type:

float or None

undefined

Resamples where the metric was undefined; excluded from the bounds.

Type:

int

rank_counts

rank_counts[k] is how many resamples ranked the strategy k-th (zero-based, best first).

Type:

tuple of int