Drive NinjaTrader 8 from the command line — deploy a NinjaScript strategy, compile it inside NinjaTrader, read NinjaTrader's own compile/load errors, run Strategy Analyzer backtests, export historical data, and watch live accounts/feeds — all returning structured JSON (plus PDF reports). Built so an AI agent (or any script) can run the full edit → compile → fix → backtest loop, and keep a live account safe, with no manual clicking.
Everything runs locally and in-process: a small Python CLI talks to a NinjaScript AddOn running inside NinjaTrader through plain JSON files. No UI automation, no network.
NinjaTrader's offline compilers can't see the errors that only surface when NinjaTrader itself loads and compiles your code (custom-indicator references, properties set from the wrong State, and so on). NT8 Bridge compiles through NinjaTrader's own compiler and hands you the real Roslyn diagnostics — so a fix-and-retry loop runs against ground truth. Then it drives the Strategy Analyzer for you and reads the performance back as data, exports the replay data your backtests need, and gives you an out-of-band read of live account/feed state that keeps working when your strategy's own status feed stalls.
┌──────────────────────────────┐ files ┌──────────────────────────────┐
│ Python CLI (you / agent run │ ──────────▶ │ NT8BridgeServer.cs (AddOn) │
│ it from the shell) │ trigger/ │ runs INSIDE NinjaTrader 8 │
│ - offline precheck │ │ - compile via NT's compiler │
│ - deploy (.cs -> bin/Custom)│ ◀────────── │ - read Roslyn diagnostics │
│ - read JSON, build PDF │ result/ │ - run the Strategy Analyzer │
│ - export data, watch live │ │ - read live account/feed │
└──────────────────────────────┘ └──────────────────────────────┘
The AddOn polls …\Documents\NinjaTrader 8\NT8Bridge\trigger\ for request files and writes results to …\result\. Every command prints structured JSON to stdout.
- NinjaTrader 8 (developed/verified against 8.1.6.x), with historical data loaded
- Python 3.10+
- Windows (NinjaTrader is Windows-only)
git clone git@github.com:eman007/cli-nt-bridge.git
cd cli-nt-bridge
python -m venv .venv
.venv\Scripts\Activate.ps1 # PowerShell: activate the venv for this shell
python -m pip install -e ".[dev,report]" # numpy+pyarrow are base deps; report adds matplotlib for --pdfEvery command below assumes the venv is active — you'll see (.venv) in your prompt. If activation is inconvenient or PowerShell blocks the activate script, prefix every command with the venv's Python instead: .venv\Scripts\python -m nt8bridge …
Then load the AddOn into NinjaTrader once:
python -m nt8bridge deploy --strategy addon/NT8BridgeServer.cs --kind addon(copies it to…\NinjaTrader 8\bin\Custom\AddOns\)- In NinjaTrader, open the NinjaScript Editor and compile (F5). After this one compile the AddOn loads on every NinjaTrader start, and strategy compiles no longer need F5.
Verify the setup:
python -m nt8bridge doctorWith the AddOn loaded (above) and NinjaTrader running:
python -m nt8bridge doctor # preconditions OK?
python -m nt8bridge deploy --strategy MyStrategy.cs # copy your strategy into bin/Custom
python -m nt8bridge compile # compile INSIDE NT8 -> real errors as JSON
# ...fix any errors, redeploy, recompile until clean...
python -m nt8bridge backtest --config config/config.json --pdf report.pdfSet up a Strategy Analyzer tab once (strategy, instrument, dates, commission); backtest injects your config.json's params, fires Run, and reads the result back. That's the whole loop.
33 commands, grouped by what they do:
Setup & diagnostics
doctor check preconditions (NT8 dir, AddOn compiled)
precheck offline compile gate (optional; see note)
deploy atomic copy a .cs into bin/Custom (--kind strategy|indicator|addon)
compile compile INSIDE NinjaTrader, return its real Roslyn errors
reload compile AND load it (what F5 does) — DISRUPTIVE, see below
regions find/strip DUPLICATED NinjaScript generated regions
restart restart NinjaTrader
windows inventory NinjaTrader's top-level windows
probe dump the SA tab's writable property names (discover names for `configure`)
peek read the SA tab's latest result + param read-back, without a new Run
Read-only state — what is this NinjaTrader actually doing right now?
ntstatus is NT running the code on disk? (catches a stale DLL; exit 2 if so)
workspace charts + indicators + strategies on them, and their State
strategies Control Center strategies: enabled AND running? --enable/--disable to change
playback replay transport: clock, MOVING?, speed, .nrd coverage
screenshot capture a window (or the screen) as PNG
Backtesting
backtest run a configured Strategy Analyzer backtest (--pdf for a report)
batch run N param-sets -> one combined report (--pdf)
sweep backtest a matrix: instrument x bar-type x param-set
configure write instrument/dates/bar-type/fill/params onto the SA tab (headless setup)
Data export
histget download missing MarketReplay .nrd files for a date range
histdump offline .nrd -> L1/L2 UTC parquet (default, no NinjaTrader; --nt8 for legacy CSV)
Live ops & recovery (out-of-band; keeps working when a strategy's own feed stalls)
account read NinjaTrader live state (positions/orders/PnL/fills)
flatten force-close an account's positions + orders (kill switch)
watch auto-flatten NAKED (unprotected) positions (loop)
connections read connection status (live / inadvertently dropped)
reconnect reconnect a dropped connection (on-demand override)
connwatch auto-reconnect INADVERTENT drops only (loop)
feedhealth detect a FROZEN-but-connected feed via last-tick age
feedwatch loop-alert on a frozen feed (detect-only)
chartseries change a LIVE chart's data series (instrument + bar type/period)
Resilience
watchdog restart NinjaTrader if it hangs (stale heartbeat) or crashes
Triggers a compile inside NinjaTrader via the AddOn and returns NinjaTrader's own Roslyn errors — including the ones an offline compiler can't see:
{ "ok": false, "errors": [ { "file": "MyStrategy.cs", "line": 42, "code": "CS0103", "message": "…" } ] }--type is accepted but ignored — NinjaTrader's compiler always builds the whole tree, exactly as F5 does, so there is nothing to scope. compile on its own is enough.
compile validates; it does not load. The AddOn calls NinjaTrader's compiler with checkCompileOnly=true, so assemblyReloaded is always false — your code is proved correct, but the running NinjaTrader is still executing the previously loaded assembly. See below for what does load it.
Writing a .cs into bin\Custom while NinjaTrader is running makes NinjaTrader recompile and hot-reload the assembly by itself — no F5, nobody at the GUI. Measured on NinjaTrader 8.1.7.2: NinjaTrader.Custom.dll was rebuilt ~19 s after the file landed, and the new code was live. deploy writes a temp file and renames it over the destination; that rename path was separately verified to trigger the same reload (Custom.dll rebuilt within seconds on 8.1.6.x), so deploy is not an inert copy either.
That is useful — it is the missing "load" step, and it works headlessly — but it has two teeth worth knowing before you script it:
- It reloads the assembly underneath whatever is running. A code sync during a live session restarts strategies and indicators mid-flight. Deploy deliberately, not casually.
- The reload does not close already-open AddOn windows. A
NTWindowyou opened survives, still executing the old assembly's code, while the new assembly's statics start empty. So an AddOn that opens a window on load can stack a second instance on top of a live one, and neither statics norTypeidentity can detect it (both reset across the reload). If your AddOn auto-opens anything, interlock it on an artifact that crosses the reload boundary — a file, or a timestamped line in your own log — and test the interlock by deploying while the window is open.
A bars type is the exception: its instance is sticky on a chart, so new bars-type code needs an Editor F5 and a chart reload.
Drives an open, configured Strategy Analyzer. Set the SA tab up once (strategy, instrument, dates, commission); the bridge injects config.json's params onto the strategy, fires the Run button's command, waits for the run to finish, and reads the completed SystemPerformance:
{ "status": "ok", "strategy": "MyStrategy",
"metrics": { "totalTrades": 42, "netProfit": 1250.0, "profitFactor": 1.62, "maxDrawdown": -380.0 },
"trades": [ { "pnl": 72.5, "entryTime": "2024-05-19T09:30:03" } ] }python -m nt8bridge backtest --config config/config.json --timeout 300 --pdf report.pdfconfig.json (see config/config.json):
| field | notes |
|---|---|
typeName |
the NinjaScript class name |
instrument, barType, from, to, capital, commission, slippageTicks |
reference fields |
params |
strategy inputs injected before each run (matched by property name) |
batch runs many param-sets (same strategy) through the Strategy Analyzer and aggregates them into one report:
python -m nt8bridge batch --batch config/batch.json --timeout 600 --pdf batch_report.pdfsweep runs a full matrix — every combination of instrument × bar-type × param-set — interleaving configure + backtest per cell:
python -m nt8bridge sweep --config config/config.json --instruments "MNQ 09-26" --bars "Minute:1" --params-file sets.json --pdf sweep.pdfAutomate the SA tab so a sweep needs no manual clicking:
probe— dumps the SA tab /TabStrategyProperties/ strategy-template writable property names (NT8's members are partly obfuscated, so discover before you write).configure --config c.json— writes each key in the config onto whichever of (tab, tab-strategy-properties, template) has a matching writable property; type-aware (Instrument, DateTime, BarsPeriod, enums). Returns a per-keyapplied[]status (set/skip/error), eachsetcarryingnowReads— the value the live tab holds afterwards, re-read off a freshly resolved chain rather than off the object just written to.Strategyis always applied first, because writing it makes NT8 install a fresh strategy template and anything written to the old one is silently discarded (see the 1.5.1 note in the changelog).peek— reads the SA tab's latest completed result plus a read-back of the injected params, without firing a new Run — useful to capture a result a watcher missed, and to confirm param injection actually took.
histget --instrument 'MNQ 09-26' --from 20260706 --to 20260709— downloads the missing MarketReplay.nrdfiles for a date range (drives NT8's ownRequestMarketReplayper date; skips weekends and dates already present).histdump --instrument 'MNQ*' --out ./out/PARQUET— offline by default: decodes the replay.nrdbinary directly to per-day L1 + L2 UTC parquet (<out>/<SEASON>/<SYM>-<SEASON>_<L1|L2>/<date>.parquet), no NinjaTrader, ~3× faster than driving NT8, and verified byte-exact against NT8's ownDumpMarketDepth. Truncated.nrdare salvaged cleanly (every valid row, no garbage tail).--validatere-checks a decode against a fresh NT8 dump;--nt8uses the legacy CSV engine (needs NinjaTrader);--levels L1 L2and--forceas expected.
The CSV is always the source of truth; parquet is opt-in and never replaces it. A byte-equivalence gate catches any NT8-side format drift before a batch write.
Reads NinjaTrader's live account state directly from the in-process AddOn — an independent, out-of-band channel from any status/position feed your strategy publishes. Use --name to filter to one account; omit it for all accounts.
python -m nt8bridge account --name Sim101{ "status": "ok", "ts": "2026-01-02T15:04:05Z",
"accounts": [ { "name": "Sim101", "realizedPnl": 90.9, "unrealizedPnl": 0.0,
"positions": [],
"workingOrders": [],
"recentExecutions": [ { "instrument": "MNQ 06-26", "marketPosition": "Long", "quantity": 1,
"price": 28678.5, "time": "2026-01-02T14:04:00Z", "commission": 0.65,
"orderName": "E_13c1bd63" } ] } ] }It answers the questions a stalled status feed can't: is a position actually open right now, what are the live stop/target orders, and what were the real fills. Read-only — it never submits, cancels, or flattens.
performance --name Sim101 [--from D --to D] [--instrument 'MNQ 09-26'] [--pdf]— an account trade-performance report (round-trip trades + profit factor / win% / expectancy / drawdown), sourced from NinjaTrader's trade database. Uses public NT8 APIs.perfwindow --name Sim101 [--generate]— reads commissions and fees straight from an open Trade Performance window's view model. This matters for live/funded (e.g. prop) accounts, where NinjaTrader does not persist per-fill commission locally — only the Trade Performance window has the broker's cash history.--generateopens and builds the window hands-off.
Trust the
feesCalculatedflag.perfwindowreads a non-public view model. If it returnsfeesCalculated: false, treattotalFeesas unverified — either the fees haven't been generated in the window yet, or you're on an NT8 build that renamed the underlying member. WhenfeesCalculated: true, the totals are real. (A build mismatch is also surfaced as afeeReadNoteand aWARNING:line.)
Out-of-band recovery that works independently of whatever feed your strategy uses, so it still functions when that feed stalls.
Positions
flatten --name X— force-closes accountX's open position(s) and cancels its working orders; a kill switch for a position a strategy lost track of. The account name is required (it refuses to flatten everything); add--instrument "MNQ 06-26"to limit it to one instrument.watch --name X— a loop that flattens naked positions (an open position with no working protective stop; a lone profit-target limit is not protection). Scoped to the--nameallow-list, with a--graceperiod so it never kills a trade mid-bracket-placement.
Connections
connections— lists every configured connection with its live status and whether it dropped inadvertently.reconnect --name X— reconnects a connection on demand (an unconditional override).connwatch --name X— a loop that auto-reconnects only inadvertent drops (ConnectionLost/ error-disconnect). A connection you disconnect yourself is classified parked and never auto-reconnected. Allow-list (--name, repeatable) +--grace+ exponential backoff; it logs and gives up (surfacing the problem) if a connection won't come back.
Feeds
feedhealth --instrument 'MNQ 09-26'— reports each watched instrument's last-tick age, so a feed NinjaTrader still callsconnectedbut whose ticks have stopped (a "dark" feed) is detectable. A tick older than a threshold is a frozen feed.feedwatch --instrument 'MNQ 09-26'— a detect-only loop that alerts (durable jsonl + stdout) when a watched feed freezes past a grace period. Deliberately detect-only: thawing a frozen feed needs an operator to cycle NinjaTrader.
Reads, and changes, the enabled state of the strategies in the Control Center's Strategies tab.
python -m nt8bridge strategies # read: what is enabled, and is it running?
python -m nt8bridge strategies --strategy Breakout # assert one is at state=Realtime (exit 2 if not)
python -m nt8bridge strategies --enable 'Morning Breakout' # turn it back on
python -m nt8bridge strategies --disable X --dry-run # what would happen, clicking nothingReturns {status, gridResolved, strategies:[{name, type, enabled, state, account, instrument}], changed:[…], skipped:[{name, code, reason}], notes:[…]}. Skip codes — branch on these, not on the
prose — are alreadyEnabled, alreadyDisabled, notInGrid, exposure, bothLists, clickFailed.
Exit 0 did what was asked · 1 could not reach the grid, or the AddOn errored · 2 partially:
refused by the exposure guard, not in the grid, or enabled without state reaching Realtime before
the settle expired.
Why it exists. An explicit Connection.Disconnect() disables every running strategy, and NinjaTrader
restores none of them — not on reconnect, and not on an app restart either. So "are my strategies
running on that machine?" needed a remote desktop session, and "no" needed a human clicking checkboxes.
strategies vs workspace. workspace walks the chart windows, so it sees chart-attached
strategies. strategies reads the Control Center grid — the other population, and the one a
connection cycle turns off. Neither is a superset of the other.
⚠
enabledis not proof a strategy is running.enabledis the grid checkbox: it says the click landed. The evidence is the strategy's ownstatereachingRealtime, which is why every row carries both and why an acting call waits--settle-ms(default 3000) before re-reading. Where they disagree, believestate. Rows reported underunverifiedwere clicked but had not reachedRealtimeyet — re-read rather than clicking again; a strategy loading historical data is legitimately mid-transition, and a second click would toggle it back off.
⚠
--disabledoes not flatten. It stops the strategy managing what it holds; the position and any working orders remain. So--disablerefuses when the strategy's account has exposure on its instrument, and--forceis the deliberate override. The guard checks the account's position rather than the strategy's own, because the strategy-level view can read flat while the account still carries the fill.
Enabling something already running exits 0, not 2 — "make sure X is on" is the normal shape of an
unattended caller, and failing its no-op would make every retry look like a failure. The exception is
alreadyEnabled on a row whose state is not live: an enabled checkbox above a Terminated strategy
is the looks-healthy-but-isn't case, and the checkbox is the less trustworthy of the two readings.
Change a live chart's data series (instrument and/or bar type + period) from the CLI:
python -m nt8bridge chartseries --instrument 'MES 09-26' --bars-type Minute --bars-value 5Safety guard (fails closed). Because it mutates a live chart,
chartseriesrefuses to switch when the chart has an enabled/realtime strategy or an open position on the current chart-trader account — and if it cannot verify that state, it blocks rather than guessing. Pass--forceto override.
python -m nt8bridge watchdog --threshold 60 --interval 10The AddOn writes a heartbeat from NinjaTrader's main UI thread each second. The watchdog restarts NinjaTrader if that heartbeat goes stale (UI hang) or the process disappears (crash). If your NinjaTrader isn't at the default C:\Program Files\NinjaTrader 8\bin\NinjaTrader.exe, pass --exe.
--pdf (on backtest/batch/sweep/performance) renders a one-page report (needs the report extra / matplotlib): KPI tiles, a filled equity curve with the running peak, an underwater-drawdown panel, and a win/loss trade histogram.
python -m nt8bridge deploy --strategy MyStrategy.cs
python -m nt8bridge compile # fix errors until: {"ok": true, "errors": []}
python -m nt8bridge configure --config config/config.json # set instrument/dates/bar-type on the SA tab
python -m nt8bridge backtest --config config/config.json --pdf report.pdf
python -m nt8bridge peek # re-read the result + confirm params injectedprecheck is an optional fast offline gate. It needs an external NinjaScript offline-compiler PowerShell script — point at it with the NT8BRIDGE_COMPILER environment variable. Without it, precheck errors clearly (it will not pretend your code is clean), and its fixture tests skip. The in-NinjaTrader compile command does not need it and is the primary error-checking path.
- Compile: the AddOn calls
NinjaTrader.Code.Compiler.Compile(...)(a public static method inNinjaTrader.Core.dll) via reflection and reads the returned RoslynEmitResult.Diagnostics. No UI scraping. - Backtest: it locates the open Strategy Analyzer window via
NinjaTrader.Core.Globals.AllWindows, reads itsStrategyAnalyzerViewModel, injects params onto the configuredStrategyTemplate, and executes the RunRoutedCommand— exactly what the Run button does, so NinjaTrader runs it correctly on a background thread. Do not callStrategyRunner.RunStrategyAsyncdirectly: on the SA UI thread it deadlocks and crashes NinjaTrader. - Data export:
histdumpdecodes the.nrdbinary offline (a 44-slot header + a variable-length event stream) straight to L1/L2 parquet — verified byte-exact against NT8'sMarketReplay.DumpMarketDepth, which--nt8still drives for the legacy CSV path. - Live reads:
account,performance,feedhealthuse public NT8 APIs;perfwindowandchartseriesread non-public view-model / chart internals via reflection. - Strategy enablement:
strategiesdrives the Control Center's own grid, and three things have to be true at once or it silently returns nothing. NinjaTrader's real windows are not inApplication.Current.Windows(only custom AddOn windows are), so the staticControlCenter.Instanceis the only way in. The Control Center owns a UI thread separate fromGlobals.MainThreadDispatcher, and a WPF read from the wrong one throws "calling thread cannot access this object", which reflection re-wraps and a silentcatchturns into a convincingnull— so every read is marshalled onto((DispatcherObject)cc).Dispatcher, on a boundedInvokeso a saturated UI thread cannot wedge the poller. And the Strategies tab is virtualized while inactive, so the grid isn't in the visual tree until its tab is selected; the command cycles tabs to materialize it and restores yours afterwards. Then the part that is genuinely surprising: settingStrategiesGridEntry.IsEnabled = truedoes not start the strategy — the read-back saysTrueand nothing runs. Neither does executing the grid'sEnableStrategyCommand(it acts on the grid selection, which is unset, soCanExecuteis false) nor its per-row sibling, which does execute and does flip the bool while the strategy stays stopped. What starts it is the checkbox'sCheckedrouted event, which executing a command never raises — so the AddOn clicks the real checkbox viaButtonBase.OnClick(). In-process; no synthetic mouse or keystrokes.
These reach into NinjaTrader's non-public internals, so they may need adjusting across NinjaTrader versions. The bridge is built to fail loud where it can (a moved type fails at compile/deploy time), and the two commands that read non-public members defensively — perfwindow (via its feesCalculated flag) and chartseries (fail-closed) — tell you when they can't trust what they read rather than returning a wrong answer.
.venv/Scripts/python -m pytestThe live offline-compile fixture tests skip automatically when no offline compiler is configured.
Developed and verified against NinjaTrader 8.1.6.x on Windows. NT8 Bridge uses some of NinjaTrader's internal APIs; a NinjaTrader update could change them. On a different build, perfwindow may report feesCalculated: false (trust that flag) and other internal reads may error out loudly rather than mislead. This project is not affiliated with or endorsed by NinjaTrader. Use it on your own NinjaTrader installation at your own risk — backtests are not predictions, and automated order handling carries real financial risk.
MIT — free to use, copy, modify, and distribute; provided "as is" with no warranty and no liability.