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
- Draft the condition. Check it with
POST /api/v1/dsl/validate, and dry-run it against the live book withPOST /api/v1/dsl/evaluate. Neither needs auth, and neither creates anything. - Create the strategy with
POST /api/v1/strategies, supplying anexpression,legs, or both. A created strategy arms — the engine picks it up and evaluates it without a further call. Whether it is still armed isis_activeon the strategy row. - 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_idis the fire-once guard. - The attempt is logged either way — as an execution row with
action_taken: trueand anorder_id, oraction_taken: falseand askip_reason. See List executions. - Disarm, edit, or delete.
PUT /api/v1/strategies/{id}withis_active: falsedisarms without losing the row;DELETE /api/v1/strategies/{id}hard-deletes it and its legs. A strategy withexpires_atauto-deactivates at that time.
trigger_order_id set stays dormant until that entry order
fires — that is how OTO legs wait for their entry.
List strategies
Request
Example
Response
Array of strategy objects.Create strategy
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 ” 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 readsref_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:
% 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:
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 a400. max_drawdown/max_drawdown_pct— both which key you used and its value.total_capitalitself 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).
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
404 if it does not exist or is not yours.
Update strategy
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
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 everythingPUT /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” (therepeatSeries 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_configand 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.
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
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
null is treated the same as omitting it.
Delete leg
204 No Content.
DSL endpoints
The Krisis DSL is the small expression language used inexpression,
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 as0, soask <= 0.35is 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, withtimeframeone of1s,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
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.
