StopWarden is not open yet. Nothing here can be downloaded or bought today; these pages describe how StopWarden is planned to work when it opens. Get one email when it opens.
Safety Scan (free)
The Safety Scan reads NinjaTrader's own log files on this PC (<Documents>\NinjaTrader 8\log\log.*.txt) for the last 30 days and reports what they show. It is read-only: it never places, changes or cancels an order, never writes into the NinjaTrader folder, and sends nothing anywhere. It needs no licence key and no settings file. When there is a settings file, the scan also counts orders named with your watchdog.extra_stop_name_prefixes as stops, as the watchdog does.
It reports what the log shows ("no stop order seen in NinjaTrader's log"), not what happened at the broker. Everything it cannot see is listed under Unknowns in every report.
Command
autopilot scan [--days 30] [--html <path>] [--open] [--json] [--text] [--anonymize]
[--log-dir <folder>] [--transition-max S]
| Option | Meaning |
|---|---|
--days N | How far back to look (default 30). NinjaTrader deletes old log files, so the real window can be shorter; the report says so. |
--html <path> | Write the HTML report there. In the default output mode the report is always written (default %TEMP%\StopWarden-scan.html). |
--open | Open the HTML report when done. |
--json | Print the whole report as one JSON document (schema below) and nothing else. |
--text | Print the plain-English report. |
--anonymize | Replace account, strategy and connection names (for sharing a scan with support). |
--log-dir | Scan another NinjaTrader log folder. Default: the folder in the settings file, else <Documents>\NinjaTrader 8\log (Documents is looked up the Windows way, so a redirected or synced Documents folder is found). |
--transition-max S | Seconds a stop order in flight still counts as covering (a built-in default applies when you leave it out). |
Default output (for the StopWarden window)
One KEY=VALUE line each, then RESULT=OK:
| Key | Meaning |
|---|---|
SCAN_NAKED | Positions with no stop order seen in NinjaTrader's log for longer than the wait |
SCAN_ORPHAN | Working orders still resting for a while after their strategy was disabled, as the Scan counts it |
SCAN_DISCONNECT | Connection drops (a connection that was Connected and then was not; closing a connection yourself is not counted) |
SCAN_RESTART | NinjaTrader restarts while a position was open |
SCAN_STRATEGY_DISABLED | A strategy disabled and not re-enabled within 2 minutes |
SCAN_NO_STOP | Positions open longer than the wait with no working stop at any point |
SCAN_STRATEGIES | Number of (account, strategy, instrument) rows in the stop-timing table |
HTML | Path of the HTML report |
RESULT | OK |
How gaps are measured
For every account and instrument, the scan rebuilds the position and the order book from the log, line by line.
- A gap starts when the position is larger than the working stop contracts on the closing side (Sell stops for a long, Buy stops for a short, Stop Market or Stop Limit, same instrument and contract month) and ends when the stops cover it again or the position is flat. Only
WorkingandAcceptedstops count as working. - Stops in flight (
ChangePending,ChangeSubmitted,Submitted,TriggerPending,Initialized) count as covering for at most--transition-maxseconds (a built-in default applies when you leave it out). A gap that was covered in flight the whole time and ended within that limit is a normal amend or arrival: it is counted inin_flight_covered, not as a gap. A stop that stays in flight longer, or endsRejected, is a gap for its whole length and is counted instop_rejected. (Why: a stop that sits in Submitted for several seconds and is then rejected protected nothing; an unlimited "in flight counts" rule would hide it.) - The strategy of a gap is the strategy that sent the order whose fill opened the position, read from NinjaTrader's
NinjaScript strategy '<name>/<id>' submitting orderlines. Orders without such a line (manual, ATM or order-file orders) are grouped as(no strategy in log). - A session is a trading date on which the row had at least one position.
Does the strategy fit Protect's wait?
StopWarden waits before it acts on a position with no working stop. It also acts when a position has had no working stop for that long in total within one minute (a stop that keeps going missing for a moment adds up). For each strategy row the scan says whether its stops fit that wait, in the report and in the JSON. This is the same verdict the Learn report (autopilot learn) gives before Protect may act for a strategy.
A strategy fits when, in normal trading (leaving out gaps with a rejected stop and gaps cut short by a restart or the end of the log), both of these are well inside the wait:
- its slowest stop: the longest gap before its stop was working (
max_healthy_gap_s); - its most time without a working stop within one minute (
max_uncovered_60s_s), counted the way StopWarden's own check counts it while it runs: a stop that is being changed, or that has just been sent, still counts as cover for a short time (transition_max_s,arriving_max_s), and a stop that is being cancelled does not. This is what catches a strategy that cancels and re-places its stop many times a minute.
| Mode | When | Fits the wait |
|---|---|---|
BRACKETED | both figures above are well inside the wait | yes |
SLOW | either figure is not well inside the wait (and p99 of the gaps is within the wait) | no: Protect stays off for it; Watch (alerts only) |
STOPLESS | 20% or more of the positions were open well past the wait with no working stop at any point | no: a backup stop would change how the strategy trades; Watch (alerts only) |
UNSTABLE | p99 of the gaps is over the wait | no: Watch (alerts only) until the long gaps are explained |
A row is provisional until it has at least 20 round trips over at least 5 sessions (the Learn period).
The JSON also keeps proposed_grace_s (BRACKETED rows only). It is a measurement of how much of the wait the strategy's own stops use. It is not a second wait, and the report does not print it.
JSON schema (stopwarden.scan/2)
autopilot scan --json prints one object. Fields are only ever added within schema stopwarden.scan/2; a removed or renamed field changes the schema string. Schema /2 (2026-10-02): the count of long gaps is now ge_naked_limit (renamed from the /1 field), and the threshold is the wait. Added within /2 on the same day: max_healthy_gap_s, max_uncovered_60s_s, mode SLOW, and the params fit_factor, budget_window_s, arriving_max_s, exit_settle_s.
| Field | Type | Meaning |
|---|---|---|
schema | string | "stopwarden.scan/2" |
generated | string | Local time the scan ran (ISO 8601) |
version | string | StopWarden version |
window | object | See below |
params | object | See below |
counts | object | See below |
findings | array | See below, oldest first |
calibration | array | One row per (account, strategy, instrument), see below |
no_stop_seen | array | Positions open longer than the wait with no working stop at any point, oldest first |
unknowns | array of string | What this scan cannot see, in plain English |
anonymized | bool | true when --anonymize was used |
window: days (int, asked for), from / to (ISO 8601, the asked window), log_files (int, log files in the folder), log_files_read (int), first_event / last_event (ISO 8601 or null, the log lines actually inside the window), sessions (int, NinjaTrader starts inside the window).
params: naked_min_seconds (the wait), orphan_min_seconds, transition_max_s, in_flight_states (array of the five state names), grace_min_s, grace_max_s, grace_margin_s, learn_min_round_trips (20), learn_min_sessions (5), stopless_share (0.2), fit_factor, budget_window_s (60, the one-minute window), arriving_max_s and exit_settle_s (StopWarden's cover rules used for max_uncovered_60s_s).
counts: NAKED, ORPHAN, DISCONNECT, RESTART, STRATEGY_DISABLED (int, findings of each kind), naked_seconds (float, sum over NAKED findings), worst_naked_seconds (float or null), no_stop_seen (int), strategies (int, calibration rows).
findings[]: kind (NAKED | ORPHAN | DISCONNECT | RESTART | STRATEGY_DISABLED), when (ISO 8601, start of the episode), account (string, empty for DISCONNECT), strategy (string, may be empty), instrument (string, full name such as NQ DEC26, may be empty), detail (plain English, in the log's words), seconds (float, length of the episode; 0 for RESTART and STRATEGY_DISABLED).
calibration[]:
| Field | Type | Meaning | |||
|---|---|---|---|---|---|
account, strategy | string | ||||
instrument | string | Instrument root, such as NQ (contract months are combined) | |||
round_trips | int | Positions opened in the window | |||
sessions | int | Trading dates with a position | |||
episodes | int | Gaps counted (not covered in flight) | |||
in_flight_covered | int | Gaps covered by a stop in flight for their whole length, within transition_max_s | |||
p50_s, p90_s, p95_s, p99_s, max_s | float or null | Gap length percentiles (linear interpolation), seconds | |||
ge_naked_limit | int | Gaps of naked_min_seconds or more | |||
stop_rejected | int | Gaps during which a closing-side stop was rejected | |||
local_stop_episodes | int | Gaps that involved a TriggerPending or Initialized stop (may be held on this PC, not at the broker) | |||
positions_without_stop | int | Positions open longer than the wait with no working stop at any point | |||
mode | string | BRACKETED \ | SLOW \ | STOPLESS \ | UNSTABLE |
max_healthy_gap_s | float | Slowest stop in normal trading: the longest counted gap without a rejected stop and not cut by a restart or the end of the log (0 when none) | |||
max_uncovered_60s_s | float | Most seconds without a working stop within any one minute of normal trading, counted with StopWarden's own cover rules | |||
proposed_grace_s | float or null | How much of the wait the strategy's own stops use (null unless BRACKETED); not printed in the report | |||
gaps_longer_than_grace | int or null | Gaps in the window longer than proposed_grace_s | |||
learn_complete | bool | At least 20 round trips over at least 5 sessions | |||
longest | array | Up to 5 of the longest gaps, longest first: start, seconds, opened_by, closed_by (the log events, without order names), stop_rejected | |||
recommendation | string | The plain-English recommendation shown in the report |
no_stop_seen[]: account, strategy, instrument (root), start, end (ISO 8601), seconds (float), max_qty (int, largest position size), note (plain English: no stop order seen, or a stop was sent but rejected / never seen working, and whether the position was still open when NinjaTrader restarted or the log ended).
What the scan cannot see
Every report lists these under Unknowns, plus anything specific to that log (for example when NinjaTrader's log starts later than the window asked for):
- Orders held at the broker that NinjaTrader never wrote to its log.
- Broker timing: times are the PC clock as NinjaTrader logged them.
- Whether a
TriggerPendingorInitializedstop is held at the broker or by NinjaTrader on this PC. When the scan saw one in a gap, it adds that StopWarden, while it runs, counts such a stop as working only if the connection name in its settings matches the one in NinjaTrader's log and that connection is up. - A position being closed by a market or limit exit is not recognised as an exit in progress; those seconds count as gaps.
- The most time without a working stop within one minute uses the times NinjaTrader logged. StopWarden counts the same seconds while it runs, and looks at the log about once a second.