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: object

Result 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: object

Result 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: object

Result 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=True the 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. If target_profit is 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. Pass allow_lay=True to 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:

ArbitrageResult

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:

ValueBetResult | MultipleValueBetResult

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