QUANTRESEARCHER STUDIO · C# AUTHORING

A clearer way
to write your strategy.

Use one Robot class. Update SMA, EMA, RSI, Bollinger Bands, MACD or ATR with a readable call. Submit orders from typed callbacks. Learn the QR strategy API, with its execution behavior made explicit.

Robot[Parameter]Named callbacksOrder helpers

01Run mode and order queries

The host exposes read-only Boolean properties on ReplayContext. Exactly one is true for a normal Studio run; custom hosts that do not supply a mode report RunMode == StrategyRunMode.Unknown and all five flags are false.

PropertyTrue when
context.IsPaperBroker simulation/demo execution, including Lightspeed Paper
context.IsLiveExecution on a live broker account
context.IsBacktestStandalone historical backtest
context.IsOptimizeStandalone optimization trial
context.IsResearchAny research training, validation or out-of-sample trial

Optimization and research flags are exclusive; they do not also set IsBacktest. These values identify execution purpose, not the quote vendor, and are available from startup through termination callbacks and derived bar callbacks. The same replay flags apply to Windows and Ubuntu command-line workflows.

C# · QR strategy API
if (context.IsPaper) Print("Paper execution");
var orders = context.PendingOrders.Where(x => x.Label == "maker");
var positions = context.Positions.Where(x => x.Label == "maker");
var firstOrder = context.PendingOrders.FirstOrDefault(x => x.Label == "maker");

Where returns a collection. Use FirstOrDefault for one match and check for null. LINQ is imported automatically by Studio's strategy compiler; an explicit using System.Linq; remains valid and is needed when compiling with another tool that does not provide it. An omitted label is null; x.Label == "" compiles but does not match null. Use string.IsNullOrEmpty(x.Label) for unlabeled orders. Raw context.PendingOrders records cancel through context.CancelOrder(order.Id); context.Positions exposes position handles. Broker pending snapshots contain this strategy instance's orders; account positions may include other owners' exposure.

Editor suggestions use context.PendingOrders and context.Positions, with label filtering, lambda members, run-mode flags, Symbol.TickSize, Symbol.TimeZone, Symbol.NormalizePrice(...), and observed context.Bars / context.Quotes history. Legacy TradingPositions and TradingPendingOrders remain compatible but are not offered as primary context suggestions. Suggestions are curated API assistance; Validate C# remains the compiler check.

02Quick start

Open Strategies → New strategy, replace the source with the complete example below, and choose Save source… or Ctrl+S. Choose Validate C#, then Use this code in backtest and select Quote data. Review quantity, cash, costs, execution feed and date range before running.

Validation discovers the strategy type. When a file declares several eligible strategies, select the intended type. The old fully qualified type textbox is not needed. Saving source and validating a library snapshot are separate actions.

C# · QR strategy API
using QuantResearch.Engine;
using QuantResearch.Strategies;

namespace QuantResearcherExamples;

public sealed class EmaRsiExample : Robot
{
    [Parameter("Quantity", Group = "Trading", DefaultValue = 1,
        MinValue = 1, MaxValue = 10000, Step = 1)]
    public int Quantity { get; set; }

    public override void OnQuote(ReplayContext context, Quote quote)
    {
        ExponentialMovingAverage fast = Indicators.ExponentialMovingAverage(quote.Bid, 20);
        ExponentialMovingAverage slow = Indicators.ExponentialMovingAverage(quote.Bid, 50);
        RelativeStrengthIndex rsi = Indicators.RelativeStrengthIndex(quote.Bid, 14);

        if (!fast.IsReady || !slow.IsReady || !rsi.IsReady) return;

        decimal target = fast.Close > slow.Close && rsi.Close < 70m ? Quantity : 0m;
        SetTargetPosition(quote.Symbol, target);
    }
}

This educational example targets a long quantity or flat; it submits no startup order and does not accumulate a new position on every quote. It updates all three indicators before checking readiness, so the slow indicators continue warming up. The thresholds are illustrative, not a profitability claim. Start with one symbol in replay and review its quantity units. With several symbols, this callback makes an independent decision for each delivered symbol.

No OnStart is needed here. If initialization is required, use one public override void OnStart(ReplayContext context). For completed-bar signals, move the logic to OnBarClosed(ReplayContext context, BarClose bar) and use bar.Close and bar.Symbol. A period counts delivered observations, not elapsed minutes or days.

03Validated code handover

An Execution instance can opt into Apply validated updates automatically. Explicit successful validation replaces matching enabled instances through a controlled stop and start. The old runtime's stop callbacks run with trading commands disabled; Studio cancels its owned pending orders and confirms cleanup before constructing the new activation. Open positions and compatible parameters remain; private fields, managed-order tracking and indicators reset. OnStart runs again with current account state. A fill racing cancellation remains an account position; handover does not flatten it. Do not rely on private state surviving code updates. Ordinary request recovery never restarts a strategy.

04Parameters

Declare editable inputs using [Parameter] on public instance auto-properties with public get; set; accessors. Supported C# types are int, decimal, double, bool and string.

C# · QR strategy API
[Parameter("Enabled", Group = "Core", DefaultValue = true)]
public bool Enabled { get; set; }

[Parameter("Threshold", Group = "Signal", DefaultValue = "0.50",
    MinValue = "0.00", MaxValue = "1.00", Step = "0.05")]
public decimal Threshold { get; set; }

Each parameter needs an explicit default. Numeric minimums, maximums and steps apply only to numeric inputs. A step is a validation grid, not just a visual spinner increment. Decimal attribute arguments can use invariant strings; a decimal literal such as 0.05m is not a valid C# attribute argument.

The property name is the case-sensitive saved configuration key. The attribute's display name and Group organize the form without renaming that key. Validation binds parameters before OnStart; cross-parameter rules can be checked there. EffectiveParameters and ParameterSchema describe the validated run inputs. Changing a property during execution does not rewrite the saved request.

Static, nullable, enum, arbitrary object, indexer, custom-accessor and init-only properties are not parameter declarations in this API. There are at most 128 declared parameters. Source edits invalidate the previous validation; live compile diagnostics alone do not authorize execution.

A Symbol property is optional. Use quote.Symbol, trade.Symbol or bar.Symbol in callbacks, and Symbols for selected execution instruments. The omitted-symbol market-order overload uses the current event. At startup it requires exactly one selected instrument. For several instruments, supply the intended symbol explicitly. A strategy symbol is an exact application alias, not a broker account ID or a fixed enumeration.

05Lifecycle & callbacks

Robot derives from ResearchStrategy and provides protected Context, Positions, PendingOrders, Symbols, Indicators, Buy, Sell, Print and order helpers. The following are public overrides unless noted otherwise.

CallbackArgumentsMeaning
OnStartReplayContext contextInitialization after parameter binding. No artificial current market event exists.
OnBarOpenedReplayContext context, BarOpen barThe opening price is available.
OnBarClosedReplayContext context, BarClose barFinal completed OHLCV is available.
OnTradeReplayContext context, Trade tradeAn observed trade and optional volume.
OnQuoteReplayContext context, Quote quoteAn observed bid/ask and optional side sizes.
OnInformationReplayContext context, InformationEvent informationAvailable news or financial information.
OnFxRateReplayContext context, FxRate rateAn explicit conversion observation.
OnFuturesSettlementReplayContext context, FuturesSettlement settlementExplicit futures settlement.
OnStockSplitReplayContext context, StockSplit splitExplicit split observation.
OnDividendEntitlementReplayContext context, DividendEntitlement entitlementDividend entitlement observation.
OnDividendPaymentReplayContext context, DividendPayment paymentDividend payment observation.
OnStopReplayContext context, or protected parameterless overrideRead-only cleanup after successful startup, including cancellation/failure.

The default OnEvent(context, MarketEvent) dispatches to the appropriate typed callback. OnTick(context, MarketEvent) handles Trade/Quote and dispatches to OnTrade/OnQuote. Overriding a generic method takes ownership of its dispatch; call its base implementation exactly once if the typed hooks should also run. Prefer one processing level per observation.

There is no parameterless OnStart. Normally override only one OnStop form. A failed OnStart must clean up its own partial initialization. Legacy OnFinish(context) runs only on normal completion before OnStop. Neither cleanup hook can submit, replace or cancel an order. End of history does not force liquidation.

Order, fill and position notifications

OnOrderUpdate(ReplayContext context, StrategyOrderUpdate update) dispatches to named methods with the same arguments: OnOrderSubmitted, OnOrderAccepted, OnOrderAcknowledged, OnOrderReplaced, OnOrderTriggered, OnOrderPartiallyFilled, OnOrderFilled, OnOrderCancelled, OnOrderExpired, OnOrderRejected, OnOrderUnknown.

OnFill(ReplayContext context, StrategyFill fill) reports an actual fill. OnPositionChanged(ReplayContext context, StrategyPositionUpdate update) reports a quantity/basis transition and dispatches applicable OnPositionOpened, OnPositionClosed or OnPositionReversed hooks. Mark-price movement alone is not a position transition. A reversal is one transition, not an invented intermediate flat position.

Check the notification Stamp: Source, ObservedAt, optional OccurredAt, and available local/broker identities distinguish replay, local intent, response and reconciliation. A submitted order is not a fill. A rejected cancel is not necessarily a rejection of the original order; inspect CommandKind.

C# · QR strategy API
public override void OnFill(ReplayContext context, StrategyFill fill)
{
    Print("FILL", fill.Symbol, fill.Side, fill.Quantity, fill.Price,
        fill.Stamp.OccurredAt);
}

This is a method to place inside a Robot class. Print writes invariant-formatted output to the run's captured Log; it does not change trading state. Callbacks are synchronous. Do not retain a context/command handle for background trading or expect an asynchronous callback task to be awaited. The worker controls execution and termination; it is not a security sandbox for hostile code.

06Indicators

Call an indicator with its current observed value. The returned value is a snapshot; the Robot retains the calculation history. No separate initialization class or history queue is needed.

The author-facing signatures below omit optional compiler-supplied call-site metadata. All input/output prices and indicator values shown are decimal; periods/counts are int.

C# · QR strategy API
SimpleMovingAverage SimpleMovingAverage(decimal value, int period, string? key = null)
ExponentialMovingAverage ExponentialMovingAverage(decimal value, int period, string? key = null)
RelativeStrengthIndex RelativeStrengthIndex(decimal value, int period, string? key = null)
BollingerBands BollingerBands(decimal value, int period = 20, decimal deviations = 2m, string? key = null)
MovingAverageConvergenceDivergence MovingAverageConvergenceDivergence(
    decimal value, int fastPeriod = 12, int slowPeriod = 26, int signalPeriod = 9, string? key = null)
AverageTrueRange AverageTrueRange(
    decimal high, decimal low, decimal close, int period = 14, string? key = null)

Call these methods through Indicators, for example Indicators.RelativeStrengthIndex(quote.Bid, 14).

Result typeValue membersReadiness and convention
SimpleMovingAverageClose, Period, Count, IsReadyArithmetic mean of the rolling window; ready after Period values.
ExponentialMovingAverageClose, Period, Count, IsReadySeeded by the first Period values' arithmetic mean; then alpha = 2 / (Period + 1).
RelativeStrengthIndexClose, Period, Count, IsReadyWilder smoothing of gains/losses, alpha = 1 / Period; Count measures changes. RSI 14 requires 15 prices.
BollingerBandsClose, Middle, Upper, Lower, Deviations, Period, Count, IsReadyMiddle/Close is SMA; width is positive Deviations × population standard deviation; ready after Period values.
MovingAverageConvergenceDivergenceClose, Line, Signal, Histogram, FastPeriod, SlowPeriod, SignalPeriod, Count, IsReadyLine/Close is fast EMA minus slow EMA; Signal is an EMA of the ready line; Histogram is Line minus Signal.
AverageTrueRangeClose, Period, Count, IsReadyWilder-smoothed true range; ready after Period high/low/close observations.

During SMA, EMA, Bands and ATR warm-up, values describe the partial seed/window. Always check IsReady before using a full-period trading rule. RSI returns 50 when gains and losses are both zero, 100 when losses alone are zero and 0 when gains alone are zero. Its first price has Count 0; subsequent price changes build the seed. Do not mistake a provisional warm-up value for a complete RSI.

MACD requires fastPeriod < slowPeriod. Both price EMAs use SMA seeds. The signal starts receiving values only when the slow EMA is ready. Full readiness is after slowPeriod + signalPeriod - 1 prices: the default 12/26/9 needs 34. Before slow readiness, line/signal/histogram snapshots are zero; before signal readiness the signal is provisional. Check the shared IsReady flag.

ATR uses max(high - low, abs(high - previousClose), abs(low - previousClose)); the first range is high minus low. Inputs require low <= close <= high. Use completed OHLC values that are actually available:

C# · QR strategy API
public override void OnBarClosed(ReplayContext context, BarClose bar)
{
    BollingerBands bands = Indicators.BollingerBands(bar.Close, 20, 2m);
    MovingAverageConvergenceDivergence macd =
        Indicators.MovingAverageConvergenceDivergence(bar.Close, 12, 26, 9);
    AverageTrueRange atr = Indicators.AverageTrueRange(bar.High, bar.Low, bar.Close, 14);

    if (!bands.IsReady || !macd.IsReady || !atr.IsReady) return;
    Print(bands.Middle, bands.Upper, bands.Lower, macd.Line, macd.Signal, atr.Close);
}

This method is a separate non-trading example for a Robot class. It must not be used to read a bar's future high/low/close from OnBarOpened. Bands use a population variance calculation with double-precision square root; do not assume bit-identical values to a different library's seeding or numerical conventions.

History identity and limits

Each call site, input expression, indicator type/settings, symbol and market-event kind has independent history. Quote calls for different symbols remain separate; BarOpen and BarClose are separate series. Local variables do not recreate the stored calculation on every event.

Update every indicator on every intended observation before a readiness or pending-order early return. Conditional calls count only observations actually supplied. Repeating the same logical call within one event does not advance history twice; supplying different inputs to the same series/event raises an error. Equal timestamps can still be distinct events.

Use a short explicit key to identify series passing through shared helper code. Different logical streams need different keys. Keys do not merge symbols or event kinds. Changing a period/settings creates a separate series. State resets for every replay/live activation and survives the live host's per-callback context rebinding. No future history or automatic pre-window warm-up is loaded.

Periods accept 1–1,000,000, subject to a shared 4,096-series and 1,000,000 configured-value capacity budget per Robot; MACD reserves its component periods. Explicit keys are at most 256 characters. Indicators require a Quote, Trade, BarOpen or BarClose callback; they cannot be initialized with a fabricated price in OnStart. Invalid inputs and arithmetic overflow are reported.

07Market & limit orders

Robot's exact-decimal convenience methods return a long local order ID:

C# · QR strategy API
SubmitMarketOrder(OrderSide side, string symbol, decimal quantity,
    string? label = null, decimal? stopLossPips = null, decimal? takeProfitPips = null,
    string? comment = null)

SubmitMarketOrder(OrderSide side, decimal quantity,
    string? label = null, string? comment = null)

SubmitLimitOrder(OrderSide side, string symbol, decimal quantity, decimal targetPrice,
    string? label = null, decimal? stopLossPips = null, decimal? takeProfitPips = null,
    DateTimeOffset? expiresAt = null, string? comment = null)

The omitted-symbol overload is on Robot and has label/comment options, not pip-protection options. The explicit-symbol decimal methods also exist on context.

C# · QR strategy API
long orderId = SubmitMarketOrder(Buy, quote.Symbol, 1m,
    label: "Entry", comment: "Observed quote signal");

This is a callback-body illustration; use signal and exposure rules rather than submitting unconditionally on every observation. Buy/Sell are Robot constants for OrderSide. Quantity must be positive and uses the instrument's units. A market order has no target price or pending expiry. Limit targetPrice is an absolute price. Null expiry selects GTC; an explicit expiry requests GTD in replay and is exclusive.

Double-valued aliases also return long:

C# · QR strategy API
ExecuteMarketOrder(OrderSide tradeType, string symbolName, double volume,
    string? label = null, double? stopLossPips = null, double? takeProfitPips = null,
    string? comment = null)

PlaceLimitOrder(OrderSide tradeType, string symbolName, double volume, double targetPrice,
    string? label = null, double? stopLossPips = null, double? takeProfitPips = null,
    DateTimeOffset? expiration = null, string? comment = null)

These convert to the decimal execution model. Nonfinite or unrepresentable values are rejected. Prefer decimal methods when your values are already decimal. expiration and expiresAt are different parameter names on these two APIs.

For other supported order configurations, the lower-level context method is:

C# · QR strategy API
long SubmitOrder(string symbol, OrderSide side, decimal quantity,
    OrderType type = OrderType.Market, decimal? limitPrice = null, decimal? stopPrice = null,
    TimeInForce timeInForce = TimeInForce.Gtc, DateTimeOffset? expiresAt = null)

It is symbol-first and does not add the convenience label/protection arguments. Actual type/duration support depends on replay or broker adapter. context.ReplaceOrder(long orderId, decimal quantity, decimal? limitPrice = null, decimal? stopPrice = null) changes total quantity and supplied prices; null prices retain existing values. context.CancelOrder(long orderId) cancels the pending remainder. Unknown/terminal identities return false. Live operations additionally need observed broker identities.

Target exposure

SetTargetPosition(string symbol, decimal desired) is a protected Robot helper. Desired is a signed net position, not a new order amount: positive long, negative short, zero flat. It includes owned pending quantities, waits for cancellation acknowledgement, and preserves reservations for uncertain submissions. OrderState describes its latest decision. It does not cancel unrelated orders. In live execution, net positions may include manual or other-strategy exposure; matching symbols do not establish exclusive ownership.

08Positions & pending orders

Positions is an IReadOnlyList<TradingPosition> of net exposures. PendingOrders is an IReadOnlyList<TradingPendingOrder> of user pending intents. They are observations/guarded handles, not hedged tickets.

TypeMain members and commands
TradingPositionNullable Id, EntryPrice, Currency, labels/comments; Symbol, signed Quantity, Side, LabelState, protection distances/prices; long? Close().
TradingPendingOrderId, Symbol, Side, Type, total quantity, nullable filled/remaining quantities, prices, labels/comments, duration/expiry; bool Cancel().
BothModifyStopLossPips(double?), ModifyTakeProfitPips(double?), and exact-decimal ModifyStopLossPipsDecimal(decimal?), ModifyTakeProfitPipsDecimal(decimal?), returning bool.

Read fresh position handles in each callback. Close requests an opposite market order; it does not promise an immediate fill. A stale or changed position handle is rejected, and existing pending orders—including attached protection—must be reviewed before another close request. Repeating Close on the same handle returns its earlier request.

Use HasFilledQuantity, FilledQuantityOrNull and RemainingQuantityOrNull when live information may be unknown. Do not turn missing broker values into zero. Labels are local attribution; LabelState is Unknown, Homogeneous or Mixed. Modification needs a controllable, unambiguous position identity.

Robot PendingOrders filters out attached replay protective children. The raw context.PendingOrders remains available for diagnostics; manage protection through its owning position. Passing null to a protection modification removes/omits it; a supplied distance must be positive. Unsupported capability throws instead of silently discarding the request.

09Pips & protection behavior

A pip is an explicitly declared price distance, independent of the minimum price tick. Configure positive PipSize metadata before using pip distances. No value is guessed from symbol spelling, currency or decimal precision. A declared pip size of 0.01 and 20 pips imply 0.20 price units before grid normalization; this is arithmetic, not a universal convention for a currency.

Replay SL/TP protections are linked reduce-only stop/limit exits tied to a net-position generation. They begin after entry and become eligible on later observations. Levels derive from the actual entry basis and outward price-grid normalization; inspect StopLossPrice and TakeProfitPrice. Partial exits resize siblings, reaching flat cancels siblings, and a protective fill cancels its related unfilled entry remainder. A stale bracket cannot reverse or reopen the position.

Conflicting protection/ownership declarations are rejected. A whole-position change affects the net position, not an invented independent ticket. Stops can fill past their threshold after a gap. Completed OHLCV does not reveal an intrabar path; a bar's low crossing a stop is not itself a fill in BarOpen execution. Explicit stock-split policy cancels pending orders and clears brackets; it does not invent adjusted orders.

The current live authoring layer does not provide attached native SL/TP, pip-protection modification or GTD. These requests fail explicitly. It does not replace them with unprotected orders or represent replay brackets as broker-hosted protection.

10Data & execution timing

EventInformation available at the callback
BarOpenOpen only; no future high, low, close or final volume.
BarCloseCompleted Open, High, Low, Close, Volume. It is not an execution-price event.
TradePrice, Volume and HasVolume; missing volume is distinct from zero.
QuoteBid, Ask, Midpoint and optional BidSize/AskSize.
InformationEventSource, identity, metrics and explicit publication/fetch/availability timing.
FxRateDirect conversion observation; it does not fill orders.

Market events expose UTC Timestamp, exact Symbol and source-local Sequence. Context exposes CurrentEvent, global EventOrdinal, Portfolio, raw PendingOrders, CancellationToken and optional LiveAccount. Equal timestamps can have different ordinals. Startup/reconciliation callbacks can have no current market event.

Replay processes accounting/marks, expirations and eligible older orders before the market callback, and delivers resulting notifications at stable boundaries. An OnStart order may use the first eligible observation.

For new desktop requests, current-quote market orders are enabled. A Market order for the currently delivered Quote symbol, with Quote execution selected, buys at Ask or sells at Bid within remaining side liquidity. Accounting/positions update before submission returns; notifications are deferred, avoiding recursive callback entry. Older orders consume liquidity first. Partial GTC remainders wait; IOC remainders cancel. Attached protection waits for later eligible observations.

Other orders and execution feeds use their established later-observation rules. The option cannot use another symbol's quote, cached prices, completed OHLC or information events. Turning it off restores next-observation market timing. Older saved requests retain their original semantics. A signal at the history end may leave an unfilled order or open position.

Choose one execution feed per instrument. With EventVolume liquidity, trades need volume and quotes need side sizes; missing quantities are not invented. Unbounded liquidity is a replay assumption. Commission, contract multipliers, corporate actions and margin choices affect accounting. ResearchLedger can permit negative equity.

Foreign-currency replay needs explicit historical FX rates and a maximum permitted age. The age setting does not fetch a rate; future or today's reference rates cannot substitute for past observations. Information-event SnapshotFetchedUtc and PublicationApproximation conventions have different availability meanings. A returned publication date does not establish that revised values existed unchanged at that time.

Date-window and Research segments do not automatically load earlier warm-up, carry positions/state or force final liquidation. Each research segment starts fresh. Use only observations delivered to your callbacks; RollingWindow<T> is available for custom bounded history. There is no implicit provider client or unrestricted historical Bars collection attached to Robot.

11Live capability boundaries

Replay and live use related callback types but different evidence and capability boundaries. Compilation/backtest success is not broker acceptance or exchange certification.

Provider/sourceCurrent scope
Saxo OpenAPIStocks, FX Spot, supported CFDs and dated futures; exact contracts/account permissions required.
Interactive BrokersStocks through Client Portal Web API or Gateway/TWS socket. Socket mode uses a separately installed official C# API.
Lightspeed ConnectSupported stock execution; separate entitled market data required.
Databento liveSeparately selected top-of-book/trade/status feed with explicit mappings; it is not an order broker.

Options are not supported in these current desktop product scopes. Bloomberg and Trading Technologies are unavailable. Integration names do not imply every market, account, order type or subscription is supported.

Current desktop Connect/Apply actions reconcile the account. Strategy Start and reviewed manual Submit are explicit; the older separate desktop Risk/Arm step has been removed. Old quote source time alone no longer blocks this direct workflow; timestamps, delayed-data warnings and invalid-price, disconnected-feed, future-time, status and contract checks remain. Account reconciliation age is separate. API callers explicitly using the older risk policy retain that policy.

Live acknowledgement and applicable authorization remain required. An owner/development session setting is not a commercial customer license or production issuer. Connections do not submit orders. The broker remains authoritative for margin, permissions, quantities and venue rules; the desktop does not silently increase a user's quantity to a minimum.

Live OnOrderSubmitted is a local intent. Acknowledgement is a command response, not proof of a fill. Missing broker identity can prevent cancel/replace. Unknown outcomes and absent pending snapshots must be reconciled, not blindly resubmitted. Manual and strategy actions share net account positions; stopping or disconnecting is not confirmation of all broker cancellations. The Journal and strategy Log have different roles.

12Strategy examples

Moving average crossover

Fast/slow SMA crossover with separate timeframe-minute parameters. Register once in OnStart; completed candles update automatically; prior selected replay data or compatible live history warms the averages. Supports bars, bid quotes or trades. Any minute interval from 1 to 1440 is supported. Averages are compared as entered; equal settings stay flat.

Strategy type
StudioExamples.MovingAverageCrossover
Source file
MovingAverageCrossover.cs
C# · MovingAverageCrossover.cs
Download .cs
using System;
using QuantResearch.Engine;
using QuantResearch.Strategies;
namespace StudioExamples;
// Independently authored educational example. No profitability or execution guarantee.
// Register once in OnStart: completed candles update both SMAs automatically.
// Prior selected replay data and compatible live history warm indicators automatically.
// IsReady protects against insufficient history; OnBarClosed runs once per candle.
// Choose separate timeframes in minutes. No Update call is needed.
// SetTargetPosition handles pending orders; bar-based orders fill on a later eligible observation.
public sealed class MovingAverageCrossover : Robot
{
    private BarIndicator<SimpleMovingAverage> sma_fast = null!;
    private BarIndicator<SimpleMovingAverage> sma_slow = null!;

    [Parameter("Symbol", Key = "Symbol", Group = "Trading", DefaultValue = "DEMO")]
    public string SymbolName { get; set; } = "DEMO";

    // Keep Symbol metadata bound to this configured instrument in every callback.
    protected override string? SymbolNameOverride => SymbolName;

    [Parameter("Quantity", Group = "Trading", DefaultValue = 1, MinValue = 1, MaxValue = 1000000, Step = 1)]
    public int Quantity { get; set; } = 1;

    [Parameter("Fast", Group = "Signal", DefaultValue = 10, MinValue = 1, MaxValue = 1000, Step = 1)]
    public int Fast { get; set; }

    [Parameter("Slow", Group = "Signal", DefaultValue = 30, MinValue = 1, MaxValue = 1000, Step = 1)]
    public int Slow { get; set; }

    [Parameter("Fast timeframe minutes", Group = "Timeframe", DefaultValue = 1, MinValue = 1, MaxValue = 1440, Step = 1)]
    public int FastTimeframeMinutes { get; set; } = 1;

    [Parameter("Slow timeframe minutes", Group = "Timeframe", DefaultValue = 1, MinValue = 1, MaxValue = 1440, Step = 1)]
    public int SlowTimeframeMinutes { get; set; } = 1;

    public override TimeSpan BarTimeframe => TimeSpan.FromMinutes(FastTimeframeMinutes);

    public override void OnStart(ReplayContext context)
    {
        sma_fast = Indicators.SimpleMovingAverage(TimeSpan.FromMinutes(FastTimeframeMinutes), Fast, SymbolName);
        sma_slow = Indicators.SimpleMovingAverage(TimeSpan.FromMinutes(SlowTimeframeMinutes), Slow, SymbolName);

        if (string.IsNullOrWhiteSpace(SymbolName) || SymbolName != SymbolName.Trim())
            throw new ArgumentException("Set Symbol to the selected data symbol.");
        // Compare the configured averages as entered; equal settings stay flat.
    }
    public override void OnBarClosed(ReplayContext context, BarClose bar)
    {
        if (bar.Symbol != SymbolName) return;
        if (!sma_fast.IsReady || !sma_slow.IsReady) return;

        if (sma_fast.Close > sma_slow.Close)
        {
            SetTargetPosition(SymbolName, Quantity); // Buy up to the desired position.
        }
        else
        {
            SetTargetPosition(SymbolName, 0m); // Sell the long position and stay flat.
        }
    }

}

Open and validate one source at a time. Each example derives directly from Robot. The ZIP contains individual strategies and templates.

Source verification

SHA-256 · d8c5258cbf1e6e751afc109425ba1e8a6c4c190b1f2b5079fff3bd759acce71e

Download the source manifest

The source collection contains 15 library examples and two starter templates, all using a direct Robot strategy class. Open, review and validate each source individually. Parameters and data must match its input model.

SourcePurpose / inputs
DemoRoundTrip.csBasic entry/exit lifecycle demonstration.
BuyAndHold.csStartup entry; position can remain at history end.
EventObserver.csRead-only event observation.
MovingAverageCrossover.csStateful SMA trend using completed bars.
MeanReversion.csCompleted-bar prior-window z-score.
ChannelBreakout.csPrior completed-bar high/low channel.
TradeMomentum.csObserved trade momentum.
QuoteMomentum.csQuote-midpoint momentum with spread/size conditions.
MultiSymbolTrend.csIndependent completed-bar trends across two symbols.
MultiTimeZoneSessionTrend.csTrade observations within named local session windows.
QuoteTradeConfirmation.csCombined quote/trade observations and one selected execution feed.
PassiveBidAskLadder.csBounded quote-based limit ladder with pending intent handling.
NewsFilteredMovingAverage.csCompleted-bar trend gated by available news information.
JQuantsStatementForecast.csExplicit statement/price rule; not trained machine learning.
JQuantsFundamentalForecast.csExplicit fundamentals/price rule; not trained machine learning.
TypedCallbackObserver.csRead-only typed-callback starter.
MyStrategy.csBlank editable starter.

These examples are programming references, not validated profitable systems. Signals using time zones are not automatically exchange holiday calendars. A signal freshness rule does not cancel an older GTC order unless the strategy requests that cancellation.

13Migrate existing source

  1. Save your source and open it in the installed release. Validate again rather than reusing a compiled artifact from another release.
  2. Derive a custom class directly from Robot to use Indicators, collections and helpers. Existing ResearchStrategy code can remain on its explicit API if desired.
  3. Replace manually managed SMA/EMA/RSI queues with the matching indicator only after matching input, seed and warm-up semantics. Move all indicator updates before readiness/early-return checks.
  4. Keep explicit Symbol parameters when useful, or use event.Symbol and Symbols. Do not guess a broker contract from an alias.
  5. Review current-quote execution in new requests when comparing an older result; different fill timing can change P&L materially. Do not reinterpret a printed decision quote as an old fill.
  6. Reopen current library samples to obtain their rewritten source. Editor recovery intentionally preserves your existing document; an upgrade does not overwrite your custom code.

Use Save source/Ctrl+S to write the source file and Validate C# to create a runnable library snapshot. A disk file conflict is reported rather than silently overwritten. Keep source, data identities, settings and saved receipts when comparing runs.

14Troubleshooting

SymptomCheck
Indicator never becomes readySupply every intended observation before any early return. RSI 14 needs 15 prices; default MACD needs 34. Confirm callback kind and sufficient history.
Duplicate/conflicting indicator inputGive separate logical series distinct call sites or explicit keys; one series accepts one input per event.
Indicator unavailable during startupCall it in Quote/Trade/Bar callbacks, not OnStart or information/lifecycle callbacks.
No orders despite a signalCheck selected execution instruments, parameters, target/pending state, account/contract settings and history.
Order never fillsCheck execution feed, timing, later observations, liquidity, limit/stop, expiry and sessions. Completed bars are not fill evidence.
Unexpected profit or lossCompare actual Trade History to decision logs, current-quote setting, quantities, cash, commission, multiplier and FX.
Protection rejectedProvide valid PipSize for replay and check host capability. Current live attached pip protections are unsupported.
Close/modify/cancel failsRead current handles and observed identities, inspect pending orders/ownership and reconcile live state.
Sharpe N/AHover over Results' Sharpe card. Daily-return count, nonpositive equity, zero variance or absent statistics can make it undefined.
Older result has no fill historyRun again with capture enabled; saved outcomes are not backfilled.
SDK/hash/validation mismatchUse one complete matching package and revalidate source. Do not bypass integrity checks.
Notification-limit failureRemove recursive/unconditional submission cycles and keep synchronous callbacks bounded.

Provide the release, operation, source/strategy identity, selected data/range and sanitized diagnostic when requesting support. Do not include API keys, passwords or raw account identifiers.

15Downloads & version

OFFLINE REFERENCE · MARKDOWN

Keep the guide beside your code.

The same versioned reference, ready to search and read locally.

Download API guide ↓
ORIGINAL QR EXAMPLES · C# / ZIP

Start from working source.

Library strategies, the typed-event observer and a blank Robot scaffold, with dataset requirements.

Download all examples ↓

This M71 reference describes Strategy API protocol version 1 and the current source-level conveniences. It does not promise binary compatibility with older compiled strategies or implement another vendor's complete API. Recompile source against the installed assemblies.

The archive's source identities should match the published edition. No customer strategy, data, credential or provider entitlement is included. Runtime dependencies, optional SDKs and software notices remain separate from customer account permissions and commercial terms.

16M73: timeframe indicators and automatic warm-up

Register timeframe indicators once in OnStart. TimeSpan.FromMinutes(5) means five minutes; C# 5m is the decimal value five, not a duration. These overloads update automatically from completed candles. Do not call Update on them.

C# · QR strategy API
using System;
using QuantResearch.Engine;
using QuantResearch.Strategies;

public sealed class TimeframeAverage : Robot
{
    private BarIndicator<SimpleMovingAverage> fast = null!;
    private BarIndicator<ExponentialMovingAverage> slow = null!;

    public override TimeSpan BarTimeframe => TimeSpan.FromMinutes(5);

    public override void OnStart(ReplayContext context)
    {
        fast = Indicators.SimpleMovingAverage(TimeSpan.FromMinutes(5), 20);
        slow = Indicators.ExponentialMovingAverage(TimeSpan.FromHours(1), 50);
    }

    public override void OnBarClosed(ReplayContext context, BarClose bar)
    {
        if (!fast.IsReady || !slow.IsReady) return;
        SetTargetPosition(bar.Symbol, fast.Close > slow.Close ? 1m : 0m);
    }
}

SMA, EMA, RSI, Bollinger Bands, MACD and ATR all have a TimeSpan overload. For example Indicators.RelativeStrengthIndex(TimeSpan.FromMinutes(5), 14). They return BarIndicator<T> with Close, IsReady, Current (the full numerical snapshot), and Bars (symbol, timeframe, source, last completed candle). The bar's OpenTime/EndTime are the interval; ObservedTime is when the close became available. Parameters keep their existing numerical definitions and readiness rules.

Specify symbol: "AAPL" when several instruments are selected. source: defaults to BarPriceSource.Automatic: the selected execution feed chooses bid quotes, actual trades, or native OHLC bars. Explicit Bid, Ask, Midpoint, Trade and NativeBars sources are also available. A series never mixes quote and trade prices. The supplied source must actually be present; it is not substituted. Multiple indicators can share one bar series while retaining independent calculations.

See [Timeframe indicators and historical warm-up](TIMEFRAME_INDICATORS_M73.md) for source selection, causal aggregation, live history limits and manual-API compatibility.

17Configurable timeframe samples (M74)

Nine indicator samples now register in OnStart and request compatible live history automatically. MovingAverageCrossover has FastTimeframeMinutes and SlowTimeframeMinutes; the other eight have TimeframeMinutes. Defaults are one minute. Configure these parameters after validation. Readiness guards wait when history is unavailable. New sample copies are available in Strategy library; saved user code is retained. See [the sample guide](TIMEFRAME_SAMPLES_M74.md) for changed momentum semantics and history limitations.

18Automatic replay warm-up and candle callbacks (M75)

Timeframe indicators registered in OnStart now warm automatically from earlier prices in the selected datasets. Backtest and Optimize use prices before Date From; Research warms each training, validation and out-of-sample segment independently from its earlier prices. Historical events never call trading callbacks or enter fills, P&L, equity, drawdown, Sharpe or event counts. No future prices are read by an earlier segment. Missing history leaves IsReady false until enough candles finish.

Set public override TimeSpan BarTimeframe => TimeSpan.FromMinutes(TimeframeMinutes); for the decision frequency. Robot invokes OnBarClosed once per completed candle and updates all timeframe indicators first, including when the input contains finer OHLC bars. The samples no longer need lastDecision timestamp guards. OnQuote, OnTrade and OnTick still receive each selected-period observation for strategies that need tick decisions. See [warm-up and callback details](REPLAY_WARMUP_M75.md).

19Optimization of timeframe indicators (M76)

All whole-minute intervals from 1 through 1440 are valid for the samples. A range starting at 1 with step 15 means 1, 16, 31, ...; to search conventional intervals use Values, for example 1, 5, 15, 30, 60. Candle boundaries have a fixed UTC origin (0001-01-01); unusual intervals continue at full length across midnight. Existing day-dividing intervals retain their boundaries. Native OHLC inputs must still fit wholly inside each requested candle.

MovingAverageCrossover compares the two configured averages as entered. It permits equal or reversed periods; equal settings stay flat. A large timeframe and period need enough historical data to become ready. A successfully completed candidate with no trades can therefore have a zero score.

Failed searches now show the actual candidate, parameters, worker stage and error. Cancelled peers do not obscure the cause. Genuine worker failures still honor ContinueOnFailure. Open a fresh sample from Strategy library to get the relaxed period check; saved user code is preserved. See [details](OPTIMIZATION_TIMEFRAMES_M76.md).

20Invalid optimization candidates and larger spaces (M77)

Optimize and Research automatically skip parameter combinations rejected before trading. A parameter validation error, or an ArgumentException thrown in OnStart, records an unscored skipped_invalid attempt with its reason. Invalid candidates cannot win, become Genetic parents, or enter the validation shortlist. Other execution errors still follow ContinueOnFailure. Input, timeout, cancellation and worker cleanup errors are not silently skipped.

Results shows SKIPPED INVALID, grouped reasons, and every attempted candidate in Search trials with its parameters and phase. Research counts attempts across training, validation and OOS, so the same combination in separate phases counts separately. If a fold has no eligible candidate, it is marked skipped and later folds continue. OOS selection stays frozen; a rejected OOS candidate is never replaced using OOS information. A missing fold leaves the overall independent OOS aggregate unavailable.

The search supports up to 128 varying parameters, stored as compact domains. Genetic can sample spaces larger than 10,000,000 combinations without enumerating them. Genetic search evaluates a bounded subset, not all combinations. MaximumEvaluations remains 1–10,000 attempts per search, including skips; generation limits still apply. Grid requires the whole product to fit that evaluation budget. An entirely invalid Genetic generation explores new, unseen combinations in subsequent generations. No eligible candidate within the limits means no winner, with the skip reasons shown. Values outside declared single-parameter bounds remain form errors to correct before running.

For a custom cross-parameter rule, check it in OnStart before submitting orders: if (Fast >= Slow) throw new ArgumentException("Fast must be smaller than Slow.");. Use the rule only if your strategy requires it; MovingAverageCrossover itself permits equal or reversed periods. See [candidate skipping details](CANDIDATE_SKIPS_M77.md).

21Long workflows and parallel preparation (M78)

Backtest, Optimize and Research forms now run without automatic workflow, search or candidate deadlines. Use Cancel task when you want to stop. Evaluation and generation budgets still apply. Independent daily files are verified and prepared concurrently, with up to 16 file workers capped by available logical processors and shared across candidates. Progress counts completed files. Replay order, corruption checks and past-only warm-up remain deterministic. Speed depends on CPU and storage.

For programmatic requests, timeout defaults are Timeout.Infinite (-1 milliseconds); worker calls accept Timeout.InfiniteTimeSpan. Existing saved/CLI requests with explicit positive deadlines retain those limits. Studio forms create unlimited requests even when populated from an older request. See [workflow details](UNLIMITED_PARALLEL_WORKFLOWS_M78.md).

22Reusable inputs and saved result parameters (M79)

Studio reuses verified datasets, prepared replay inputs and date slices across Backtest, Optimize and Research in the current application session. Changed files are verified again. Exact dates and warm-up settings are part of the cache identity, so different Research windows remain isolated. The first use after restarting still prepares data; each worker retains its integrity check. Prepared entries have a 32 GiB / 256-entry idle budget, with active inputs protected until consumers exit.

Results now shows short strategy class names and a sortable # of Trades column. Trades means executed fills (including entries, exits and partial fills), matching Trade History. Select a saved run and open Parameters to inspect its recorded effective values. Optimize shows the winning candidate; Research lists each OOS fold and identifies the final fold used for the displayed metrics. Older records without effective values show labelled submitted parameters. Current editor changes do not alter saved results. See [cache and Results details](DATA_CACHE_RESULTS_M79.md).

23Managed limit-order refresh (M91)

The explicit RefreshLimitOrders(quote, everyQuotes, build) helper remains supported. The current bundled PassiveBidAskLadder uses the automatic default described in the M93 section below. Call it on each matching quote. The first usable quote builds the ladder; later refreshes use the configured quote count. The build callback defines the desired prices and quantities with orders.Add(side, quantity, price):

C# · QR strategy API
RefreshLimitOrders(quote, RefreshEveryQuotes, orders =>
{
    var position = context.Positions
        .Where(p => p.Symbol == quote.Symbol).Sum(p => p.Quantity);
    if (position < MaximumInventory)
        orders.Add(Buy, Math.Min(QuantityPerLevel, MaximumInventory - position), quote.Bid);
    if (position > -MaximumInventory)
        orders.Add(Sell, Math.Min(QuantityPerLevel, MaximumInventory + position), quote.Ask);
});

The engine owns submission reservations, cancellation requests and terminal-order tracking for this set. It never cancels manual orders, another robot's orders or orders submitted outside the managed set. On refresh it waits for every old order to complete or confirm cancellation before calling build, so the callback reads fresh positions after any intervening fills. Broker submission acknowledgements are not fills; a rejected cancellation does not terminate an order. Unknown or missing outcomes keep the reservation and do not trigger resubmission. Live pending-list disappearance alone is not completion proof. Reconcile unresolved outcomes before restarting. Confirmed full fills, expiry and submit rejection release reservations even if the order never appeared pending. A user override of OnOrderUpdate does not disable the internal tracking.

An explicit zero bid/ask size or an invalid two-sided price starts cancellation immediately, regardless of the refresh interval. Missing sizes remain supported. The new set is built only on a later/current usable quote, never from a terminal notification using an old quote. Repeated calls for the same symbol and event do not create another set. Each robot and symbol has its own set; this helper is for replacing a set, not accumulating independent orders.

context.Positions is the preferred alias of context.TradingPositions; both expose the same net trading positions and position commands. Existing code remains compatible. Robot.Positions uses the same view. Live positions may include manual and other-strategy exposure; position quantity is not an ownership claim. This does not require unavailable research Portfolio accounting.

The sample retains trader decisions: level count, tick spacing, quantity, refresh interval and signed inventory budget. It no longer declares ownOrders, ObservePending, SeenPending, CancelSent or awaitingCancellation. Choose the updated bundled sample after reopening Studio; user-edited strategy copies are preserved.

24Automatic quote limit orders (M93)

Robot automatically manages same-symbol limit orders submitted inside its quote decision. There is no required ManageLimitOrders call in OnStart, and a strategy does not need an OnStart method at all. Ordinary context.SubmitLimitOrder, context.SubmitOrder with OrderType.Limit, and Robot limit conveniences join the same per-instance, per-symbol set and return actual local IDs.

The first limit submission activates managed refresh for that symbol. The default interval is 20 matching quote observations. Before activation, market/stop-only strategies keep receiving every quote, and their orders retain ordinary behavior. After activation, the entire quote decision (OnEvent, OnTick and OnQuote) runs only when a usable refresh is due and all previous managed orders have confirmed completion or cancellation. Quote-based decisions that mix order types therefore share this cadence; only their same-symbol limits are included in the managed set. Indicator streams and bar/trade/lifecycle callbacks continue normally.

The PassiveBidAskLadder sample uses plain context.SubmitLimitOrder calls. It retains its existing configurable RefreshEveryQuotes parameter and connects that value to the optional cadence property:

C# · QR strategy API
[Parameter("RefreshEveryQuotes", Group = "Signal", DefaultValue = 20,
    MinValue = 1, MaxValue = 10000, Step = 1)]
public int RefreshEveryQuotes { get; set; }

public override int LimitOrderRefreshQuotes => RefreshEveryQuotes;

The override only changes the refresh interval. Omitting both this parameter and the override still enables automatic management with the 20-quote default. It does not enable protection: protection is already enabled. The engine uses the declared policy property, not arbitrary variable names. Its value must be positive and is captured at startup; parameter changes take effect on the next run. This is a quote count, not a timer or guaranteed broker-message rate.

Old limits must finish or confirm cancellation before the next decision can calculate fresh position budgets. Unusable quotes cancel the previous set before the interval without running another decision. An initial limit request on an unusable quote is rejected before submission. Unknown submissions, absent pending orders, partial fills, rejected cancellations and transport acknowledgements keep their reservations. Exact terminal fills, expiry, cancellations and rejected submissions release them, including orders never seen pending. No unknown order is resent. Callback errors cannot erase IDs already returned. A usable later quote is needed for the next decision; lifecycle updates do not invoke a stale quote.

Manual orders, other strategy instances, and limits from startup, trade, bar or lifecycle callbacks are outside this quote set. Cross-symbol limits inside the automatic quote callback are rejected before submission, with an explanatory diagnostic. Strategies that intentionally maintain independent persistent or cross-symbol quote limits can opt out explicitly:

C# · QR strategy API
public override bool AutomaticallyManageLimitOrders => false;

This opt-out is captured at startup and preserves ordinary submission semantics; the author then manages pending orders. Existing custom Robot strategies that submit same-symbol quote limits now receive automatic refresh by default, so review this policy when migrating persistent-order strategies. ResearchStrategy and ordinary orders outside quote decisions retain their existing behavior.

The M92 ManageLimitOrders(symbol, everyQuotes) startup method remains an optional per-symbol override and retains its strict same-symbol limit-only callback rules, even when the default is opted out. The M91 RefreshLimitOrders helper also remains supported without adding an opt-out. Its explicit batch owns the symbol's orders; do not mix it with direct limits for that symbol. Both mixing directions are rejected, including later callbacks. Existing saved parameter names are retained. Reopen Studio and choose the updated bundled sample; edited source copies are preserved.

25CFD charts, ladder inventory and history PnL (M94)

IBKR stock-CFD historical candles use the exact broker-reported UnderConId. Studio resolves that ID as a stock in the CFD currency, uses the stock exchange timezone, and labels the chart as underlying-stock data. CFD order and subscription identities remain unchanged. Unavailable or inconsistent underlying metadata produces an actionable chart error. Market-data subscriptions are still required. Stock and futures histories retain their own contracts; FX retains Bid bars. This follows IBKR's CFD data guidance.

PassiveBidAskLadder defaults to RefreshEveryQuotes=1 and TickSize=0. Zero chooses context.PriceTickSize(Symbol), the configured replay or broker instrument price increment. If the host has no increment, specify TickSize explicitly. A positive TickSize retains custom rounding and level spacing. Existing saved values remain unchanged: choose 1 and 0 to use these defaults in a saved setup. With a 0.01 explicit increment, smaller USDJPY price movements can leave the rounded ladder prices unchanged.

Existing net inventory occupies inner levels on its side. For Levels=2 and QuantityPerLevel=1000, a long position of 1000 leaves buy level 2 and sell levels 1 and 2. A long position of 2000 leaves no buy levels. A partial inner fill of 500 leaves 500 at buy level 1 and 1000 at level 2. Shorts work symmetrically; closing inventory frees that capacity. MaximumInventory is an additional signed exposure cap. This uses account net inventory, including manual or other-strategy positions; it does not claim ownership of individual broker fill lots.

The engine still owns cancellation tracking and waits for all prior managed orders to complete or confirm cancellation before submitting replacements from the latest quote and positions. Unknown outcomes remain reserved. Manual orders, other strategies and orders outside this managed set are not cancelled. RefreshEveryQuotes counts matching quote observations, not seconds; broker latency and cancellation acknowledgement can delay repricing.

The sample enables the optional Robot policy LimitOrderRefreshOnlyOnChange. With this policy the engine retains the managed set while bid, ask, net inventory and owned-order lifecycle state are unchanged. Fills and terminal updates make another refresh necessary. Unusable quotes still trigger cancellation. The policy defaults to false for general robots, whose decisions may depend on time or other inputs. No OnStart management call is required. The existing RefreshLimitOrders helper and explicit startup policy retain their behavior.

Execution History adds sortable PnL and Currency columns. For Saxo these carry ClosedProfitLossInBaseCurrency exactly as reported by the broker; positive, negative and zero values are preserved and absent PnL stays blank. Studio does not recompute it from prices or apply an additional fee adjustment. See Saxo ClosedPosition. This optional history never resolves uncertain orders.

Reopen Studio to use the release, and select the updated bundled PassiveBidAskLadder sample. Edited source copies and saved parameters are preserved. Running strategies are never migrated or restarted automatically.

26Selective ladder amendments and position detail (M95)

PassiveBidAskLadder now retains existing managed order IDs and modifies only changed prices/quantities when the host supports amendment. Unchanged orders retain their queue position. The host waits for broker-confirmed amended terms before another decision; rejected changes pause this set, and uncertain changes are never resent. An unavailable modification capability retains the cancellation-confirmation fallback. Manual and other-strategy orders are outside this managed set. Only the same strategy activation owns its orders. Filled inner capacity remains occupied. A partial fill uses total quantity = confirmed filled quantity + desired remainder; when filled quantity is unknown a price-only change retains the original total, and a changed quantity requires cancellation confirmation. When inventory shrinks or reverses, the affected side waits for cancellation confirmation before expanding its available levels; opposite-side orders remain in place. This avoids overlapping an unconfirmed quantity reduction with a new order. No additional OnStart call is required. Existing custom source is preserved; select the updated bundled sample.

The price-driven Robot policy LimitOrderRefreshOnlyOnChange enables this selective maintenance. Inside its managed quote callback, SubmitLimitOrder may retain and return the real existing local order ID; it represents the current desired pending remainder, not a second independent order. The ordinary independent-order API is unchanged outside the managed callback. Unsupported modifications are never retried as submissions after an ambiguous response. Broker modification can affect venue queue priority even when the order ID is retained. See IBKR modification rules.

ReplayContext.NormalizeLimitPrice(symbol, price, side, levelsAway=0) rounds passively with the host's verified resting-limit tick rule, including price bands and limit-specific increments. The ladder's TickSize=0 uses successive valid ticks across band boundaries. A positive TickSize supplies custom spacing, followed by passive broker-rule rounding. If rounding would collapse adjacent levels, the next level moves out by a valid broker tick. Prices at or below zero are not submitted. IBKR's minimum contract tick is not necessarily the applicable increment at every price; see IBKR minimum increments.

Positions initially displays individual broker-reported open legs, earliest entry first, with each leg's identity, signed quantity and entry price. The full signed leg total must match the current net position and newer fills invalidate stale leg details. Opposing Saxo legs can remain until broker netting. If individual open legs are not supplied (including IBKR's net-only position interface), the row explicitly remains a net position; Studio does not guess an open-lot allocation from incomplete executions. Expand Net positions and actions for the existing reviewed account-net close/target actions. Selecting a display leg never silently closes a full net position. See Saxo position records.

For a confirmed, completely filled Saxo order, absent/zero historical order prices remain unknown when no confirmed price history exists. The execution price, fill amount, immutable order identity and original journal request are retained. Present contradictory fields still fail reconciliation. Unknown working or partially filled order prices remain invalid. Missing historical limit prices do not prove a price amendment was accepted; uncertain amendments stay unresolved. No order is resent. Saxo audit history is scoped to the exact account and order, and unchanged terminal evidence is cached. See Saxo order activity history.

History labels Side as Entry side (Long/Short of the original position). PnL is rounded to the nearest whole currency unit, with midpoint values away from zero, comma thousands separators and no decimal digits. Sorting and stored evidence retain the original decimal PnL; unknown values stay blank. Reopen Studio to load M95. Active sessions, orders, saved parameters and edited source are not changed by installation.

27Fill detail and connected-account recovery (M96)

IBKR Positions now displays remaining execution quantities separately, earliest fill first. Available execution records are allocated FIFO against later opposite fills. A reversal retains only the remaining quantity of the reversing fill. Corrected executions replace earlier revisions for this display, without changing raw broker evidence. Equal timestamps use execution ID order. This is an execution-derived display, not broker-reported tax lots or authoritative individual positions. The IBKR execution window is limited by Gateway/TWS history availability; any older remaining balance has no invented date or price and is labelled Unattributed balance. With no usable fills the net position remains labelled as net. Broker account net quantities still control strategy risk and Net positions and actions. No position is closed by selecting a display row. Saxo's verified individual broker legs remain unchanged.

When a newly appearing IBKR account exposure interrupts polling, Studio makes one bounded read-only attempt to resolve exact missing supported contracts and append them to the account catalogue, then rereads the account. This also covers currency exposures that appear while trading a foreign-currency CFD. Existing mappings, selected strategy inputs and quote selection remain unchanged. Unsupported contracts, unavailable metadata, capacity limits and a second failed read still stop trading with the diagnostic retained. Explicit Reconcile account also discovers existing supported exposures.

Orders cancellation remains available after a connected session faults, using the last confirmed exact broker order IDs. A cancellation review verifies the session/account/mapping and order identity again. New orders and amendments remain blocked until reconciliation. A pending uncertain amendment does not prevent a separately requested cancellation, but its original audit remains unresolved until exact evidence arrives. A second uncertain cancellation cannot be duplicated. Broker-side fills or termination can still reject cancellation, and IBKR orders belonging to another API client must still be handled through that client or TWS. Cancellation is never a global account cancel or an automatic retry.

Each distinct Saxo create/modify operation now sends a stable x-request-id equal to its journal idempotency key. This distinguishes intentional A-to-B-to-A price changes within Saxo's 15-second duplicate window. An exact HTTP 409 response to a create/modify operation is recorded as a rejection; it is not automatically resent. A saved Unknown HTTP 409 create/modify record can become Rejected during explicit reconciliation while preserving its original request, diagnostic and timestamp. Contradictory exact submit evidence still faults. DELETE conflicts and transport ambiguity remain Unknown. This follows Saxo duplicate-order protection. Reconcile the account, review Orders and Journal, and explicitly restart the strategy after the previous fault; installation does not start strategies or send broker commands.

The update leaves running Studio and broker connections untouched. Reopen Studio to load M96. Tests use synthetic broker responses only.

28Individual position actions and PnL (M97; Saxo updated below in M98)

Right-click an individual Positions row and choose Close position. The review shows its remaining quantity, the account net quantity, and the resulting target. For example, a row with 60 remaining in a net long position of 100 generates one Sell 60 Market order and leaves a target net quantity of 40. This is a quantity-based account reduction, not a tax-lot allocation instruction. Broker lot matching may consume a different displayed FIFO row. Rows opposing the net direction or exceeding the net quantity cannot be closed through this net-reduction action; review those hedged legs at the broker. Existing pending strategy orders remain active and can change exposure.

Before sending, Studio validates the originating session/account/mapping, row identity, remaining quantity, entry details, net quantity, cost and observed fill evidence. Broker position legs are reread; the account is reconciled under the execution gate. Changed evidence requires a fresh review. An unresolved command or an earlier working manual order in the closing direction blocks another row close. Unknown outcomes retain their journal and are never automatically resent. Normal broker permissions, market readiness and risk checks still apply.

Positions includes numeric-sortable PnL and Currency columns. Individual fill/leg estimates use (broker mark minus entry price) times signed remaining quantity times contract multiplier in instrument currency. They exclude fees and financing and are identified as estimates on hover. Net fallback rows retain broker-reported unrealized PnL (Saxo account currency; IBKR instrument currency). Missing entry/mark/currency data stays blank. Display rounds to the nearest integer, with commas grouping thousands; raw values are retained for sorting. The broker mark may lag a streaming quote. IBKR portfolio-value conventions are documented at https://www.ibkrguides.com/traderworkstation/portfolio-values.htm.

When a different product uses the same symbol, the instrument finder now proposes an unused strategy alias before Add, e.g. CDI_CfdOnStock. Futures aliases include expiry; further collisions add the contract ID. The broker symbol and existing mapping stay unchanged. Use the displayed alias in strategy code/parameters. Exact contract duplicates are still rejected; use the existing catalogue entry. Manually edited aliases must remain unique. The same suggestion works in the account catalogue and a strategy's staged instrument selection.

Reopen Studio to load this update. Validation is offline with synthetic broker responses; installation does not send live commands or close the running app.

29Exact Saxo closes, quote display and request recovery (M98)

Saxo individual rows now use the broker PositionId for Close position. The review shows the exact leg and the resulting account net quantity, including opposing hedged legs and zero-net pairs. Closing one leg can increase net exposure. Studio rereads that exact position and its account netting profile before sending a single related market close. End-of-day netting and eligible force-open real-time legs are supported. Locked positions, related orders or unsupported netting modes require review at the broker; Studio does not substitute a net order or cancel unrelated orders. IBKR fill rows retain the quantity-based net reduction described above. See [the M98 guide](POSITION_ID_AND_QUOTES_M98.md) for the supported conditions.

DOM and tick Chart use a separate display feed: IBKR quote callbacks are buffered independently of account reconciliation, and Saxo display quotes are polled about once per second subject to broker limits. Open market windows refresh up to four times per second. Price changes with the same broker timestamp are retained and displayed by local receipt time; source freshness still uses the broker observation time. Historical candles retain their separate refresh interval. Display quotes do not bypass execution reconciliation or trading risk checks.

Acknowledged IBKR cancellations can reconcile from exact completed-order evidence, including after reconnect. Ordinary order amendments clear the unused volatility-only DeltaNeutralOrderType field. The exact API 10363 non-ASCII-field validation response is treated as rejection; matching saved Unknown submissions/amendments can resolve during reconciliation while retaining the original diagnostic and request. Saxo replacement HTTP 404 remains Unknown unless an explicit OrderNotFound rejection or exact terminal order evidence establishes the outcome. Targeted order-activity lookup supplements the recent activity window. Absence alone never proves rejection or cancellation, and no request is resent automatically.

Reopen Studio to load M98, then use Reconcile account to refresh the reported requests. The update is verified with offline broker fixtures; no live orders were placed or changed during validation.

30Push quotes and position updates (M99)

Saxo DOM and tick Chart use pushed InfoPrice subscriptions. Studio requests 100 ms updates; Saxo's assigned interval and data permissions govern delivery. IBKR socket and Databento already use pushed data. Display refresh is capped at ten times per second. Historical bars and account/order reconciliation retain their own schedules. This replaces M98's separate Saxo display polling.

Positions PnL follows fresh quotes: bid for long rows, ask for short rows, in instrument currency with the contract multiplier. It excludes fees/financing and rounds to the nearest integer. Hover for the calculation basis; stale or invalid quotes fall back to broker-derived values. Quote updates retain row selection and never change position quantities.

Close position no longer requires adding a mapped account exposure to strategy inputs. Its review and submission still check the account, quantities, permissions, market data and unresolved requests. Running ladders retain terminal-history observation for their owned orders, so confirmed fills/cancellations release reservations and flat inventory can quote again. Faulted or stopped strategies require explicit restart. See [the M99 guide](STREAMING_POSITIONS_M99.md), including the limits of the supplied CDI evidence.

31Catalogue startup and request recovery (M100)

Studio saves a metadata index for faster catalogue reopening. Unchanged partitions reuse that index; tick files stay on disk until needed. Imports, metadata edits and unload/restore operations invalidate the relevant cached snapshot. Discovery runs in the background, and retention work no longer occupies the research operation slot. Results, Execution and other unrelated pages remain available. Dataset-dependent pages wait for discovery, and actual dataset use still verifies its files.

Prepared, Acknowledged and Unknown requests receive automatic broker confirmation. An acknowledgement is not proof of final cancellation or completion. Recovery preserves exact evidence, does not resend an order, and does not restart strategies. Foreground actions can preempt background reads without cancelling order commands. IBKR reads with no request IDs retain their old response boundary before starting a replacement read, preserving the connection and keeping late responses separate. Genuine broker conflicts and journal failures remain visible. See [the M100 guide](CATALOGUE_RECOVERY_M100.md).

32Instrument price normalization (M118)

Use Symbol.NormalizePrice(price, OrderSide.Buy) or Symbol.NormalizePrice(price, OrderSide.Sell, levelsAway: 1) for passive resting-limit prices. Buys round down; sells round up. Additional levels walk valid broker price bands and limit-specific increments. Replay uses its configured fixed tick. No tick argument is needed and no order is sent. Check the result is positive before submission. Missing metadata throws; invalid prices, sides and levels outside 0..10000 are rejected. The existing Symbol.NormalizeLimitPrice API remains available. For multiple instruments or a legacy string parameter named Symbol, use Symbols.GetSymbol(alias).NormalizePrice(...). See [normalization examples](PRICE_NORMALIZATION_M118.md).

33Compatible parameter rename (M119)

PassiveBidAskLadder now uses the C# property SymbolName, while [Parameter("Symbol", Key = "Symbol", ...)] preserves the existing saved key, label and schema. Direct Symbol.TickSize, Symbol.TimeZone and Symbol.NormalizePrice are enabled through protected override string? SymbolNameOverride => SymbolName;. The metadata view always resolves that selected instrument, including clock callbacks. The override does not change subscriptions or route orders. Existing strategies default to callback/sole-instrument inference. See [SymbolName compatibility](LADDER_SYMBOL_NAME_M119.md).

34Current bundled sample syntax (M120)

All bundled single-instrument samples now name their string parameter SymbolName, retain the saved key with Key = "Symbol", and bind the Symbol metadata view through SymbolNameOverride. This leaves Symbol.TickSize, Symbol.TimeZone, Symbol.ToLocalTime(...) and Symbol.NormalizePrice(...) available directly. MultiSymbolTrend retains per-callback instrument metadata and uses SymbolName and OtherSymbol for its two configured names; the blank starter operates on the callback instrument without a symbol parameter. Explicit AsiaTimeZone and AmericaTimeZone session rules in MultiTimeZoneSessionTrend remain configurable and unchanged. Existing saved custom source is not rewritten. See [current sample syntax](SAMPLE_API_SYNTAX_M120.md).

35Historical observations with Last(offset)

context.Bars.Open.Last(10) reads ten bar openings before the latest observed opening. context.Quotes.Bid.Last(8) reads eight quote observations before the latest quote. Last(0) is the newest observation, including the current event before its callback; Last(1) is the preceding observation. These are event offsets, not minutes or seconds. Each symbol and strategy activation has independent history, in both replay and live execution.

C# · QR strategy API
public override void OnBarOpened(ReplayContext context, BarOpen bar)
{
    if (context.Bars.Open.TryLast(10, out decimal openTenBarsAgo))
        Print($"Current open={bar.Open}, ten openings ago={openTenBarsAgo}");
}

public override void OnQuote(ReplayContext context, Quote quote)
{
    if (context.Quotes.Bid.Count > 8)
    {
        decimal bidEightQuotesAgo = context.Quotes.Bid.Last(8);
        Print($"Current bid={quote.Bid}, eight quotes ago={bidEightQuotesAgo}");
    }
}

Each series retains at most 4,096 values. Check Count or use TryLast(offset, out value) during startup. Last throws when the offset is negative or unavailable; TryLast returns false for unavailable observations and rejects negative offsets. No price is invented or forward-filled. History starts empty for each run, restart or code handover. It does not download earlier market data or include indicator-only warmup/history seeds.

context.Quotes exposes Bid, Ask, nullable BidSize/AskSize, and UTC Time. context.Bars exposes opening Open and Time. Completed OHLCV is available separately through context.Bars.Closed.Open/High/Low/Close/Volume/Time; these series share completed-bar indexes and never expose a forming bar's final values. Closed.Time is the time the completed bar became available. Native bar callbacks use native history; Robot-generated bar callbacks use that callback's timeframe history. Quote history remains shared across those callbacks.

The current event chooses the instrument. Use context.GetQuotes("GOOG") or context.GetBars("GOOG") to address another selected instrument explicitly; at startup an explicit symbol is needed when several instruments are selected. Raw bar.Open, quote.Bid and quote.Ask remain decimals, preserving existing arithmetic and indicator calls. Histories are synchronous live views and should be read within strategy callbacks.

Quote-history warm-up and runtime errors

TryLast(offset, out value) returns a bool; TryLast(offset) alone is not supported. Last(0) includes the current quote, so Last(1500) requires 1,501 delivered quotes for that instrument. Each activation starts fresh; chart history is not copied into these series. Return before requesting orders until both values are available:

C# · QR strategy API
if (!context.Quotes.Bid.TryLast(1500, out var referenceBid)
    || !context.Quotes.Ask.TryLast(1500, out var referenceAsk))
    return;
var reference = side == OrderSide.Buy ? referenceBid : referenceAsk;

Newly validated strategies report the exception type, message, callback, source filename and line for runtime errors. Revalidate older assemblies to include line information. Recognizable credentials are redacted; failed orders are never automatically retried.

36Explicit JPX auction research replay (M174)

context.SupportsJpxAuctionReplay is true only when all selected instruments use the explicit JpxAuction replay feed. Current Paper/Live connectors do not expose this capability. Ordinary quotes, candles and the first ordinary trade are never treated as an opening-auction print.

Override OnJpxAuctionSession(ReplayContext context, JpxAuctionSession session), OnJpxMarket(ReplayContext context, JpxMarket market) and OnReplayTimer(ReplayContext context, ReplayTimer timer). A session supplies dated raw-close/tick references and eligibility evidence. Atomic market messages preserve status, short-sale regulation, identified auction trades and up to ten depth levels per side. Trade matching uses the book before that message; its depth update is applied afterward.

Configure matching in OnStart with ConfigureJpxAuctionReplay(symbol, options). SubmitOpeningAuctionLimitOrder(side, symbol, quantity, price, expiresAt) creates a research auction-only limit with exclusive expiry. Unmatched opening orders cancel after the first identified auction. Reducing exits use explicit GTC limit/market orders; they cannot open an unrelated position. ScheduleReplayTimer(symbol, due, key) never advances beyond available observations. Each instrument's declared CoverageThrough prevents borrowing another instrument's longer data window.

The .jpx.csv input format is strict: timestamp,jpxKind,symbol,payload. Kinds are session and market; payloads are JSON for JpxAuctionSessionData and JpxMarketData. Timestamps require explicit UTC offsets. Source preparation must retain point-in-time references, status, depth and the actual auction marker. Invalid rows fail the import rather than silently breaking message continuity.

Queue execution is an estimate from displayed aggregate depth. Borrow availability, individual queue priority and counterfactual auction impact are not proved by this data. Unresolved positions remain open; no final liquidation is invented. QR Studio's general portfolio equity/drawdown uses its native mark accounting; it is not the original simulator's depth-liquidation valuation or unknown-mark metric. Compare realized fills and completed results separately.

37Single-symbol quote and trade inputs (M186)

A strategy that operates on one instrument can declare its historical data contract:

C# · QR strategy API
[SingleSymbolData(MarketDataKinds.Quote | MarketDataKinds.Trade,
                  MarketDataKinds.Quote, "SymbolName")]
public sealed class MyMarketMaker : Robot
{
    [Parameter(DefaultValue = "1605")]
    public string SymbolName { get; set; } = "1605";
    protected override string? SymbolNameOverride => SymbolName;
    public override void OnQuote(ReplayContext context, Quote quote) { /* BBO signals */ }
    public override void OnTrade(ReplayContext context, Trade trade) { /* trade observations */ }
}

The arguments are allowed market kinds, required market kinds, and the saved Text parameter key. This example requires quotes and allows optional trades for the same symbol. To require both, use MarketDataKinds.Quote | MarketDataKinds.Trade as the second argument too. Declare the attribute on each concrete strategy; it is not inherited. Strategies without the declaration retain their multi-symbol capabilities.

Backtest, Optimize and Research accept multiple non-overlapping files forming each stream, but reject mixed symbols, missing required kinds, unsupported kinds, inconsistent instrument conventions, and symbol-parameter/search values that differ from the data. The picker shows the rule; forms and the actual run enforce it. Required observations must also exist inside the replay window. Missing tick/quantity-step metadata can use a single consistent value declared by another selected stream; conflicting declarations are rejected, and missing values are not guessed.

Quote and Trade for the same symbol are merged in UTC event order and both reach strategy callbacks. Choose Trade as the passive execution source. With Market orders use fresh quotes enabled (M204+), market orders buy at executable Ask or sell at executable Bid, subject to side size, maximum quote age and session/status rules. Passive limits use later trade observations. Without the option, the selected execution feed applies to all order types. No missing-data or end-of-session fill is invented. Trade data does not prove queue priority or full depth.

Results Trade History now shows FIFO matched round trips (M191+). Fill time/price identify the entry, Close time/price identify the matched exit; partial closures can create several rows. Realized P&L follows each matched quantity, before commission. Raw fills remain recorded separately. Unclosed quantities do not receive an invented close. The DD field remains the recorded position-episode MAE, not a separate FIFO-lot loss. Times are stored in UTC and shown in the chosen display zone.

38Live quote and command timing (M208-M209)

Live execution delivers external-feed records to each strategy in source order while a broker command awaits its response. Strategy callbacks and final preflight remain serialized. Prepared commands retain FIFO ordering, conservative exposure reservations and send-time freshness, authorization and risk checks; only one broker command is dispatched at a time. An unknown outcome stops dispatch and is never resent. A temporary quote wait retires unsent targets rather than replaying them when quotes return.

Managed ladders can still withhold their user OnQuote callback until their own pending-order changes are confirmed. This intentional wait is separate from feed delivery. Lower receipt-to-callback latency does not remove network round-trip time or the wait behind earlier order commands. Compare the same command IDs when interpreting timing logs.

Quote.ReceivedUtc is local UTC at the completed feed read containing the record's last byte, before parsing or strategy dispatch. ProviderEventUtc, ProviderReceivedUtc, and ProviderSentUtc expose Databento's ts_event, ts_recv, and ts_out respectively (100 ns DateTimeOffset precision). Quote.Timestamp retains its existing replay-ordering semantics; Databento uses ts_recv.

StrategyOrderUpdate.Timing carries SendStartedUtc, SendCompletedUtc, and ResponseReceivedUtc for the exact command identified by CommandId. Send start is after validation and the transport send gate, immediately before the managed write; completion is the successful write return. Response receipt is the complete WebSocket read, before parsing and lifecycle delivery. These are host measurements, not NIC timestamps or exchange fill times. A response can arrive before the write task completes. Duplicate acknowledgements do not move the original timestamp; late exact responses can enrich an uncertain command without resending it.

All timing fields are nullable. Historical replay and uninstrumented connectors leave them unavailable. Reconciliation time is not substituted for response receipt. Submitting a managed limit target can queue, defer or suppress a command; use the order update for actual send measurements. Cross-provider/local comparisons require synchronized clocks; same-host intervals can still be affected by wall-clock adjustments.

C# · QR strategy API
public override void OnQuote(ReplayContext context, Quote quote)
{
    Print($"Receipt={quote.ReceivedUtc:O} Callback={DateTimeOffset.UtcNow:O}");
}
public override void OnOrderUpdate(ReplayContext context, StrategyOrderUpdate update)
{
    var timing = update.Timing;
    Print($"Command={update.CommandId} Send={timing?.SendStartedUtc:O} " +
          $"Written={timing?.SendCompletedUtc:O} Response={timing?.ResponseReceivedUtc:O}");
    base.OnOrderUpdate(context, update);
}