keeks 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,548 @@
1
+ import numpy as np
2
+
3
+ from keeks.binary_strategies.base import BaseStrategy
4
+
5
+ __author__ = "willmcginnis"
6
+
7
+
8
+ class NaiveStrategy(BaseStrategy):
9
+ """
10
+ A simple betting strategy that bets based on expected value.
11
+
12
+ This strategy calculates the expected value of a bet and bets a fixed
13
+ fraction of the bankroll if the expected value is positive.
14
+
15
+ Parameters
16
+ ----------
17
+ payoff : float
18
+ The amount won per unit bet on a successful outcome.
19
+ loss : float
20
+ The amount lost per unit bet on an unsuccessful outcome.
21
+ transaction_cost : float
22
+ The fixed cost per transaction, regardless of outcome.
23
+ """
24
+
25
+ def __init__(self, payoff, loss, transaction_cost):
26
+ """
27
+ Initialize the NaiveStrategy.
28
+
29
+ Parameters
30
+ ----------
31
+ payoff : float
32
+ The amount won per unit bet on a successful outcome.
33
+ loss : float
34
+ The amount lost per unit bet on an unsuccessful outcome.
35
+ transaction_cost : float
36
+ The fixed cost per transaction, regardless of outcome.
37
+ """
38
+ super().__init__(payoff, loss, transaction_cost)
39
+
40
+ def evaluate(self, probability, current_bankroll):
41
+ """
42
+ Calculate the bet size based on expected value.
43
+
44
+ The expected value is calculated as:
45
+ EV = (probability * payoff) - ((1 - probability) * loss) - transaction_cost
46
+
47
+ If EV is positive, bet a fixed fraction of the bankroll.
48
+ If EV is negative, do not bet.
49
+
50
+ Parameters
51
+ ----------
52
+ probability : float
53
+ The probability of a successful outcome, typically between 0 and 1.
54
+ current_bankroll : float
55
+ The current bankroll amount.
56
+
57
+ Returns
58
+ -------
59
+ float
60
+ The proportion of the bankroll to bet.
61
+ """
62
+ # Calculate expected value
63
+ expected_value = (
64
+ (probability * self.payoff)
65
+ - ((1 - probability) * self.loss)
66
+ - self.transaction_cost
67
+ )
68
+
69
+ # If expected value is negative, do not bet
70
+ if expected_value <= 0:
71
+ return 0.0
72
+
73
+ # Calculate bet size based on expected value
74
+ bet_size = expected_value / self.payoff
75
+
76
+ # Ensure we never bet more than would result in negative bankroll
77
+ return min(max(0, bet_size), self.get_max_safe_bet(current_bankroll))
78
+
79
+
80
+ class FixedFractionStrategy(BaseStrategy):
81
+ """
82
+ A simple strategy that bets a fixed percentage of the bankroll.
83
+
84
+ This strategy ignores probabilities and odds completely, and simply
85
+ wagers a fixed fraction of the bankroll. It can be used as a baseline
86
+ for comparison or as a simple approach for risk management.
87
+
88
+ Parameters
89
+ ----------
90
+ fraction : float
91
+ The fixed fraction of the bankroll to bet (between 0 and 1).
92
+ payoff : float
93
+ The amount won per unit bet on a successful outcome.
94
+ loss : float
95
+ The amount lost per unit bet on an unsuccessful outcome.
96
+ transaction_cost : float, default=0
97
+ The fixed cost per transaction, regardless of outcome.
98
+ min_probability : float, default=0.5
99
+ The minimum probability required to place a bet.
100
+ """
101
+
102
+ def __init__(self, fraction, payoff, loss, transaction_cost=0, min_probability=0.5):
103
+ """
104
+ Initialize the FixedFractionStrategy.
105
+
106
+ Parameters
107
+ ----------
108
+ fraction : float
109
+ The fixed fraction of the bankroll to bet (between 0 and 1).
110
+ payoff : float
111
+ The amount won per unit bet on a successful outcome.
112
+ loss : float
113
+ The amount lost per unit bet on an unsuccessful outcome.
114
+ transaction_cost : float, default=0
115
+ The fixed cost per transaction, regardless of outcome.
116
+ min_probability : float, default=0.5
117
+ The minimum probability required to place a bet.
118
+ """
119
+ if not 0 <= fraction <= 1:
120
+ raise ValueError("Fraction must be between 0 and 1")
121
+ if not 0 <= min_probability <= 1:
122
+ raise ValueError("Minimum probability must be between 0 and 1")
123
+
124
+ super().__init__(payoff, loss, transaction_cost)
125
+ self.fraction = fraction
126
+ self.min_probability = min_probability
127
+
128
+ def evaluate(self, probability, current_bankroll):
129
+ """
130
+ Return the fixed fraction if the probability meets the minimum threshold.
131
+
132
+ Parameters
133
+ ----------
134
+ probability : float
135
+ The probability of a successful outcome, typically between 0 and 1.
136
+ current_bankroll : float
137
+ The current bankroll amount.
138
+
139
+ Returns
140
+ -------
141
+ float
142
+ The fixed fraction if probability >= min_probability, otherwise 0.
143
+ """
144
+ if probability >= self.min_probability:
145
+ # Ensure we never bet more than would result in negative bankroll
146
+ return min(self.fraction, self.get_max_safe_bet(current_bankroll))
147
+ else:
148
+ return 0.0
149
+
150
+
151
+ class CPPIStrategy(BaseStrategy):
152
+ """
153
+ Implementation of Constant Proportion Portfolio Insurance (CPPI) for binary betting.
154
+
155
+ This strategy maintains a dynamic floor below which the bankroll should not fall,
156
+ and invests a multiple of the cushion (amount above floor) in each bet.
157
+
158
+ Parameters
159
+ ----------
160
+ floor_fraction : float
161
+ The fraction of initial bankroll to maintain as a floor.
162
+ multiplier : float
163
+ The multiplier to apply to the cushion for determining exposure.
164
+ initial_bankroll : float
165
+ The initial bankroll amount.
166
+ payoff : float
167
+ The amount won per unit bet on a successful outcome.
168
+ loss : float
169
+ The amount lost per unit bet on an unsuccessful outcome.
170
+ transaction_cost : float
171
+ The fixed cost per transaction, regardless of outcome.
172
+ min_probability : float, default=0.5
173
+ The minimum probability required to place a bet.
174
+ """
175
+
176
+ def __init__(
177
+ self,
178
+ floor_fraction,
179
+ multiplier,
180
+ initial_bankroll,
181
+ payoff,
182
+ loss,
183
+ transaction_cost=0,
184
+ min_probability=0.5,
185
+ ):
186
+ """Initialize the CPPI strategy."""
187
+ if not 0 < floor_fraction < 1:
188
+ raise ValueError("Floor fraction must be between 0 and 1")
189
+ if multiplier <= 0:
190
+ raise ValueError("Multiplier must be greater than 0")
191
+ if initial_bankroll <= 0:
192
+ raise ValueError("Initial bankroll must be greater than 0")
193
+ if not 0 <= min_probability <= 1:
194
+ raise ValueError("Minimum probability must be between 0 and 1")
195
+
196
+ super().__init__(payoff, loss, transaction_cost)
197
+ self.floor_fraction = floor_fraction
198
+ self.multiplier = multiplier
199
+ self.floor = floor_fraction * initial_bankroll
200
+ self.min_probability = min_probability
201
+ self.current_bankroll = initial_bankroll
202
+ self.peak_bankroll = initial_bankroll
203
+
204
+ def update_bankroll(self, new_bankroll):
205
+ """
206
+ Update the current bankroll value and adjust floor based on peak value.
207
+
208
+ Parameters
209
+ ----------
210
+ new_bankroll : float
211
+ The current value of the bankroll.
212
+ """
213
+ self.current_bankroll = new_bankroll
214
+ if new_bankroll > self.peak_bankroll:
215
+ self.peak_bankroll = new_bankroll
216
+ # Adjust floor to maintain the same fraction of peak value
217
+ self.floor = self.floor_fraction * self.peak_bankroll
218
+
219
+ def evaluate(self, probability, current_bankroll):
220
+ """
221
+ Calculate the CPPI bet size based on the cushion above the floor.
222
+
223
+ Parameters
224
+ ----------
225
+ probability : float
226
+ The probability of a successful outcome, typically between 0 and 1.
227
+ current_bankroll : float
228
+ The current bankroll amount.
229
+
230
+ Returns
231
+ -------
232
+ float
233
+ The proportion of the current bankroll to bet, or 0 if below
234
+ the minimum probability threshold.
235
+ """
236
+ # Update current bankroll
237
+ self.update_bankroll(current_bankroll)
238
+
239
+ # Check probability threshold
240
+ if probability < self.min_probability:
241
+ return 0.0
242
+
243
+ # Calculate expected value per unit bet
244
+ expected_value = probability * (self.payoff - self.transaction_cost) - (
245
+ 1 - probability
246
+ ) * (self.loss + self.transaction_cost)
247
+
248
+ # If expected value is negative or too close to zero, don't bet
249
+ if expected_value <= 0.01: # Require at least 1% edge after transaction costs
250
+ return 0.0
251
+
252
+ # Calculate the cushion (amount above the floor)
253
+ cushion = max(0, current_bankroll - self.floor)
254
+
255
+ # Calculate the exposure (amount to bet)
256
+ # Scale the multiplier by the expected value to be more conservative
257
+ # when the edge is smaller
258
+ adjusted_multiplier = self.multiplier * min(1.0, expected_value)
259
+ exposure = adjusted_multiplier * cushion
260
+
261
+ # Convert to a proportion of the current bankroll
262
+ if current_bankroll <= 0:
263
+ return 0.0
264
+
265
+ # Calculate proportion
266
+ proportion = min(1.0, exposure / current_bankroll)
267
+
268
+ # Adjust for transaction costs and ensure we don't risk going below floor
269
+ if proportion > 0:
270
+ # Calculate the maximum bet that ensures we stay above the floor
271
+ # even in the worst case (loss + transaction cost)
272
+ max_floor_bet = (current_bankroll - self.floor) / (
273
+ current_bankroll * (self.loss + self.transaction_cost)
274
+ )
275
+
276
+ # Get the maximum safe bet considering ruin
277
+ max_safe_bet = self.get_max_safe_bet(current_bankroll)
278
+
279
+ # Take the minimum of all constraints
280
+ proportion = min(proportion, max_floor_bet, max_safe_bet)
281
+
282
+ return max(0, proportion)
283
+
284
+
285
+ class DynamicBankrollManagement(BaseStrategy):
286
+ """
287
+ A dynamic bankroll management strategy that adjusts bet sizes based on performance.
288
+
289
+ This strategy adjusts the base bet fraction based on:
290
+ 1. Recent performance (win/loss streak)
291
+ 2. Volatility of returns
292
+ 3. Current drawdown level
293
+ 4. Probability of success
294
+
295
+ Parameters
296
+ ----------
297
+ base_fraction : float
298
+ The base fraction of bankroll to bet.
299
+ payoff : float
300
+ The amount won per unit bet on a successful outcome.
301
+ loss : float
302
+ The amount lost per unit bet on an unsuccessful outcome.
303
+ transaction_cost : float
304
+ The fixed cost per transaction, regardless of outcome.
305
+ window_size : int, default=10
306
+ The number of recent results to consider for adjustments.
307
+ max_fraction : float, default=0.2
308
+ The maximum fraction of bankroll that can be bet.
309
+ min_fraction : float, default=0.05
310
+ The minimum fraction of bankroll to bet.
311
+ """
312
+
313
+ def __init__(
314
+ self,
315
+ base_fraction,
316
+ payoff,
317
+ loss,
318
+ transaction_cost,
319
+ window_size=10,
320
+ max_fraction=0.2,
321
+ min_fraction=0.05,
322
+ ):
323
+ """
324
+ Initialize the DynamicBankrollManagement strategy.
325
+ """
326
+ if not 0 <= base_fraction <= 1:
327
+ raise ValueError("Base fraction must be between 0 and 1")
328
+ if not window_size > 0:
329
+ raise ValueError("Window size must be positive")
330
+ if not 0 <= min_fraction <= max_fraction <= 1:
331
+ raise ValueError(
332
+ "Min fraction must be between 0 and max fraction, and max fraction must be between 0 and 1"
333
+ )
334
+
335
+ super().__init__(payoff, loss, transaction_cost)
336
+ self.base_fraction = base_fraction
337
+ self.window_size = window_size
338
+ self.max_fraction = max_fraction
339
+ self.min_fraction = min_fraction
340
+ self.results = []
341
+ self.initial_bankroll = None
342
+ self.current_bankroll = None
343
+ self.peak_bankroll = None
344
+
345
+ def record_result(self, won, return_pct=None):
346
+ """
347
+ Record the result of a bet.
348
+
349
+ Parameters
350
+ ----------
351
+ won : bool
352
+ Whether the bet was won.
353
+ return_pct : float, optional
354
+ The return percentage of the bet. If not provided, calculated from won/loss.
355
+ """
356
+ if return_pct is None:
357
+ return_pct = self.payoff if won else -self.loss
358
+
359
+ self.results.append(return_pct)
360
+ if len(self.results) > self.window_size:
361
+ self.results.pop(0)
362
+
363
+ def get_streak_factor(self):
364
+ """Calculate the adjustment factor based on recent performance."""
365
+ if not self.results:
366
+ return 1.0
367
+
368
+ # Scale factor by window size to make smaller windows more responsive
369
+ scale = min(len(self.results), self.window_size) / self.window_size
370
+
371
+ wins = sum(1 for r in self.results if r > 0)
372
+ losses = sum(1 for r in self.results if r < 0)
373
+
374
+ if losses == 0:
375
+ return 1.0 + (0.5 * scale) # Maximum boost for all wins
376
+ if wins == 0:
377
+ return 1.0 - (0.5 * scale) # Maximum reduction for all losses
378
+
379
+ win_ratio = wins / (wins + losses)
380
+ return 1.0 + ((win_ratio - 0.5) * scale)
381
+
382
+ def get_volatility_factor(self):
383
+ """Calculate the adjustment factor based on return volatility."""
384
+ if not self.results:
385
+ return 1.0
386
+
387
+ returns = np.array(self.results)
388
+ volatility = np.std(returns)
389
+
390
+ if volatility == 0:
391
+ return 1.0
392
+
393
+ # Scale factor by window size to make smaller windows more responsive
394
+ scale = min(len(self.results), self.window_size) / self.window_size
395
+ return max(0.5, 1.0 - (volatility * scale))
396
+
397
+ def get_drawdown_factor(self):
398
+ """Calculate the adjustment factor based on current drawdown."""
399
+ if self.current_bankroll is None or self.peak_bankroll is None:
400
+ return 1.0
401
+
402
+ drawdown = 1.0 - (self.current_bankroll / self.peak_bankroll)
403
+ return max(0.5, 1.0 - drawdown)
404
+
405
+ def get_probability_factor(self, probability):
406
+ """Calculate the adjustment factor based on probability."""
407
+ # Only apply probability factor if we have some results
408
+ if not self.results:
409
+ return 1.0
410
+
411
+ # Scale linearly from 0.5 at 50% probability to 1.5 at 100% probability
412
+ return max(0.5, min(1.5, 1.0 + (probability - 0.5)))
413
+
414
+ def evaluate(self, probability, current_bankroll):
415
+ """
416
+ Calculate the bet size based on all adjustment factors.
417
+
418
+ Parameters
419
+ ----------
420
+ probability : float
421
+ The probability of a successful outcome.
422
+ current_bankroll : float
423
+ The current bankroll amount.
424
+
425
+ Returns
426
+ -------
427
+ float
428
+ The proportion of the current bankroll to bet.
429
+ """
430
+ # Initialize or update bankroll tracking
431
+ if self.initial_bankroll is None:
432
+ self.initial_bankroll = current_bankroll
433
+ self.peak_bankroll = current_bankroll
434
+ self.current_bankroll = current_bankroll
435
+ self.peak_bankroll = max(self.peak_bankroll, current_bankroll)
436
+
437
+ # Get all adjustment factors
438
+ streak_factor = self.get_streak_factor()
439
+ volatility_factor = self.get_volatility_factor()
440
+ drawdown_factor = self.get_drawdown_factor()
441
+ probability_factor = self.get_probability_factor(probability)
442
+
443
+ # Combine all factors
444
+ combined_factor = (
445
+ streak_factor * volatility_factor * drawdown_factor * probability_factor
446
+ )
447
+
448
+ # Calculate adjusted bet size
449
+ bet_size = self.base_fraction * combined_factor
450
+
451
+ # Ensure bet size is within bounds
452
+ return max(self.min_fraction, min(self.max_fraction, bet_size))
453
+
454
+
455
+ class OptimalF(BaseStrategy):
456
+ """
457
+ Implementation of the Optimal f strategy for binary betting.
458
+
459
+ This strategy calculates the optimal fraction of bankroll to bet based on
460
+ the win rate and payoff ratio. It's similar to Kelly but uses a different
461
+ approach to risk management.
462
+
463
+ Parameters
464
+ ----------
465
+ payoff : float
466
+ The amount won per unit bet on a successful outcome.
467
+ loss : float
468
+ The amount lost per unit bet on an unsuccessful outcome.
469
+ transaction_cost : float
470
+ The fixed cost per transaction, regardless of outcome.
471
+ win_rate : float
472
+ The historical or expected win rate (between 0 and 1).
473
+ max_risk_fraction : float, default=0.2
474
+ The maximum fraction of bankroll that can be risked on a single bet.
475
+ """
476
+
477
+ def __init__(self, payoff, loss, transaction_cost, win_rate, max_risk_fraction=0.2):
478
+ """
479
+ Initialize the OptimalF strategy.
480
+
481
+ Parameters
482
+ ----------
483
+ payoff : float
484
+ The amount won per unit bet on a successful outcome.
485
+ loss : float
486
+ The amount lost per unit bet on an unsuccessful outcome.
487
+ transaction_cost : float
488
+ The fixed cost per transaction, regardless of outcome.
489
+ win_rate : float
490
+ The historical or expected win rate (between 0 and 1).
491
+ max_risk_fraction : float, default=0.2
492
+ The maximum fraction of bankroll that can be risked on a single bet.
493
+ """
494
+ if not 0 <= win_rate <= 1:
495
+ raise ValueError("Win rate must be between 0 and 1")
496
+ if not 0 < max_risk_fraction <= 1:
497
+ raise ValueError("Maximum risk fraction must be between 0 and 1")
498
+
499
+ super().__init__(payoff, loss, transaction_cost)
500
+ self.win_rate = win_rate
501
+ self.max_risk_fraction = max_risk_fraction
502
+
503
+ def evaluate(self, probability, current_bankroll):
504
+ """
505
+ Calculate the optimal f bet size based on win rate and payoff ratio.
506
+
507
+ This implementation follows Ralph Vince's approach to Optimal f, which
508
+ is based on maximizing the terminal wealth relative (TWR) for a given
509
+ risk-to-reward profile.
510
+
511
+ Parameters
512
+ ----------
513
+ probability : float
514
+ The probability of a successful outcome, typically between 0 and 1.
515
+ current_bankroll : float
516
+ The current bankroll amount.
517
+
518
+ Returns
519
+ -------
520
+ float
521
+ The optimal proportion of the bankroll to bet.
522
+ """
523
+ if probability < 0.5: # Use 0.5 as default minimum probability
524
+ return 0.0
525
+
526
+ # Use the provided probability to adjust our expectations
527
+ # If no specific edge is known, the win_rate will be used directly
528
+ adjusted_win_rate = probability if probability > 0 else self.win_rate
529
+ adjusted_loss_rate = 1 - adjusted_win_rate
530
+
531
+ # Calculate the risk-to-reward ratio (R-multiple)
532
+ reward = self.payoff - self.transaction_cost
533
+ risk = self.loss + self.transaction_cost
534
+
535
+ # If transaction costs make the bet unprofitable, don't bet
536
+ if reward <= 0:
537
+ return 0.0
538
+
539
+ # Calculate optimal f using the formula:
540
+ # f* = W - (1-W)/(R/L) where W is win rate, R is reward, L is risk
541
+ # This is more aligned with Ralph Vince's approach
542
+ optimal_f = adjusted_win_rate - (adjusted_loss_rate / (reward / risk))
543
+
544
+ # Cap at our maximum risk fraction
545
+ optimal_f = min(max(0, optimal_f), self.max_risk_fraction)
546
+
547
+ # Ensure we never bet more than would result in negative bankroll
548
+ return min(optimal_f, self.get_max_safe_bet(current_bankroll))
@@ -0,0 +1,11 @@
1
+ """
2
+ Simulators for evaluating betting strategies in the keeks package.
3
+
4
+ This module provides various simulators for testing betting strategies:
5
+ - RandomBinarySimulator: Simulates bets with random probabilities
6
+ - RandomUncertainBinarySimulator: Adds uncertainty to the actual outcome probabilities
7
+ - RepeatedBinarySimulator: Simulates repeated bets with a fixed probability
8
+
9
+ These simulators can be used to evaluate the performance of different betting strategies
10
+ under various conditions.
11
+ """
@@ -0,0 +1,66 @@
1
+ import random
2
+
3
+ import numpy as np
4
+
5
+
6
+ class RandomBinarySimulator:
7
+ """
8
+ Simulator for binary betting strategies with random probabilities.
9
+
10
+ This simulator generates random probabilities for each trial, centered around 0.5
11
+ with a configurable standard deviation. It evaluates a betting strategy against
12
+ these random probabilities and updates the bankroll accordingly.
13
+
14
+ Parameters
15
+ ----------
16
+ payoff : float
17
+ The amount won per unit bet on a successful outcome.
18
+ loss : float
19
+ The amount lost per unit bet on an unsuccessful outcome.
20
+ transaction_costs : float
21
+ The fixed cost per transaction, regardless of outcome.
22
+ trials : int, default=1000
23
+ The number of betting trials to simulate.
24
+ stdev : float, default=0.1
25
+ The standard deviation of the normal distribution used to generate probabilities.
26
+ """
27
+
28
+ def __init__(self, payoff, loss, transaction_costs, trials=1000, stdev=0.1):
29
+ self.payoff = payoff
30
+ self.loss = loss
31
+ self.transaction_costs = transaction_costs
32
+ self.trials = trials
33
+ self.stdev = stdev
34
+
35
+ def evaluate_strategy(self, strategy, bankroll):
36
+ """
37
+ Evaluate a betting strategy over multiple trials.
38
+
39
+ For each trial, a random probability is generated, the strategy is evaluated
40
+ with this probability, and the bankroll is updated based on the outcome.
41
+
42
+ Parameters
43
+ ----------
44
+ strategy : BaseStrategy
45
+ The betting strategy to evaluate.
46
+ bankroll : BankRoll
47
+ The bankroll to use for the simulation.
48
+
49
+ Returns
50
+ -------
51
+ None
52
+ The bankroll object is updated in-place with the results of the simulation.
53
+ """
54
+ for _ in range(self.trials):
55
+ probability = np.random.normal(0.5, self.stdev, 1)[0]
56
+ proportion = strategy.evaluate(probability, bankroll.total_funds)
57
+ if random.random() < probability:
58
+ amt = (
59
+ self.payoff * bankroll.bettable_funds * proportion
60
+ ) - self.transaction_costs
61
+ bankroll.deposit(amt)
62
+ else:
63
+ bankroll.withdraw(
64
+ (self.loss * bankroll.bettable_funds * proportion)
65
+ - self.transaction_costs
66
+ )