Skip to main content

Backtesting and Optimization

RunStrategyBacktest is the MCP entry point into NT8's actual Strategy Analyzer engine. It takes a compiled NinjaScript strategy class and runs it through the same Strategy.RunBacktest() path the NT8 UI uses, so the returned trades and metrics are bit-identical to what you would see in NT8 Strategy Analyzer for the same configuration.

Two modes are supported on the same tool, switched by the presence of an optimization argument:

  • Single backtest: one run with the supplied parameters. Returns the full SystemPerformance dump with every trade and every metric NT8 computes.
  • Parameter sweep: cartesian enumeration of caller-supplied parameter ranges. Returns a ranked list of per-iteration summaries, sorted best-first.

Quick Start

Single backtest

RunStrategyBacktest(
strategy_class: "SampleMACrossOver",
instrument: "MES 06-26",
bars_period: {period_type: "Minute", value: 5},
from: "2026-04-01T00:00:00Z",
to: "2026-04-30T00:00:00Z",
parameters: {Fast: 10, Slow: 25},
fill: {slippage_ticks: 0},
account: {initial_cash: 100000}
)
→ { job_id, poll_with: "GetMcpJob" }

Poll GetMcpJob(job_id) until status: "completed". The result block contains trades[], performance.{all,long,short,realtime}, equity_curve[], and actual_bars_range.

Parameter sweep

RunStrategyBacktest(
strategy_class: "SampleMACrossOver",
instrument: "MES 06-26",
bars_period: {period_type: "Minute", value: 5},
from: "2026-04-01T00:00:00Z",
to: "2026-04-30T00:00:00Z",
optimization: {
fitness: "MaxNetProfit",
parameters_sweep: [
{name: "Fast", min: 5, max: 15, step: 5},
{name: "Slow", min: 20, max: 30, step: 5}
]
}
)

Returns a results[] array (sorted best-first by NetProfit) plus a best callout at the top level.

Required Arguments

FieldTypeMeaning
strategy_classstringBare NinjaScript class name (e.g. MyEma20Reversal). Must be compiled into NinjaTrader.Custom.dll.
instrumentstringFull instrument name (e.g. MES 06-26). Resolved through NT8's InstrumentManager.
bars_periodobject{period_type, value, value2?, market_data_type?}. See Bars period table below.
from / tostringISO-8601 UTC. from must be earlier than to.

Optional Arguments

FieldTypeDefaultMeaning
parametersobject{}Map of {property_name: value} for every [NinjaScriptProperty] on the strategy. Unknown property names return bad_parameter with the full list of writable property names.
trading_hoursstringinstrument defaultNT8 TradingHours template name.
fillobjectsee belowFill/commission/slippage settings.
accountobject{initial_cash: 100000.0, denomination: "UsDollar"}Synthetic isolated backtest account. The Sim101 account is reused with ResetSimulationAccount(true) so each backtest starts with a clean ledger.
optimizationobjectabsentWhen present, switches the run from single backtest to parameter sweep. See Optimization section.
timeout_msintnoneMaximum elapsed time before the job aborts.

Bars period

bars_period.period_type accepts every value from NT8's BarsPeriodType enum (PascalCase): Minute, Tick, Second, Day, Volume, Range, Renko, HeikenAshi, Kagi, PointAndFigure, LineBreak, Volumetric, Delta, PriceOnVolume, Week, Month, Year.

Casing differs from GetBars

GetBars (the historical-bars query tool) uses lowercase period types (minute, day, week, month, year) and supports a smaller subset. RunStrategyBacktest.bars_period.period_type uses NT8's native PascalCase enum and supports the full set.

Fill settings

FieldTypeDefaultMeaning
typestringStandardNT8 OrderFillResolution enum name.
slippage_ticksint0Slippage applied per side.
commission_templatestringemptyNT8 commission template name from Tools → Commissions. Default is no commission to match NT8 Strategy Analyzer's out-of-the-box behavior.

Single-Backtest Result Shape

{
"engine": "nt8_strategy_analyzer",
"nt8_version": "8.1.6.3",
"fingerprint": "sha256:...",
"strategy": "SampleMACrossOver",
"strategy_full_name": "NinjaTrader.NinjaScript.Strategies.SampleMACrossOver",
"instrument": "MES 06-26",
"from": "2026-04-01T00:00:00Z",
"to": "2026-04-30T00:00:00Z",
"account": "Sim101",
"trades": [
{
"TradeNumber": 0,
"Quantity": 1,
"Commission": 0.0,
"ProfitCurrency": -21.25,
"ProfitPercent": -0.00061,
"ProfitPoints": -4.25,
"ProfitTicks": -17.0,
"MaeCurrency": 28.75,
"MfeCurrency": 3.75,
"EntryEfficiency": 0.12,
"ExitEfficiency": 0.23,
"TotalEfficiency": -0.65,
"entry": {"time": "2026-04-13T17:15:00Z", "price": 6922.25, "quantity": 1, "market_position": "Short", "order_action": "SellShort", "name": "Sell short"},
"exit": {"time": "2026-04-13T19:25:00Z", "price": 6926.50, "quantity": 1, "market_position": "Long", "order_action": "BuyToCover", "name": "Close position"}
}
],
"performance": {
"all": { "NetProfit": 800.00, "ProfitFactor": 1.1425, "SharpeRatio": 0.80, "TradesCount": 264, "..." },
"long": { "NetProfit": 1718.10, "..." },
"short":{ "NetProfit": -1366.90, "..." },
"realtime": { "..." }
},
"equity_curve": [{"t": "...", "equity": 0.0}, "..."],
"actual_bars_range": {"bar_count": 5979, "first_bar_time": "...", "last_bar_time": "..."},
"state": "Finalized",
"denomination": "UsDollar"
}

Every field of NT8's TradesPerformance (Currency/Pips/Points/Ticks/Percent sub-objects, Sharpe/Sortino/RSquared/Probability/ProfitFactor, MaxConsecutiveWinner/Loser, MaxTimeToRecover, LongestFlatPeriod, etc.) is dumped verbatim. If NT8 adds a metric in a future build, it shows up automatically.

Optimization (Parameter Sweep)

When optimization is present, MCP enumerates every combination of the supplied parameters_sweep ranges and runs one backtest per combination. Each combination's parameter values are pushed into the strategy via reflection on the property setters before the backtest runs, so the strategy code sees the swept values exactly as it would in NT8 Strategy Analyzer.

Optimization argument

optimization: {
optimizer: "DefaultOptimizer", // or GeneticOptimizer / StrategyGenerator
fitness: "MaxNetProfit", // or array for multi-objective
keep_best_results: 10,
instantiated_on_each_iteration: true,
parameters_sweep: [
{name: "Fast", min: 5, max: 15, step: 5},
{name: "Slow", min: 20, max: 30, step: 5}
],
extra: { /* forwarded verbatim to the optimizer instance */ }
}

Optimization result shape

{
"engine": "nt8_strategy_analyzer",
"mode": "optimization",
"strategy": "SampleMACrossOver",
"expected_iterations": 9,
"iterations_completed": 9,
"optimization_config": { "..." },
"optimizer": "NinjaTrader.NinjaScript.Optimizers.DefaultOptimizer",
"fitness": "NinjaTrader.NinjaScript.OptimizationFitnesses.MaxNetProfit",
"results": [
{
"iteration": 1,
"parameter_values": [{"name": "Fast", "value": 5}, {"name": "Slow", "value": 25}],
"performance_value": 1277.50,
"performance_summary": {"NetProfit": 1277.5, "ProfitFactor": 1.22, "SharpeRatio": 0.81, "TradesCount": 332, "..."}
},
"..."
],
"best": { "iteration": 1, "parameter_values": [...], "performance_value": 1277.50, "performance_summary": {...} }
}

Results are sorted best-first by performance_value. The best field is a top-level callout for easy access.

Known optimization caveats

These limitations apply to the current optimization implementation. Plan around them; they are documented because they are real.

  1. Only int / long / double / float / decimal parameter types are sweepable. Booleans, enums, and string parameters are accepted as fixed inputs via parameters but cannot be put in parameters_sweep.

  2. All sweeps are exhaustive cartesian enumeration. The optimizer field ("DefaultOptimizer" / "GeneticOptimizer" / "StrategyGenerator") is resolved to a real NT8 optimizer instance and recorded in the response, but the iteration loop is driven by MCP itself. Genetic-algorithm semantics (random initial population, crossover, mutation) and StrategyGenerator's evolutionary search are NOT honored. Every sweep enumerates every combination.

  3. Ranking is always by NetProfit. fitness (single or multi-objective array) is resolved to a real NT8 OptimizationFitness instance and reported in the result, but the actual sort key is currently performance_summary.NetProfit. Fitness-specific scoring (Sharpe, Sortino, ProfitFactor, drawdown minimization, etc.) is wired into the result metadata but not into ranking.

  4. keep_best_results and instantiated_on_each_iteration are forwarded to the optimizer instance but do not affect MCP's cartesian loop. All iterations are kept and returned.

  5. Sweep results omit full per-trade detail. Each iteration entry includes performance_summary (NetProfit, ProfitFactor, Sharpe, Sortino, trade counts) and the winning parameter_values, but not the full trades[] array. To inspect every trade for the best parameters, call RunStrategyBacktest again with parameters set to best.parameter_values and no optimization arg.

Failure Modes

Every error path returns a structured {success: false, error, detail, ...} envelope so AI clients can self-correct without a human in the loop.

CodeMeaningExtra fields
nt8_build_unsupportedThe user's NT8 build is missing one of the reflection targets the engine needs.missing_symbols[], nt8_version, fingerprint
strategy_class_not_foundstrategy_class does not match any compiled NinjaScript Strategy.compiled_strategies[]
instrument_unknowninstrument not resolvable through NT8 InstrumentManager.requested
bad_argInvalid ISO-8601 date, From ≥ To, invalid bars_period.period_type.detail includes the valid enum list
bad_parameterUnknown property in parameters or coercion failure.parameter_name, expected_type, valid_parameters[]
no_historical_barsBarsRequest returned an empty bars set for the range.instrument, from, to, bars_period
trading_hours_unknowntrading_hours template not found.requested
optimization_empty_sweepparameters_sweep is empty.(none)
optimization_parameter_unknownSweep entry references a non-existent or non-writable strategy property.requested, optimizable_parameters[]
optimization_parameter_unsupported_typeSweep parameter type is not int/long/double/float/decimal.(none)
optimizer_unknown / fitness_unknownName does not resolve against the probe's supported list.requested, available[]
bad_sweep_entryMissing min/max, or coercion failure on min/max/step.(none)
strategy_runtime_failure / optimization_runtime_failureAn iteration's OnBarUpdate threw.exception_type, stack_trace
backtest_abortedUser cancel or timeout_ms exceeded.(none)

Parity Verification

The single-backtest path was verified bit-identical to NT8 Strategy Analyzer on a known reference run:

  • Strategy: SampleMACrossOver (ships with NT8)
  • Instrument: MES 06-26 (Globex Micro E-mini S&P 500)
  • Bars: 5-minute, April 1–30, 2026
  • Inputs: Fast=10, Slow=25
  • Account: Sim101, $100,000 initial cash, no commission, 0 slippage
MetricNT8 SA UIRunStrategyBacktest
Net Profit$800.00$800.00
Profit Factor1.141.1425
Total Trades264264 (counter) / 263 (trade list, see note)
Sharpe0.800.80
Long / Short132 / 132132 / 132
Long NetProfit$1,718.10$1,718.10
Short NetProfit-$1,366.90-$1,366.90
One-trade off-by-one

performance.all.TradesCount (the counter NT8 increments live during the run) reports 264, but the trades[] array length is 263. This matches NT8 Strategy Analyzer UI behavior: the counter includes a partial position straddling the date boundary; the trade-list view only shows completed round-trips. Use performance.all.TradesCount for "how many fills happened" and trades.length for "how many round-trip P&L events."

Performance

ScenarioTypical elapsed
Single backtest, 1 month of 5-min bars, ~5000 bars0.5 – 1.5 s
9-iteration cartesian sweep (3x3), same date range0.4 – 1.0 s (bars loaded once, reused)
100+ iteration sweepscales linearly per backtest; bars are loaded once

The job manager auto-expires job records 30 minutes after completion. For very long sweeps, set timeout_ms to bound the run and poll GetMcpJob for progress_pct and progress_note.

GetMcpCapabilities // confirm backtest_engine.available
GetNinjaScriptHelp(topic: "strategy_template")
LookupNinjaScriptSymbol(name: "Strategy")
CompileNinjaScript(source: <C# source>, in_memory: true) // verify syntax
WriteNinjaScriptFile(name: "MyStrat", kind: "strategy", source: ..., overwrite: true)
RunStrategyBacktest(strategy_class: "MyStrat", instrument: ..., ..., optimization: {...})
GetMcpJob(job_id: ...) // poll until completed
// Inspect best.parameter_values, decide whether to deploy
RunStrategyBacktest(strategy_class: "MyStrat", parameters: best.parameter_values) // full single-backtest detail
DeployStrategy(strategy_class: "MyStrat", account: "Sim101", instrument: ..., parameters: best.parameter_values)

For copy-ready prompts, see MCP Trading with AI.