Betting#
Betting Utilities
Functions for bet sizing, Kelly Criterion, arbitrage, value betting, and other betting strategies.
- class penaltyblog.betting.ArbitrageResult(has_arbitrage: bool, total_implied_probability: float, guaranteed_return: float, arbitrage_margin: float, best_odds: List[float], best_bookmakers: List[int], stake_percentages: List[float], outcome_labels: List[str], num_bookmakers: int, num_outcomes: int)[source]#
Bases:
objectResult of arbitrage opportunity analysis across multiple bookmakers.
- arbitrage_margin: float#
- best_bookmakers: List[int]#
- best_odds: List[float]#
- guaranteed_return: float#
- has_arbitrage: bool#
- num_bookmakers: int#
- num_outcomes: int#
- outcome_labels: List[str]#
- stake_percentages: List[float]#
- total_implied_probability: float#
- class penaltyblog.betting.MultipleValueBetResult(individual_results: List[ValueBetResult], bookmaker_odds: List[float], estimated_probabilities: List[float], total_value_bets: int, average_edge: float, total_expected_value: float, portfolio_overround: float, kelly_stakes: List[float], total_kelly_stake: float, portfolio_expected_return: float, best_value_bet_index: int, best_edge: float, worst_edge: float)[source]#
Bases:
objectResult of value bet analysis for multiple bets.
- average_edge: float#
- best_edge: float#
- best_value_bet_index: int#
- bookmaker_odds: List[float]#
- estimated_probabilities: List[float]#
- individual_results: List[ValueBetResult]#
- kelly_stakes: List[float]#
- portfolio_expected_return: float#
- portfolio_overround: float#
- total_expected_value: float#
- total_kelly_stake: float#
- total_value_bets: int#
- worst_edge: float#
- class penaltyblog.betting.ValueBetResult(bookmaker_odds: float, estimated_probability: float, implied_probability: float, expected_value: float, expected_return_percentage: float, is_value_bet: bool, edge: float, recommended_stake_kelly: float, recommended_stake_fraction: float, win_probability: float, lose_probability: float, potential_profit: float, potential_loss: float, margin_over_fair_odds: float, overround_contribution: float)[source]#
Bases:
objectResult of value bet analysis for a single bet.
- bookmaker_odds: float#
- edge: float#
- estimated_probability: float#
- expected_return_percentage: float#
- expected_value: float#
- implied_probability: float#
- is_value_bet: bool#
- lose_probability: float#
- margin_over_fair_odds: float#
- overround_contribution: float#
- potential_loss: float#
- potential_profit: float#
- recommended_stake_fraction: float#
- recommended_stake_kelly: float#
- win_probability: float#
- penaltyblog.betting.arbitrage_hedge(existing_stakes: List[float], existing_odds: List[float], hedge_odds: List[float], target_profit: float | None = None, hedge_all: bool = True, allow_lay: bool = False, tolerance: float = 1e-10) ArbitrageHedgeResult[source]#
Calculate hedge bet sizes to guarantee profit or minimize loss from existing positions.
This function determines how much to bet on other outcomes to either lock in a guaranteed profit or minimize potential losses from existing bets.
IMPORTANT: Understanding “Guaranteed Profit”
The “guaranteed profit” is the worst-case profit across all possible outcomes. In asymmetric scenarios (uneven existing stakes, different odds), individual outcome profits may be unequal, and the guaranteed profit represents the minimum you will receive regardless of which outcome occurs.
Example: If outcome A would yield +$50 and outcome B would yield -$10, your guaranteed profit is -$10 (you’re guaranteed to get at least this amount).
Equal profits across all outcomes are only achievable in symmetric scenarios or when laying (betting against) is allowed and mathematically optimal.
- Parameters:
existing_stakes (List[float]) – Amount already staked on each outcome (in currency units)
existing_odds (List[float]) – Decimal odds for existing bets
hedge_odds (List[float]) – Current decimal odds available for hedging (should match length of existing stakes)
target_profit (float, optional) – Target profit to achieve. If None, maximizes guaranteed profit
hedge_all (bool, default=True) – If True, hedge all outcomes. If False, only hedge profitable outcomes
allow_lay (bool, default=False) – If True, allows negative (lay) stakes in results. If False, redistributes negative stakes to other outcomes
tolerance (float, default=1e-10) – Numerical tolerance for comparisons and calculations
- Returns:
Structured result containing raw and practical hedge stakes and the guaranteed profit (or loss).
- Return type:
ArbitrageHedgeResult
Examples
>>> # Basic symmetric arbitrage (equal profits possible): >>> # You have $100 on outcome A at 3.0 odds, want to hedge with outcome B at 2.5 odds >>> res = arbitrage_hedge([100, 0], [3.0, 2.5], [3.0, 2.5]) >>> res.practical_hedge_stakes # Hedge $80 on outcome B [0.0, 80.0] >>> res.guaranteed_profit # Guaranteed $20 profit either way 20.0 >>> # Verification: If A wins: $300 payout - $100 original - $80 hedge = $120 net >>> # If B wins: $0 + $200 hedge payout - $100 original - $80 hedge = $20 net
>>> # Asymmetric case (individual profits will be unequal): >>> res = arbitrage_hedge([100, 0], [3.0, 2.5], [2.9, 2.4]) >>> # Individual outcome profits might be: +$50 and -$20 >>> # Guaranteed profit would be -$20 (the worst case)
>>> # Three-way market with existing positions: >>> res = arbitrage_hedge([50, 30, 0], [2.5, 4.0, 3.0], [2.4, 3.8, 2.9]) >>> # Will calculate optimal hedge stakes for best worst-case outcome
>>> # Target specific profit: >>> res = arbitrage_hedge([100, 0], [3.0, 2.5], [3.0, 2.5], target_profit=50) >>> # Will calculate stakes needed to achieve exactly $50 profit
>>> # Only hedge existing positions (don't hedge outcome with 0 stake): >>> res = arbitrage_hedge([100, 0], [3.0, 2.5], [3.0, 2.5], hedge_all=False) >>> # Will only hedge the $100 position on outcome A
Notes
Guaranteed Profit Interpretation: The guaranteed_profit field represents the minimum profit you will receive regardless of outcome. Individual outcome profits may differ significantly in asymmetric scenarios due to mathematical constraints.
When Profits Are Equal: Equal profits across outcomes occur when: - Existing stakes and odds are symmetric, OR - allow_lay=True and negative stakes are mathematically optimal, OR - The linear program finds a solution where equal profits are achievable
When Profits Are Unequal: Unequal profits occur when: - Asymmetric existing positions with different odds ratios - allow_lay=False forces redistribution of negative stakes - Mathematical constraints prevent equal profit solutions
Set hedge_all=False to only hedge outcomes where you have existing exposure.
Implementation#
When
hedge_all=Truethe function solves a linear program to maximize the guaranteed profit G under non-negative hedge stakes h_i.Linear Program Formulation:
Variables: [h_0, h_1, …, h_{n-1}, G] where h_i are hedge stakes, G is guaranteed profit
Objective: Maximize G (minimize -G in linprog)
- Constraints (for each outcome i):
existing_payouts[i] + hedge_odds[i]*h_i - total_existing_stakes - sum(h_j) >= G
Intuition: If outcome i occurs, we get existing_payouts[i] from our original bets, plus hedge_odds[i]*h_i from our hedge on outcome i, minus all our original stakes and all our hedge stakes. This net amount must be at least G for all outcomes.
Negative Stake Redistribution:
When laying (betting against) is not allowed, negative hedge stakes are converted to zero and the equivalent hedge amount is redistributed to other outcomes. The redistribution is proportional to the hedge odds - outcomes with better odds get a larger share of the redistributed hedge.
Example: If outcome A needs -$50 hedge (impossible) and outcomes B,C have odds 2.0, 3.0 respectively, then B gets $50 * 2/(2+3) = $20 and C gets $50 * 3/(2+3) = $30.
This is solved with
scipy.optimize.linprog. Iftarget_profitis provided, G is fixed to that value via bounds. If the LP fails, the function falls back to a conservative heuristic and then converts any theoretical “lay” (negative) stakes to practical non-negative stakes by redistribution. Passallow_lay=Trueto permit negative (lay) stakes in the returned raw_hedge_stakes and practical_hedge_stakes.
- penaltyblog.betting.calculate_bet_value(bookmaker_odds: float, estimated_probability: float) float[source]#
Calculate the expected value of a bet as a simple utility function.
- Parameters:
bookmaker_odds (float) – Decimal odds from bookmaker
estimated_probability (float) – Your estimated probability (0-1)
- Returns:
Expected value per unit staked
- Return type:
float
Examples
>>> value = calculate_bet_value(2.0, 0.6) # 60% chance at 2.0 odds >>> print(f"Expected value: {value:.3f}") # 0.200
- penaltyblog.betting.convert_odds(odds: List[float | str], odds_format: str | OddsFormat, market_names: List[str] | None = None) List[float][source]#
Converts odds from a specified format to decimal odds.
This is a convenience function that wraps the functionality from the penaltyblog.implied submodule.
- Parameters:
odds (List[Union[float, str]]) – The odds to convert.
odds_format (str or OddsFormat) – The format of the provided odds.
market_names (List[str], optional) – Names for each market outcome.
- Returns:
The odds converted to decimal format.
- Return type:
List[float]
Examples
>>> from penaltyblog.betting import convert_odds >>> american_odds = [+170, +130, +340] >>> convert_odds(american_odds, "american") [2.7, 2.3, 4.4] >>> fractional_odds = ['7/4', '13/10', '7/2'] >>> convert_odds(fractional_odds, "fractional") [2.75, 2.3, 4.5]
- penaltyblog.betting.find_arbitrage_opportunities(bookmaker_odds_list: List[List[float]], outcome_labels: List[str] | None = None) ArbitrageResult[source]#
Find arbitrage opportunities across multiple bookmakers for the same event.
An arbitrage opportunity exists when you can bet on all outcomes across different bookmakers and guarantee a profit regardless of the result.
- Parameters:
bookmaker_odds_list (List[List[float]]) – List of odds from each bookmaker. Each inner list contains odds for all outcomes. Example: [[2.1, 1.9], [2.0, 2.0]] for two bookmakers with two outcomes each.
outcome_labels (List[str], optional) – Labels for each outcome (e.g., [“Home”, “Away”])
- Returns:
Structured result containing arbitrage analysis: - has_arbitrage: bool indicating if arbitrage exists - total_implied_probability: float (< 1.0 indicates arbitrage) - best_odds: list of best odds for each outcome - best_bookmakers: list of bookmaker indices offering best odds - stake_percentages: recommended stake allocation - guaranteed_return: guaranteed profit percentage
- Return type:
Examples
>>> # Two bookmakers, two outcomes >>> odds = [[2.1, 1.85], [1.95, 2.0]] >>> arb = find_arbitrage_opportunities(odds, ["Home", "Away"]) >>> if arb.has_arbitrage: ... print(f"Guaranteed return: {arb.guaranteed_return:.2%}")
- penaltyblog.betting.identify_value_bet(bookmaker_odds: float | List[float] | ndarray[tuple[int, ...], dtype[_ScalarType_co]], estimated_probability: float | List[float] | ndarray[tuple[int, ...], dtype[_ScalarType_co]], kelly_fraction: float = 1.0, min_edge_threshold: float = 0.0) ValueBetResult | MultipleValueBetResult[source]#
Identify value bets by comparing bookmaker odds to estimated true probabilities.
A value bet occurs when your estimated probability of an outcome is higher than the bookmaker’s implied probability, creating positive expected value.
- Parameters:
bookmaker_odds (float | list[float] | np.ndarray) – Bookmaker odds in decimal format (e.g., 2.0 for even money)
estimated_probability (float | list[float] | np.ndarray) – Your estimated true probability of the outcome (0-1)
kelly_fraction (float, default=1.0) – Fraction of optimal Kelly stake to recommend (e.g., 0.5 for half Kelly)
min_edge_threshold (float, default=0.0) – Minimum edge required to consider a bet as having value
- Returns:
Comprehensive analysis of value betting opportunities
- Return type:
Examples
>>> # Single value bet analysis >>> result = identify_value_bet(2.5, 0.50) # 50% chance, 2.5 odds >>> print(f"Expected value: {result.expected_value:.3f}") >>> print(f"Kelly stake: {result.recommended_stake_kelly:.2%}")
>>> # Multiple bet analysis >>> odds = [2.0, 3.0, 1.8] >>> probs = [0.6, 0.4, 0.5] >>> results = identify_value_bet(odds, probs) >>> print(f"Found {results.total_value_bets} value bets")
- Raises:
ValueError – If odds <= 1.0, probabilities outside [0,1], or mismatched array lengths
- penaltyblog.betting.kelly_criterion(decimal_odds: float | ndarray[tuple[int, ...], dtype[_ScalarType_co]] | List[float], true_prob: float | ndarray[tuple[int, ...], dtype[_ScalarType_co]] | List[float], fraction: float = 1.0) KellyResult[source]#
Calculate optimal bet size using the Kelly Criterion with comprehensive analysis.
This function provides a complete Kelly Criterion analysis including stake recommendations, expected growth rates, edge calculations, and risk metrics with robust input validation.
- Parameters:
decimal_odds (float | np.ndarray | list[float]) – The odds in European decimal format (e.g., 1.50 for 50% implied probability). Scalar or array-like. Lists are accepted and will be converted to
np.ndarray.true_prob (float | np.ndarray | list[float]) – The true probability of the event (0-1). Scalar or array-like. Lists are accepted and will be converted to
np.ndarray.fraction (float, default=1.0) – Fraction of optimal Kelly to use (e.g., 0.5 for Half Kelly conservative betting)
- Returns:
Comprehensive result object containing: - stake: Recommended fraction of bankroll to wager - expected_growth: Expected logarithmic growth rate - edge: Betting edge (expected value) - is_favorable: Whether the bet has positive expected value - risk_of_ruin: Simplified probability of losing stake - risk_metrics: Comprehensive risk analysis (for scalar inputs only) - warnings: List of data quality or configuration warnings
- Return type:
KellyResult
Examples
>>> # Single bet analysis >>> result = kelly_criterion(2.1, 0.55, fraction=0.5) >>> print(f"Stake: {result.stake:.2%}, Expected Growth: {result.expected_growth:.4%}")
>>> # Array of bets >>> odds = np.array([2.0, 1.8, 3.0]) >>> probs = np.array([0.6, 0.5, 0.4]) >>> results = kelly_criterion(odds, probs)
- Raises:
ValueError – If probabilities are outside [0,1] or odds are exactly 1.0
- penaltyblog.betting.multiple_kelly_criterion(decimal_odds: List[float] | ndarray[tuple[int, ...], dtype[_ScalarType_co]], true_probs: List[float] | ndarray[tuple[int, ...], dtype[_ScalarType_co]], fraction: float = 1.0, max_total_stake: float = 1.0, method: Literal['simultaneous', 'independent'] = 'simultaneous', optimization_methods: List[Literal['SLSQP', 'trust-constr']] = ['SLSQP', 'trust-constr'], tolerance: float = 1e-10) MultipleKellyResult[source]#
Calculate optimal portfolio bet sizes using Kelly Criterion with comprehensive analysis.
This function handles portfolio optimization across multiple bets, using either simultaneous optimization for maximum expected log growth or independent Kelly calculations. Includes comprehensive risk analysis and robust optimization with multiple fallback methods.
- Parameters:
decimal_odds (list[float] | np.ndarray) – Odds in European decimal format for each outcome. Lists or arrays are accepted and will be coerced to
np.ndarray.true_probs (list[float] | np.ndarray) – True probabilities for each outcome (should sum to ≤ 1.0). Lists or arrays are accepted and will be coerced to
np.ndarray.fraction (float, default=1.0) – Fraction of optimal Kelly to use (e.g., 0.5 for Half Kelly conservative betting)
max_total_stake (float, default=1.0) – Maximum fraction of bankroll to stake across all bets
method ({"simultaneous", "independent"}, default="simultaneous") – Method to use: “simultaneous” for portfolio optimization, “independent” for independent Kelly calculations
optimization_methods (list of {"SLSQP", "trust-constr"}, default=["SLSQP", "trust-constr"]) – List of optimization methods to try in order
tolerance (float, default=1e-10) – Numerical tolerance for calculations
- Returns:
Comprehensive result object containing: - stakes: List of recommended stakes for each outcome - total_stake: Total fraction of bankroll to stake - expected_growth: Expected logarithmic growth rate - expected_return: Expected return on investment - portfolio_edge: Weighted average betting edge - risk_metrics: Dict with volatility, Sharpe ratio, etc. - optimization details and warnings
- Return type:
MultipleKellyResult
Examples
>>> # Three-way football market >>> result = multiple_kelly_criterion([2.5, 3.2, 2.8], [0.45, 0.30, 0.25]) >>> print(f"Stakes: {[f'{s:.2%}' for s in result.stakes]}") >>> print(f"Total: {result.total_stake:.2%}, Growth: {result.expected_growth:.4%}")
>>> # Conservative Half Kelly approach >>> result = multiple_kelly_criterion([2.1, 1.9], [0.50, 0.48], fraction=0.5)
- Raises:
ValueError – If array lengths don’t match, probabilities are invalid, or sum exceeds 1.0