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
| Field | Type | Meaning |
|---|---|---|
strategy_class | string | Bare NinjaScript class name (e.g. MyEma20Reversal). Must be compiled into NinjaTrader.Custom.dll. |
instrument | string | Full instrument name (e.g. MES 06-26). Resolved through NT8's InstrumentManager. |
bars_period | object | {period_type, value, value2?, market_data_type?}. See Bars period table below. |
from / to | string | ISO-8601 UTC. from must be earlier than to. |
Optional Arguments
| Field | Type | Default | Meaning |
|---|---|---|---|
parameters | object | {} | 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_hours | string | instrument default | NT8 TradingHours template name. |
fill | object | see below | Fill/commission/slippage settings. |
account | object | {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. |
optimization | object | absent | When present, switches the run from single backtest to parameter sweep. See Optimization section. |
timeout_ms | int | none | Maximum 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.
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
| Field | Type | Default | Meaning |
|---|---|---|---|
type | string | Standard | NT8 OrderFillResolution enum name. |
slippage_ticks | int | 0 | Slippage applied per side. |
commission_template | string | empty | NT8 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.
These limitations apply to the current optimization implementation. Plan around them; they are documented because they are real.
-
Only int / long / double / float / decimal parameter types are sweepable. Booleans, enums, and string parameters are accepted as fixed inputs via
parametersbut cannot be put inparameters_sweep. -
All sweeps are exhaustive cartesian enumeration. The
optimizerfield ("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. -
Ranking is always by NetProfit.
fitness(single or multi-objective array) is resolved to a real NT8OptimizationFitnessinstance and reported in the result, but the actual sort key is currentlyperformance_summary.NetProfit. Fitness-specific scoring (Sharpe, Sortino, ProfitFactor, drawdown minimization, etc.) is wired into the result metadata but not into ranking. -
keep_best_resultsandinstantiated_on_each_iterationare forwarded to the optimizer instance but do not affect MCP's cartesian loop. All iterations are kept and returned. -
Sweep results omit full per-trade detail. Each iteration entry includes
performance_summary(NetProfit, ProfitFactor, Sharpe, Sortino, trade counts) and the winningparameter_values, but not the fulltrades[]array. To inspect every trade for the best parameters, callRunStrategyBacktestagain withparametersset tobest.parameter_valuesand nooptimizationarg.
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.
| Code | Meaning | Extra fields |
|---|---|---|
nt8_build_unsupported | The user's NT8 build is missing one of the reflection targets the engine needs. | missing_symbols[], nt8_version, fingerprint |
strategy_class_not_found | strategy_class does not match any compiled NinjaScript Strategy. | compiled_strategies[] |
instrument_unknown | instrument not resolvable through NT8 InstrumentManager. | requested |
bad_arg | Invalid ISO-8601 date, From ≥ To, invalid bars_period.period_type. | detail includes the valid enum list |
bad_parameter | Unknown property in parameters or coercion failure. | parameter_name, expected_type, valid_parameters[] |
no_historical_bars | BarsRequest returned an empty bars set for the range. | instrument, from, to, bars_period |
trading_hours_unknown | trading_hours template not found. | requested |
optimization_empty_sweep | parameters_sweep is empty. | (none) |
optimization_parameter_unknown | Sweep entry references a non-existent or non-writable strategy property. | requested, optimizable_parameters[] |
optimization_parameter_unsupported_type | Sweep parameter type is not int/long/double/float/decimal. | (none) |
optimizer_unknown / fitness_unknown | Name does not resolve against the probe's supported list. | requested, available[] |
bad_sweep_entry | Missing min/max, or coercion failure on min/max/step. | (none) |
strategy_runtime_failure / optimization_runtime_failure | An iteration's OnBarUpdate threw. | exception_type, stack_trace |
backtest_aborted | User 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
| Metric | NT8 SA UI | RunStrategyBacktest |
|---|---|---|
| Net Profit | $800.00 | $800.00 |
| Profit Factor | 1.14 | 1.1425 |
| Total Trades | 264 | 264 (counter) / 263 (trade list, see note) |
| Sharpe | 0.80 | 0.80 |
| Long / Short | 132 / 132 | 132 / 132 |
| Long NetProfit | $1,718.10 | $1,718.10 |
| Short NetProfit | -$1,366.90 | -$1,366.90 |
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
| Scenario | Typical elapsed |
|---|---|
| Single backtest, 1 month of 5-min bars, ~5000 bars | 0.5 – 1.5 s |
| 9-iteration cartesian sweep (3x3), same date range | 0.4 – 1.0 s (bars loaded once, reused) |
| 100+ iteration sweep | scales 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.
Recommended Workflow
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.