Scenario Configuration
The tektii scenario run --file command and the POST /v1/strategies/{id}/scenarios API both take a config file: a JSON document describing the subscriptions, the date window, and the run settings for a backtest. This page is the reference for that file — the inline schema. A near-identical saved-config schema backs tektii config create, and the two are not interchangeable; see The two config schemas.
The config file unlocks two shapes the web wizard deliberately does not author: per-instrument events (each instrument subscribing to its own set of events) and a precise start time (any time of day, not just 00:00 UTC).
The CLI parses this file directly into the API request body, which uses camelCase keys: strategyVersionId, subscriptions, startTime, endTime, positionMode, initialCapital. The MCP tools and the raw public REST surface use snake_case (start_time, position_mode) — but the file you pass to --file is camelCase. Unknown keys are rejected.
The two config schemas
Two schemas describe a backtest config, and they are not interchangeable. Both reject unknown keys, so handing one command's file to the other fails with an unknown-field error rather than a helpful message.
| Inline schema | Saved-config schema | |
|---|---|---|
| Consumed by | tektii scenario run --file, POST /v1/strategies/{id}/scenarios | tektii config create --file, tektii config update --file, POST /v1/scenario-configurations |
strategyVersionId | Required | Rejected — a saved config does not pin a version, so scenario run --config supplies one with --version-id at launch |
name | Rejected | Required — from --name, or a name key in the file |
description, isActive | Rejected | Optional |
Every other field — subscriptions, startTime, endTime, positionMode, initialCapital, env, costs — is identical on both, with the same validation.
So converting an inline config file into a saved preset means deleting strategyVersionId and giving it a name; going the other way means dropping name (plus description / isActive if present) and adding strategyVersionId. See config create for that command's arguments.
The rest of this page documents the inline schema. Every shared field behaves identically under the saved-config schema.
Fields
| Field | Required | Type | Notes |
|---|---|---|---|
strategyVersionId | Yes | string | The version to run (a UUID). |
subscriptions | Yes | array | 1–50 instrument subscriptions. Each has its own events. See Subscriptions. |
startTime | Yes | string | Inclusive start of the window, as a full ISO-8601 timestamp (e.g. 2024-03-04T13:30:00Z). Any time of day. |
endTime | Yes | string | Exclusive end of the window, ISO-8601. Must be after startTime. |
positionMode | No | string | netting (default) or hedging. Omitting it is a real choice: under netting every fill on a symbol collapses into one position, so a grid or scale-in loses its per-leg ids without any error. See Position Modes. |
initialCapital | No | number | Starting capital. Default 100000; range 1–100000000. |
env | No | object | Environment overrides (string→string) passed to the strategy container. See Environment variables. Stored in plain text — never put secrets here. |
costs | No | object | Optional transaction costs — commission, slippage, funding. Each defaults to zero; omit for a spread-only backtest. See Transaction costs. |
startTime/endTime must fall within every subscribed instrument's available data, and within your plan's lookback and duration limits. Check Available Instruments for each symbol's dataStartDate → dataEndDate, and Plan Limits before sizing a long window.
Subscriptions
Each entry in subscriptions is one instrument and the events that instrument should receive:
| Field | Required | Type | Notes |
|---|---|---|---|
instrument | Yes | string | An exact platform symbol — F:EURUSD, C:BTCUSD. Use the platform symbol, not the hyphenated data symbol. |
events | Yes | array | 1–100 event patterns this instrument subscribes to. Must include at least one candle timeframe. See Event patterns. |
Valid events:
candle_<timeframe>— one of1m,2m,5m,10m,15m,30m,1h,2h,4h,12h,1d,1w(e.g.candle_1m,candle_1h). Brace/wildcard patterns are accepted:candle_{1,5,15}m,candle_*.order_update,position_update,account_update,trade_update— trading lifecycle events.
quote is not a valid subscription eventquote exists as an event type elsewhere on the platform, but it is not a valid subscription event for a backtest — it loads no market data and selects no broadcast stream. For price data, subscribe to a candle_* timeframe instead. A config that subscribes to quote is rejected.
Event patterns
Candle events accept a small pattern grammar so one entry can subscribe to several timeframes. The candle_ prefix is always required, and the * may appear only as a suffix.
| Pattern | Expands to |
|---|---|
candle_1m | Exactly that timeframe. |
candle_* | All twelve timeframes. |
candle_*m | The minute timeframes — 1m, 2m, 5m, 10m, 15m, 30m. |
candle_*h | The hour timeframes — 1h, 2h, 4h, 12h. |
candle_{1,5,15}m | Exactly candle_1m, candle_5m, candle_15m. |
Those five shapes are the whole grammar. Anything else is parsed as a literal timeframe and rejected if it isn't one — candle_*d is invalid, because the star is only recognised before m, before h, or on its own. Expansions are deduplicated, so overlapping patterns cannot subscribe you twice.
Lifecycle events — order_update, position_update, account_update, trade_update — are additive: they can accompany a candle subscription but cannot stand alone. A subscription whose events contains no candle timeframe is rejected.
Per-instrument events
Because events lives inside each subscription, different instruments can subscribe to different event sets in the same run. Here EUR/USD drives the strategy on 1-minute candles while BTC/USD only supplies hourly candles:
{
"strategyVersionId": "b2c3d4e5-f6a7-4b2c-8d3e-4f5a6b7c8d9e",
"subscriptions": [
{
"instrument": "F:EURUSD",
"events": ["candle_1m", "order_update", "position_update"]
},
{
"instrument": "C:BTCUSD",
"events": ["candle_1h"]
}
],
"startTime": "2024-03-04T13:30:00Z",
"endTime": "2024-03-08T21:00:00Z",
"positionMode": "netting",
"initialCapital": 100000
}
Save it and launch a run:
tektii scenario run \ --strategy-id a1b2c3d4-e5f6-4a1b-9c2d-3e4f5a6b7c8d \ --file ./scenario-config.json
That file is the inline schema, so it carries strategyVersionId. To keep the same window as a reusable preset, copy it, delete strategyVersionId, and pass the copy to config create — the saved-config schema rejects that key:
tektii config create \ --name "eurusd-btc-march" \ --file ./saved-config.json
Custom start time
startTime is a full timestamp, so a run can begin at any moment — for example at a market open rather than midnight. The window above starts at 13:30 UTC, not 00:00:
"startTime": "2024-03-04T13:30:00Z", "endTime": "2024-03-08T21:00:00Z"
endTime is exclusive: the run includes data up to, but not including, that instant.
Transaction costs
By default a backtest pays only the bid/ask spread. The optional costs object layers on commission, slippage, and funding — each defaults to zero, so omitting costs (or leaving a field at its default) keeps the run spread-only and identical to a run without it.
Unlike the top-level camelCase keys, costs and its nested fields use snake_case on every surface.
| Field | Required | Type | Notes |
|---|---|---|---|
costs.commission | No | object | { "model": "zero" } (default) or { "model": "bps_of_notional", "fee_rate_bps": "10" } — basis points of notional charged per side (entry and exit). |
costs.slippage | No | object | Adverse price movement on top of the spread, in basis points. See Slippage. Defaults to zero. |
costs.funding | No | object | Overnight financing on positions held across the rollover. See Funding. Defaults to { "model": "disabled" }. |
Every decimal inside costs travels as a quoted string, not a JSON number — "fee_rate_bps": "10", not 10. This preserves exact decimal precision on the wire. Passing a bare number is the most common cause of a rejected costs object.
Commission
costs.commission is a tagged object; the model key is the discriminator.
| Model | Fields | Behaviour |
|---|---|---|
zero | — | Default. No commission — matches spread-only dealer brokers. |
bps_of_notional | fee_rate_bps (required) | notional × fee_rate_bps / 10000, charged once at entry and once at exit. Must be >= 0 and < 10000. |
Slippage
costs.slippage is a plain object, not a tagged one. If you supply it, all three fields are required; each defaults to "0" when costs.slippage is omitted entirely.
| Field | Required | Type | Notes |
|---|---|---|---|
base_slippage_bps | Yes | string | Flat slippage applied to every fill, in basis points. Must be >= 0. |
size_impact_bps_per_100k | Yes | string | Additional basis points per $100k of notional — the size-impact term. Must be >= 0. |
max_slippage_bps | Yes | string | Upper cap on total slippage, in basis points. Must be >= 0 and < 10000. The cap applies only when it is greater than zero. |
The engine computes:
slippage_bps = base_slippage_bps + (notional / 100_000) * size_impact_bps_per_100k slippage_bps = min(slippage_bps, max_slippage_bps)
A buy then fills at ask × (1 + slippage_bps / 10000), a sell at bid × (1 - slippage_bps / 10000) — always adverse, on top of the spread you already cross.
Funding
costs.funding is a tagged object charging financing on positions held across the rollover.
| Model | Fields | Behaviour |
|---|---|---|
disabled | — | Default. No financing is charged. |
daily | long_rate, short_rate (required); day_multipliers, day_count_basis (optional) | Charges at the 17:00 America/New_York rollover (DST-aware). |
For the daily model:
| Field | Required | Type | Notes |
|---|---|---|---|
long_rate | Yes | string | Annual rate applied to long positions, as a decimal ("-0.03" = −3%). Negative debits the account, positive credits it. Must satisfy abs(rate) <= 1. |
short_rate | Yes | string | Annual rate applied to short positions; same rules as long_rate. |
day_multipliers | No | array | Seven integers, Monday→Sunday, weighting each weekday's day count. Defaults to [1, 1, 3, 1, 1, 0, 0] — triple charge on Wednesday, nothing at the weekend. |
day_count_basis | No | number | Annualisation denominator: 360 or 365. Omitted means 365. |
The charge is position_value × annual_rate_side × days_charged / day_count_basis.
daily funding does not apply to spot cryptoThe daily model represents forex/CFD overnight carry. If any subscription in the run is a spot crypto instrument (a C: symbol), a daily funding model is rejected — configure funding only on runs whose instruments are all forex.
One cost model applies to every subscribed instrument. Per-instrument cost overrides are not configurable — if you need different assumptions per symbol, run separate scenarios.
{
"strategyVersionId": "b2c3d4e5-f6a7-4b2c-8d3e-4f5a6b7c8d9e",
"subscriptions": [{ "instrument": "F:EURUSD", "events": ["candle_1m"] }],
"startTime": "2024-03-04T13:30:00Z",
"endTime": "2024-03-08T21:00:00Z",
"costs": {
"commission": { "model": "bps_of_notional", "fee_rate_bps": "10" }
}
}
See Backtest Execution & Costs for how each cost is applied and how gross vs net P&L is reported in results.
Environment variables
env is a flat string→string map passed to your strategy container as environment variables — the usual way to parameterise a strategy without rebuilding its image.
The same key can be set at several levels. They layer, lowest to highest:
- Image — whatever your Docker image bakes in via
ENV. The implicit bottom layer. - Config — the
envmap on the scenario configuration. Overrides the image. - Per-run — the
envpassed when launching a run. Overrides the config.
A key set at only one level simply falls through unchanged. On the CLI, an inline --env beats the env block in your config file.
env values are stored in plain textValues are stored and returned as plain text — they are not secrets and are visible to anyone who can read the configuration. Never put API keys, tokens, or credentials here.
Keys and values are validated against the merged map:
| Rule | Limit |
|---|---|
| Key format | Must match [A-Za-z_][A-Za-z0-9_]*. |
| Reserved prefix | Keys beginning TEKTII_ are rejected. |
| Reserved keys | PROXY_WS_URL, PROXY_REST_URL, and TRADING_GATEWAY_URL are rejected. |
| Maximum keys | 100. |
| Maximum value size | 4096 bytes. |
| Maximum total size | 65536 bytes. |
| Value contents | No control characters. |
| Reserved value | The literal <redacted> is rejected — it is the placeholder the API returns for withheld values. |
Updating a configuration replaces the whole env map — omitting it clears every key rather than leaving the previous map in place.
What the web wizard leaves out
The Create Scenario wizard in the web app is deliberately the simple path. It applies the same events to every instrument and starts every run at 00:00 UTC on the chosen day. Its Review step points power users here for anything else.
To get per-instrument events or a non-midnight start time, author a config file as shown above and launch it with the CLI or API. The wizard restriction is a UI simplification only — the platform itself supports both shapes natively.
Next steps
- Command Reference — full
scenarioandconfigcommand options - Available Instruments — symbols and their data ranges
- Plan Limits — lookback and duration caps