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.
- keeks/__init__.py +14 -0
- keeks/bankroll.py +175 -0
- keeks/binary_strategies/__init__.py +41 -0
- keeks/binary_strategies/base.py +84 -0
- keeks/binary_strategies/kelly.py +227 -0
- keeks/binary_strategies/simple.py +548 -0
- keeks/simulators/__init__.py +11 -0
- keeks/simulators/random_binary.py +66 -0
- keeks/simulators/random_uncertain_binary.py +82 -0
- keeks/simulators/repeated_binary.py +68 -0
- keeks/utils.py +9 -0
- keeks-0.2.0.dist-info/METADATA +237 -0
- keeks-0.2.0.dist-info/RECORD +15 -0
- keeks-0.2.0.dist-info/WHEEL +4 -0
- keeks-0.2.0.dist-info/licenses/LICENSE.md +8 -0
|
@@ -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
|
+
)
|