Strategy System
How strategies are defined — the DSL structure, all available functions, operators, and a complete example.
#Overview
A strategy is a JSON object with six top-level keys:configuration, inputs,conditions, score, decision, and riskManagement. Each serves a distinct role in the evaluation pipeline.
strategySnapshotJson field. Internally, the engine evaluates it against historical OHLCV data candle by candle.Want a strategy to exist as its own asset, independent of any one backtest or live subscription — save it once, then reuse the same definition across both? See MCP Server (Strategy).
#Configuration
Defines the execution timeframe. The engine uses this to determine which candle interval to evaluate the strategy against.
"configuration": {
"timeframe": "1H",
"warmupBars": 21,
"maxOpenPositions": 1,
"direction": "long",
"entryFeePct": 0.1,
"exitFeePct": 0.1,
"timezone": "America/Sao_Paulo"
}Supported timeframes
| Value | Description |
|---|---|
| 5M | 5-minute candles |
| 15M | 15-minute candles |
| 30M | 30-minute candles |
| 1H | 1-hour candles (most common) |
| 2H | 2-hour candles |
| 4H | 4-hour candles |
| 1D | Daily candles |
warmupBars
Optional, defaults to 0. Number of leading candles to exclude from trading while indicators stabilize. Set it to the largest lookback period used by any indicator in the strategy — e.g. if you use ema(close, 21)and rsi(close, 14), use warmupBars: 21. This is not derived automatically. Candles inside the warmup window still appear in the results, but entry and score are forced to false / 0 so no trade opens during that range.
maxOpenPositions
Optional, defaults to unlimited. Caps how many trades can be open at the same time — once the cap is reached, new entry signals are skipped until a position closes. Set it to 1 for single-position mode. See Multiple Positionsfor how this interacts with the engine's default (unlimited) behavior.
direction
Optional, defaults to "long". Picks which side the whole backtest trades — "long" or "short". A single strategy trades one direction, not both at once. decision.entry/exit and riskManagement keep the exact same meaning either way — only the stop-loss/take-profit price placement and win/loss sign flip for "short". See Direction for the full mechanics.
entryFeePct / exitFeePct
Optional, both default to 0 (no fee, backward compatible). Simulate a per-leg exchange fee as a percent of price — e.g. 0.1for 0.1%, a typical taker rate at Binance VIP0. When set, every trade's pnlR/pnlPct in the result are net of this fee, so expectancyR/winRate/profitFactor account for it automatically — a trade that looks marginally profitable before fees can correctly flip to a loss. Real exchanges can charge different maker/taker rates per leg (e.g. a resting take-profit vs. a triggered stop-loss), so the two fields are independent — not assumed equal.
expectancyRis only a fair comparison within one strategy's own stop convention. Prefer pnlPct-based comparisons across strategies or timeframes with different typical stop widths — see the Metrics Reference.timezone
Optional, defaults to "America/Sao_Paulo". An IANA timezone id that localizes the hour()/minute()/dayOfWeek()/isWeekend() functions (see Time below) — use it for time-of-day or weekday gating conditions, e.g. only trading during a liquid session or skipping weekends.
#Inputs
Inputs are named, reusable computed values. They are evaluated once per candle before conditions are checked. An input can reference market data, built-in functions, or other previously defined inputs.
"inputs": {
"emaFast": "ema(close, 9)",
"emaSlow": "ema(close, 21)",
"rsiValue": "rsi(close, 14)",
"atrValue": "atr(14)",
"volAvg": "sma(volume, 20)",
"volSpike": "volume > volAvg * 1.5"
}Available market data
closeopenhighlowvolumeRules
- Input names must be unique.
- Inputs can reference other inputs defined above them (order matters).
- Circular references are not allowed.
- Input expressions must resolve to a numeric value.
#Conditions
Conditions are named boolean expressions. They are evaluated per candle using inputs, market data, and comparison operators. A condition must resolve to true orfalse.
"conditions": {
"trendUp": "emaFast > emaSlow",
"rsiHealthy": "rsiValue > 40 AND rsiValue < 65",
"breakout": "crossUp(close, emaSlow)",
"volConfirm": "volSpike"
}Supported operators
| Operator | Type | Example |
|---|---|---|
| > | Relational | "rsiValue > 40" |
| < | Relational | "rsiValue < 70" |
| >= | Relational | "score >= 50" |
| <= | Relational | "atrValue <= 200" |
| == | Equality | "timeframe == 1H" |
| != | Equality | "close != open" |
| AND | Logical | "condA AND condB" |
| OR | Logical | "condA OR condB" |
Rules
- All variables referenced in a condition must be defined in
inputs. - Conditions must resolve to a boolean. A numeric value of 0 is false; non-zero is true.
- No future data may be referenced. All expressions use only current or past candle data.
- Randomness is not permitted — strategies must be fully deterministic.
#Score
The score block assigns a numeric weight to each condition. On every candle, the engine sums the scores of all conditions that evaluate to true. This total is the candle's score.
"score": {
"trendUp": 20,
"rsiHealthy": 30,
"breakout": 40,
"volConfirm": 10
}The score system is what makes EmidLabs strategies compositional. Rather than requiring all conditions to be true simultaneously, you define a threshold the combined score must reach. This mirrors how real conviction is built — multiple aligned signals, not a rigid checklist.
The score values are arbitrary integers. What matters is their relative weight. A condition with score 40 contributes twice as much as one with score 20.
#Decision
The decision block defines the entry rule. The engine evaluates this expression per candle after computing the total score. When it evaluates to true, a new trade is opened at the close of that candle.
"decision": {
"entry": "score >= 50"
}The decision expression can reference score (the total for the candle) as well as any defined condition by name.
"decision": {
"entry": "score >= 50 AND breakout"
}exit (optional)
An optional signal-based exit rule, evaluated with the same rules asentry. When it evaluates to trueon a candle where a position is open, that position closes at the candle's close — independent of riskManagement. If omitted, positions only close via stop-loss or take-profit. See Execution Engine for how exit interacts with stop-loss/take-profit on the same candle.
"decision": {
"entry": "score >= 50 AND breakout",
"exit": "crossDown(emaFast, emaSlow)"
}#Risk Management
Configures how each open trade's stop-loss and take-profit are calculated. This block is optional — if omitted, the engine defaults to a 1% stop-loss and a 1:3 risk-reward take-profit (see Execution Engine for full details on the default and how R-units are computed).
"riskManagement": {
"stopLoss": { "type": "percent", "percent": 1.0 },
"takeProfit": { "type": "riskReward", "multiple": 3.0 }
}stopLoss
| type | Parameters | Description |
|---|---|---|
| percent | percent (> 0) | Stop distance as a percentage of entry price. |
| atr | period (int > 0), multiplier (> 0) | Stop distance = ATR(period) at entry × multiplier. |
takeProfit
| type | Parameters | Description |
|---|---|---|
| riskReward | multiple (> 0) | Take-profit distance = stop-loss distance × multiple. |
| percent | percent (> 0) | Take-profit distance as a percentage of entry price, independent of the stop-loss. |
"riskManagement": {
"stopLoss": { "type": "atr", "period": 14, "multiplier": 1.5 },
"takeProfit": { "type": "percent", "percent": 2.0 }
}You can set only one of stopLoss / takeProfit — the other falls back to its own default independently.
#Built-in Functions
Indicators
| Function | Description |
|---|---|
| ema(series, period) | Exponential moving average of series over period candles. |
| sma(series, period) | Simple moving average of series over period candles. |
| rsi(series, period) | Relative Strength Index. Returns 0–100. |
| atr(period) | Average True Range over period candles. |
| adx(period) | Average Directional Index. Trend strength, 0–100 — does not indicate direction. |
| adxPlusDi(period) | +DI. Directional indicator — compare against adxMinusDi to read trend direction. |
| adxMinusDi(period) | -DI. Directional indicator — compare against adxPlusDi to read trend direction. |
adx, adxPlusDi, and adxMinusDi share one underlying calculation — calling any one of them for a given period computes all three at no extra cost. A common pattern is adx(14) > 25 AND adxPlusDi(14) > adxMinusDi(14)for "strong, confirmed uptrend."
Signals
| Function | Description |
|---|---|
| crossUp(a, b) | Returns true on the candle where a crosses above b. |
| crossDown(a, b) | Returns true on the candle where a crosses below b. |
Series operators
| Function | Description |
|---|---|
| highest(series, n) | Highest value of series over the last n candles. |
| lowest(series, n) | Lowest value of series over the last n candles. |
| change(series) | Difference between the current and previous candle value. |
| shift(series, n) | Value of series n candles ago. Look-back only — returns NaN until enough history exists. |
| any(boolSeries, n) | True if boolSeries was true on any of the n candles before the current one. |
| all(boolSeries, n) | True if boolSeries was true on every one of the n candles before the current one. |
| count(boolSeries, n) | How many of the n candles before the current one had boolSeries true. |
There is no volumeSma()/volumeSpike() — volume is a market-data series like close/open/high/low, so use sma(volume, period) for a volume moving average, and volume > sma(volume, period) * multiplier for a volume-spike condition.
Structure
| Function | Description |
|---|---|
| swingHigh(series, confirmBars) | True when a confirmed swing high is recognized — confirmBars candles AFTER the actual peak, never at the peak itself. |
| swingLow(series, confirmBars) | True when a confirmed swing low is recognized — confirmBars candles AFTER the actual trough, never at the trough itself. |
The confirmation delay is intentional, not a limitation: a live evaluation genuinely cannot know a candle was a peak/trough until confirmBars candles later, so the signal fires at that same later candle in backtest too — this is what keeps backtest and live results consistent instead of the backtest quietly seeing structure before live ever could.
Candle anatomy
| Function | Description |
|---|---|
| body() | Absolute difference between open and close. |
| range() | Absolute difference between high and low. |
| upperWick() | Size of the upper wick: high - max(open, close). |
| lowerWick() | Size of the lower wick: min(open, close) - low. |
| isBullish() | True when close > open. Not the exact opposite of isBearish() — both are false when close equals open. |
| isBearish() | True when close < open. Not the exact opposite of isBullish() — both are false when close equals open. |
Math
| Function | Description |
|---|---|
| abs(x) | Absolute value. |
| min(a, b) | Minimum of two values. |
| max(a, b) | Maximum of two values. |
Time
Localized to configuration.timezone (an IANA id, e.g. "America/Sao_Paulo", default when omitted) — use these to gate entries/exits to time-of-day or weekday windows, e.g. "hour() >= 9 AND hour() < 13" or "NOT isWeekend()".
| Function | Description |
|---|---|
| hour() | Local hour, 0-23. |
| minute() | Local minute, 0-59. |
| dayOfWeek() | Local day of week, 0 (Sunday) through 6 (Saturday). |
| isWeekend() | True on Saturday or Sunday, local time. |
Candlestick patterns
All 28 functions below take no arguments, return true/false per candle, and are read directly from closed-candle OHLC (open/high/low/close) — same closed-candle-only data every other function reads, so they behave identically in backtest and live.
| Function | Description |
|---|---|
| hammer() | Small body, long lower wick (≥2× body), little/no upper wick — bullish reversal shape. |
| shootingStar() | Small body, long upper wick (≥2× body), little/no lower wick — bearish reversal shape. |
| doji() | Body is ≤10% of the candle range — indecision. |
| bullishEngulfing() | Bullish candle whose body fully engulfs the prior bearish candle’s body. |
| bearishEngulfing() | Bearish candle whose body fully engulfs the prior bullish candle’s body. |
| morningStar() | Long bearish candle, small-bodied star, then a bullish candle closing back above the first candle’s midpoint. |
| eveningStar() | Long bullish candle, small-bodied star, then a bearish candle closing back below the first candle’s midpoint. |
| bullishMarubozu() | Bullish candle with almost no wicks — body is ≥95% of the range. |
| bearishMarubozu() | Bearish candle with almost no wicks — body is ≥95% of the range. |
| spinningTop() | Small body with wicks on both sides at least as large as the body — indecision. |
| dragonflyDoji() | Doji with a long lower wick and virtually no upper wick. |
| gravestoneDoji() | Doji with a long upper wick and virtually no lower wick. |
| longLeggedDoji() | Doji with long wicks on both sides. |
| piercingLine() | Bearish candle then a bullish candle opening below its close and closing back above its body’s midpoint (without fully engulfing). |
| darkCloudCover() | Bullish candle then a bearish candle opening above its close and closing back below its body’s midpoint (without fully engulfing). |
| bullishHarami() | Small bullish body fully contained inside the prior, larger bearish body. |
| bearishHarami() | Small bearish body fully contained inside the prior, larger bullish body. |
| haramiCross() | Harami shape (either direction) where the contained candle is itself a doji. |
| tweezerTop() | Two candles with matching highs, bullish then bearish — bearish reversal. |
| tweezerBottom() | Two candles with matching lows, bearish then bullish — bullish reversal. |
| threeWhiteSoldiers() | Three consecutive bullish candles, each opening inside and closing above the prior, small upper wicks. |
| threeBlackCrows() | Three consecutive bearish candles, each opening inside and closing below the prior, small lower wicks. |
| threeInsideUp() | Bullish harami followed by a third candle closing above the first candle’s open. |
| threeInsideDown() | Bearish harami followed by a third candle closing below the first candle’s open. |
| threeOutsideUp() | Bullish engulfing followed by a third candle closing higher still. |
| threeOutsideDown() | Bearish engulfing followed by a third candle closing lower still. |
| risingThreeMethods() | Long bullish candle, three small candles contained within its range, then a bullish candle closing at a new high — 5-candle continuation. |
| fallingThreeMethods() | Long bearish candle, three small candles contained within its range, then a bearish candle closing at a new low — 5-candle continuation. |
hammer() and shootingStar()only check candle shape — they don't know whether the prior trend was up or down. Classically the same shape is a Hammer after a downtrend but a Hanging Man after an uptrend (and Shooting Star vs. Inverted Hammer, respectively). Pair them with a trend/momentum filter (e.g. rsi or a moving-average condition) rather than using the shape alone as an entry signal.#Full Strategy Example
A trend-following strategy with EMA crossover, RSI confirmation, and volume spike entry.
{
"configuration": {
"timeframe": "1H",
"warmupBars": 21
},
"inputs": {
"emaFast": "ema(close, 9)",
"emaSlow": "ema(close, 21)",
"rsiValue": "rsi(close, 14)",
"volAvg": "sma(volume, 20)",
"volRatio": "volume > volAvg * 1.3"
},
"conditions": {
"trendUp": "emaFast > emaSlow",
"rsiHealthy": "rsiValue > 40 AND rsiValue < 65",
"volConfirm": "volRatio",
"crossover": "crossUp(emaFast, emaSlow)"
},
"score": {
"trendUp": 20,
"rsiHealthy": 25,
"volConfirm": 15,
"crossover": 40
},
"decision": {
"entry": "score >= 60"
}
}In this example, entry requires either a crossover (40pts) + any one other signal, or all three non-crossover conditions simultaneously (60pts). The crossover alone is not enough — which prevents false triggers in noisy markets.
#Invalid Patterns
What to avoid
- Referencing undefined inputs in conditions or the decision block.
- Using conditions that do not resolve to a boolean (e.g., a numeric expression).
- Referencing future data (lookahead bias).
- Circular input references.
- Using randomness or non-deterministic logic of any kind.
"conditions": {
"breakout": "close > resistance" // ERROR: 'resistance' not defined in inputs
}#Copy Spec for AI
Building strategies with an LLM? Copy the condensed reference below into your system or user prompt — it strips out the tutorial prose above and keeps only the schema, functions, operators, and rules the model needs to generate valid strategy JSON.
# EmidLabs Strategy DSL — Reference for AI Strategy Generation
A strategy is a single JSON object with six top-level keys: configuration, inputs, conditions, score, decision, riskManagement.
Output must be valid JSON only — no markdown, no comments, no explanations.
## configuration
{
"timeframe": "5M" | "15M" | "30M" | "1H" | "2H" | "4H" | "1D",
"warmupBars": <int, optional, default 0>,
"maxOpenPositions": <int > 0, optional, default null = unlimited>,
"direction": "long" | "short", optional, default "long",
"entryFeePct": <number 0-5, optional, default 0>,
"exitFeePct": <number 0-5, optional, default 0>,
"timezone": <IANA id string, optional, default "America/Sao_Paulo">
}
warmupBars should be set to the largest lookback period among the indicators used in "inputs"
(e.g. if the strategy uses ema(close, 21) and rsi(close, 14), set warmupBars to 21) — the backend
now raises an unset/too-low value up to that same floor automatically, but still set it explicitly
for best results, since the auto-floor is a conservative minimum, not a guarantee of a fully
converged indicator value. Candles inside the warmup window are excluded from trading — entry
is forced false and score to 0 — because their indicator values have not fully stabilized yet.
maxOpenPositions caps how many trades can be open at the same time. Omit it (or leave it null)
for unlimited concurrent positions (the default). Set it to 1 for single-position mode — a new
entry is skipped whenever a position is already open.
direction picks which side the whole backtest trades — every position opened by "decision.entry"
is a long (buy) or a short (sell) accordingly. A single strategy is one direction, not both at
once. "entry"/"exit"/"riskManagement" keep the exact same meaning either way — only the
stop-loss/take-profit price placement and win/loss sign are mirrored for "short" (stop above
entry, take below entry, profit when price falls).
entryFeePct/exitFeePct simulate a per-leg exchange fee as a percent of price (e.g. 0.1 = 0.1%,
a typical taker rate at Binance VIP0). Both default to 0 (no fee). When set, every trade's
pnlR/pnlPct in the result are net of this fee, so expectancyR/winRate/profitFactor account for it
automatically — a trade that looks marginally profitable before fees can correctly flip to a loss.
Real exchanges can charge different maker/taker rates per leg, so the two fields are independent,
not assumed equal. Caveat: a trade's fee cost in R-units scales inversely with that trade's own
stop distance, so once a fee is set, expectancyR is only a fair comparison within one strategy's
own stop convention — prefer pnlPct-based comparisons across strategies/timeframes with different
typical stop widths.
timezone is an IANA id (e.g. "America/Sao_Paulo") that localizes the hour()/minute()/dayOfWeek()/
isWeekend() functions above — use it for time-of-day or weekday gating conditions.
## inputs
Named, reusable computed values. Evaluated once per candle, before conditions.
Available market data: close, open, high, low, volume
Built-in functions:
Indicators: ema(series, period), sma(series, period), rsi(series, period) [0-100], atr(period),
adx(period) [0-100, trend strength], adxPlusDi(period), adxMinusDi(period) [+DI/-DI, trend direction —
compare adxPlusDi vs adxMinusDi alongside adx]
Signals: crossUp(a, b), crossDown(a, b)
Series operators: highest(series, n), lowest(series, n), change(series)
Lag / window aggregation: shift(series, n) [look-back only, NaN before enough history],
any(boolSeries, n), all(boolSeries, n), count(boolSeries, n) [over the n candles before the current one]
Structure: swingHigh(series, confirmBars), swingLow(series, confirmBars) [confirmed N-bar swing point —
true confirmBars candles AFTER the actual peak/trough, never at the peak itself, so it can't repaint
between backtest and live]
Candle anatomy: body(), range(), upperWick(), lowerWick(), isBullish(), isBearish()
Math: abs(x), min(a, b), max(a, b)
Time: hour() [0-23], minute() [0-59], dayOfWeek() [0=Sunday..6=Saturday], isWeekend() [boolean] —
all localized to "configuration.timezone" (default "America/Sao_Paulo"), for time-of-day or
weekday gating, e.g. "hour() >= 9 and hour() < 13" or "not isWeekend()".
Volume: there is no volumeSma()/volumeSpike() — volume is a market-data series like close/open/high/low,
so use sma(volume, period) for a volume moving average, and volume > sma(volume, period) * multiplier
for a volume-spike condition.
Candlestick patterns (all no-arg, boolean, read only closed-candle OHLC — same in backtest and live):
Single-candle: hammer(), shootingStar(), doji(), bullishMarubozu(), bearishMarubozu(), spinningTop(),
dragonflyDoji(), gravestoneDoji(), longLeggedDoji()
Two-candle: bullishEngulfing(), bearishEngulfing(), piercingLine(), darkCloudCover(), bullishHarami(),
bearishHarami(), haramiCross(), tweezerTop(), tweezerBottom()
Three-plus-candle: morningStar(), eveningStar(), threeWhiteSoldiers(), threeBlackCrows(),
threeInsideUp(), threeInsideDown(), threeOutsideUp(), threeOutsideDown(), risingThreeMethods(),
fallingThreeMethods()
Caveat: hammer()/shootingStar() are shape-only and don't know the prior trend (the same shape is a
Hammer after a downtrend but a Hanging Man after an uptrend, and vice versa for shootingStar/Inverted
Hammer) — pair with a trend/momentum condition (e.g. rsi, ema) rather than using the shape alone.
Rules:
- Names must be unique.
- An input can only reference inputs defined above it (order matters).
- Circular references are not allowed.
- Must resolve to a numeric value.
Example: { "emaFast": "ema(close, 9)", "rsiValue": "rsi(close, 14)" }
## conditions
Named boolean expressions using inputs, market data, and operators.
Operators: > < >= <= == != AND OR
Rules:
- Every variable referenced must be defined in inputs.
- Must resolve to a boolean (0 = false, non-zero = true).
- No future data — only current or past candle data (no lookahead bias).
- No randomness — strategies must be fully deterministic.
Example: { "trendUp": "emaFast > emaSlow", "rsiHealthy": "rsiValue > 40 AND rsiValue < 65" }
## score
Integer weight per condition. On each candle, the engine sums the scores of all conditions
that evaluate to true. Values are arbitrary — only their relative weight matters.
Example: { "trendUp": 20, "rsiHealthy": 30 }
## decision
{ "entry": "<expression>", "exit": "<expression, optional>" }
"entry" is evaluated per candle after the total score is computed. May reference "score" (the
candle's total) and any condition by name. When true, a trade opens at that candle's close.
"exit" is optional and uses the same expression rules as "entry". When true on a candle where a
position is open, that position closes at that candle's close — independent of "riskManagement".
If omitted, positions only close via stop-loss/take-profit.
Example: { "entry": "score >= 50 AND breakout", "exit": "crossDown(emaFast, emaSlow)" }
## riskManagement
Optional. Configures how each open trade's stop-loss and take-profit are calculated. If omitted
entirely, the engine defaults to { stopLoss: percent 1.0, takeProfit: riskReward 3.0 } — the
same as older strategies that predate this section.
stopLoss (pick one type):
{ "type": "percent", "percent": <number > 0, e.g. 1.0 for 1%> }
{ "type": "atr", "period": <int > 0>, "multiplier": <number > 0> }
takeProfit (pick one type):
{ "type": "riskReward", "multiple": <number > 0> } // take-profit distance = stopLoss distance * multiple
{ "type": "percent", "percent": <number > 0> }
Example: { "stopLoss": { "type": "atr", "period": 14, "multiplier": 1.5 },
"takeProfit": { "type": "percent", "percent": 2.0 } }
## Execution model
- Entry fills at the close of the candle where "decision.entry" turns true — no lookahead.
- Risk per trade is governed by "riskManagement" (stop-loss and take-profit levels calculated
at entry). If "riskManagement" is omitted, the default is a fixed 1% stop-loss and a 1:3
risk-reward take-profit (win = +3.0R, loss = -1.0R). With a custom "riskManagement", win/loss
R magnitudes can vary per trade (e.g. an ATR-based stop-loss).
- A position closes on the first of three events: stop-loss hit, take-profit hit, or
"decision.exit" turning true, checked in that order each candle. If a candle's stop-loss (or
take-profit) and "decision.exit" would both trigger on the same candle, the stop-loss/
take-profit wins and "decision.exit" is not evaluated for that position that candle.
- "configuration.direction" picks long or short for the whole backtest (default long) — see
"configuration" above. There is no per-trade direction; one strategy trades one side.
- By default, multiple positions can be open at once — every candle where entry triggers opens
a new, independent trade, regardless of other open trades. Set "configuration.maxOpenPositions"
to cap concurrent trades (e.g. 1 for single-position mode).
## Invalid patterns — do not generate
- Referencing an input or condition that isn't defined.
- A condition that doesn't resolve to a boolean.
- Referencing future candle data.
- Circular input references.
- Randomness or any non-deterministic logic.
## Full example
{
"configuration": { "timeframe": "1H", "warmupBars": 21 },
"inputs": {
"emaFast": "ema(close, 9)",
"emaSlow": "ema(close, 21)",
"rsiValue": "rsi(close, 14)",
"volAvg": "sma(volume, 20)",
"volSpike": "volume > volAvg * 1.3"
},
"conditions": {
"trendUp": "emaFast > emaSlow",
"rsiHealthy": "rsiValue > 40 AND rsiValue < 65",
"volConfirm": "volSpike",
"crossover": "crossUp(emaFast, emaSlow)"
},
"score": { "trendUp": 20, "rsiHealthy": 25, "volConfirm": 15, "crossover": 40 },
"decision": { "entry": "score >= 60" },
"riskManagement": {
"stopLoss": { "type": "percent", "percent": 1.0 },
"takeProfit": { "type": "riskReward", "multiple": 3.0 }
}
}
## Submitting to the API
POST https://backtest.emidlabs.com/api/public/v1/backtest
Nest the strategy object under "strategySnapshotJson", alongside "assetPair", "initialDate",
and "finalDate".