Backtest & live

Strategies

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.

You do not orchestrate the run yourself. The concrete backtest engine (the type that wires a strategy to in-memory feeds, a simulated broker and the analytics) lives in the host engine assembly, not in the contracts-only SDK package. A plugin that references the SDK therefore does not start backtests in code — it ships a discoverable 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 cadenceOnBar 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).

Run state. A run moves through 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}");
}