Skip to main content
This page documents the low-level strategy primitives: creating and editing strategies, attaching legs, and checking DSL expressions. Reach for it when the dedicated Conditional Orders endpoints don’t express the condition you want — those endpoints build strategies for you under the hood. A strategy is the core Krisis object: a named rule attached to a market that the engine evaluates on every tick. A strategy carries an optional top-level DSL expression and/or a set of legs — each leg is one condition paired with one order to submit when that condition is true. All endpoints require an Authorization: Bearer <jwt> header except dsl/validate and dsl/evaluate, which are public.

Strategy lifecycle

  1. Draft the condition. Check it with POST /api/v1/dsl/validate, and dry-run it against the live book with POST /api/v1/dsl/evaluate. Neither needs auth, and neither creates anything.
  2. Create the strategy with POST /api/v1/strategies, supplying an expression, legs, or both. A created strategy arms — the engine picks it up and evaluates it without a further call. Whether it is still armed is is_active on the strategy row.
  3. The engine evaluates it every tick. When a leg’s condition is truthy and no order is already in flight for the strategy, the leg’s order is submitted. pending_order_id is the fire-once guard.
  4. The attempt is logged either way — as an execution row with action_taken: true and an order_id, or action_taken: false and a skip_reason. See List executions.
  5. Disarm, edit, or delete. PUT /api/v1/strategies/{id} with is_active: false disarms without losing the row; DELETE /api/v1/strategies/{id} hard-deletes it and its legs. A strategy with expires_at auto-deactivates at that time.
A strategy with trigger_order_id set stays dormant until that entry order fires — that is how OTO legs wait for their entry.

List strategies

Returns every strategy owned by the caller.

Request

Example

Response

Array of strategy objects.

Create strategy

Creates a strategy and, optionally, its legs in the same call. Returns 201 Created with the strategy object.

Request

* You must supply at least one of expression or legs.

Example

Validation

action_config

action_config is stored as-is and read by the engine when a leg fires, so it is where per-strategy execution detail lives that the strategy row itself has no column for. Keys the engine acts on: The automation drawer accepts decimal whole cents. Clear its Exit slippage field to restore the shared entry cap. The review summary shows the configured exit cap; incomplete values block launch. Worked example: “Stop the strategy if it gives back 10% of its allocation” is total_capital: 200, max_drawdown_pct: 10 — the engine deactivates the strategy once strategy_drawdown_pct (its loss from the best mark-to-market it has reached) hits 10% of the 200allocation.Inthebuilderthisisthe"default;switchitto"200 allocation. In the builder this is the "%" side of the Max drawdown toggle, which is the default; switch it to "” to set a fixed dollar amount instead (max_drawdown — needs the same total_capital allocation either way).

Exit policy

action_config.exit is how a strategy leaves a position — say it once here instead of hand-writing a sell leg. It has three shapes, and the builder offers them as three buttons under “When to exit”: Worked example: to take profit once the market trades above 90¢, choose “Sell when…” and write bid >= 0.90 — the builder sends {"kind":"market","when":"bid >= 0.90"}.

Stop loss and take profit

“Sell when…” carries three quick-picks above the condition blocks, so the two exits most strategies want are one click rather than an expression: Both are relative to entry_price, never to a fixed level — a stop written as a hardcoded 45¢ stops meaning anything once the entry fills somewhere else. The percentages are only starting points: they are ordinary block rows underneath, so edit them. “All of” vs “any of”. Blocks normally join with and — every one has to be true. A stop loss and a take profit joined that way would need the price to be below and above the entry at once, so it would never sell. The exit block list therefore carries an All of / Any of switch as soon as it holds two blocks, and the quick-picks set it to “any of” for you. “Any of” compiles to or, with the guards kept outside the group:
strategy_position_qty > 0 is there because entry_price reads 0 before the first fill, which would make a take profit trivially true the moment the strategy armed.

Selling part of a position

fraction on a limit or market exit sells that share of the position instead of all of it — {"kind":"market","when":"bid >= 0.90","fraction":0.5} sells half and leaves the rest running. It is a number in 0 < f <= 1; absent means the whole position, hold rejects it outright. The builder offers All / 50% / 25% / custom under “Sell how much”, and sends no fraction at all for “All”. Whichever shape you pick, do not also submit a leg labelled exit yourself — Krisis reserves that label for the leg it generates, and the two together are a 400. This choice is locked once the strategy launches. Switching from Hold to Limit, or editing the limit price or sell condition, requires stopping the strategy and creating a new one — see What’s locked after launch.

Oracle conditions

The oracle is not a separate setting. A strategy declares its feed by using it: the moment a condition reads ref_spot_price or ref_strike, the caller sends the matching reference_feeds / reference_values alongside it. The builder does exactly that — the Oracle fields sit in the same condition picker as ask and bid, and picking one adds the declaration to action_config. The two shapes are: Compare relatively, not absolutely. The price to beat moves every window, so a hardcoded level stops meaning anything after the first rollover. Express the edge as an offset instead:
That is what the builder’s % value mode produces; # still takes an absolute number when you want one, and field compares two variables directly (ref_spot_price >= ref_strike). Gate entry on the size of the move, either direction. “Only enter if the oracle is more than 0.5% away from the price to beat” doesn’t care which way it moved — that’s abs(), not a signed offset:
The builder’s Oracle distance from price to beat (%) field compiles to exactly this. Oracle move vs price to beat (%) is the signed version — positive above the price to beat, negative below — for when the direction matters as well as the distance. Where the builder gets the symbol and the price to beat. For a one-off strategy the builder first tries to identify the market’s series (asset + interval); if that succeeds, the oracle symbol follows the same provider-specific mapping the engine uses (buildOracleSymbol), e.g. a Polymarket 5m/15m market becomes <asset>-usd-polymarket-twap60. If no series is found — a next-window contract, or a market the discovery step doesn’t recognise — the builder falls back to the terminal’s own contract classifier, and the “Oracle:” line under the condition says “(from this market’s contract)” so you can tell which source it used. Either way it also prefills Price to beat from the venue’s published number for that window; a Reset to current button reappears if you edit the field by hand, so you can snap back to the live value. Kalshi oracle conditions only exist for BTC and ETH. Kalshi’s CF Benchmarks feed only covers those two assets — an oracle block on any other Kalshi crypto market has no symbol to declare, so the builder disables the Oracle group and shows “Available on crypto up/down markets” instead.

What’s locked after launch

A few things are asked once, at creation, because changing them mid-run would either invalidate the risk control they protect or leave a stale generated leg behind:
  • Exit policy (action_config.exit) — see Exit policy. Update requests that change it are rejected with a 400.
  • max_drawdown / max_drawdown_pct — both which key you used and its value. total_capital itself stays editable, and a percent-based stop rescales automatically when it changes; a dollar-based stop does not.
  • A recurring run’s per-window price to beat — each window is rebuilt fresh from the run’s template, so editing an already-armed window’s strike has no lasting effect. Change the template and start a new run instead (the builder’s “Edit as new run” does this for you).
Everything else — the strategy’s name, expires_at, total_capital, max_slippage_cents, a one-off strategy’s price to beat, and any non-generated leg’s condition_expr / quantity — can be changed on a live strategy with PUT /api/v1/strategies/{id} and PUT /api/v1/strategies/{id}/legs/{leg_id}.

Get strategy

Returns one strategy object, or 404 if it does not exist or is not yours.

Update strategy

Returns the updated strategy object.

Request

All fields are optional — only those supplied are changed.
Gotcha: you cannot clear a field back to empty. Sending a field as null is treated the same as omitting it, so an omitted and an explicitly-null field are both left untouched. Once fund_id or expires_at is set, this endpoint cannot unset it.

Delete strategy

Hard-deletes the strategy and its legs. Returns 204 No Content.
Gotcha: prefer the conditional-order cancel for conditional orders. DELETE /api/v1/conditional-orders/{id} deactivates the row (keeping history) and cancels any resting venue order; this endpoint destroys the row and does neither. See Conditional Orders.

The Details drawer (web app)

Clicking a running strategy opens its Details drawer — the same read/write split the API gives you, in one panel. The top half is what’s locked after launch, shown with a lock icon and “Fixed at launch — relaunch to change”. Below that is everything PUT /api/v1/strategies/{id} and PUT /api/v1/strategies/{id}/legs/{leg_id} will actually accept on a live strategy: each entry leg’s condition and size, name, max_slippage_cents, total_capital, expires_at, and — for a one-off strategy only — the oracle price to beat. Save order. The drawer saves legs first, one at a time, and stops at the first one the engine rejects — pushing the strategy-level fields on top of a half-applied set of leg changes would leave a strategy nobody actually asked for. It reports how many legs saved before a failure. Only once every leg change succeeds does it send the strategy-level update. Pause, resume, stop. “Pause” and “Resume” toggle is_active — the strategy row and its legs are untouched, so resuming continues exactly where it left off. “Stop” hard-deletes the strategy and its legs (DELETE /api/v1/strategies/{id}); the drawer asks you to confirm first, since this is not reversible the way pausing is.

Continuous runs (recurring)

“Keep running on this series” (the repeatSeries toggle in the builder) asks the automation controller to relaunch the same template on every new window of a series, rather than arming one strategy on one market. It only unlocks once the builder has identified a current series for the selected market — a market with no discoverable series can only run as a one-off. The Continuous runs list shows each run’s state:
  • Waiting: <reason> — shown under a running run when it isn’t between windows for an ordinary reason (e.g. still waiting on the current window to close). This is normal, not an error.
  • Retrying ×N — appears once a run has hit 3 consecutive backend failures in a row, retried with increasing delay each time. It clears the next time a check succeeds.
  • Next check <time> — when the controller will next look for a window to arm.
  • View configuration — shows the most recently armed window’s actual action_config and legs (the controller doesn’t serve the template itself, so this is the receipt of what the template produced).
  • Edit as new run — opens the builder pre-filled from the current armed window, for changing the template. It does not touch the run in progress; stop the old run yourself once the new one is armed.
A run’s per-window strategies are children, rebuilt from the template each window — editing one directly in the Details drawer has no lasting effect past that window, which is why the run view offers “Edit as new run” instead of an in-place edit.

Strategy legs

A leg binds one DSL condition to one order. When the condition evaluates truthy and no order is already in flight, the engine submits the leg’s order.

List legs

Response

Array of leg objects.

Create leg

Returns 201 Created with the leg object.

Request

Validation: side must be "buy" / "sell"; order_type must be "market" / "limit"; every DSL expression must compile.

Update leg

Takes the same fields as Create leg, all optional — only fields present in the request body are updated. As with updating a strategy, sending a field as null is treated the same as omitting it.

Delete leg

Returns 204 No Content.

DSL endpoints

The Krisis DSL is the small expression language used in expression, condition_expr, quantity_expr, and price_expr. The two endpoints below let you check and trial expressions without creating a strategy.

DSL variables

The engine injects these into every evaluation:
Gotcha: ref_* variables read as 0 until the strategy declares the feed. Reference them only alongside a matching reference_feeds / reference_values entry in action_config — see Oracle conditions. POST /api/v1/dsl/validate accepts the same action_config, and compiles against the names it declares, so validating a ref_* expression without one reports an unknown variable that the armed strategy would not have hit.
Gotcha: an absent quote reads as 0, so ask <= 0.35 is true on a dead book and your strategy fires into nothing. Always pair a price comparison with its guard — ask > 0 and ask <= 0.35.
Gotcha: seconds_until_expiry only ever counts down on a recurring run. A one-off strategy has no expires_at, so the variable reads +inf and a condition built on it arms and never fires. The builder blocks submit if you reference it without “Keep running on this series” turned on, and the error names the fix.
Gotcha: the live engine has no candle series. atr(period, "timeframe") is available, with timeframe one of 1s, 1m, 5m, 15m, 1h, 4h, 1d. Beyond that, expressions referencing candle indicators, volume, or bar history are rejected at compile time rather than armed and left to never fire. Trial them against Backtest instead, which replays candles you supply.

Validate an expression

Parses and compiles an expression. Public — no authentication required.

Request

Example

Response

Gotcha: a rejected expression is still 200 OK. The status does not change — check valid and read error. A client that branches on HTTP status alone will treat every broken expression as valid.

Evaluate an expression

Compiles the expression and runs it once against the current live market context for a market. Public — no authentication required.

Request

Example

Response

The builder groups market selection, entry conditions, order settings, and exit risk into separate sections. Use the section shortcuts to jump between them, Review to check the full trade, or Expand all to see every section together. Browse strategy templates opens the full template library; closing it keeps your settings. All condition types, advanced expressions, simulation options, market-maker settings, and run limits remain available. The automation list separates strategy details from actions. Active, History, and Presets switch between your running strategies, past activity, and saved configurations. Each strategy retains its detail, history, reuse, save, pause/resume, rename, and delete actions where applicable. Setup returns to the market settings. Entry, Exit, and Review jump directly to those sections. Expand all is available beside the template browser, or above Conditions when using the expression editor. Strategy status and details appear before the management actions in the automation list.