Tektii just went live. We're shipping fixes daily — spot a bug? Let us know

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 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 schemaSaved-config schema
Consumed bytektii scenario run --file, POST /v1/strategies/{id}/scenariostektii config create --file, tektii config update --file, POST /v1/scenario-configurations
strategyVersionIdRequiredRejected — a saved config does not pin a version, so scenario run --config supplies one with --version-id at launch
nameRejectedRequired — from --name, or a name key in the file
description, isActiveRejectedOptional

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

FieldRequiredTypeNotes
strategyVersionIdYesstringThe version to run (a UUID).
subscriptionsYesarray1–50 instrument subscriptions. Each has its own events. See Subscriptions.
startTimeYesstringInclusive start of the window, as a full ISO-8601 timestamp (e.g. 2024-03-04T13:30:00Z). Any time of day.
endTimeYesstringExclusive end of the window, ISO-8601. Must be after startTime.
positionModeNostringnetting (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.
initialCapitalNonumberStarting capital. Default 100000; range 1100000000.
envNoobjectEnvironment overrides (string→string) passed to the strategy container. See Environment variables. Stored in plain text — never put secrets here.
costsNoobjectOptional transaction costs — commission, slippage, funding. Each defaults to zero; omit for a spread-only backtest. See Transaction costs.

Subscriptions

Each entry in subscriptions is one instrument and the events that instrument should receive:

FieldRequiredTypeNotes
instrumentYesstringAn exact platform symbol — F:EURUSD, C:BTCUSD. Use the platform symbol, not the hyphenated data symbol.
eventsYesarray1–100 event patterns this instrument subscribes to. Must include at least one candle timeframe. See Event patterns.

Valid events:

  • candle_<timeframe> — one of 1m, 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.

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.

PatternExpands to
candle_1mExactly that timeframe.
candle_*All twelve timeframes.
candle_*mThe minute timeframes — 1m, 2m, 5m, 10m, 15m, 30m.
candle_*hThe hour timeframes — 1h, 2h, 4h, 12h.
candle_{1,5,15}mExactly 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.

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.

FieldRequiredTypeNotes
costs.commissionNoobject{ "model": "zero" } (default) or { "model": "bps_of_notional", "fee_rate_bps": "10" } — basis points of notional charged per side (entry and exit).
costs.slippageNoobjectAdverse price movement on top of the spread, in basis points. See Slippage. Defaults to zero.
costs.fundingNoobjectOvernight financing on positions held across the rollover. See Funding. Defaults to { "model": "disabled" }.

Commission

costs.commission is a tagged object; the model key is the discriminator.

ModelFieldsBehaviour
zeroDefault. No commission — matches spread-only dealer brokers.
bps_of_notionalfee_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.

FieldRequiredTypeNotes
base_slippage_bpsYesstringFlat slippage applied to every fill, in basis points. Must be >= 0.
size_impact_bps_per_100kYesstringAdditional basis points per $100k of notional — the size-impact term. Must be >= 0.
max_slippage_bpsYesstringUpper 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.

ModelFieldsBehaviour
disabledDefault. No financing is charged.
dailylong_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:

FieldRequiredTypeNotes
long_rateYesstringAnnual rate applied to long positions, as a decimal ("-0.03" = −3%). Negative debits the account, positive credits it. Must satisfy abs(rate) <= 1.
short_rateYesstringAnnual rate applied to short positions; same rules as long_rate.
day_multipliersNoarraySeven 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_basisNonumberAnnualisation denominator: 360 or 365. Omitted means 365.

The charge is position_value × annual_rate_side × days_charged / day_count_basis.

{
  "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:

  1. Image — whatever your Docker image bakes in via ENV. The implicit bottom layer.
  2. Config — the env map on the scenario configuration. Overrides the image.
  3. Per-run — the env passed 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.

Keys and values are validated against the merged map:

RuleLimit
Key formatMust match [A-Za-z_][A-Za-z0-9_]*.
Reserved prefixKeys beginning TEKTII_ are rejected.
Reserved keysPROXY_WS_URL, PROXY_REST_URL, and TRADING_GATEWAY_URL are rejected.
Maximum keys100.
Maximum value size4096 bytes.
Maximum total size65536 bytes.
Value contentsNo control characters.
Reserved valueThe 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