Backtest & live
Backtest & live
The same class backtests in the Strategy Analyzer and runs live from the Control Center, with deterministic backtest results.
Backtest & live
Your plugin contains no run plumbing. The Strategy Analyzer backtests a strategy over historical
bars, and the Control Center Strategies tab runs the same class live on a broker account. The
strategy only talks to the IStrategyContext seam, so both modes use the same code; only what
stands behind the context differs.
Strategy, and the Strategy Analyzer runs it. The SDK contains the
PerformanceReport family that describes a run's figures.
The run configuration
The host describes every run, backtest or live, from the Analyzer or Control Center dialog: the instrument, the account its orders route to, a run name, the parameter values for this run, the trading-hours template, and the instrument metadata from the host's instrument catalog.
The strategy reads that metadata as TickSize, PointValue,
QuantityStep and MinQuantity, and UseAtm uses the first two. How the
venue counts the instrument sets WireUnitsPerQuantity. The parameter values are what an
optimizer varies: each pass is a fresh run with different values, applied after OnInitialize
declares the parameters and before OnStart.
A live run carries two names. StrategyName is the strategy's own name: its
[StrategyMetadata] display name, or its type name made readable. The strategy reads it, and
may look its own setup up by it, so it stays the same however the trader names the run. The run's display name is the name the trader gave the saved
strategy in the Strategies tab. The host stamps it on every order the run places as
OriginId = "strategy:" + display name (see
OrderId & OriginId provenance), unless the order
already carries an origin. The Trade Journal shows that name as the trade's source. A run without a display
name stamps its StrategyName.
Reading the result
When a backtest finishes, the host reports the final account and position, the fills and orders, the
account equity after each replayed bar, whether the run was cancelled part-way (the figures then cover the
bars replayed until then), how many decision asks went unanswered (see
Decision models) and how the run ended (a
StrategyState). The figures themselves are a PerformanceReport.
C#// p: the PerformanceReport of a finished backtest
Console.WriteLine($"Net {p.Pnl.NetProfit} PF {p.Pnl.ProfitFactor} trades {p.Trades.TotalTrades}");
Console.WriteLine($"Win% {p.Trades.WinRatePct:F1} Sharpe {p.RiskAdjusted.SharpeRatio}");
Console.WriteLine($"MaxDD {p.Drawdown.MaxDrawdown} ({p.Drawdown.MaxDrawdownPct:F1}%)");
PerformanceReport groups its figures into blocks; the trade count, for example, is
Trades.TotalTrades. Ratios that can be undefined are double? and null in that case
(ProfitFactor with no losing trades, the risk-adjusted ratios on a very short run). A run with
no trades reports PerformanceReport.Empty(startingCash); check HasTrades first.
| Block | Type | Key members |
|---|---|---|
Pnl |
PnLSummary |
NetProfit, GrossProfit, GrossLoss, TotalCommission, ProfitFactor?, ReturnPct, StartingCash, EndingEquity
|
Trades |
TradeStatistics |
TotalTrades, WinningTrades, WinRatePct, AverageTrade, AverageWinner, AverageLoser, WinLossRatio?, MaxConsecutiveWinners, MaxConsecutiveLosers, Sqn? |
Drawdown |
DrawdownMetrics |
MaxDrawdown, MaxDrawdownPct, AverageDrawdown, LongestDrawdownDuration, MaxRunup, UlcerIndex, RecoveryFactor? |
RiskAdjusted |
RiskAdjustedMetrics |
SharpeRatio?, SortinoRatio?, CalmarRatio?, AnnualReturnPct, DailyReturnStdDevPct? |
Excursion |
ExcursionMetrics |
AverageMae, AverageMfe, WorstMae, BestMfe, AverageEndTradeDrawdown, AverageTradeEfficiencyPct? |
Exposure |
ExposureMetrics |
TimeInMarketPct, TotalContractsTraded, AveragePositionSize
|
TradeList |
IReadOnlyList<Trade> |
Per round trip: Direction, EntryPrice, ExitPrice, NetPnL, IsWinner, MaeCurrency, MfeCurrency, Duration, ExitTag, IsOpen
|
EquityCurve |
IReadOnlyList<EquityPoint> |
TimeUtc, Equity per sample |
Every member of each type is listed on the Strategies reference.
Replay resolution & tie-break
A backtest is single-threaded and synchronous: the same code on the same bars gives byte-identical results,
which makes regression tests straightforward. How finely the engine replays each bar is a setting of the
run, chosen in the Strategy Analyzer, not something the strategy sets. It decides how your orders
fill, so read a result together with the resolution it came from. The resolution leaves the callback
cadence unchanged (OnBar fires as it would live); it sets how many prices the fill engine sees
inside each bar, and so how many stop and target fills it catches.
| Resolution | Prices per bar | Use |
|---|---|---|
BarOpenOnly |
1 (open) | Fastest; long-horizon trend tests. |
BarClose |
1 (close) | Smoke tests, quick sweeps. |
BarOHLC |
4 (open, extremes, close) | Default; catches most intra-bar fills. |
BarMagnifier |
4 × N | Good accuracy at moderate cost. |
EveryTickGenerated |
About N synthetic | Tick-like replay without tick data. |
EveryTickReal |
Every recorded tick | Most accurate and slowest; needs tick data. With a recorded L2 or MBO tape it also streams the order book into the strategy (recorded order book). |
When a stop and a target both lie inside one bar's range, the run's intra-bar tie-break decides which fills
first. It has the largest effect on accuracy of any setting: UseBarDirection (the default
heuristic), LowBeforeHigh (conservative for longs) or HighBeforeLow (conservative
for shorts).
StrategyState:
Created, Initialized, Running, then Stopped, or
Faulted if a callback throws. A backtest reports where it ended; a live run exposes the same
value as State.
Fills and the stated position
On a live account a venue can report a fill before it states the position that includes it, so
Position can lag a fill for a moment. CatchUp tells you which case you are in.
CatchUp.Kind is CaughtUp when the position accounts for every fill heard,
NotCaughtUp while a fill is still waiting for its position statement, and
SettleTimePassed when the venue has not stated the position within its settle time.
ImpliedQuantity is the signed quantity the fills imply; it is the run's own count, not the
venue's position. The managed helpers already wait for this, so read it only in code that sizes raw
orders from Position.
C#protected override void OnBar()
{
// Position does not show the last fill yet: do not size an order from it.
if (CatchUp.Kind == RunCatchUpKind.NotCaughtUp) return;
// ...
}
ADVANCED Full example: breakout + ATM + logging
This example pulls the chapter together: an N-bar breakout entry whose exits belong entirely to an ATM plan.
It has optimizable lookback and quantity parameters, a one-target bracket with auto-breakeven and one
trailing step, an edge-triggered entry guarded by IsFlat and
HasPendingManagedOrder, and logging from OnBar and OnPositionUpdate.
Once ATM owns the brackets, little trading code remains.
BreakoutAtmStrategy.csusing System;
using SabrTrader.Pipeline.Indicators;
using SabrTrader.Pipeline.Strategies;
using SabrTrader.Pipeline.Strategies.Catalog;
using SabrTrader.Pipeline.Venues.Trading;
using SabrTrader.Pipeline.Venues.Trading.Atm;
[StrategyMetadata(DisplayName = "Breakout + ATM", Description = "N-bar breakout, managed by ATM brackets.")]
public sealed class BreakoutAtmStrategy : Strategy
{
private StrategyParameter<int> _lookback = null!, _qty = null!;
protected override void OnInitialize()
{
_lookback = IntParameter("Lookback", 20, 2, 200, 1);
_qty = IntParameter("Quantity", 2, 1, 50, 1);
Calculate = CalculationMode.OnBarClose;
}
protected override void OnStart()
{
UseAtm(new AtmStrategy("BO", AtmParameterUnit.Ticks,
new[] { new AtmBracket(1.0m, StopLoss: 12m, ProfitTarget: 36m) },
new AtmStopStrategy(AtmStopOrderType.StopMarket, 0m,
AutoBreakeven: new AtmAutoBreakeven(18m, 4m),
AutoTrail: new AtmAutoTrail(new[] { new AtmAutoTrailStep(24m, 10m, 2m) }))));
}
protected override void OnBar()
{
int lb = _lookback.Value;
if (BarCount < lb + 1 || !IsFlat || HasPendingManagedOrder) return;
double hi = double.MinValue;
for (int i = 1; i <= lb; i++) hi = Math.Max(hi, GetBar(i).High);
if (CurrentBar.Close > hi)
{
EnterLong(_qty.Value); // ATM brackets the fill
Log($"Breakout long {_qty.Value} @ {CurrentBar.Close}");
}
}
protected override void OnPositionUpdate(Position pos)
=> Log($"Qty={pos.Quantity} entry={pos.AverageEntryPrice} uPnL={pos.UnrealizedPnL}");
}