The NQ/MNQ simulator has a rule that prevents a long entry while a short position is open. Several strategies share a counter for that rule. The strategy can forget a position while the counter still remembers it, which is what happened at a day boundary.
The ordinary end-of-day close is checked when a price bar arrives at or after 15:45 New York time. A truncated input stream can stop earlier. No closing bar means no closing check.
A regression test makes the problem small enough to follow:
| Input | Position state | Shared short count |
|---|---|---|
| Prior session, 12:50 | A short opens | 1 |
| Last input at 12:59:59 | The short remains open | 1 |
| Next session, 09:30 | Daily reset is about to run | Still 1 |
Resetting the strategy first loses the position before its exit can reduce the shared counter. New long signals are then blocked by a short the strategy no longer holds.
Finish the old position before starting the new day
The engine caches the last timestamp and prices. When the date changes, _force_close_eod_for_previous_day() sends those prices through the normal exit code using a synthetic previous-day 15:45 timestamp. Only then does the daily reset run.
Close, release the gate, then reset
- 01Date changesThe next session brings a new bar
- 02Close old shortUse the normal position-close path
- 03Release gateNoHedgeGate.on_exit() reduces the count
- 04Reset strategyClear daily state after the close
The important ordering is short:
# Simplified sequence, not the complete implementation.
if bar_date != current_date:
close_remaining_previous_day_positions()
reset_regime()
reset_strategies()Using the ordinary close path does two things together: it adds a closed-position record and calls NoHedgeGate.on_exit() to release the count. Clearing the counter alone would make new entries possible while silently losing the old trade from the results.
This is a simulator repair. The last observed price and synthetic timestamp do not reconstruct an executable fill during missing market data. Rollover also requires another bar, so a final truncated day still needs its own closing treatment.
The test ends the old input at 12:59:59 and resumes at 09:30 the next day. It now requires exactly one closed SHORT with reason EOD, and a shared short count of zero. All five engine tests pass, including that missing-event replay.
Project sources & checks
mnq-nq-reverse-engineering: src/nq_scalper/engine_v2/engine.py, no_hedge.py, exits.py and tests/engine_v2/test_engine.py.
All five existing engine tests passed locally, including the missing-close day-rollover regression.
docs/research_lineage_audit.md and docs/knowledge_base.md document earlier invalidated results and data-handling mistakes.