Coding Java strategies
Emit trades and information signals, configure orders, and attach chart metadata.
A QTSurfer strategy consumes market data, updates indicators and state, and emits signals. This guide covers signal emission — the point where an observation becomes either an instruction to trade or data to inspect later — and receiving a command from outside a live run.
For the complete class API, use the Engine Javadoc, particularly the strategy
signal package. For agent-assisted authoring, install the maintained qtsurfer-java-strategy skill:
npx skills add QTSurfer/strategy-skills --skill qtsurfer-java-strategy The skill also covers choosing a strategy base class, configuring indicators, and managing per-instrument state. Once the source is ready, compile and validate it through the API.
The signal helpers in this guide are not tied to a Java class: the { } bodies of a QTScript strategy (beta) — a compact way to write a strategy that leaves out the
class, imports and listener — call emitBuy, emitSell, emitInfo and emitSignal exactly as
shown here.
Execution signals and information signals
These signal families have different effects:
| Signal | Purpose | Causes a trade? |
|---|---|---|
BuySignal | Expresses a buy instruction and its order configuration | Yes |
SellSignal | Expresses a sell instruction and its order configuration | Yes |
InfoStrategySignal | Records indicators, diagnostics, or visualization metadata | No |
All three are in com.wualabs.qtsurfer.engine.strategy.event.signal, and a strategy that names one imports it
from there: import com.wualabs.qtsurfer.engine.strategy.event.signal.InfoStrategySignal;, the same way as BuySignal below. Using only emitBuy, emitSell and emitInfo, which never name the type, needs no import.
An information signal labelled BUY is still only information. Conversely, emitBuy(price) emits an executable buy signal even if no chart metadata is attached.
The README example deliberately emits both. It publishes an information signal on every window update so the indicator series can be inspected, but emits a buy or sell only when the moving averages cross:
import com.wualabs.qtsurfer.engine.strategy.event.signal.InfoStrategySignal;
InfoStrategySignal signal = createInfoSignal();
signal.set("fast", fast);
signal.set("slow", slow);
if (isBullish && !wasBullish) {
emitBuy(price);
} else if (!isBullish && wasBullish) {
emitSell(price);
}
emitSignal(signal); Signal helpers
Inside an AbstractWindowListener, the listener already knows its strategy and instrument:
| Helper | Result |
|---|---|
emitBuy(price) | Creates and immediately emits a market BuySignal |
emitSell(price) | Creates and immediately emits a market SellSignal |
createBuySignal(price) | Creates a buy signal to customize before emission |
createSellSignal(price) | Creates a sell signal to customize before emission |
createInfoSignal() | Creates an information signal to populate before emission |
emitInfo(key, values...) | Creates, populates, and immediately emits one information signal |
emitSignal(signal) | Emits a signal created or customized by the listener |
At strategy-class level the equivalent trade helpers take the instrument explicitly: emitBuy(instrument, price), emitSell(instrument, price), createBuySignal(instrument, price),
and createSellSignal(instrument, price). Use createInfoStrategySignal(instrument) when building
an information signal there.
The price passed to the basic trade helpers is the strategy’s current reference price. The
signal defaults to market; when a signal is changed to limit, that price becomes its limit
price.
Customizing buy and sell signals
The immediate helpers intentionally accept only a price. To configure an order, create its signal, set the required options, and emit it exactly once:
import com.wualabs.qtsurfer.engine.exchange.trade.OrderFlag;
import com.wualabs.qtsurfer.engine.strategy.event.signal.BuySignal;
import com.wualabs.qtsurfer.engine.strategy.event.signal.MarketHintSignal.OrderKind;
BuySignal buy = createBuySignal(price);
buy.setOrderKind(OrderKind.limit);
buy.setMaxTries(3);
buy.setFlags(OrderFlag.GTC);
buy.set("reason", "ema-cross");
emitSignal(buy); BuySignal and SellSignal inherit these options from MarketHintSignal, the common base class and authoritative method reference:
| Method | Meaning |
|---|---|
setOrderKind(OrderKind.market) | Market order; this is the default |
setOrderKind(OrderKind.limit) | Limit order at the signal’s price |
setMaxTries(n) | Maximum attempts for a limit buy; n must be positive |
setFlags(flags...) | Order flags such as FOK, IOC, or GTC; actual support depends on the venue |
setSellPercent(percent) | Percentage of the position to close; intended for multiple-long execution, default 100 |
setStopPrice(price) | Fixed protective stop to arm after the entry fills |
setStopLimitPrice(price) | Optional limit price for that fixed stop; without it the stop exits at market |
setTrailPercent(percent) | Trailing protective stop, expressed as a percentage from the running favourable price extreme |
setStopCondition(condition) | Live predicate that gates an engine-managed fixed or trailing stop |
set(key, values...) | Arbitrary analytics, provenance, or visualization metadata carried with the signal |
Treat stop and stopTrailing as engine-managed order kinds. Strategy code should express
protective risk on the entry signal with setStopPrice or setTrailPercent, rather than emitting a
standalone stop order.
Protective stops
A long entry can arm a fixed stop as part of the same signal:
BuySignal buy = createBuySignal(price);
buy.setStopPrice(price * 0.95);
emitSignal(buy); Use setStopLimitPrice as well when the protective exit must be stop-limit rather than
stop-market. A trailing stop follows the favourable extreme and triggers after the configured
percentage retracement:
BuySignal buy = createBuySignal(price);
buy.setTrailPercent(2.0);
emitSignal(buy); The same fields apply symmetrically to a short entry. A stop condition is evaluated repeatedly by the engine and can suppress the stop until a wider strategy condition permits it. It is live strategy logic, not serializable signal data.
Sizing
A strategy says when and which way, not how much. emitBuy(price) and emitSell(price) carry a price and nothing
else, and no call puts a quantity or a notional on a signal. The size of each entry is a setting of the run, percentAmountToLock: the percentage of the account’s free balance that an entry locks, above 0 and up to 100.
Where you give it depends on the run: baseConfig for a backtest or a sweep, the paper block for a paper live run. Left out, a backtest puts the
whole free balance into each entry (all-in) and a live run’s paper account locks 10%. To see how a strategy behaves
at another size, run it again with another percentAmountToLock; the class does not change.
Information and chart metadata
createInfoSignal() is listener-local syntactic sugar: it creates an InfoStrategySignal already
bound to the current strategy and instrument. Populate it with set and emit it when ready:
import com.wualabs.qtsurfer.engine.strategy.event.signal.InfoStrategySignal;
InfoStrategySignal signal = createInfoSignal();
signal.set("price", price);
signal.set("fast", fast);
signal.set("slow", slow);
emitSignal(signal); set stores one value directly. An even list of name/value pairs creates a nested object under the
given key, which is why chart markers use this form:
signal.set("_m",
"position", "belowBar",
"shape", "arrowUp",
"color", "#26a69a",
"text", "BUY"); The marker positions used by the standard visualization are aboveBar, belowBar, and inBar;
the portable shapes are circle, arrowUp, arrowDown, and square. Prefixing a property with _ reserves it as control metadata rather than a normal plotted series, as _m does here.
Everything you set on a signal is its data, and it is published with the signal in a live run: whoever may read the run
may read it, so on a public run it is public. A signal whose data is over 8 KiB (8,192 bytes of its JSON) is not pushed
on the WebSocket channel; GET /live/{runId}/signals still returns it whole.
For a single value, emitInfo is the shortest form:
emitInfo("zscore", zscore); It also accepts nested name/value pairs:
emitInfo("averages", "fast", fast, "slow", slow); emitInfo takes the same arguments in a QTScript body — the README shows the moving-average example above with its chart markers written that way.
Use the longer createInfoSignal() form when one event needs several top-level values or marker
metadata. Information signals are useful for explaining a decision, but they never replace the
corresponding emitBuy or emitSell when the strategy is meant to trade.
Window listeners and the previous value
A window listener fires once when its period closes, and onChange receives the indicator’s value then, actual,
and the value it had when the previous window closed, prev: the close[1] of other tools. A crossing, or
anything else that compares one close with the one before, needs nothing kept in the StateStore.
This enters when the RSI climbs back through 30, once, on the window where it happens:
import com.wualabs.qtsurfer.engine.core.state.StateStore;
import com.wualabs.qtsurfer.engine.indicators.helpers.WindowTimeRTIndicator.WindowTime;
import com.wualabs.qtsurfer.engine.indicators.helpers.group.InstrumentGroupRTIndicator;
import com.wualabs.qtsurfer.engine.strategy.AbstractTickerStrategy;
import com.wualabs.qtsurfer.engine.strategy.AbstractWindowListener;
public class RsiReentry extends AbstractTickerStrategy {
@Override
protected void setupIndicators(InstrumentGroupRTIndicator indicators) {
indicators
.addPrice()
.rsi(14)
.window("rsi14", WindowTime.m1, new Reentry(this, indicators));
}
private class Reentry extends AbstractWindowListener {
public Reentry(AbstractTickerStrategy strategy, InstrumentGroupRTIndicator indicators) {
super(strategy, indicators);
}
@Override
public void onChange(StateStore store, double prev, double actual) {
if (prev < 30 && actual >= 30) emitBuy(indicators.getValue("price"));
}
}
} On a kline strategy a window on close hands you the previous bar’s close as prev, which is what the true range
needs next to the bar’s own high and low. The bar’s fields are indicators too, registered from a KlineValueSource:
import com.wualabs.qtsurfer.engine.core.KlineInterval;
import com.wualabs.qtsurfer.engine.core.state.StateStore;
import com.wualabs.qtsurfer.engine.indicators.core.KlineValueSource;
import com.wualabs.qtsurfer.engine.indicators.helpers.WindowTimeRTIndicator.WindowTime;
import com.wualabs.qtsurfer.engine.indicators.helpers.group.InstrumentGroupRTIndicator;
import com.wualabs.qtsurfer.engine.strategy.AbstractKlineStrategy;
import com.wualabs.qtsurfer.engine.strategy.AbstractWindowListener;
public class TrueRange extends AbstractKlineStrategy {
@Override
public KlineInterval getInterval() {
return KlineInterval.s1;
}
@Override
protected void setupIndicators(InstrumentGroupRTIndicator indicators) {
indicators
.add("high", KlineValueSource.High)
.add("low", KlineValueSource.Low)
.add("close", KlineValueSource.Close)
.window("close", WindowTime.m1, new Range(this, indicators));
}
private class Range extends AbstractWindowListener {
public Range(AbstractKlineStrategy strategy, InstrumentGroupRTIndicator indicators) {
super(strategy, indicators);
}
@Override
public void onChange(StateStore store, double prev, double actual) {
double high = indicators.getValue("high");
double low = indicators.getValue("low");
double trueRange = Math.max(high - low, Math.max(Math.abs(high - prev), Math.abs(low - prev)));
emitInfo("trueRange", trueRange);
}
}
} A Java kline strategy has to override getInterval(): the base class leaves it abstract, and a source without it is
rejected. In a backtest the bar width is not what it returns but the cadence the data was prepared at, so KlineInterval.s1 serves any run; a QTScript kline strategy gets this method written for it.
The first time a window fires there is no earlier window to compare with: prev is the value the indicator had when
that first window opened. A rule that needs two real closes should let the first firing pass, and an indicator with a
warm-up period, such as an RSI or an EMA, has not settled in its first windows either. Keep the StateStore for what
a window cannot hand you: a flag, a counter, a value from several windows ago. A listener that only needs the
store, prev and actual can be a lambda; one that emits a signal extends AbstractWindowListener, as above.
Reading indicators from update()
A strategy that overrides update() instead of using windows advances the indicators itself with updateIndicators(instrument, source). It returns the instrument’s indicator map, an InstrumentMapRTIndicator,
not an indicator: write var, or this type, and read each indicator from it by name.
import com.wualabs.qtsurfer.engine.core.Ticker;
import com.wualabs.qtsurfer.engine.core.instrument.Instrument;
import com.wualabs.qtsurfer.engine.indicators.helpers.group.InstrumentMapRTIndicator;
@Override
public void update(Ticker ticker) {
Instrument instrument = ticker.instrument();
updateInstrument(instrument, ticker.timestamp());
InstrumentMapRTIndicator indicators = updateIndicators(instrument, ticker);
if (!indicators.getReadOnlyExisting("emaSlow").isReady()) return; // wait for warm-up
double fast = indicators.getValue("emaFast");
double slow = indicators.getValue("emaSlow");
// ...
} getReadOnlyExisting(name) returns a read-only view: it reads the live indicator’s value and ignores update(...),
without an error, so nothing in update() can change a shared indicator’s value by accident, which is what a
strategy that only reads values wants. getExisting(name) returns the live indicator: an update or reset called
on it changes the series for everyone who reads it, and nothing reports it. Both throw if there is no indicator of
that name. Do not spell the map’s type as RichRTIndicator, the
interface for writing an indicator of your own: the map does not implement it, and the compiler answers incompatible
types. The cannot find symbol for getExisting that follows it is a consequence, and the answer says so under it. RTIndicator does compile, since the map is one, but it hides getExisting and getReadOnlyExisting, which only the
map has.
updateIndicators on its own publishes no signals: a strategy that calls it from its own update() trades normally
and leaves the signals that a backtest stores empty. To keep them, call super.update(ticker) first, which already
does updateInstrument and updateIndicators, and read the indicators with getRTIndicator(instrument, name). It
returns the live indicator in an Optional, not a read-only view; .map(RTIndicator::ro) gives the view.
Receiving commands
A live run’s owner can tell it a command from outside — POST /live/{runId}/commands — while it keeps
running, without restarting it. To act on one, implement CommandRequestHandler:
import com.wualabs.qtsurfer.engine.strategy.event.request.CommandRequest;
import com.wualabs.qtsurfer.engine.strategy.event.request.CommandRequestHandler;
public class MyStrategy extends AbstractTickerStrategy implements CommandRequestHandler {
@Override
public void handle(CommandRequest request) {
if ("flatten".equals(request.getCommand())) {
// close the position, cancel pending orders, whatever "flatten" means for this strategy
}
}
} handle runs on the same thread as update(), right before the market event the command targets, so it
sees the strategy’s state exactly as it was at that point and can call anything update() can — read
indicators, emit a signal, change internal fields. A RuntimeException it throws is caught and counted,
the same as one from update(); an Error unwinds the run.
A command is always a plain string, and it is transient. It may also carry a properties object of your
own choosing, alongside command in the request body — not params, which stays what a run starts with
and PUT /live/{runId}/params changes. Each property lands as a top-level entry on CommandRequest’s own
map, so read one straight off request by name — request.get("<key>") — no key is off limits, since the
command’s own text is kept separately (getCommand() reads it, unaffected by any of it). A
value keeps whatever JSON type it arrived as, so assigning it to a String field when the caller sent a
number or an object throws a ClassCastException inside handle; a QTScript onCommand body reads the
same value with $command.<key> instead, which always widens it to a String (null for an absent key,
never a cast failure). A command, and its properties, are not stored as part of the run: a replica that
restarts replays only the last stretch of market data, and a command from before that window simply never
reaches it.
A command has no instrument attached the way update() does; when its own properties name one, reach that
instrument’s store with getStateStore(String):
@Override
public void handle(CommandRequest request) {
String instrument = request.get("instrument");
if (instrument != null) {
getStateStore(instrument).set("flattened");
}
} Neither a @StrategyProperty field nor a StateStore written from inside handle is durable. Both
change immediately, in memory, the same as any other assignment, but neither is written to the run’s
stored parameter set — a replica that restarts (or one that starts later, and never ran handle for that
command) starts from whatever PUT /live/{runId}/params last set, not from what a command assigned. The
only durable write is a real PUT /live/{runId}/params call, from outside the run — a strategy cannot
call its own REST API from inside handle.
A run whose strategy does not implement CommandRequestHandler answers every command with a 409 —
implementing the interface is what makes POST /live/{runId}/commands do anything at all.
A QTScript strategy implements it too, through its own onCommand { } section (see QTScript) — the platform recognizes the generated class as CommandRequestHandler the same way it recognizes this one.
See Commands for the request/response shape and error codes.
The pro indicators
The pro indicators come with the Pro and Elite plans. In Java they are the methods of Pro, in com.wualabs.qtsurfer.engine.indicators.pro: Pro.on(indicators) takes the indicator group that setupIndicators receives, and each call registers one pro indicator with the same names, forms and ids as the QTScript table lists. Pro.on(indicators).adx(14) registers adx14 and, beside it, adx14PlusDI and adx14MinusDI. A window goes on the main one (adx14), and the listener reads the others by name.
They read whole bars, so use them in a kline strategy. This one buys when the ADX says there is a trend and the plus line is above the minus line, and sells when it is below:
import com.wualabs.qtsurfer.engine.core.KlineInterval;
import com.wualabs.qtsurfer.engine.core.state.StateStore;
import com.wualabs.qtsurfer.engine.indicators.core.KlineValueSource;
import com.wualabs.qtsurfer.engine.indicators.helpers.WindowTimeRTIndicator.WindowTime;
import com.wualabs.qtsurfer.engine.indicators.helpers.group.InstrumentGroupRTIndicator;
import com.wualabs.qtsurfer.engine.indicators.pro.Pro;
import com.wualabs.qtsurfer.engine.strategy.AbstractKlineStrategy;
import com.wualabs.qtsurfer.engine.strategy.AbstractWindowListener;
public class AdxTrend extends AbstractKlineStrategy {
@Override
public KlineInterval getInterval() {
return KlineInterval.s1;
}
@Override
protected void setupIndicators(InstrumentGroupRTIndicator indicators) {
indicators.addPrice();
indicators.add("open", KlineValueSource.Open);
indicators.add("high", KlineValueSource.High);
indicators.add("low", KlineValueSource.Low);
indicators.add("close", KlineValueSource.Close);
indicators.add("volume", KlineValueSource.Volume);
Pro.on(indicators).adx(14);
indicators.window("adx14", WindowTime.m1, new TrendListener(this, indicators));
}
private class TrendListener extends AbstractWindowListener {
TrendListener(AbstractKlineStrategy strategy, InstrumentGroupRTIndicator indicators) {
super(strategy, indicators);
}
@Override
public void onChange(StateStore store, double prev, double actual) {
double close = indicators.getValue("close");
double plus = indicators.getValue("adx14PlusDI");
double minus = indicators.getValue("adx14MinusDI");
if (actual > 20 && plus > minus) emitBuy(close);
if (actual > 20 && plus < minus) emitSell(close);
}
}
} On a ticker strategy a pro indicator is not refused, but its values are not the bar’s and mean nothing, and validating or
running the strategy returns an engine notice that says so.
Registering a strategy that uses them is accepted on every plan, and the answer of POST /strategy carries requires: ["indicators.pro"]. Running it on a plan that does not include them is refused with 402, when validating it,
starting a backtest or a sweep with it and starting a live run with it. The message names the first call the strategy makes and
the line of your source, here strategy uses pro indicators (Pro.adx, line 25); your plan: free. The check reads the compiled
strategy, so any other mention of a class of the pro package counts the same, and the message then names the class. The status,
the body and the upgradeUrl are the ones the QTScript guide describes.
See also
- QTScript (beta) — the same strategies written with the ceremony left out.
- Strategies — compile, validate, list and read back a strategy, in either language.