Strategy spec reference

June 26, 2026 · View on GitHub

A Wickra strategy is data, not code: a single JSON document describing the indicators, the entry/exit logic, sizing, costs, risk and execution. The same document runs identically in Rust, Python, Node.js, WASM, C, C++, C#, Go, Java and R, and the backtest matches a live run by construction.

A machine-readable JSON Schema is committed at schema/strategy_spec.schema.json and is printed by wkbt schema. This page is the human-readable companion.

Top level

{
  "spec_version": 1,            // optional, defaults to 1
  "symbol": "BTCUSDT",          // required
  "ref_symbol": "ETHUSDT",      // optional — second series for pairwise indicators
  "timeframe": "1h",            // required, free-form label (e.g. 1m/5m/1h/1d)
  "indicators": { /* name -> indicator */ },
  "entry":  { /* condition */ },     // required — opens a long
  "exit":   { /* condition */ },     // required — closes a long
  "short_entry": { /* condition */ },// optional — opens a short
  "short_exit":  { /* condition */ },// optional — closes a short
  "sizing": { /* sizing */ },        // required
  "costs":  { /* costs */ },         // optional
  "risk":   { /* risk */ },          // optional
  "execution": { /* execution */ },  // optional
  "warmup": 200                      // optional — overrides the auto warmup
}

Any indicator name referenced in a condition must be declared in indicators, or the spec is rejected at parse time.

Indicators

"indicators": {
  "ema_fast": { "type": "Ema", "params": [20] },
  "macd":     { "type": "Macd", "params": [12, 26, 9] },
  "imb":      { "type": "OrderBookImbalanceTop1", "params": [], "feed": "orderbook" }
}
FieldMeaning
typeThe Wickra indicator name (e.g. Ema, Rsi, Macd, Atr).
paramsThe constructor parameters, in order. Defaults to [].
feedkline (default), trade or orderbook — the input the indicator consumes.

Multi-output indicators expose their fields as "name.field" in operands — for example macd.macd, macd.signal, macd.histogram, bb.upper, bb.middle, bb.lower, dc.upper, dc.lower. A bare "name" refers to the indicator's primary field.

Operands

An operand evaluates to a number each bar. It is one of:

FormMeaning
"ema_fast"An indicator value (or "name.field" for a named field).
70A literal constant.
{ "price": "close" }A price field: open, high, low, close, volume, hlc3, ohlc4.
{ "prev": ["ema_fast", 1] }The operand's value n bars ago.
{ "add": [a, b] }a + b.
{ "sub": [a, b] }a - b.
{ "mul": [a, b] }a * b.
{ "div": [a, b] }a / b.

Arithmetic operands nest, so { "sub": ["ema_fast", "ema_slow"] } is itself an operand usable anywhere a number is expected.

Conditions

A condition evaluates to true/false each bar:

FormMeaning
{ "gt": [a, b] }a > b (also lt, ge, le, eq, ne).
{ "cross_above": [a, b] }a crossed above b this bar.
{ "cross_below": [a, b] }a crossed below b this bar.
{ "between": [a, lo, hi] }lo <= a <= hi.
{ "rising": [a, n] }a is greater than its value n bars ago.
{ "falling": [a, n] }a is less than its value n bars ago.
{ "all": [c1, c2, …] }All sub-conditions true (AND).
{ "any": [c1, c2, …] }Any sub-condition true (OR).
{ "not": c }Negation.
{ "in_position": true }A position is currently open.
{ "bars_since_entry": { "ge": 5 } }Bars since entry satisfies the predicate (gt/lt/ge/le/eq).

Sizing

Tagged by type:

SizingMeaning
{ "type": "fixed_fraction", "fraction": 0.95 }A fraction of current equity.
{ "type": "fixed_qty", "qty": 1.0 }A fixed quantity of the base asset.
{ "type": "fixed_cash", "cash": 1000 }A fixed cash notional.
{ "type": "vol_target", "target_vol": 0.02, "lookback": 20 }Scale notional to a target per-bar return volatility. No position until lookback bars exist.
{ "type": "risk_per_trade", "risk_pct": 1.0 }Size so a stop-loss hit loses risk_pct of equity (needs risk.stop_loss_pct).

Costs

"costs": {
  "maker_bps": 2,                 // fee on resting limit fills
  "taker_bps": 5,                 // fee on market / stop / close fills
  "slippage": { "type": "fixed_bps", "bps": 2 },
  "funding": false                // charge perpetual funding to open positions
}

Slippage models (tagged by type):

SlippageMeaning
{ "type": "fixed_bps", "bps": 2 }A fixed number of basis points.
{ "type": "spread" }The order book's half-spread (needs an order-book feed).
{ "type": "volume_impact", "coef": 0.1 }Linear impact coef * order_qty / bar_volume.

funding requires a derivatives feed: each bar an open position is charged qty * mark_price * funding_rate (longs pay when the rate is positive, shorts receive).

Risk

All fields are optional:

FieldMeaning
stop_loss_pctStop-loss as a percent move against the position.
take_profit_pctTake-profit as a percent move in favour.
trailing_stop_pctTrailing stop as a percent retrace from the favourable extreme.
max_leverageMaximum leverage (default 1× — no leverage).
max_position_pctMaximum position as a percent of equity.
liquidationLiquidate a leveraged position intrabar at its bankruptcy price (bites only above 1× leverage).

Stops and targets are evaluated intrabar along the conservative O→H→L→C path: when both a stop and a target could be hit in the same bar, the stop wins. Fills are gap-aware — if a bar opens beyond the level (a gap), the fill is the open (the worse price for a stop, the better for a take-profit), not the unreachable level.

Execution

"execution": {
  "order_type": "market",         // market | limit | stop | stop_limit
  "fill_timing": "next_open",     // next_open (default) | close
  "limit_offset_pct": -0.5,       // required for limit orders
  "stop_offset_pct": 0.5,         // required for stop orders
  "latency_bars": 0,              // extra bars before a fill
  "partial_fills": false,
  "max_participation": 0.1        // required when partial_fills is set
}

The default fill_timing is next_open: a signal computed on a bar's close fills on the next bar's open, which structurally prevents look-ahead bias. close is an opt-in, deliberately optimistic mode (fills on the very close that produced the signal — not tradeable live) and is restricted to market orders with no latency.

For limit/stop orders the offset is a percent of the signal bar's close: a negative limit_offset_pct places a long limit below the market (buy the dip), a positive stop_offset_pct places a long stop above it (breakout). A resting order is good-till-filled.

Report

run / run_json return a BacktestReport:

{
  "schema_version": 1,
  "initial_capital": 10000.0,
  "metrics": {
    "pnl": …, "return_pct": …, "sharpe": …, "sortino": …, "calmar": …,
    "max_drawdown": …, "win_rate": …, "profit_factor": …, "num_trades": …
  },
  "trades": [ { "entry_time": …, "exit_time": …, "entry_price": …, "exit_price": …,
               "qty": …, "pnl": …, "return_pct": …, "reason": "…" } ],
  "equity": [ { "time": …, "equity": … } ],
  "fees_paid": …
}

A trade's reason is one of signal (an exit rule fired), stop_loss, take_profit, trailing_stop, liquidation or end (the position was still open when the data ran out). A negative qty is a short. equity is a per-bar stream, suitable to write as JSON Lines and tail live.

See the Cookbook for complete, runnable strategies.