# QuantResearcher Studio Strategy API

> Current M219.1: Lightspeed is the available execution connector; Saxo/IBKR are hidden and Kabu.com is planned. Older milestone sections below are historical. Current workflow details are in the User Manual.

M219.1 · 6 November 2026 · Strategy API protocol version 1

Write C# strategies against `QuantResearch.Engine` and `QuantResearch.Strategies`. This reference describes the current authoring API and distinguishes historical replay from supported live execution. The protocol identifier `StrategyApi.Version == "1"` is not a product release number or a promise of compatibility with another platform's DLLs. Revalidate source against the installed release after updating.

## Run 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.

| Property | True when |
| --- | --- |
| `context.IsPaper` | Broker simulation/demo execution, including Lightspeed Paper |
| `context.IsLive` | Execution on a live broker account |
| `context.IsBacktest` | Standalone historical backtest |
| `context.IsOptimize` | Standalone optimization trial |
| `context.IsResearch` | Any 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.

```csharp
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.


## Quick 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.

```csharp
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.

## Validated 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.

## Parameters

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`.

```csharp
[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.

## Lifecycle & 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.

| Callback | Arguments | Meaning |
| --- | --- | --- |
| `OnStart` | `ReplayContext context` | Initialization after parameter binding. No artificial current market event exists. |
| `OnBarOpened` | `ReplayContext context, BarOpen bar` | The opening price is available. |
| `OnBarClosed` | `ReplayContext context, BarClose bar` | Final completed OHLCV is available. |
| `OnTrade` | `ReplayContext context, Trade trade` | An observed trade and optional volume. |
| `OnQuote` | `ReplayContext context, Quote quote` | An observed bid/ask and optional side sizes. |
| `OnInformation` | `ReplayContext context, InformationEvent information` | Available news or financial information. |
| `OnFxRate` | `ReplayContext context, FxRate rate` | An explicit conversion observation. |
| `OnFuturesSettlement` | `ReplayContext context, FuturesSettlement settlement` | Explicit futures settlement. |
| `OnStockSplit` | `ReplayContext context, StockSplit split` | Explicit split observation. |
| `OnDividendEntitlement` | `ReplayContext context, DividendEntitlement entitlement` | Dividend entitlement observation. |
| `OnDividendPayment` | `ReplayContext context, DividendPayment payment` | Dividend payment observation. |
| `OnStop` | `ReplayContext context`, or protected parameterless override | Read-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`.

```csharp
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.

## Indicators

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`.

```csharp
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 type | Value members | Readiness and convention |
| --- | --- | --- |
| `SimpleMovingAverage` | `Close`, `Period`, `Count`, `IsReady` | Arithmetic mean of the rolling window; ready after Period values. |
| `ExponentialMovingAverage` | `Close`, `Period`, `Count`, `IsReady` | Seeded by the first Period values' arithmetic mean; then alpha = `2 / (Period + 1)`. |
| `RelativeStrengthIndex` | `Close`, `Period`, `Count`, `IsReady` | Wilder smoothing of gains/losses, alpha = `1 / Period`; Count measures changes. RSI 14 requires 15 prices. |
| `BollingerBands` | `Close`, `Middle`, `Upper`, `Lower`, `Deviations`, `Period`, `Count`, `IsReady` | Middle/Close is SMA; width is positive Deviations × population standard deviation; ready after Period values. |
| `MovingAverageConvergenceDivergence` | `Close`, `Line`, `Signal`, `Histogram`, `FastPeriod`, `SlowPeriod`, `SignalPeriod`, `Count`, `IsReady` | Line/Close is fast EMA minus slow EMA; Signal is an EMA of the ready line; Histogram is Line minus Signal. |
| `AverageTrueRange` | `Close`, `Period`, `Count`, `IsReady` | Wilder-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:

```csharp
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.

## Market & limit orders

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

```csharp
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`.

```csharp
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`:

```csharp
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:

```csharp
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.

## Positions & 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.

| Type | Main members and commands |
| --- | --- |
| `TradingPosition` | Nullable `Id`, `EntryPrice`, `Currency`, labels/comments; `Symbol`, signed `Quantity`, `Side`, `LabelState`, protection distances/prices; `long? Close()`. |
| `TradingPendingOrder` | `Id`, `Symbol`, `Side`, `Type`, total quantity, nullable filled/remaining quantities, prices, labels/comments, duration/expiry; `bool Cancel()`. |
| Both | `ModifyStopLossPips(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.

## Pips & 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.

## Data & execution timing

| Event | Information available at the callback |
| --- | --- |
| `BarOpen` | Open only; no future high, low, close or final volume. |
| `BarClose` | Completed Open, High, Low, Close, Volume. It is not an execution-price event. |
| `Trade` | Price, Volume and HasVolume; missing volume is distinct from zero. |
| `Quote` | Bid, Ask, Midpoint and optional BidSize/AskSize. |
| `InformationEvent` | Source, identity, metrics and explicit publication/fetch/availability timing. |
| `FxRate` | Direct 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.

## Live 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/source | Current scope |
| --- | --- |
| Saxo OpenAPI | Stocks, FX Spot, supported CFDs and dated futures; exact contracts/account permissions required. |
| Interactive Brokers | Stocks through Client Portal Web API or Gateway/TWS socket. Socket mode uses a separately installed official C# API. |
| Lightspeed Connect | Supported stock execution; separate entitled market data required. |
| Databento live | Separately 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.

## Strategy examples

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.

| Source | Purpose / inputs |
| --- | --- |
| `DemoRoundTrip.cs` | Basic entry/exit lifecycle demonstration. |
| `BuyAndHold.cs` | Startup entry; position can remain at history end. |
| `EventObserver.cs` | Read-only event observation. |
| `MovingAverageCrossover.cs` | Stateful SMA trend using completed bars. |
| `MeanReversion.cs` | Completed-bar prior-window z-score. |
| `ChannelBreakout.cs` | Prior completed-bar high/low channel. |
| `TradeMomentum.cs` | Observed trade momentum. |
| `QuoteMomentum.cs` | Quote-midpoint momentum with spread/size conditions. |
| `MultiSymbolTrend.cs` | Independent completed-bar trends across two symbols. |
| `MultiTimeZoneSessionTrend.cs` | Trade observations within named local session windows. |
| `QuoteTradeConfirmation.cs` | Combined quote/trade observations and one selected execution feed. |
| `PassiveBidAskLadder.cs` | Bounded quote-based limit ladder with pending intent handling. |
| `NewsFilteredMovingAverage.cs` | Completed-bar trend gated by available news information. |
| `JQuantsStatementForecast.cs` | Explicit statement/price rule; not trained machine learning. |
| `JQuantsFundamentalForecast.cs` | Explicit fundamentals/price rule; not trained machine learning. |
| `TypedCallbackObserver.cs` | Read-only typed-callback starter. |
| `MyStrategy.cs` | Blank 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.

## Migrate 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.

## Troubleshooting

| Symptom | Check |
| --- | --- |
| Indicator never becomes ready | Supply 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 input | Give separate logical series distinct call sites or explicit keys; one series accepts one input per event. |
| Indicator unavailable during startup | Call it in Quote/Trade/Bar callbacks, not OnStart or information/lifecycle callbacks. |
| No orders despite a signal | Check selected execution instruments, parameters, target/pending state, account/contract settings and history. |
| Order never fills | Check execution feed, timing, later observations, liquidity, limit/stop, expiry and sessions. Completed bars are not fill evidence. |
| Unexpected profit or loss | Compare actual Trade History to decision logs, current-quote setting, quantities, cash, commission, multiplier and FX. |
| Protection rejected | Provide valid PipSize for replay and check host capability. Current live attached pip protections are unsupported. |
| Close/modify/cancel fails | Read current handles and observed identities, inspect pending orders/ownership and reconcile live state. |
| Sharpe N/A | Hover over Results' Sharpe card. Daily-return count, nonpositive equity, zero variance or absent statistics can make it undefined. |
| Older result has no fill history | Run again with capture enabled; saved outcomes are not backfilled. |
| SDK/hash/validation mismatch | Use one complete matching package and revalidate source. Do not bypass integrity checks. |
| Notification-limit failure | Remove 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.

## Downloads & version

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.

- [Download this API reference](/downloads/QuantResearcher-Studio-Strategy-API.md)
- [Download the Japanese API reference](/downloads/QuantResearcher-Studio-Strategy-API-ja.md)
- [Download the 17 strategy and template sources](/downloads/QuantResearcher-Studio-Strategy-Examples.zip)

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.


## M73: 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.

```csharp
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.


## Configurable 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.


## Automatic 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).


## Optimization 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).

## Invalid 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).

## Long 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).

## Reusable 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).

## Managed 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)`:

```csharp
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.

## Automatic 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:

```csharp
[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:

```csharp
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.

## CFD 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](https://interactivebrokers.github.io/tws-api/third_party.html).

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](https://www.developer.saxo/openapi/referencedocs/port/v1/closedpositions/post__port__subscriptions/schema-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.

## Selective 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](https://www.interactivebrokers.com/docs/tws-api/doc/orders/modifying-orders).

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](https://www.interactivebrokers.com/docs/tws-api/doc/orders/minimum-price-increment/introduction).

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](https://www.developer.saxo/openapi/referencedocs/port/v1/positions/get__port).

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](https://www.developer.saxo/openapi/referencedocs/cs/v1/audit-orderactivities/get__cs_audit_orderactivities).

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.

## Fill 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](https://www.developer.saxo/openapi/learn/rate-limiting). 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.

## Individual 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.


## Exact 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.


## Push 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.


## Catalogue 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).

## Instrument 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).

## Compatible 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).


## Current 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).

## Historical 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.

```csharp
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:

```csharp
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.

## Explicit 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.


## Single-symbol quote and trade inputs (M186)

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

```csharp
[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.

## Live 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.

```csharp
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);
}
```
