QTScript (beta)

A compact language for writing strategies — every section, windows, what's in scope, and how it compiles the same as Java.

QTScript is a compact way to write a strategy: you keep the part that is yours — indicators, windows, signals — and leave out the ceremony of a Java class (package, imports, class, base class, property annotations, listener boilerplate). Every { } body is plain Java, so everything the Java strategy API offers inside a body works unchanged. A QTScript file becomes exactly one Java class and goes through the same server-side compilation as a Java strategy.

It is in beta and will gain syntax. Java remains the established route, with the full engine API; anything QTScript cannot express is written in Java (see what it does not do).

Submit it like any strategy

There is no separate endpoint and no header to set: send the raw source to POST /strategy with Content-Type: text/plain, exactly as for Java. The platform tells the two apart by the text — a QTScript source starts with strategy, and whitespace or comments (//, /* */) before it are ignored, so a description can sit on top of the file. .qtscript is the conventional file extension; only the text is sent.

curl -X POST https://api.qtsurfer.net/v1/strategy 
  -H "Authorization: Bearer $TOKEN" 
  -H "Content-Type: text/plain" 
  --data-binary @rsi-reversion.qtscript

The answer is the same as for Java: a strategyId and the declaredProperties — every param you declared is listed there.

A whole strategy

This is the complete file: no imports, no class, no listener.

strategy "RSI reversion"

param rsiLow  = 30  "Oversold"
param rsiHigh = 70  "Overbought"

instruments */usdt

setup:
  rsi(14) window m1 {
    if (actual < rsiLow)  emitBuy(price);
    if (actual > rsiHigh) emitSell(price);
  }

It declares two parameters, accepts any base quoted in USDT, registers a 14-period RSI and watches it on a one-minute window, emitting a signal on each threshold.

The parts of a file

PartWhat it doesExample
strategyThe first line: the name and, optionally, the data source (ticker by default, kline, funding)strategy "RSI reversion" · strategy Bars kline
paramA configurable value. The type comes from the literal (9 → int, 0.5 → double, true → boolean, "text" → String). The name is the key that a run’s params and a sweep axis useparam rsiLow = 30 "Oversold"
init { }Optional. Plain Java run once when the strategy is built — engine settersinit { setPercentGain(0.5); }
instrumentsOptional. Which markets: pairs (BASE/QUOTE, either side may be *), regular expressions (~"...", matched against the whole symbol), or a Java body for full controlinstruments */usdt · instruments btc/usdt, eth/*
setup:The indicators, one builder call per line (the same catalogue a Java strategy uses). The lines under setup: are indented, and the first line back at column 0 ends the sectionema(12) · bollinger(20, 2)
WindowsWhere the logic goes — see belowrsi(14) window m1 { ... }
onCommand { }Optional, at most one per file. Runs when the run receives a command — see belowonCommand { if ("flatten".equals($command)) ... }

Windows

A window fires when its period closes, not on every tick, and wraps one indicator. A period is one of s1 s5 s10 s30 m1 m3 m5, or a whole number of seconds (window 900 { ... }); omitted, it is s1. Five ways to write one:

setup:
  rsi(14) window m1 { ... }      // inline, on the indicator this line registers
  rsi(33) window Oversold        // same, with the body in a named section below
  window price m5 { ... }        // inline, on an indicator by name
  window ema12 Trend             // by name, body in a named section

window Oversold m1 { ... }       // a named section, at column 0
Trend s5 { ... }                 // `window` is optional here

A section called Main that nothing references watches the primary value (price, or rate on funding), which makes the shortest useful file:

strategy Simple

Main m1 {
  if (actual > prev) emitBuy(price);
}

Which indicator a window is on

Inside any window body, $indicator is the name of the indicator that window is attached to, as a String: "ema12" for a window written on the line ema(12), "price" on a Main ("rate" on funding). The $ marks a name QTScript provides; the names you declare cannot start with one. A named section attached to several indicators sees, each time it fires, the indicator that fired.

It is also how a body reaches an indicator that the same builder call registered beside the one it is on. bollinger(20, 2) registers three under one name: the middle band, blgr20_2, and the outer bands, that name followed by Upper and Lower. A window written on that line attaches to the middle band, so actual is the middle band’s value, and value($indicator + "Upper") and value($indicator + "Lower") read the other two:

setup:
  bollinger(20, 2) window m1 {
    if (price > value($indicator + "Upper")) emitSell(price);
    if (price < value($indicator + "Lower")) emitBuy(price);
  }

A window that names an indicator which is not registered, such as window nosuch m1 { ... }, is not rejected when you register the source. Whether the name exists can depend on your params (a param fast used as ema(fast) registers a different name for each value) and on indicators registered from Java, so it is only found when the strategy is validated, which then fails on the window’s line: QTScript line 4: unknown indicator 'nosuch'.

Inside a body

Your Java, plus what is already in scope — nothing needs importing:

In scopeWhat it is
actual, prevthe window’s new and previous value
price (ticker) · price open high low close volume (kline) · rate (funding)the current values, as plain variables
value("name")any other indicator’s current value
$indicatorthe name of the indicator this window is on, as a String — see above
storethe per-instrument state shared by every window of that instrument
emitBuy(price), emitSell(price), emitInfo(key, values…), emitSignal(signal)signal emission
every paramreadable by its name

A position flag in the store, emitting on the crossing only once:

strategy "EMA cross"

param fast = 9  "Fast EMA"
param slow = 21 "Slow EMA"

setup:
  ema(fast)
  ema(slow)
  window price s5 {
    double f = value("ema" + fast);
    double s = value("ema" + slow);
    if (f > s && !store.is("long")) {
      store.set("long");
      emitBuy(price);
    }
    if (f < s && store.is("long")) {
      store.unset("long");
      emitSell(price);
    }
  }

value("ema" + fast) reads any registered indicator by name; store is the same store every window of the instrument shares.

Candles

strategy … kline gives the bar’s fields as plain variables. A kline strategy never takes an interval: the bar width is the cadence you prepare the data at, so the same file runs at any of them.

strategy "Range breakout" kline

param minRange = 0.5 "Minimum range, percent"

setup:
  window close m1 {
    double range = (high - low) / low * 100.0;
    if (range < minRange) return;
    if (close > open) emitBuy(close);
    else              emitSell(close);
  }

Handling a command

onCommand { } is a special section, at most one per file, that runs when the platform delivers a command to a live run — POST /live/{runId}/commands, applied without restarting the strategy. It has no period and no indicator: it is not a window, it runs once per command, not once per market event, and a file has at most one of it, the same way it has at most one init { }.

Inside its body, $command is the command’s text, as a String — nothing else a window body has (actual, $indicator, value(...), the ambient store) is in scope, because a command is not tied to a market tick or, unlike a window, to one instrument already chosen for you. Every param is still readable and settable.

A command may carry a properties object of your own choosing, alongside command in the request body. Read a value from it with $command.<key> — a String, null when the command carried no such key. $command.<key> fires only when <key> is not itself a call, so $command.equals(...), $command.startsWith(...) and the rest still read as ordinary String methods on $command itself.

A window body gets its instrument’s store handed to it; onCommand does not, since a command names no instrument on its own — but getStateStore("<symbol>") takes one directly, so a command whose own properties name an instrument can still reach that instrument’s store:

strategy "Manual flatten"

onCommand {
  if ("flatten".equals($command)) {
    getStateStore($command.instrument).set("flattened");
  }
}

A strategy with no onCommand { } does not implement the engine’s CommandRequestHandler, so a command sent to one of its runs is rejected with 409 (see Commands). $command and $command.<key> are not visible outside onCommand’s body, the same way $indicator is not visible outside a window’s.

Setting a param or writing to a StateStore from inside onCommand both take effect immediately, but neither survives a restart: a StateStore is memory, gone on a restart the same as a field. Only PUT /live/{runId}/params writes something a restarted replica actually starts from. getStateStore(...) here is not about durability — it is how onCommand reaches the per-instrument state a window body already reads, since a command carries no instrument of its own.

A Java strategy implements the same contract directly, through CommandRequestHandler — see Coding Java strategies.

Running it

A registered QTScript strategy is prepared, executed and swept like any other: prepare, execute and executeSweep, with the strategyId you got back. Sweep axes and params use the names of your param lines.

strategy …PrepareExecuteSweep
ticker (default)yesyesyes
klineyesyesyes
fundingyesnot yetnot yet

See Data sources for the details, including why a funding strategy can be registered and its data prepared but not yet run.

When something is wrong

Errors are reported against your file, never the generated Java. Registering a source with a mistake is a 400 whose message carries Line N, Column M: entries, for QTScript’s own errors (an unknown section, a bad period, a duplicate parameter, a malformed instrument pattern) and for Java errors from inside a body.

A 200 from POST /strategy means the source parsed and compiled, not that it will run. Registering does not set the strategy up, so what only shows when it does is found by validate, which reports it against your file the same way. The case to know is a window on an indicator that is not registered, window nosuch m1 { ... }: it registers, and validate ends failed on the window’s line, QTScript line 4: unknown indicator 'nosuch' (see Which indicator a window is on). Call validate before you run a strategy you have just written.

A failure while the strategy runs is recorded on the job the same way, on the line the body came from:

QTScript line 6: Index 2 out of bounds for length 1

The strategy id

POST /strategy returns the same strategyId for the same source. For QTScript the id comes from the text, because indentation is part of the grammar: a byte-order mark, the style of line endings, whitespace at the end of a line and blank lines before the first and after the last line are ignored; anything else — a comment, the indentation, a blank line in between — gives a different id. (Java strategies are more forgiving; see Strategies.)

What it does not do

By design, and each one is a reason to write the strategy in Java instead:

  • No update(). Windows are the model; a strategy that has to see every tick belongs in Java.
  • No cross-instrument logic — reading other instruments’ indicators needs the full class.
  • No custom indicator classes, no extra fields or methods beyond what the sections declare, and no multi-source strategies.
  • One class. Anything that wants helper types is a Java strategy.

Switching is never a dead end: a .qtscript file is a Java class with the ceremony left out.

Going further

The maintained qtsurfer-qtscript-strategy skill has the full language reference — every section, the instrument filter in detail, reserved names, and more examples — and works with agents that read skills:

npx skills add QTSurfer/strategy-skills --skill qtsurfer-qtscript-strategy

The bodies are written against the strategy API documented in the qtsurfer-java-strategy skill and in Coding Java strategies.