Your first optimize
KELLYIQ_API_KEY (see the Quickstart). Python needs pip install requests; JavaScript needs Node 18 or later. This recipe uses one optimize from your daily allowance each time it runs.The script
import os
import requests
API = os.environ.get("KELLYIQ_API_BASE", "https://api.kellyiq.com")
KEY = os.environ["KELLYIQ_API_KEY"]
slate = {
"bankroll": 1000,
"picks": [
{"id": "lad", "sport": "baseball_mlb", "team": "Dodgers", "opponent": "Padres",
"odds": -145, "opponentOdds": 125, "confidence": "STRONG_EDGE"},
{"id": "chc", "sport": "baseball_mlb", "team": "Cubs", "opponent": "Cardinals",
"odds": 120, "opponentOdds": -140, "confidence": "MODERATE_EDGE"},
{"id": "nyy", "sport": "baseball_mlb", "team": "Yankees", "opponent": "Red Sox",
"odds": -110, "opponentOdds": -110, "confidence": "MARKET"},
],
}
resp = requests.post(
f"{API}/v1/optimize",
headers={"Authorization": f"Bearer {KEY}"},
json=slate,
timeout=60,
)
if not resp.ok:
# API errors are JSON; a proxy timeout may not be, so show the raw text then.
raise SystemExit(f"HTTP {resp.status_code}: {resp.text[:500]}")
result = resp.json()
def usd(n):
return f"-${-n:.2f}" if n < 0 else f"${n:.2f}"
# 1. The allocation: how much to stake on each bet.
alloc = result["allocation"]
if alloc["kellyMultiplier"] is not None:
print(f"Allocation: {alloc['kellyMultiplier']:.2f}x Kelly, {usd(alloc['totalStake'])} in total")
for t in alloc["stakes"]:
print(f" {usd(t['stake']):>9} {t['team']} {t['odds']:+d}")
# 2. What happened to each pick, including the ones with no stake.
print()
for p in result["picks"]:
if p["included"]:
print(f"{p['team']} {p['odds']:+d}: staked")
else:
print(f"{p['team']} {p['odds']:+d}: no stake ({p['reason']})")
# 3. The allocation's risk, from 100,000 simulated slates.
risk = result["risk"]["riskAdjusted"]["independent"]
if risk is not None:
print(f"\nAverage profit {usd(risk['meanProfit'])}, chance of losing money {risk['probLoss']:.0%}, "
f"worst 5% average {usd(risk['cvar5'])}")
# 4. The settings that were used, and where each came from.
print()
for name, s in result["settingsApplied"].items():
print(f"{name}: {s['value']} (from {s['source']})")
for w in result["warnings"]:
print("warning:", w)
# 5. Keep these with the result: with the request, they reproduce it exactly.
print(f"\nseed={result['seed']} engine={result['engineVersion']} request={result['requestId']}")1. The allocation
allocation.stakes is how much to stake on each bet, in dollars, with the total in totalStake. kellyMultiplier is the share of full Kelly the optimizer settled on to keep risk within bounds. It is a calculation from your inputs, not advice.{
"kellyMultiplier": 1,
"riskConstraintMet": true,
"totalStake": 65.07,
"stakes": [
{
"pickIndex": 0,
"id": "lad",
"team": "Dodgers",
"betType": "MONEYLINE",
"line": null,
"odds": -145,
"fraction": 0.055356,
"stake": 55.36
},
{
"pickIndex": 1,
"id": "chc",
"team": "Cubs",
"betType": "MONEYLINE",
"line": null,
"odds": 120,
"fraction": 0.009715,
"stake": 9.71
}
]
}2. Each pick
Every pick comes back in the order you sent it, including the ones with no stake. included says whether it has one; when it does not, reason says why. NO_EDGE is the usual one: at your rating the bet is worth less than the price, so the best stake is nothing.
[
{
"index": 0,
"id": "lad",
"team": "Dodgers",
"opponent": "Padres",
"betType": "MONEYLINE",
"line": null,
"odds": -145,
"eventId": null,
"included": true,
"reason": null,
"probability": {
"market": 0.571116,
"yours": 0.614004
},
"expectedValue": 0.037456,
"sizing": {
"riskAdjusted": {
"fraction": 0.055356,
"stake": 55.36
},
"…": "full, half, quarter, fixed"
}
},
{
"index": 1,
"id": "chc",
"team": "Cubs",
"opponent": "Cardinals",
"betType": "MONEYLINE",
"line": null,
"odds": 120,
"eventId": null,
"included": true,
"reason": null,
"probability": {
"market": 0.437956,
"yours": 0.459854
},
"expectedValue": 0.011679,
"sizing": {
"riskAdjusted": {
"fraction": 0.009715,
"stake": 9.71
},
"…": "full, half, quarter, fixed"
}
},
{
"index": 2,
"id": "nyy",
"team": "Yankees",
"opponent": "Red Sox",
"betType": "MONEYLINE",
"line": null,
"odds": -110,
"eventId": null,
"included": false,
"reason": "NO_EDGE",
"probability": {
"market": 0.5,
"yours": 0.5
},
"expectedValue": -0.045455,
"sizing": {
"riskAdjusted": {
"fraction": 0,
"stake": 0
},
"…": "full, half, quarter, fixed"
}
}
]probability.market is the fair chance once the bookmaker’s margin is removed; probability.yours is that chance moved by your confidence, and is what drove the stake.
3. The risk
risk has the simulated outcome of each sizing: full, half and quarter Kelly, fixed, and riskAdjusted, which is the allocation. independent treats bets as unrelated; correlated, on Sharp and Pro, allows for bets that tend to win or lose together.
{
"riskAdjusted": {
"independent": {
"trials": 100000,
"meanProfit": 2.21,
"stdDev": 46.73,
"probLoss": 0.38552,
"var5": -65.07,
"cvar5": -65.07
},
"correlated": {
"trials": 100000,
"meanProfit": -1.44,
"stdDev": 47.87,
"probLoss": 0.42916,
"var5": -65.07,
"cvar5": -65.07
}
}
}4. Settings, and where they came from
The request sent no settings, so each came from your saved app settings or the defaults. Send maxTotalExposure, perBetCap or maxBets to override them for one call.
{
"maxTotalExposure": {
"value": 1,
"source": "default"
},
"perBetCap": {
"value": 0.2,
"source": "default"
},
"maxBets": {
"value": null,
"source": "default"
}
}5. What to keep
seed, engineVersion and requestId. With your request, the first two reproduce this exact result later; see Same answer twice.