Strategy profiles
A strategy is a .env.<name> file and a subaccount. It is never a fork, never a second API key,
and never an edit to the live .env.
The frame: two strategies, one codebase, two wallets. Everything the overlay does not name is inherited — risk gates, fee model, calibration, per-sport floors, every future bug fix — so a comparison between two books measures the strategies rather than two drifting copies of the code. That inheritance is the entire product. Anything that breaks it (a fork, a clone, a second checkout) turns "which strategy is better" into an unanswerable question.
This skill covers deciding what the strategy is and getting it from idea to live money
safely. The repo's own /edge-radar skill covers running a scan once you know what you want;
edge-radar-strategy-evidence covers proving it works.
1. Before anything: is this a profile at all?
| The idea is… | Then it is… |
|---|---|
| Different sizing, price floor, per-event cap, edge floor, or execution posture for a slice of markets | A profile. Continue. |
| A different fair-value model for a market the code already prices | Not a profile — that is an edge-detector change, and it should reach both books. |
| A new sport, category or venue nothing prices yet | Not a profile — onboard the market first at the global floor, then profile it if you want a second angle on it. |
| "The same thing but with more money" | Not a strategy. Change UNIT_SIZE / KELLY_FRACTION in the base .env and record why. |
A profile answers "what should this book buy, and how big?" If the idea changes what a contract is worth, it belongs in the model, where both books get it.
2. Pick the knob — there are two lanes, not one
The most common wasted profile is one that moves a knob which does not bind in the price band the strategy is about. Sizing has two independent lanes, and they meet around 30–60c:
| Price band | What binds | The knob | What the other knob does |
|---|---|---|---|
| below ~30c | the flat floor, round(UNIT_SIZE / price) |
UNIT_SIZE — the longshot knob |
KELLY_FRACTION is inert; Kelly never clears the floor |
| above ~60c | Kelly, edge / (1 - price) |
KELLY_FRACTION — the favourites knob |
UNIT_SIZE is irrelevant; the floor was cleared long ago |
So a longshot profile that tunes KELLY_FRACTION has changed nothing, and a favourites profile
that tunes UNIT_SIZE has changed nothing. Every knob and the band it moves →
references/knobs.md.
KELLY_FRACTION is a portfolio fraction, not per-bet — the executor divides it by
batch_size = min(len(opportunities), --max-bets). That divisor doubles as the only correlation
guard in the system, so keep it ≤ 0.5 in any profile. At 1.0 a fully correlated slate reaches
full portfolio Kelly in one night.
3. Write the overlay
cp .env.longshot.example .env.<name> # or start empty — the example is a teaching file
Four rules, each of which has already cost someone something:
- Write only what differs. Every line copied across from
.envis a line that stops inheriting fixes. The fork this system replaced shippedMAX_OPEN_EXPOSURE_PCT=0,MAX_SEGMENT_EXPOSURE_PCT=0,MAX_DAYS_TO_EVENT_FOR_GAME_MARKETS=0,MAX_BET_SIZE=100andMAX_DAILY_LOSS=250— not by decision, but because nobody re-tightened the shipped defaults after cloning, while its own roadmap recorded the risk posture as "conservative, matches main repo, no change needed." KALSHI_SUBACCOUNT=<n>is mandatory, and it is the only line that isolates money. Not a second API key, not a second checkout — both still draw on one balance, and each copy'sMAX_DAILY_LOSSand exposure gates would see only their own activity, never the combined draw-down. A subaccount is an exchange-enforced separate wallet under one login (Advanced API tier), created once withclient.create_subaccount().DRY_RUN=trueuntil the evidence window closes. The base.envruns Kalshi live and a new profile must not inherit that. Removing this line is a decision with a pre-declared trigger (§5), not a step in setup.- Comment the why, with the number.
.env.longshot.examplecarries a ⚠️ block on its ownMIN_MARKET_PRICE=0.08recording that the 8–12c band it newly admits went 0W-36L (-103.3% ROI) across all six settled months, while the 0–8c band it still excludes holds the book's two biggest winners. A value with no evidence attached is folklore within six weeks.
It fails closed. A missing .env.<name> raises rather than falling back to the base .env,
because the base .env is the live-money wallet: a typo'd --profile longshto that silently
resolved to main would run one strategy's intent against the other's bankroll, live. Same
reasoning as the venue-eligibility check.
4. Select it, and prove you selected it
python scripts/scan.py sports --profile <name> --filter mlb --date today
EDGE_RADAR_PROFILE=<name> python scripts/doctor.py # every non-scan entry point
--profile is consumed by scan.py, not forwarded to the scanners; it reaches the child as
EDGE_RADAR_PROFILE, and load_dotenv() does not override variables already set, so the overlay
survives the child's own .env load. Everything that is not scan.py takes the env var, not the
flag — doctor.py, settle, reconcile, any ad-hoc script.
Run doctor.py under the profile before the first execution. It prints PROFILE = <name> and
the subaccount; a portfolio report run without the profile is reading the wrong account's money.
Confirm the banner names the wallet you meant.
5. The ladder: dry-run → pilot → live
Never straight to live, and never on a date. Each rung has an entry condition written before it
is reachable — the discipline in full, with a worked pre-declared rule, in
references/launch-and-kill.md.
| Rung | Posture | Leaves when |
|---|---|---|
| 1 — dry-run | DRY_RUN=true. Rows log identically to live, so they stay valid evidence. |
The profile's own knob is demonstrably binding. If every rejection is on edge, the profile setting is changing nothing, and going live changes nothing except downside. |
| 2 — pilot | DRY_RUN=false plus a capped edge floor (≈2–3x global) and a MAX_SEGMENT_EXPOSURE_PCT that actually binds the segment. |
The pre-declared review fires on real settled rows. |
| 3 — live | Normal floors. | — |
The pilot cap is the response to uncertainty. When a review's bootstrap CI straddles zero — and at n≈25 it will — the answer is a capped floor, not a coin flip between frozen and unfrozen.
Before flipping DRY_RUN=false on any profile, clear the soft-spot list. Three surfaces are
safe today only because the second book never fills:
- Reporting is pooled.
daily_summary.py,risk_check.pyandbetting_analysis.pyread the whole trade log. A dry run writes zero-fill rows that never settle, so nothing blends today. Real fills blend both books immediately — these needfor_profile()or a per-profile split first. - Settlement and reconciliation only ever see subaccount 0.
Hourly-Settle,NightlySettleandReconcileall run unprofiled. A live second wallet needs its own settle and reconcile tasks underEDGE_RADAR_PROFILE=<name>, or its fills never settle and its P&L never lands. (CLV-Captureis fine — it reads the unfiltered log and calls public market data.) - Scan report filenames carry no profile tag. Two profiles scanning the same filter on the same
day into the same directory overwrite each other silently. Pin
--report-dirper profile.
6. Compare the two books
One trade log holds both, and every row carries "profile". Rows written before P1 have no key,
so every reader must default to "main".
python -c "import json,collections; print(collections.Counter(t.get('profile','main') for t in json.load(open('data/history/kalshi_trades.json'))))"
One shared log is better evidence than two logs gave — both books run identical code, odds cache,
fees and calibration, so a comparison measures the strategies. But any gate reading history
rather than the venue must be scoped or it measures the wrong book. Two do, and
trade_log.for_profile() scopes both:
| Gate | Reads | Scoped by |
|---|---|---|
| 1 — daily loss limit | trade log | for_profile() |
| 7 — series dedup | trade log | for_profile() |
| 5 — already holding | live venue positions | the subaccount |
| 6 — per-event cap | live venue positions | the subaccount |
Unscoped this is not cosmetic: a bad day on main halts the other book, and a matchup one profile
bet blocks the other — across two genuinely separate wallets. It was live for exactly one run
after the merge, and the second book's banner reported the first book's P&L and dedup count.
Settlement, CLV capture and reconciliation deliberately do not scope — they act on a row by
trade_id or ticker, and a row is a row regardless of which strategy opened it.
Any new gate or report that reads the trade log must decide, explicitly, whether it is per-book or whole-book. That is the one recurring bug in this design.
7. Checklist
- Confirm it is a profile (§1), not a model change wearing a profile's clothes.
- Name the price band the strategy is about, then pick the knob that binds there (§2).
- Create and fund the subaccount. Record which number, and when.
- Write
.env.<name>— deltas only,KALSHI_SUBACCOUNTset,DRY_RUN=true, every value carrying its evidence in a comment. - Write the exit rule now, while nothing is at stake →
references/launch-and-kill.md. EDGE_RADAR_PROFILE=<name> python scripts/doctor.py— confirm the banner names the wallet.- Dry-run a scan, then read the rejection reasons, not the candidate count, to confirm the profile's knob is binding.
- Prove it → the
edge-radar-strategy-evidenceskill. - Promote to pilot only when the pre-declared rule fires, and fix the §5 soft spots first.
- Document it in
docs/<name>/README.mdon the four-layer shape — selection, settings, money, data — plus what is deliberately shared and what is still pooled.
8. Anti-patterns
- Forking the repo to try a strategy. The fork this replaced lasted six days and, on a delta of two env vars, drifted into a live defect in each direction: it scanned zero college football all September on a stale ticker prefix and was missing two shipped features, while the main side was missing its own trade-row fix. Add a profile.
- A second API key "for isolation". It isolates nothing — both keys draw on one balance.
- Editing the live
.envto test an idea. That is the live-money wallet, and the edit outlives your memory of making it. - Copying the whole
.envinto the overlay. It stops inheriting fixes the day you write it. - Tuning
KELLY_FRACTIONfor a longshot book (orUNIT_SIZEfor a favourites book). Neither binds where you mean, so the profile is a no-op you will spend a month interpreting. KELLY_FRACTIONabove 0.5. The batch divisor is the only correlation guard there is.- Going live because the dry-run "looks good". A dry run whose knob never binds is showing you the base strategy.
- Promoting on a date rather than a rule. A date cannot lose an argument with a hot week.
DRY_RUN=falsewithout a profiled settle + reconcile task. The fills are real; the P&L never lands, and you end up comparing a book whose wins are invisible.- Two profiles scanning the same filter into the same report dir. Silent overwrite.
- An overlay value with no comment saying what evidence set it. In six weeks it is a rule nobody can defend, and therefore nobody can remove.
Related
| For | See |
|---|---|
| Proving a strategy before it gets money | the edge-radar-strategy-evidence skill |
| Running a scan, placing a bet, settling | the repo's /edge-radar skill |
| Post-hoc performance of what settled | the repo's /edge-radar-analysis skill |
| Auditing the math for money bugs | the repo's /betting-logic-review skill |
| The worked example, four layers deep | docs/longshot/README.md in the repo |