Metadata-Version: 2.4
Name: wager-math
Version: 0.1.0
Summary: Sportsbook price conversions (American/decimal) and flat-stake bet arithmetic: implied probability, expected value, per-bet profit moments, and a Sharpe-style acceptance gate.
Project-URL: Repository, https://github.com/negative-ev/packages
Project-URL: Issues, https://github.com/negative-ev/packages/issues
License-Expression: MIT OR Apache-2.0
License-File: LICENSE-APACHE
License-File: LICENSE-MIT
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Office/Business :: Financial
Classifier: Topic :: Scientific/Engineering :: Mathematics
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# wager-math (Python)

Sportsbook price arithmetic: conversions between quoting conventions, implied
probability, and the profit distribution of a flat-stake bet.

Stdlib only, no I/O, no opinion about which bets are worth making.

```python
from wager_math import Price, expected_value, bet_moments, passes_sharpe_gate

price = Price.american(120)          # or Price.decimal(2.2)
price.implied_prob()                 # 0.4545 — raw, vig included
price.payout_per_unit()              # 1.2 profit per unit risked
price.to_decimal()                   # 2.2

# You think it's a coin flip; the book says 45.5%.
expected_value(0.5, price)                       # +0.10 per unit risked
mu, sigma, sharpe = bet_moments(0.5, price)      # a NamedTuple; unpacks
passes_sharpe_gate(0.5, price, 1000, 2.326)      # 2.326 sd over 1000 bets
```

## API

| | |
| --- | --- |
| `Price.american(v)` / `Price.decimal(v)` | a quote, carrying its convention |
| `Price.implied_prob/payout_per_unit/to_decimal/is_valid` | conversions |
| `expected_value(p, price)` | expected profit per unit risked |
| `bet_moments(p, price)` | `BetMoments(mu, sigma, mu_over_sigma)` |
| `passes_sharpe_gate(p, price, window_n, confidence_z)` | `mu/sigma >= z/sqrt(n)` |

Free functions (`american_to_implied_prob`, `decimal_payout_per_unit`, …) are
available for callers that already know their convention.

### Conventions

- **Payout** means profit per unit risked, on a win: a bet gains `payout` or
  loses `1`. Decimal quotes include the stake, so `payout = decimal - 1`.
- **Implied probabilities are raw** — they include the bookmaker's margin, so
  the two sides of a market sum to more than 1. De-vigging is a modelling
  choice and is deliberately not done here.
- **Invalid prices are not defaulted.** `|American| >= 100` and `decimal > 1`;
  outside that the formulas are followed literally. Check untrusted quotes with
  `Price.is_valid()` first.

## Install

```
pip install wager-math
```

No dependencies, Python 3.10+.

## Relationship to the Rust crate

This mirrors the [`wager-math` crate](https://crates.io/crates/wager-math)
function for function, and the two are expected to agree to the last bit on the
same inputs. One documented divergence: a `probability` outside `[0, 1]` is a
caller bug, and Python reports it as a zero-variance bet where Rust produces a
NaN sigma (`math.sqrt` raises on a negative where Rust returns NaN). Both reject
such a bet at the gate.

## Provenance

Lifted 2026-08-02 out of a private simulation codebase, where the same
arithmetic had been written four separate times and drifted — three Rust copies,
which became the crate above, and this Python one.

The lift was behaviour-preserving, and the test suite is what proves it:
`test_wager_math.py` pins every function bit-exact against a verbatim copy of
the pre-lift implementation, across every American price from ±100 to ±999 × 101
probabilities, including that the Sharpe gate selects the identical set of bets
at four different gate settings. Silent numeric drift during a consolidation is
exactly the failure this package was written to end.

## Tests

```
python -m pytest
```

## License

MIT OR Apache-2.0, at your option.
