@chancemoney/bet-math
v0.2.1
Published
JavaScript/TypeScript bet math for Chance Strike, Range, Tiered, and Shuffle modes
Readme
@chancemoney/bet-math
JavaScript / TypeScript bet calculation for Chance modes:
- Strike - multiple ↔ win probability, quotes, stake/multiple caps
- Range - lo/hi multiples, distribution helpers, caps
- Tiered - solve unlocked rung probabilities to EV, float quotes
- Shuffle - rebalance unlocked card multiples to EV, float quotes
Install
yarn add @chancemoney/bet-mathProtocol caps
Build ProtocolCaps / bankroll caps from your pool or table snapshot and pass them into quotes and stake helpers.
import {
DEFAULT_EV,
strikeKellyCapFromBankroll,
maxMultiplierFromBps,
type ProtocolCaps,
} from '@chancemoney/bet-math'
// From your pool / table snapshot (example numbers)
const availableBankroll = 10_000
const maxBetLiabilityCap = 400 // MBL: max (W − B)
const maxBetCap = 500 // bankroll-relative stake ceiling
const maxBetAmount = 250 // absolute table limit (omit if uncapped)
const maxMultiplier = maxMultiplierFromBps(1_000_000) // 100×
const maxBetKellyCap = strikeKellyCapFromBankroll({
availableBankroll,
ev: DEFAULT_EV,
maxBetKellyRatioBps: 5_000, // half Kelly
})
const caps: ProtocolCaps = {
ev: DEFAULT_EV,
maxBetLiabilityCap,
maxBetKellyCap,
maxBetCap,
maxBetAmount,
maxMultiplier,
}Strike
import {
winProbFromMultiple,
multipleFromWinProb,
quoteStrike,
DEFAULT_EV,
} from '@chancemoney/bet-math'
const winProb = winProbFromMultiple({ multiple: 2, ev: DEFAULT_EV }) // 0.425
const multiple = multipleFromWinProb({ winProb: 0.425, ev: DEFAULT_EV }) // 2
const quote = quoteStrike({
stake: 100,
multiple: 2,
...caps,
})
// quote.payout, quote.winProb, quote.capped, quote.capBoundRange
import {
rangeAlpha,
rangeOutcomeDistribution,
rangeProfitChance,
quoteRange,
DEFAULT_EV,
} from '@chancemoney/bet-math'
const stake = 100
const lo = stake * 0.5
const hi = stake * 2
const mean = stake * DEFAULT_EV
const alpha = rangeAlpha(lo, hi, mean) // settle shape α, or null if invalid
const bins = rangeOutcomeDistribution(lo, hi, mean, 48) // histogram or null
const profitChance = rangeProfitChance(lo, hi, mean, stake) // P(payout ≥ stake)
const rangeQuote = quoteRange({
stake,
rminM: 0.5,
rmaxM: 2,
...caps,
})
// rangeQuote.lo / .hi / .mean, .cappedTiered
import {
LADDER_PRESETS,
ladderPresetById,
ladderRowsFromMultipliers,
unlockAllLadderRows,
DEFAULT_LADDER_ROWS,
solveLadderRungs,
quoteTieredBetter,
DEFAULT_EV,
} from '@chancemoney/bet-math'
const preset = ladderPresetById('classic')
const rows = ladderRowsFromMultipliers(preset.multipliers)
// or: LADDER_PRESETS / DEFAULT_LADDER_ROWS
const unlocked = unlockAllLadderRows(rows)
const solved = solveLadderRungs(unlocked, DEFAULT_EV)
// solved.rungs, solved.ok, solved.note, solved.maxM
const tieredQuote = quoteTieredBetter({
stake: 100,
rows: unlocked,
...caps,
})Shuffle
import {
isShuffleDiffId,
resolveShuffleDiffId,
shuffleDiffById,
shuffleRowsFromDiff,
shuffleRowsFromMultipliers,
unlockAllShuffleRows,
clampShuffleMultiple,
rebalanceShuffleRows,
shuffleTopMultiple,
shuffleDeckTopMultiple,
shuffleLiabilityMultipleAfterEvSnap,
maxShuffleStakeForOdds,
quoteShuffle,
DEFAULT_SHUFFLE_DIFF_ID,
DEFAULT_EV,
} from '@chancemoney/bet-math'
// SHUFFLE_DIFFICULTIES, MAX_SHUFFLE_BUCKETS, DEFAULT_SHUFFLE_ROWS also exported
const diffId = resolveShuffleDiffId('safe') // also maps legacy easy/medium/hard
isShuffleDiffId(diffId) // true
const diff = shuffleDiffById(diffId)
let rows = shuffleRowsFromDiff(DEFAULT_SHUFFLE_DIFF_ID)
// or: shuffleRowsFromMultipliers([0, 0.5, 1, 2])
rows = unlockAllShuffleRows(rows)
const balanced = rebalanceShuffleRows(rows, DEFAULT_EV)
const topFromDiff = shuffleTopMultiple('classic')
const topFromDeck = shuffleDeckTopMultiple(balanced.rows)
const clamped = clampShuffleMultiple(12.34)
const multiples = balanced.rows.map((r) => r.m)
const k = 1
const liabM = shuffleLiabilityMultipleAfterEvSnap({ multiples, k, ev: DEFAULT_EV })
// Ticket-aware Shuffle stake ceiling (top-K after EV snap). Prefer this over
// maxStakeForMode for Shuffle - mode helper only applies the stake backstop.
const maxShuffleStake = maxShuffleStakeForOdds({
multiples,
k,
...caps,
})
const shuffleQuote = quoteShuffle({
stake: 100,
rows: balanced.rows,
pickIndexes: [0], // K = length of picks
...caps,
})Caps helpers
Constants (selected)
| Export | Meaning |
|--------|---------|
| BPS | 10_000 |
| DEFAULT_EV | 0.85 |
| DEFAULT_MAX_MULTIPLIER | 100 |
| DEFAULT_MAX_BET_KELLY_RATIO_BPS | 5_000 |
| STRIKE_MIN_MULTIPLE / STRIKE_DEFAULT_* | Strike UI defaults |
| RANGE_MIN_MAX_MULTIPLE / RANGE_DEFAULT_* | Range UI defaults |
| MAX_BET_AMOUNT_FINITE_CEIL | Raw maxBetAmount at/above this → uncapped |
Parse / convert
import {
finiteMaxBetAmountUnits,
maxMultiplierFromBps,
effectiveMaxMultiplierBps,
DEFAULT_MAX_MULTIPLIER_BPS,
DEFAULT_MIN_MULTIPLIER_BPS,
} from '@chancemoney/bet-math'
const amount = finiteMaxBetAmountUnits('250000000') // bigint or null if uncapped
const maxM = maxMultiplierFromBps(DEFAULT_MAX_MULTIPLIER_BPS) // 100
const effectiveBps = effectiveMaxMultiplierBps({
maxMultiplierBps: DEFAULT_MAX_MULTIPLIER_BPS,
minMultiplierBps: DEFAULT_MIN_MULTIPLIER_BPS,
availableBankroll: 10_000,
pendingPayoutLiability: 2_000,
})Liability / Kelly / optional caps
import {
betLiabilityCapOrInf,
stakeCapOrInf,
absoluteMaxBetOrInf,
stakeBackstopOrInf,
strikeKellyCapFromBankroll,
kellyLiabilityCap,
kellyCapForLoMultiple,
netLiabilityCap,
effectiveLiabilityCap,
floorUsdCents,
floorMultipleTenth,
} from '@chancemoney/bet-math'
betLiabilityCapOrInf(400) // 400; omit/≤0 → Infinity
stakeCapOrInf(500)
absoluteMaxBetOrInf(250)
stakeBackstopOrInf({ maxBetCap: 500, maxBetAmount: 250 }) // min of both
const strikeKelly = strikeKellyCapFromBankroll({
availableBankroll: 10_000,
ev: 0.85,
maxBetKellyRatioBps: 5_000,
})
kellyLiabilityCap({ stakeAmount: 100, payoutLo: 0, strikeKellyCap: strikeKelly })
kellyCapForLoMultiple(strikeKelly, 0.5) // Range-style lo multiple of stake
netLiabilityCap({
maxBetLiabilityCap: 400,
maxBetKellyCap: strikeKelly,
payoutLoMultiple: 0,
})
effectiveLiabilityCap({
stake: 100,
maxBetLiabilityCap: 400,
maxBetKellyCap: strikeKelly,
maxMultiplier: 100,
payoutLo: 0,
})
floorUsdCents(12.349) // 12.34
floorMultipleTenth(3.19) // 3.1Stake ↔ multiple from L
import {
maxMultipleForStake,
maxStakeForMultiple,
maxStakeForMode,
maxShuffleStakeForOdds,
effectiveMaxBetStake,
} from '@chancemoney/bet-math'
const maxM = maxMultipleForStake({
stake: 100,
maxBetLiabilityCap: 400,
maxBetKellyCap: strikeKelly,
maxMultiplier: 100,
})
const maxStakeAt2x = maxStakeForMultiple({
multiple: 2,
maxBetLiabilityCap: 400,
maxBetKellyCap: strikeKelly,
maxBetCap: 500,
maxBetAmount: 250,
})
// Mode ceilings for Strike / Range / TieredBetter stake backstop.
// For Shuffle tickets, use maxShuffleStakeForOdds (top-K liability), not this.
const modeMax = maxStakeForMode({
mode: 'strike', // 'strike' | 'range' | 'tieredBetter' | 'shuffle'
maxBetLiabilityCap: 400,
maxBetKellyCap: strikeKelly,
maxBetCap: 500,
maxBetAmount: 250,
})
const uiMax = effectiveMaxBetStake({
poolStakeMax: modeMax,
walletBalance: 80,
})Strike / Range / Tiered stake & multiple UI
import {
defaultStrikeStake,
defaultRangeStake,
maxStrikeMultiple,
maxStrikeStake,
strikePayoutBoundsForStake,
maxRangeStake,
maxRangeMaxMultiple,
rangeHiPayoutBoundsForStake,
maxTieredBetterStakeForMaxMultiple,
clampStrikeMultiple,
clampRangeMaxMultiple,
resolveStrikeMultipleUnderCaps,
resolveRangeMaxMultipleUnderCaps,
floorStrikeDefaultStake,
} from '@chancemoney/bet-math'
const bankrollCaps = {
maxBetLiabilityCap: 400,
maxBetKellyCap: strikeKelly,
maxBetCap: 500,
maxBetAmount: 250,
maxMultiplier: 100,
}
defaultStrikeStake(bankrollCaps) // $100 or floored fallback / null
defaultRangeStake(bankrollCaps)
floorStrikeDefaultStake(0.87)
maxStrikeStake(bankrollCaps)
maxStrikeMultiple({ stake: 100, ...bankrollCaps })
strikePayoutBoundsForStake({ stake: 100, ...bankrollCaps })
maxRangeStake(bankrollCaps)
maxRangeMaxMultiple({ stake: 100, payoutLoMultiple: 0.5, ...bankrollCaps })
rangeHiPayoutBoundsForStake({ stake: 100, payoutLoMultiple: 0.5, ...bankrollCaps })
maxTieredBetterStakeForMaxMultiple({
maxMultiple: 8,
payoutLoMultiple: 0,
...bankrollCaps,
})
clampStrikeMultiple(1.05, 50) // ≥ STRIKE_MIN_MULTIPLE
clampRangeMaxMultiple(1.0, 12) // ≥ RANGE_MIN_MAX_MULTIPLE
resolveStrikeMultipleUnderCaps({
multiple: 5,
multipleMaxForStake: 3,
...bankrollCaps,
})
resolveRangeMaxMultipleUnderCaps({
rmaxM: 4,
rmaxMMaxForStake: 2.5,
payoutLoMultiple: 0.5,
...bankrollCaps,
})Types
Exported types (see src/types.ts / caps): ProtocolCaps, LadderRow, LadderRung, StrikeQuote, RangeQuote, RangeOutcomeBin, TieredQuote, ShuffleQuote, BetMode, CapBound, DualBankrollCaps, LadderPreset, LadderPresetId, ShuffleDifficulty, ShuffleDiffId.
