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:
ExceptionException 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_lossfraction of current funds. The message names the attempted amount, the configured limit, and the current funds; the simulators re-report it as aUserWarningwhen 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
Nonerather than0.0.- Return type:
- 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:
objectEnd-of-run statistics for a bankroll history.
- start, end
The first and last history entries.
- Type:
- geometric_growth_per_period¶
(end / start) ** (1 / bets) - 1;Nonewhen undefined (no recorded periods, or a nonpositivestartorend).- Type:
float or None
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 byHistoricalBinarySimulator; a strategy whose odds differ frompayoff/lossraises itsValueErrorunchanged. 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
HistorySummaryfield:total_return,geometric_growth_per_period,end,max_drawdownorruined.max_drawdownandruinedrank smaller first, the rest larger first;Nonevalues always rank last, and ties keep the mapping’s order.
- Returns:
One row per strategy, best first.
- Return type:
- 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:
objectOne strategy’s replay result: its name plus the run’s summary.
- summary¶
summarize_history()of the replayed bankroll.- Type:
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 viacompare_strategies(), so differences between strategies are paired. Intervals reflect sampling of the recorded log only, not model error or a different future. Runtime isn_resamplestimes the cost of onecompare_strategies()call.- Parameters:
strategies – As for
compare_strategies().probabilities – As for
compare_strategies().outcomes – As for
compare_strategies().payoff – As for
compare_strategies().loss – As for
compare_strategies().fee_per_bet – As for
compare_strategies().initial_funds – As for
compare_strategies().percent_bettable – As for
compare_strategies().max_transaction_loss – As for
compare_strategies().sort_by – As for
compare_strategies().n_resamples (int, default=200) – Number of bootstrap resamples (at least 1).
block_size (int, default=1) – Length of the moving blocks; 1 resamples single bets. Must not exceed the log length.
seed (int) – Required nonnegative integer seeding a private
numpy.random.Generator; the global RNG is never used.confidence (float, default=0.9) – Central interval mass, strictly between 0 and 1.
- Returns:
One row per strategy in the mapping’s order.
- Return type:
- Raises:
ValueError – If
n_resamples,block_size,confidenceorseedis 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:
objectOne strategy’s bootstrap result for the
sort_bymetric.- lower, upper
Percentile bounds over the resamples where the metric was defined (
Noneif it was never defined).- Type:
float or None