Backtest & live
Backtest & live
The same class backtests in the Strategy Analyzer, launches live from the Control Center, and drives the unit-test harness — deterministically.
Backtest & live
You do not write run plumbing inside your plugin. The Strategy Analyzer backtests your strategy
over historical bars, and the Control Center Strategies tab launches the same class live against a
real broker. Because the strategy only ever talks to the IStrategyContext seam, neither mode
requires a code change — the difference is entirely in what feeds the context.
Strategy and the Strategy
Analyzer runs it. What the SDK does expose is the run configuration and the
result: StrategyRunConfig, BacktestResult and the whole
PerformanceReport cluster are public contract types you can read, assert on, or render.
The run configuration
Every run — backtest or live — is described by a StrategyRunConfig. The Analyzer
and Control Center build one from their dialog; the same record is what a host-side test fixture passes to
the engine. It is the canonical list of everything a run needs.
StrategyRunConfig.csnew StrategyRunConfig(
instrument: "ESZ24",
account: accountId, // TradeStrike.Pipeline.Trading.AccountId
strategyName: "Breakout + ATM",
parameterOverrides: new Dictionary<string, object> { ["Lookback"] = 15 },
tickSize: 0.25, pointValue: 50,
instrumentKey: null, instrumentVenue: null, // cross-venue routing (optional)
tradingHoursTemplateName: "CME US Index Futures RTH");
tickSize and pointValue are instrument metadata, not tunable
parameters — the host fills them from its instrument catalog and the strategy reads them via
TickSize / PointValue (and they feed UseAtm). Each must be positive
and finite when supplied, or null when unknown. parameterOverrides is the seam an optimizer
drives: each pass is a fresh run with a different override map.
Reading the result
A BacktestResult carries the final account and position, the raw Fills and
Orders, an EquityCurve (per-bar account equity), and a full
PerformanceReport. Convenience members: NetProfit (final equity minus starting
cash), RealizedPnL, StartingCash, FinalAccount,
FinalPosition, FillCount, BarsReplayed, WasCancelled
(a partial run that was cancelled mid-replay still carries fully-analysed partial results),
FaultException, and FinalState (a StrategyState).
C#// `result` is a BacktestResult handed back by the Analyzer / a host fixture.
PerformanceReport p = result.Performance;
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 sub-records rather than a flat bag — note the
trade count is Trades.TotalTrades, there is no top-level one. Many ratios are
double? and null when undefined (e.g. ProfitFactor with no losing trades, the
risk-adjusted ratios on a too-short run). For a run with no trades it is
PerformanceReport.Empty(startingCash); check HasTrades first.
| Block | Type | Key members |
|---|---|---|
Pnl |
PnLSummary |
NetProfit, GrossProfit, GrossLoss, ProfitFactor?, ReturnPct, EndingEquity
|
Trades |
TradeStatistics |
TotalTrades, WinningTrades, WinRatePct, AverageTrade, WinLossRatio?, MaxConsecutiveWinners/Losers, 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, IsOpen
|
EquityCurve |
IReadOnlyList<EquityPoint> |
TimeUtc, Equity per sample |
The full member list of each record is on the Strategies reference.
Replay resolution & tie-break
A backtest is single-threaded and synchronous — same code + same bars yield byte-identical results,
which makes regression tests trivial. The BacktestResolution enum selects how finely the
engine replays each primary bar between OnBar calls. It never changes the callback
cadence — OnBar still fires once per primary bar at close, exactly as live —
only how many quotes the fill engine sees, which decides how many take-profit / stop-loss fills it catches
within a bar.
| Resolution | Quotes / bar | Use |
|---|---|---|
BarOpenOnly |
1 (open) | Fastest; long-horizon trend tests. |
BarClose |
1 (close) | Smoke tests, quick sweeps. |
BarOHLC |
4 (O→extremes→C) | Default; catches most intrabar fills. |
BarMagnifier |
4 × N | Sweet spot — strong accuracy, moderate cost. |
EveryTickGenerated |
~N synthetic | No tick data required. |
EveryTickReal |
Every recorded tick | Gold standard; slowest; needs tick data. |
When a stop and a target both sit inside one bar's range, IntrabarTieBreak decides which
fills first — the single most impactful accuracy knob: UseBarDirection (the safe
default heuristic), LowBeforeHigh (conservative for longs), or HighBeforeLow
(conservative for shorts).
StrategyState:
Created → Initialized → Running → Stopped, or Faulted if a
callback throws. BacktestResult.FinalState reports where it ended; live runs expose the same
State on the strategy.
ADVANCED Full example: breakout + ATM + logging
This last example pulls the chapter together: an N-bar breakout entry whose exits are handled entirely by
an ATM plan. It has optimizable lookback and quantity parameters, a one-target bracket with auto-breakeven
and a single trailing step, an edge-triggered entry guarded by IsFlat and
HasPendingManagedOrder, and logging from both OnBar and
OnPositionUpdate. Notice how little trading code remains once ATM owns the brackets.
BreakoutAtmStrategy.csusing TradeStrike.Pipeline.Indicators;
using TradeStrike.Pipeline.Strategies;
using TradeStrike.Pipeline.Strategies.Catalog;
using TradeStrike.Pipeline.Trading;
using TradeStrike.Pipeline.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 auto-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}");
}