Source code for keeks.binary_strategies.simple

import numpy as np

from keeks.binary_strategies.base import BaseStrategy

__author__ = "willmcginnis"


[docs]class NaiveStrategy(BaseStrategy): """ A simple betting strategy that bets based on expected value. This strategy calculates the expected value of a bet and bets a fixed fraction of the bankroll if the expected value is positive. Parameters ---------- payoff : float The amount won per unit bet on a successful outcome. loss : float The amount lost per unit bet on an unsuccessful outcome. transaction_cost : float The fixed cost per transaction, regardless of outcome. """ def __init__(self, payoff, loss, transaction_cost): """ Initialize the NaiveStrategy. Parameters ---------- payoff : float The amount won per unit bet on a successful outcome. loss : float The amount lost per unit bet on an unsuccessful outcome. transaction_cost : float The fixed cost per transaction, regardless of outcome. """ super().__init__(payoff, loss, transaction_cost)
[docs] def evaluate(self, probability, current_bankroll): """ Calculate the bet size based on expected value. The expected value is calculated as: EV = (probability * payoff) - ((1 - probability) * loss) - transaction_cost If EV is positive, bet a fixed fraction of the bankroll. If EV is negative, do not bet. Parameters ---------- probability : float The probability of a successful outcome, typically between 0 and 1. current_bankroll : float The current bankroll amount. Returns ------- float The proportion of the bankroll to bet. """ # Calculate expected value expected_value = ( (probability * self.payoff) - ((1 - probability) * self.loss) - self.transaction_cost ) # If expected value is negative, do not bet if expected_value <= 0: return 0.0 # Calculate bet size based on expected value bet_size = expected_value / self.payoff # Ensure we never bet more than would result in negative bankroll return min(max(0, bet_size), self.get_max_safe_bet(current_bankroll))
[docs]class FixedFractionStrategy(BaseStrategy): """ A simple strategy that bets a fixed percentage of the bankroll. This strategy ignores probabilities and odds completely, and simply wagers a fixed fraction of the bankroll. It can be used as a baseline for comparison or as a simple approach for risk management. Parameters ---------- fraction : float The fixed fraction of the bankroll to bet (between 0 and 1). payoff : float The amount won per unit bet on a successful outcome. loss : float The amount lost per unit bet on an unsuccessful outcome. transaction_cost : float, default=0 The fixed cost per transaction, regardless of outcome. min_probability : float, default=0.5 The minimum probability required to place a bet. """ def __init__(self, fraction, payoff, loss, transaction_cost=0, min_probability=0.5): """ Initialize the FixedFractionStrategy. Parameters ---------- fraction : float The fixed fraction of the bankroll to bet (between 0 and 1). payoff : float The amount won per unit bet on a successful outcome. loss : float The amount lost per unit bet on an unsuccessful outcome. transaction_cost : float, default=0 The fixed cost per transaction, regardless of outcome. min_probability : float, default=0.5 The minimum probability required to place a bet. """ if not 0 <= fraction <= 1: raise ValueError("Fraction must be between 0 and 1") if not 0 <= min_probability <= 1: raise ValueError("Minimum probability must be between 0 and 1") super().__init__(payoff, loss, transaction_cost) self.fraction = fraction self.min_probability = min_probability
[docs] def evaluate(self, probability, current_bankroll): """ Return the fixed fraction if the probability meets the minimum threshold. Parameters ---------- probability : float The probability of a successful outcome, typically between 0 and 1. current_bankroll : float The current bankroll amount. Returns ------- float The fixed fraction if probability >= min_probability, otherwise 0. """ if probability >= self.min_probability: # Ensure we never bet more than would result in negative bankroll return min(self.fraction, self.get_max_safe_bet(current_bankroll)) else: return 0.0
[docs]class CPPIStrategy(BaseStrategy): """ Implementation of Constant Proportion Portfolio Insurance (CPPI) for binary betting. This strategy maintains a dynamic floor below which the bankroll should not fall, and invests a multiple of the cushion (amount above floor) in each bet. Parameters ---------- floor_fraction : float The fraction of initial bankroll to maintain as a floor. multiplier : float The multiplier to apply to the cushion for determining exposure. initial_bankroll : float The initial bankroll amount. payoff : float The amount won per unit bet on a successful outcome. loss : float The amount lost per unit bet on an unsuccessful outcome. transaction_cost : float The fixed cost per transaction, regardless of outcome. min_probability : float, default=0.5 The minimum probability required to place a bet. """ def __init__( self, floor_fraction, multiplier, initial_bankroll, payoff, loss, transaction_cost=0, min_probability=0.5, ): """Initialize the CPPI strategy.""" if not 0 < floor_fraction < 1: raise ValueError("Floor fraction must be between 0 and 1") if multiplier <= 0: raise ValueError("Multiplier must be greater than 0") if initial_bankroll <= 0: raise ValueError("Initial bankroll must be greater than 0") if not 0 <= min_probability <= 1: raise ValueError("Minimum probability must be between 0 and 1") super().__init__(payoff, loss, transaction_cost) self.floor_fraction = floor_fraction self.multiplier = multiplier self.floor = floor_fraction * initial_bankroll self.min_probability = min_probability self.current_bankroll = initial_bankroll self.peak_bankroll = initial_bankroll
[docs] def update_bankroll(self, new_bankroll): """ Update the current bankroll value and adjust floor based on peak value. Parameters ---------- new_bankroll : float The current value of the bankroll. """ self.current_bankroll = new_bankroll if new_bankroll > self.peak_bankroll: self.peak_bankroll = new_bankroll # Adjust floor to maintain the same fraction of peak value self.floor = self.floor_fraction * self.peak_bankroll
[docs] def evaluate(self, probability, current_bankroll): """ Calculate the CPPI bet size based on the cushion above the floor. Parameters ---------- probability : float The probability of a successful outcome, typically between 0 and 1. current_bankroll : float The current bankroll amount. Returns ------- float The proportion of the current bankroll to bet, or 0 if below the minimum probability threshold. """ # Update current bankroll self.update_bankroll(current_bankroll) # Check probability threshold if probability < self.min_probability: return 0.0 # Calculate expected value per unit bet expected_value = probability * (self.payoff - self.transaction_cost) - ( 1 - probability ) * (self.loss + self.transaction_cost) # If expected value is negative or too close to zero, don't bet if expected_value <= 0.01: # Require at least 1% edge after transaction costs return 0.0 # Calculate the cushion (amount above the floor) cushion = max(0, current_bankroll - self.floor) # Calculate the exposure (amount to bet) # Scale the multiplier by the expected value to be more conservative # when the edge is smaller adjusted_multiplier = self.multiplier * min(1.0, expected_value) exposure = adjusted_multiplier * cushion # Convert to a proportion of the current bankroll if current_bankroll <= 0: return 0.0 # Calculate proportion proportion = min(1.0, exposure / current_bankroll) # Adjust for transaction costs and ensure we don't risk going below floor if proportion > 0: # Calculate the maximum bet that ensures we stay above the floor # even in the worst case (loss + transaction cost) max_floor_bet = (current_bankroll - self.floor) / ( current_bankroll * (self.loss + self.transaction_cost) ) # Get the maximum safe bet considering ruin max_safe_bet = self.get_max_safe_bet(current_bankroll) # Take the minimum of all constraints proportion = min(proportion, max_floor_bet, max_safe_bet) return max(0, proportion)
[docs]class DynamicBankrollManagement(BaseStrategy): """ A dynamic bankroll management strategy that adjusts bet sizes based on performance. This strategy adjusts the base bet fraction based on: 1. Recent performance (win/loss streak) 2. Volatility of returns 3. Current drawdown level 4. Probability of success Parameters ---------- base_fraction : float The base fraction of bankroll to bet. payoff : float The amount won per unit bet on a successful outcome. loss : float The amount lost per unit bet on an unsuccessful outcome. transaction_cost : float The fixed cost per transaction, regardless of outcome. window_size : int, default=10 The number of recent results to consider for adjustments. max_fraction : float, default=0.2 The maximum fraction of bankroll that can be bet. min_fraction : float, default=0.05 The minimum fraction of bankroll to bet. """ def __init__( self, base_fraction, payoff, loss, transaction_cost, window_size=10, max_fraction=0.2, min_fraction=0.05, ): """ Initialize the DynamicBankrollManagement strategy. """ if not 0 <= base_fraction <= 1: raise ValueError("Base fraction must be between 0 and 1") if not window_size > 0: raise ValueError("Window size must be positive") if not 0 <= min_fraction <= max_fraction <= 1: raise ValueError( "Min fraction must be between 0 and max fraction, and max fraction must be between 0 and 1" ) super().__init__(payoff, loss, transaction_cost) self.base_fraction = base_fraction self.window_size = window_size self.max_fraction = max_fraction self.min_fraction = min_fraction self.results = [] self.initial_bankroll = None self.current_bankroll = None self.peak_bankroll = None
[docs] def record_result(self, won, return_pct=None): """ Record the result of a bet. Parameters ---------- won : bool Whether the bet was won. return_pct : float, optional The return percentage of the bet. If not provided, calculated from won/loss. """ if return_pct is None: return_pct = self.payoff if won else -self.loss self.results.append(return_pct) if len(self.results) > self.window_size: self.results.pop(0)
[docs] def get_streak_factor(self): """Calculate the adjustment factor based on recent performance.""" if not self.results: return 1.0 # Scale factor by window size to make smaller windows more responsive scale = min(len(self.results), self.window_size) / self.window_size wins = sum(1 for r in self.results if r > 0) losses = sum(1 for r in self.results if r < 0) if losses == 0: return 1.0 + (0.5 * scale) # Maximum boost for all wins if wins == 0: return 1.0 - (0.5 * scale) # Maximum reduction for all losses win_ratio = wins / (wins + losses) return 1.0 + ((win_ratio - 0.5) * scale)
[docs] def get_volatility_factor(self): """Calculate the adjustment factor based on return volatility.""" if not self.results: return 1.0 returns = np.array(self.results) volatility = np.std(returns) if volatility == 0: return 1.0 # Scale factor by window size to make smaller windows more responsive scale = min(len(self.results), self.window_size) / self.window_size return max(0.5, 1.0 - (volatility * scale))
[docs] def get_drawdown_factor(self): """Calculate the adjustment factor based on current drawdown.""" if self.current_bankroll is None or self.peak_bankroll is None: return 1.0 drawdown = 1.0 - (self.current_bankroll / self.peak_bankroll) return max(0.5, 1.0 - drawdown)
[docs] def get_probability_factor(self, probability): """Calculate the adjustment factor based on probability.""" # Only apply probability factor if we have some results if not self.results: return 1.0 # Scale linearly from 0.5 at 50% probability to 1.5 at 100% probability return max(0.5, min(1.5, 1.0 + (probability - 0.5)))
[docs] def evaluate(self, probability, current_bankroll): """ Calculate the bet size based on all adjustment factors. Parameters ---------- probability : float The probability of a successful outcome. current_bankroll : float The current bankroll amount. Returns ------- float The proportion of the current bankroll to bet. """ # Initialize or update bankroll tracking if self.initial_bankroll is None: self.initial_bankroll = current_bankroll self.peak_bankroll = current_bankroll self.current_bankroll = current_bankroll self.peak_bankroll = max(self.peak_bankroll, current_bankroll) # Get all adjustment factors streak_factor = self.get_streak_factor() volatility_factor = self.get_volatility_factor() drawdown_factor = self.get_drawdown_factor() probability_factor = self.get_probability_factor(probability) # Combine all factors combined_factor = ( streak_factor * volatility_factor * drawdown_factor * probability_factor ) # Calculate adjusted bet size bet_size = self.base_fraction * combined_factor # Ensure bet size is within bounds return max(self.min_fraction, min(self.max_fraction, bet_size))
[docs]class OptimalF(BaseStrategy): """ Implementation of the Optimal f strategy for binary betting. This strategy calculates the optimal fraction of bankroll to bet based on the win rate and payoff ratio. It's similar to Kelly but uses a different approach to risk management. Parameters ---------- payoff : float The amount won per unit bet on a successful outcome. loss : float The amount lost per unit bet on an unsuccessful outcome. transaction_cost : float The fixed cost per transaction, regardless of outcome. win_rate : float The historical or expected win rate (between 0 and 1). max_risk_fraction : float, default=0.2 The maximum fraction of bankroll that can be risked on a single bet. """ def __init__(self, payoff, loss, transaction_cost, win_rate, max_risk_fraction=0.2): """ Initialize the OptimalF strategy. Parameters ---------- payoff : float The amount won per unit bet on a successful outcome. loss : float The amount lost per unit bet on an unsuccessful outcome. transaction_cost : float The fixed cost per transaction, regardless of outcome. win_rate : float The historical or expected win rate (between 0 and 1). max_risk_fraction : float, default=0.2 The maximum fraction of bankroll that can be risked on a single bet. """ if not 0 <= win_rate <= 1: raise ValueError("Win rate must be between 0 and 1") if not 0 < max_risk_fraction <= 1: raise ValueError("Maximum risk fraction must be between 0 and 1") super().__init__(payoff, loss, transaction_cost) self.win_rate = win_rate self.max_risk_fraction = max_risk_fraction
[docs] def evaluate(self, probability, current_bankroll): """ Calculate the optimal f bet size based on win rate and payoff ratio. This implementation follows Ralph Vince's approach to Optimal f, which is based on maximizing the terminal wealth relative (TWR) for a given risk-to-reward profile. Parameters ---------- probability : float The probability of a successful outcome, typically between 0 and 1. current_bankroll : float The current bankroll amount. Returns ------- float The optimal proportion of the bankroll to bet. """ if probability < 0.5: # Use 0.5 as default minimum probability return 0.0 # Use the provided probability to adjust our expectations # If no specific edge is known, the win_rate will be used directly adjusted_win_rate = probability if probability > 0 else self.win_rate adjusted_loss_rate = 1 - adjusted_win_rate # Calculate the risk-to-reward ratio (R-multiple) reward = self.payoff - self.transaction_cost risk = self.loss + self.transaction_cost # If transaction costs make the bet unprofitable, don't bet if reward <= 0: return 0.0 # Calculate optimal f using the formula: # f* = W - (1-W)/(R/L) where W is win rate, R is reward, L is risk # This is more aligned with Ralph Vince's approach optimal_f = adjusted_win_rate - (adjusted_loss_rate / (reward / risk)) # Cap at our maximum risk fraction optimal_f = min(max(0, optimal_f), self.max_risk_fraction) # Ensure we never bet more than would result in negative bankroll return min(optimal_f, self.get_max_safe_bet(current_bankroll))