Files
swym/docs/strategy-dsl.md

9.2 KiB

This document provides guidance on how to translate a trading strategy into the Swym rules-based strategy DSL.

Swym requires a strategy to be expressed as a single valid JSON object conforming exactly to the schema below. No prose, no markdown fences, just the JSON.

Top-level shape

{ "type": "rule_based", "candle_interval": "", "rules": [ , ... ] }

candle_interval must be one of: "1m" | "5m" | "15m" | "1h" | "4h" | "1d"

Rule

{ "comment": "", // omit if empty "when": , "then": { "side": "buy" | "sell", "quantity": "" } }

quantity is the per-order base-asset size. Use "0.001" for BTC, "0.01" for ETH, etc. All rules that fire on the same candle close will execute. Typically you have one buy rule (gated on position=flat) and one sell rule (gated on position=long).

Conditions

All conditions have a "kind" discriminator.

Combinators

{ "kind": "all_of", "conditions": [, ...] } { "kind": "any_of", "conditions": [, ...] } { "kind": "not", "condition": }

Position

{ "kind": "position", "state": "flat" | "long" | "short" }

Legacy EMA shortcuts (use these for simple EMA strategies; avoid for complex ones)

{ "kind": "ema_crossover", "fast_period": , "slow_period": , "direction": "above" | "below" } { "kind": "ema_trend", "period": , "direction": "above" | "below" } // ema_trend: true when close is above (or below) the EMA

Legacy RSI shortcut

{ "kind": "rsi", "period": <int, default 14>, "threshold": "", "comparison": "above" | "below" }

Legacy Bollinger shortcut

{ "kind": "bollinger", "period": <int, default 20>, "num_std_dev": "<decimal, default 2>", "band": "above_upper" | "below_lower" }

Price level

{ "kind": "price_level", "price": "", "direction": "above" | "below" }

Generic numeric comparison ← use this for any indicator comparison

{ "kind": "compare", "left": , "op": ">" | "<" | ">=" | "<=" | "==", "right": }

Crossover / crossunder ← detects left crossing above/below right vs. previous bar

{ "kind": "cross_over", "left": , "right": } { "kind": "cross_under", "left": , "right": }

Expressions (Expr)

All expressions have a "kind" discriminator.

Literal constant

{ "kind": "literal", "value": "" }

OHLCV candle field

{ "kind": "field", "field": "open"|"high"|"low"|"close"|"volume" } { "kind": "field", "field": "close", "offset": 1 } // 1 = previous bar, 2 = 2 bars ago

Rolling-window function over a candle field

{ "kind": "func", "name": , "field": "open"|"high"|"low"|"close"|"volume", // which candle field (ignored for atr/adx/supertrend) "period": , "offset": // omit or 0 = current bar; 1 = window shifted one bar back // "multiplier": "" — only for supertrend (ATR multiplier, typically "3.0") }

Rolling function applied to an arbitrary sub-expression (function composition)

{ "kind": "apply_func", "name": , // see valid names below — NOT atr/adx/supertrend/rsi "input": , // inner expression evaluated at each bar in the window "period": // "offset": — omit or 0 = ending at current bar } // Use this for: EMA of EMA, WMA of expression (Hull MA), VWAP = Sum(close*vol)/Sum(vol), etc.

Arithmetic

{ "kind": "bin_op", "op": "add"|"sub"|"mul"|"div", "left": , "right": }

Unary math

{ "kind": "unary_op", "op": "abs"|"sqrt"|"neg"|"log", "operand": }

FuncName values

name description field used?
highest rolling max yes
lowest rolling min yes
sma simple moving average yes
ema exponential moving average (EMA multiplier 2/(n+1)) yes
wma weighted moving average (oldest weight=1, newest=n) yes
rsi RSI via Wilder's smoothing, result in [0,100] yes
std_dev population standard deviation yes
sum rolling sum yes
atr Average True Range (Wilder simple avg, not smoothed) NO (uses H/L/C)
supertrend Supertrend line (band-flip, ATR-based) NO (uses H/L/C)
adx Average Directional Index, Wilder smoothing [0,100] NO (uses H/L/C)

atr, supertrend, adx, and rsi are NOT valid inside "apply_func" (return None silently).

Warm-up / minimum bar counts required before a condition can fire

expression / condition minimum candle history
sma / ema / wma / sum(N) N bars
rsi(N) N+1 bars
std_dev(N) N bars
highest/lowest(N) N bars
atr(N) N+1 bars
adx(N) 2*N+1 bars
supertrend(N) N+2 bars
apply_func(outer_N, inner) outer_N + inner warmup
ema_crossover(fast, slow) slow+1 bars

Until warm-up is satisfied the condition returns false (no order is placed). Use a candle_interval and backtesting window long enough to cover warm-up.

Rules

  • Rules are evaluated independently on every candle close.
  • All rules whose when is true simultaneously fire their then action.
  • Gate buy rules with { "kind": "position", "state": "flat" } to prevent pyramiding.
  • Gate sell rules with { "kind": "position", "state": "long" }.
  • You can have multiple exit rules (e.g. profit target AND stop-loss AND indicator exit) by writing separate sell rules — the first one to fire closes the position.
  • There is no native stop-loss or take-profit level primitive; express those as compare conditions against close vs. a Literal or a lagged Field.

Common patterns

EMA crossover entry

"when": { "kind": "all_of", "conditions": [ { "kind": "ema_crossover", "fast_period": 9, "slow_period": 21, "direction": "above" }, { "kind": "position", "state": "flat" } ]}

RSI oversold entry

"when": { "kind": "all_of", "conditions": [ { "kind": "rsi", "period": 14, "threshold": "30", "comparison": "below" }, { "kind": "position", "state": "flat" } ]}

ATR-based volatility filter (close must be > 1 ATR above SMA)

"when": { "kind": "compare", "left": { "kind": "field", "field": "close" }, "op": ">", "right": { "kind": "bin_op", "op": "add", "left": { "kind": "func", "name": "sma", "field": "close", "period": 20 }, "right": { "kind": "func", "name": "atr", "field": "close", "period": 14 } } }

Supertrend crossover entry (close crosses above supertrend line)

"when": { "kind": "all_of", "conditions": [ { "kind": "cross_over", "left": { "kind": "field", "field": "close" }, "right": { "kind": "func", "name": "supertrend", "field": "close", "period": 10, "multiplier": "3.0" } }, { "kind": "position", "state": "flat" } ]}

VWAP entry (close crosses above VWAP over last 20 bars)

"when": { "kind": "cross_over", "left": { "kind": "field", "field": "close" }, "right": { "kind": "bin_op", "op": "div", "left": { "kind": "apply_func", "name": "sum", "period": 20, "input": { "kind": "bin_op", "op": "mul", "left": { "kind": "field", "field": "close" }, "right": { "kind": "field", "field": "volume" } }}, "right": { "kind": "func", "name": "sum", "field": "volume", "period": 20 } } }

Hull Moving Average (HMA = WMA(2*WMA(N/2) - WMA(N), sqrt(N)))

// Step 1: wma_half = WMA(close, N/2) // Step 2: wma_full = WMA(close, N) // Step 3: raw = 2*wma_half - wma_full // Step 4: hma = WMA(raw, floor(sqrt(N))) { "kind": "apply_func", "name": "wma", "period": 4, // sqrt(16) = 4 "input": { "kind": "bin_op", "op": "sub", "left": { "kind": "bin_op", "op": "mul", "left": { "kind": "literal", "value": "2" }, "right": { "kind": "func", "name": "wma", "field": "close", "period": 8 }}, // N/2 "right": { "kind": "func", "name": "wma", "field": "close", "period": 16 }}} // N

Constraints to respect

  1. All numeric values (quantity, threshold, multiplier, value) must be JSON strings, not numbers.
  2. "offset" and "multiplier" may be omitted when their value is 0 / None respectively.
  3. "field" in "func" defaults to "close" when omitted — include it explicitly to be safe.
  4. Decimal values in "field": use snake_case: "open", "high", "low", "close", "volume".
  5. "kind" is required on every Condition and Expr node.
  6. Do not invent new kind/name/op/field values — only use the ones listed above.
  7. The strategy must have at least one buy rule and at least one sell rule.
  8. A condition returning false (e.g. during warm-up) never fires an order — this is safe.