Strategy grammar

Every TinyCorp Signal strategy is a JSON config. The Builder lets you assemble it visually, but the raw shape is documented here so power users can fork-and-edit. The engine runs your config against live market data every 5 minutes and logs simulated fills.

Reference

Overall shape

A strategy config is a JSON object with five top-level keys:

{
  "platforms": ["kalshi", "poly", "manifold"],
  "categories": ["weather", "sports", "crypto", ...],
  "entry": { /* all filter fields below go here */ },
  "exit":  { "at": "expiry" },
  "sizing": { "type": "flat", "amount_cents": 500 },
  "side":   "YES"
}

Every cycle (every 5 minutes), the engine fetches markets matching platforms + categories, then applies the entry filter rules in order. Matching markets get a simulated fill at the current ask.

platforms + categories

FieldTypeNotes
platformslist<str>Subset of kalshi, poly, manifold. Defaults to ["kalshi"] if omitted.
categorieslist<str>Optional. Empty list = all categories. Kalshi has weather. Poly has crypto, sports, politics, economics, commodities, stocks, other, world-events, entertainment.

Price filters

FieldTypeNotes
entry.yes_ask_minfloat 0-1Minimum YES ask (e.g., 0.85 = 85¢).
entry.yes_ask_maxfloat 0-1Maximum YES ask. Use both for a band.
entry.yes_bid_minfloat 0-1Minimum YES bid.
entry.yes_bid_maxfloat 0-1Maximum YES bid.

Liquidity filters

FieldTypeNotes
entry.vol_24h_minintMinimum 24h volume in USD (or MANA on Manifold).
entry.open_interest_minintMinimum open interest contracts.
Kalshi quirk: volume_24h is NULL on most weather brackets. If you set vol_24h_min on a kalshi+weather strategy, you'll exclude most candidates. Leave it off for weather; use it for poly markets.

Timing filters

FieldTypeNotes
entry.hours_to_close_minfloatMin hours until market close. 0.5 = at least 30 min out.
entry.hours_to_close_maxfloatMax hours until close. 8 = within 8 hours of close.
entry.title_containslist<str>Market title must contain each substring (case-insensitive). E.g., ["minimum"] for LOW brackets.

Momentum signals

Price-delta filters that require a lookback window. Engine reads from market_price_snapshots (cached every 1-3 min per platform).

FieldTypeNotes
entry.ya_change_window_minint (minutes)Required when using ya_change filters. Defines the lookback. E.g., 30 = look 30 min back.
entry.ya_change_min_centsintYES ask must have RISEN by at least this many cents in the window. Positive value.
entry.ya_change_max_centsintYES ask must have DROPPED by at least this many cents (use negative). E.g., -5 = dropped 5+ cents.
// "Buy when ya dropped 5+¢ in the last 30 min"
{
  "yes_ask_min": 0.40,
  "yes_ask_max": 0.85,
  "ya_change_max_cents": -5,
  "ya_change_window_min": 30
}

Volume-spike signals

FieldTypeNotes
entry.vol_spike_ratio_minfloatCurrent 24h volume / lookback-ago volume. 2.0 = doubled. Requires ya_change_window_min.

Calendar gates

Short-circuit filters that fire before any market query. Use for strategies that should only run during specific times.

FieldTypeNotes
entry.hour_of_day_minint 0-23UTC hour. Strategy skips entire cycle outside the range.
entry.hour_of_day_maxint 0-23UTC hour.
entry.day_of_week_inlist<int>Mon=0..Sun=6. [0,1,2,3,4] = weekdays only.

External anchors

Signals that reference external data sources we poll.

FieldTypeNotes
entry.crypto_anchorobject {"symbol": "bitcoin", "usd_above": 70000} or "usd_below". Spot price from CoinGecko (free, polled every 5 min). Supported symbols: bitcoin, ethereum, solana, ripple, dogecoin. Fails closed if no fresh data in 30 min.
entry.nws_anchorobject {"value_above": 50} or {"value_below": 60} — degrees F. Per-candidate filter: parses the Kalshi weather ticker (e.g. KXLOWTSFO-26MAY24-B49.5 → SFO low for May 24), fetches the latest NWS forecast for that station/date, applies the threshold. Supported stations: KSFO, KAUS, KATL, KBOS, KDCA, KDEN, KDAL, KHOU, KLAS, KLAX, KMDW, KMIA, KMSP, KNYC, KOKC, KPHL, KPHX, KSAT, KSEA. NWS forecasts updated hourly. Fails closed when no forecast available.
entry.finance_anchorobject {"symbol": "^VIX", "above": 20} — also below, change_pct_above, change_pct_below (% move today). Gates on stocks / indices / commodities / rates / macro from our finance feed (Stooq quotes + FRED). Supported symbols: ^GSPC (S&P 500), ^IXIC (Nasdaq), ^DJI (Dow), ^VIX, GC=F (gold), CL=F (WTI oil), AAPL, TSLA, NVDA, MSFT, AMZN, GOOGL, META, US10Y, FEDFUNDS, UNRATE, CPIYOY, YC2S10S, GDPGROWTH. Fails closed on stale data.
entry.energy_anchorobject {"metric": "crude", "draw_above": 1000} — also build_above. Gates on the latest weekly EIA report: crude oil stocks (kbbl) or natgas storage (Bcf), keyed on the week-over-week draw / build. Fails closed when no fresh report.
entry.funding_anchorobject {"symbol": "BTC", "above": 0.05} — also below. Perp funding rate (% per 8h) from OKX. High = crowded longs; negative = crowded shorts. Supported symbols: BTC, ETH, SOL. Fails closed on stale data.
entry.sports_anchorobject {"min_edge": 5}. Restricts to Kalshi MLB winner markets where our in-house win-probability model (Elo + starter-ERA, forward-tracked) beats the market price by ≥ min_edge percentage points. Uses precomputed model edges; fails closed when none.
entry.launch_anchorobject {"provider": "SpaceX", "within_days": 7} — or {"rocket": "Falcon 9"} / {"any": true}; optional status. Gates on upcoming rocket launches. Fails closed when no matching launch in the window.
entry.tropical_anchorobject {"min_classification": "HU"} (TD / TS / HU / MH) — also active, active_count_above, formation_7d_above, formation_48h_above (NHC formation odds %); optional basin (atlantic / east_pacific / central_pacific). Source: NHC CurrentStorms + Tropical Weather Outlook. Fails closed when no fresh advisory.
Combining anchors: a strategy can use any/all of these in the same entry block. E.g., a weather-fade strategy can require both nws_anchor.value_above AND a price band — both must pass.

Exit + sizing

FieldTypeNotes
exit.atstr"expiry" is the only supported value in v1. Strategies hold to market close; engine settles based on official result (Kalshi) or last_price (Poly/Manifold).
sizing.typestr"flat" only in v1.
sizing.amount_centsintDollars-per-trade in cents. 500 = $5/trade. House strategies use $5.
sizing.max_fills_per_dayintOptional cap on total fills per UTC day. Useful for broad-match strategies (cheap-price lottery, vol-spike on Poly's "other" category) that would otherwise fire on dozens of markets per cycle. Once the cap is reached, the engine stops opening new positions for the strategy until tomorrow.
sizing.max_concurrentintOptional cap on simultaneously-open positions for this strategy; the engine skips new entries while the cap is full. A surprisingly powerful risk control — losing trades tend to cluster, so capping concurrency (e.g. 5) can turn a losing config profitable.
sidestr"YES" or "NO". Most strategies are YES.

Backtesting

The backtester replays a strategy config against captured market-price history (market_price_snapshots, recorded ~every 5 minutes) and settles each simulated trade with the same exit math the live engine uses. It answers: "how would these rules have done over the data we've recorded so far?"

Backtest ≠ leaderboard. A backtest is in-sample research — you score rules against history you can already see. The leaderboard is forward, out-of-sample: every strategy paper-trades live from a clean slate going forward. Trust the leaderboard for "is this real"; use backtests to explore ideas before committing one to forward tracking.

The snapshot window grows continuously as history accrues. Delta signals (ya_change_*, vol_spike_*) are replayed with a sliding per-ticker buffer that mirrors the live engine's lookback, so a backtest verdict matches what the cron-paced runner would actually have done. max_concurrent is applied as a post-pass over the time-sorted fills.

Signals the backtester faithfully replays

Replayed from captured history
Price (yes_ask_min/max), liquidity (vol_24h_min, open_interest_min), timing (hour_of_day_min/max, day_of_week_in, title_contains)
Momentum (ya_change_window_min, ya_change_min_cents, ya_change_max_cents) and volume-spike (vol_spike_ratio_min, vol_spike_window_min) — snapshot-delta replay
wx_anchor (against wx_cf6_projections history) and eco_anchor (against the dated macro calendar)

Signals not yet replayable

These work in the live engine but aren't backtested yet — they need historical series we don't capture retroactively. When a config uses one, the backtester flags it as unsupported rather than silently skipping it (so a backtest never quietly ignores part of your rules):

FieldWhy not yet
crypto_anchorNeeds a historical crypto price series (forward capture in progress).
nws_anchorNeeds a historical NWS observation series.
injury_anchorOnly current injury state is stored; needs forward injury-history snapshots.
lineup_anchorOnly today's probable starters are stored; needs forward lineup-history snapshots.
post_peak_only, dewpoint_gateNeed derived weather series not yet captured per-snapshot.
finance_anchor, energy_anchor, funding_anchorLive in the engine; need a forward-captured quote / report / funding-rate history (accruing now).
sports_anchorDepends on the precomputed MLB model-edge table; backtest replay needs point-in-time model edges (forward-tracked from day 1).
launch_anchor, tropical_anchorOnly current launch schedule / NHC advisories are stored; need forward snapshots of past state.

How we measure performance

Every number on a strategy card, the leaderboard, and the detail page is computed the same way, in engine.perf_stats. All P&L is paper-traded at a flat $5 stake per trade — so "+$6" means +$6 on $5 bets, not on $50. Only closed trades (settled at expiry against the real market outcome) count toward P&L, win rate, EV, Sharpe, and drawdown; open positions are not marked-to-market. Headline figures are lifetime (every closed trade, across all versions of the strategy) unless a surface explicitly says "30d".

MetricExactly how it's computed
Realized P&LSum of every closed trade's profit/loss, in dollars, at the flat $5 stake.
EV / tradeRealized P&L ÷ number of closed trades — the average dollars a single trade has actually returned.
Win rateClosed trades that settled profitable (P&L > $0) ÷ all closed trades.
Sharpe (per-trade)Mean ÷ standard deviation of the per-trade percent returns (each trade's P&L ÷ its stake). Unannualized: a value of 1.0 means the average trade lands one standard deviation above breakeven. Shown only with ≥ 5 closed trades (otherwise "—"); we deliberately skip √252 annualization, which inflates tiny samples into fake 80+ Sharpes.
Max drawdownThe largest peak-to-trough drop of the cumulative-P&L curve, walked trade by trade across the whole history — i.e. the deepest the running total ever fell below a prior high-water mark. A Max DD of −$30 means a losing run gave back $30 from a peak before recovering. It is not the worst single day, and not the worst single trade. Dollars, on flat $5 stakes.
Avg win / Avg lossAverage dollars of a winning / losing closed trade — the fade-asymmetry check (many small wins vs the occasional large loss, or vice-versa).
Sample nNumber of closed (settled) trades — the denominator for win rate, EV, and Sharpe. Small n = low confidence.
Days liveCalendar days since the strategy went live and started forward paper-trading.
Per trade, not per day. Both Sharpe and Max drawdown are measured over the sequence of trades, not calendar days — a drawdown can build inside one bad cluster on a single day or stretch across several. If you want a per-day view (worst single day, or a daily-equity drawdown), that's a separate metric we can add.
Lifetime vs 30-day. The leaderboard and marketplace rank on lifetime stats (every closed trade, all versions) so a creator can't reset a weak record by shipping a "v2". A separate last-30-days window (P&L, win rate, sample n) is computed for recency-focused views.

Full examples

Fade weather brackets in the late window:

{
  "platforms": ["kalshi"],
  "categories": ["weather"],
  "entry": {
    "yes_ask_min": 0.85,
    "yes_ask_max": 0.95,
    "hours_to_close_max": 18
  },
  "exit": { "at": "expiry" },
  "sizing": { "type": "flat", "amount_cents": 500 },
  "side": "YES"
}

Multi-signal: weekday US morning Poly mover with vol spike:

{
  "platforms": ["poly"],
  "categories": ["other", "politics", "economics"],
  "entry": {
    "yes_ask_min": 0.25,
    "yes_ask_max": 0.75,
    "vol_24h_min": 10000,
    "vol_spike_ratio_min": 1.3,
    "ya_change_window_min": 60,
    "hour_of_day_min": 13,
    "hour_of_day_max": 17,
    "day_of_week_in": [0,1,2,3,4]
  },
  "exit": { "at": "expiry" },
  "sizing": { "type": "flat", "amount_cents": 500 },
  "side": "YES"
}
Live preview: drop any config into the Builder — the "Markets matching now" panel re-evaluates against live data on every chip click via POST /api/quants/preview.

API reference

EndpointMethodNotes
/api/quants/strategiesGETList public strategies. Params: category, status, sort (sharpe/pnl/win_rate/newest), limit.
/api/quants/strategies/<slug>GETSingle strategy preview + config (when entitled).
/api/quants/strategies/<slug>/pnlGETCumulative realized P&L points for the chart.
/api/quants/strategies/<slug>/tradesGETSimulated trade log (5 for preview, 50 for unlocked).
/api/quants/strategies/<slug>/candidatesGETMarkets currently matching all entry rules.
/api/quants/strategies/<slug>/versionsGETEdit history snapshots.
/api/quants/previewPOSTEvaluate an ad-hoc config (used by the Builder). Body: {"config": <config>}.
/api/quants/leaderboardGETSortable list. Params: sort, limit.
/api/quants/activityGETRecent fills + publishes + unlocks platform-wide.
/api/quants/statsGETTopline platform numbers.

House strategies using each signal

Live snapshot from /api/quants/strategies. Click any strategy name to see its full config + live performance.

Loading…