Strategy API
A reference to the events and capabilities available to strategy code.
Lifecycle callbacks
A C# strategy implements IStrategy. OnInit(StrategyContext) receives the environment services. OnTick handles tick callbacks; OnBar(ISymbolKey, IKline) handles closed bars for a subscription. OnFinish(StrategyFinishContext) runs when the test finishes.
Use closed bars for decisions that must not read future data. You can put indicator calculations in a separate source file so they can be checked independently.
public class UserStrategy : IStrategy
{
public void OnInit(StrategyContext context) { }
public void OnTick(decimal? price = null) { }
public void OnBar(ISymbolKey symbolKey, IKline kline) { }
public void OnFinish(StrategyFinishContext context) { }
}Parameters, market data, and orders
StrategyContext exposes Api, Indicators, Parameters, MarketData, and Logger. Use [Parameter] attributes to expose strategy settings, such as an indicator period or order quantity. Read the installed API signatures in the editor; support depends on the EngineHost version.
Order placement returns a result. Check Success before assuming an order was accepted, and inspect ErrorCode and ErrorMessage on failure. Keep account, quantity, and instrument assumptions explicit.
Order, fill, position and balance events
Four optional callbacks tell a strategy what happened on its account. OnOrderUpdate(ISymbolKey, OrderUpdate): an order was accepted, partly or fully filled, canceled, rejected, expired, modified, or its trigger fired (update.Kind, update.Order, update.Reason, update.PreviousOrderId).
OnExecution(ISymbolKey, IExecution): a fill of the strategy's order with its price, quantity, fee and fee asset, maker or taker, realized PnL and a liquidation mark. OnPositionChanged(ISymbolKey, IPosition): a futures position changed; Quantity 0 means it closed. OnBalanceChanged(ISymbolKey, IBalance): an asset balance changed; symbolKey.Symbol is empty because the balance belongs to the market's account.
Implement only the ones you need; a strategy without them runs exactly as before.
Events arrive on the strategy's thread and never inside another callback: an order that fills inside PlaceOrder called from OnBar is reported after OnBar returns. Events of one moment come in this order: accepted orders, fills, the other order updates, positions, balances. Only the strategy's own orders are reported; another robot or a manual order on the same account is not.
In the simulator and in Paper the events come at the model time the account changed. In LIVE they come from the exchange's private WebSocket where the connection has one: HyperLiquid, Aster, Binance, Bybit, OKX, Bitget, Kraken, Kucoin, MEXC, BingX, Gate.io, Coinbase, Bitfinex, Crypto.com, HTX, BloFin, CoinW, DeepCoin, Weex, XT.
On BingX futures, Coinbase, BloFin and CoinW it carries orders only, and fills are polled; Bitstamp and the inverse futures of MEXC and XT have none. Otherwise the robot asks the exchange over REST — only while the strategy has open orders, every 5 seconds by default, so an event may come up to one interval late.
Set Order Poll Interval in the launch profile (Studio) or in the robot profile (launcher), or change it from code: context.SetAccountPollInterval(TimeSpan.FromSeconds(1)) while you wait for a fill, and null to return to the profile value; the host keeps it between 1 second and 10 minutes. context.Runtime.AccountEventDelivery says how events come: Simulated, Stream, Polling or None.
The account reconciliation still runs every 30 seconds as the safety net.
A backtest can reproduce that delay: switch on Emulate Order Polling in Backtests in the launch profile.
The answer to the strategy's own call (PlaceOrder, CancelOrder, ModifyOrder, ClosePosition) still comes right after the callback, but a resting order that the market fills, triggers or expires is reported at the next poll — every Order Poll Interval after the previous one, only while the strategy has open orders, and once more after the last one ended; context.SetAccountPollInterval moves the schedule at once.
Positions and balances also come at the 30-second reconciliation, so OnPositionChanged may arrive before the order's Filled. context.Runtime.AccountEventDelivery is then Polling. A backtest step is coarser than the interval, so an event comes at the first delivery point after the poll: with 5 seconds and one-minute bars the delay hardly shows, with a long interval or in tick mode it does.
The launcher's verification backtest of the exported bundle does the same; Paper and LIVE are not affected.
Api.ModifyOrder(symbolKey, orderId, newPrice, newQuantity) changes a resting order in one command. newPrice is the limit price, or the trigger price of an order without one; newQuantity is the new total quantity and must exceed what has already filled; null leaves a field unchanged. The order loses its place in the queue, and a marketable new price fills at once as a taker.
Use the returned order's OrderId: an exchange may give the order a new id, and OnOrderUpdate then reports Modified with PreviousOrderId. On an exchange that cannot modify orders the call fails with ApiErrorCode.NotSupported; it never cancels and re-places on its own, so do that yourself if you want it. In LIVE the new terms pass the same risk limits as a new order.
Modification works on HyperLiquid, Aster, Binance (USD-M and COIN-M futures: limit orders; Spot: lowering the quantity only), Bybit, OKX (regular orders; trigger orders: quantity only), Gate (Spot and futures, regular orders), Coinbase, Crypto.com and XT (limit orders), Kraken, Kucoin and Bitstamp Spot (a limit order with nothing filled; the exchange replaces it with an order under a new id), DeepCoin futures (limit orders) and BingX futures (quantity only); elsewhere it returns NotSupported.
On inverse (coin-margined) contracts the new size is a whole number of contracts, like every inverse quantity.
Other languages have the same events: Python on_order_update, on_execution, on_position_changed, on_balance_changed and api.modify_order; JavaScript and TypeScript onOrderUpdate, onExecution, onPositionChanged, onBalanceChanged and api.modifyOrder; Java, Go, C++ and R the same names in their conventions. A Rust strategy also returns true from account_events(). Pine has no such handlers.
private StrategyContext _context = null!;
public void OnInit(StrategyContext context) => _context = context;
public void OnOrderUpdate(ISymbolKey symbolKey, OrderUpdate update)
{
if (update.Kind == OrderUpdateKind.Filled && update.Order.ClientOrderId == "entry")
_context.SetAccountPollInterval(null); // back to the profile value
}
public void OnExecution(ISymbolKey symbolKey, IExecution fill) =>
_context.Logger.Info($"{fill.Quantity} @ {fill.Price}, fee {fill.Fee} {fill.FeeAsset}, maker {fill.IsMaker}");
// Waiting for the entry to fill: ask the exchange more often (polling only: LIVE, or a backtest emulating it).
_context.SetAccountPollInterval(TimeSpan.FromSeconds(1));
// Move a resting limit order; the exchange may give it a new id.
var moved = _context.Api.ModifyOrder(symbolKey, orderId, newPrice: 101.5m, newQuantity: null);
if (moved.Success) orderId = moved.Data!.OrderId;
else if (moved.ErrorCode == ApiErrorCode.NotSupported) { /* cancel and place a new order */ }Stopping and continuing a LIVE robot
When the trader presses Stop, OnFinish gets Reason = UserStopped and context.StopChoice, the choice made in the dialog: KeepOrdersAndPosition, CancelOrders or CancelOrdersAndClosePosition. When context.ExitOrdersAllowed is true, the strategy may cancel its orders in OnFinish (up to 30 seconds) and send ReduceOnly orders only, to reduce or close the position its own way; it cannot open anything new.
With «keep» ExitOrdersAllowed is false and any order sent from OnFinish is refused. After OnFinish XTester carries out the trader's choice itself.
context.SetStopPolicy(StrategyStopChoice…) in OnInit says what the strategy recommends at a stop. The trader always chooses.
The launcher's Stop dialog offers four choices and chooses none beforehand: keep the orders and the position, cancel the orders, cancel the orders and close the position, remove the robot. The choice the strategy recommends (context.SetStopPolicy) is labelled «— recommended by the strategy». «Remove the robot» asks as a second step what to do with its orders and position, and removes the robot only when the exchange confirmed the stop.
A crash of the robot (an exception in the strategy's code included), a lost connection to XTester, a service stop and a server reboot are not a stop: OnFinish is not called and the orders and position stay on the exchange. At the next start OnInit gets context.Restart: IsContinuation, Position, OrderIds, ClientOrderIds and LastVerifiedUtc, as the robot's journal recorded them at the last reconciliation.
Read what the exchange holds now through Api; XTester compares the exchange with the journal after OnInit and before the first trading event.
After a crash the launcher starts the robot again at once, in the same mode — LIVE and Paper, without asking, at most 3 times in 10 minutes. When the start needs a person (keys, quarantine, the acceptance of a real order), the robot stays stopped. When it crashes again and again, the restarts end; its orders and position are not touched either way. The launcher's notification, Telegram and XTester (with the Robots view closed too) tell of the crash; «Journal» opens the robot's journal in the launcher.
context.SetRestartMismatchPolicy(StrategyRestartMismatchPolicy…) sets the restart strictness when the exchange differs from the journal (see «Restarting a LIVE robot» in the launcher topic). The launch profile's setting, when set, wins. An unknown outcome of a request is always decided by a person.
In the tester and Paper StopChoice = None, Restart = StrategyRestartInfo.None, and there is no exit window.
Strategies in other languages get the same through their context: context.restart at init and context.finish (the reason, the trader's choice, the exit window) before on_finish; set_stop_policy and set_restart_mismatch_policy declare the recommendation and the strictness (setStopPolicy and setRestartMismatchPolicy in JS/TS, Java, Go and C++, context$set_stop_policy in R). Pine has no handlers.
public void OnInit(StrategyContext context)
{
_api = context.Api;
_symbol = context.Parameters.SymbolKey;
context.SetStopPolicy(StrategyStopChoice.CancelOrdersAndClosePosition);
context.SetRestartMismatchPolicy(StrategyRestartMismatchPolicy.WarnAndContinue);
if (context.Restart.IsContinuation)
context.Logger.Info($"Continuing: position {context.Restart.Position}, orders {context.Restart.OrderIds.Count}");
}
public void OnFinish(StrategyFinishContext context)
{
if (context.ExitOrdersAllowed)
_api.CancelAllOrders(_symbol); // your own exit; to close use a ReduceOnly order
}Pair price: OnTick(symbolKey, price)
The optional OnTick(ISymbolKey symbolKey, decimal price) comes when the price of one of the strategy's pairs changes — separately for each pair, so branch on the key you receive. It arrives on the strategy's thread, never inside another callback, and a pair whose price did not change gets nothing. The old OnTick(price) is unchanged and keeps coming as before; a strategy that does not implement the new one runs exactly as before.
In the simulator the price is the close of the last closed minute known at Global.Now, so a pair gets at most one call a minute. In Paper and LIVE the price comes from the exchange's public trade stream as it changes — several times a second on a busy pair; where there is no trade stream, or no trade for 90 seconds, it is the close of the forming minute, read every second.
A price that changes faster than the strategy handles it comes as the newest price of that pair; other pairs are not lost. A live price can be newer than the last closed bar, and Global.Now stays at that bar. With the shared market data bank (XTESTER_MARKET_DATA_HUB=1) the pair's trade stream is read once for every robot of the machine. Only C# strategies receive it so far.
public void OnTick(ISymbolKey symbolKey, decimal price)
{
if (symbolKey.Symbol == "ETHUSDT")
_lastEth = price;
}Refine a Closed Interval
Obtain context.DataRefinement() beside context.MarketPhases() in OnInit. Require(symbol, fromUtc, toUtc, options) preserves the calling callback stack while the host waits; it does not replay OnInit or OnBar. The UTC interval [fromUtc, toUtc) must be minute-aligned, closed at model time, positive, and at most 60 minutes long.
ResolutionSeconds defaults to 1 and must be a divisor of 60 between 1 and 30: 1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30. TimeoutSeconds defaults to 120 (allowed: 1–600). RealOnly prevents synthetic results; AllowDownload=false uses local real detail only. The example stops if real local detail is missing.
Every acquisition returning synthetic detail requires explicit consent, including cached synthetic detail. Concurrent identical requests may share one acquisition. Stop, close, timeout, cancellation, and a replaced run are not approval. Permission does not carry to future intervals or runs. Unattended/headless calculations stop without real detail; Paper and Live currently have no interactive refinement consent.
DataRefinementPath.NearestExtremeFirst is the default: visit the nearer High/Low first; equal distances use HighFirst. HighFirst and LowFirst force their named extreme first. CandleColor uses HighFirst when Close >= Open (including doji), otherwise LowFirst. These deterministic possible OHLC paths are synthetic prices, not a reconstruction of true trades or fills. Seed is recorded but does not randomize the current models.
DataRefinementResult exposes Candles, NoTrades, IsSynthetic, ProviderId, Model and ModelVersion. Proven no-trade intervals remain empty. A missing candle does not prove no trades. Emulation requires actual canonical m1 parents and cannot invent unknown missing minutes. Required failures stop the calculation even if strategy code catches DataRefinementException.
Local validated detail, deterministic emulation, and native/archive acquisition routes exist. Supported resolutions depend on the source: check history.refinementResolutionSeconds in get_market_data_capabilities. AllowDownload=true does not guarantee historical availability or complete coverage; unsupported routes stop or require explicit emulation consent. This is a C# API; direct Python/Node/Java bindings are unavailable. Get exact signatures through get_strategy_api_doc(type="DataRefinementService", member="Require").
In a history chart, expand Local data detail to inspect cached coverage for the visible closed minutes (up to seven days). Real, synthetic, proven no-trade and unknown intervals have separate strips. View detail opens a read-only tab using the same chart engine; Back returns to the unchanged base chart. Zooming and viewing never download or emulate data.
// Field: DataRefinementService? _detail;
// OnInit:
_detail = context.DataRefinement();
// OnBar: use model time, rounded down to the last closed minute.
var toUtc = new DateTime(time.Ticks - time.Ticks % TimeSpan.TicksPerMinute, DateTimeKind.Utc);
var detail = _detail!.Require(symbolKey, toUtc.AddMinutes(-1), toUtc,
new DataRefinementOptions { ResolutionSeconds = 5, RealOnly = true, AllowDownload = false });
// Read detail.Candles, detail.NoTrades, and detail.IsSynthetic.Use the installed API as the authority
Use completion, hover information, Go to Definition, and compile diagnostics while editing. Read the installed API signatures for the EngineHost version you use.
This guide does not promise the older desktop application’s Paper, Live, Research Lab, or visual-order-entry features in Studio. Backtest functionality should be verified with the connected engine.
Local ONNX models
Model SDK runs an already trained ONNX model locally through the host-managed Models API. It does not train models or call an AI provider. Open the intended strategy project, then choose ONNX Models from the Command Palette or its project menu.
Import ONNX inspects the local file first. Review its input shape and outputs, enter a logical model ID and the UTC end of the data used for training. Optional feature names must cover the full input in order; optional normalization vectors must have the same size. Import .xtmodel preserves the sealed package contract. Only import models you trust: format and hash checks do not isolate a hostile native graph.
Import checks the source-file hash and the exact project revision again. Replacing a model requires confirmation against its current identity. Cancel file selection or review to leave the project unchanged; once Import is submitted, wait for native validation and the commit. Removing a model detaches it from the project and preserves its source and immutable payload files.
Use Models.Open("your-id") once in OnInit, then call Predict with the exact feature order or named feature dictionary. Models trained later than the first time strategy code can use them are refused, including declared preroll. Replacing a model requires a new prepared run or exported bundle and worker restart.
Portable imports use onnx-cpu-v1; legacy windows-ml-onnx packages remain readable without changing their hashes. Tester and Launcher use the same CPU/sequential, one-thread execution contract. Missing native assets or incompatible tensors fail closed. These settings do not make every ONNX graph deterministic: test repeated representative inputs and the concrete strategy in both hosts. No GPU/NPU provider is downloaded automatically.