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