Docs menu
Recipe

Your first optimize

Size a small slate and read every part of the answer: the allocation, what happened to each bet, how risky it is and the settings used.
You need an API key in 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

first-optimize
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

This is the answer
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.