Skip to content

Mover trial mode for the adaptive NautilusTrader paper engine (SYN-verified; paper needs an isolated account) - #172

Merged
seathatflowsinourveins merged 8 commits into
mainfrom
claude/mover-paper-e2e-20260924
Sep 24, 2026
Merged

seathatflowsinourveins merged 8 commits into
mainfrom
claude/mover-paper-e2e-20260924

Conversation

@seathatflowsinourveins

Copy link
Copy Markdown
Owner

Summary

A bounded paper trial mode for the mover studies (#162): up to five scanner-selected symbols entered at trial start with marketable limits (extended hours outside RTH), sized by the preregistered leverage schedule (rung x regime x drawdown, sub-$5 at 1x, capacity caps), exited by one study rule (X1-X4), hard-flattened at the configured trial end, reconciled.

  • New modules only: mover.py (pure logic, MoverBook state machine), mover_strategy.py (Nautilus shell), mover_runner.py (check / paper / recover / synthetic), mover_simulation.py (synthetic port), config-mover.json (pre-market mechanics rule 08:00|G20|V250000|any, X2, flat by 09:25 ET), README-mover.md. Shared engine modules are unchanged.
  • Two caps: mover.max_entry_notional_usd sizes entries; the ledger per-order cap is at least 10x it so any held leg can be sold in whole shares; a share beyond that is reported under unsellable_positions.
  • mover_scan.py writes engine-valid scan files (at most 9 decimal places).
  • The mover lane must not share an Alpaca paper account with the adaptive lane: a mover fill trips the adaptive runner's next_trial_cash_mismatch, and unfilled mover orders freeze its ledger. paper refuses when the same state root holds an adaptive lane unless --allow-shared-account.

Review

Built by an Opus worker in an owned worktree; two independent Opus reviews (correctness, trading safety) found 2 blocking and several major issues (cross-session recovery with changing symbols, recovery unable to sell winners past the per-order cap, exit budgets, sub-dollar ticks); fixed in 5438e5b0; recheck found one remaining major (a single share above the per-order cap) fixed in 5819721c with tests that fail before the fix; recheck approved. The coordinator then fixed the last minor (hard-flatten latch in one-symbol evaluations, regression test fails without it).

Evidence

  • SYN only: the coordinator's synthetic trial through the real LiveNode on the live scanner's output: 5 entries, 5 X2 exits, flat, reconciled, cash delta equals realized P&L (-55.60 USD), pnl_consistent.
  • No PAPER evidence yet: no isolated paper account exists for the mover lane.

Test plan

  • pinned runtime: test_adaptive_paper*.py 725 OK (skipped 1)
  • system Python: native mover tests skip without NautilusTrader
  • python3 scripts/validate.py passed; evidence_manifest.py --check passed
  • guarded gitleaks over origin/main..HEAD: no leaks

🤖 Generated with Claude Code

Protocol mover-early-entry-v1-20260924, paper_e2e section. A bounded trial
enters up to five scanner-ranked movers at trial start and exits them by the
configured rule (X1 close, X2 +60 min, X3 0.85 trail, X4 0.85/1.50 bracket),
with a hard flatten at the configured trial end.

- mover.py: pure logic. Config and scan validation (sha256 recorded, older
  than 5 minutes refused), notional_i = equity x L_i / 5 with the dollar-volume,
  entry-bar, per-order and gross caps, the drawdown ladder and rung schedule,
  tick-rounded marketable limits, and the per-symbol order state machine
  (one buy per symbol, never chased, chunked exits, STOP/hard-flatten/guard).
- mover_strategy.py: NautilusTrader shell; orders go through the unchanged
  adapter, controller, ledger caps and reconciliation.
- mover_runner.py: check/paper/recover/synthetic commands and the native trial
  loop modeled on run_native; reuses preflight, validate_preflight, Ledger,
  reconcile and recovery.recover. Suspends quote-driven orders while a
  reconciliation snapshot is in flight (a measured race otherwise), ignores
  only foreign terminal orders, and refuses a state root shared with the
  adaptive lane unless --allow-shared-account.
- mover_simulation.py: synthetic port with resting limits and partial fills.
- config-mover.json: pre-market only, ends 09:25 ET, exit X2, rung 1,
  200 USD per order, 2000 USD gross cap with a 2x appreciation allowance.
- 44 pure tests (both runtimes) and 11 native end-to-end tests (pinned
  runtime), including the paper command wiring against the synthetic port.
  Evidence class SYN only: no broker order was sent.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…y, bounded exits

Review findings on 0480c8d (mover trial mode). Each fix has SYN tests, and each
test was checked to fail when its fix is reverted in a scratch copy:

- Recovery adopted every ledger intent, but a transport can only adopt intents for
  symbols it subscribes. From the second session on (other scan symbols) the forced
  recovery and `recover` raised TransportError before any cancel or sell, and the
  lane stuck. mover_runner.recover_mover now adopts every unresolved intent and only
  the terminal intents of subscribed symbols; skipped ones stay covered by the
  history pages and submitted_intent_absent. The synthetic port now applies
  normalize_intent on submit and adopt and needs an owned intent to cancel, as the
  transport does, and the native harness recovers on a fresh port.
- recovery.recover sized exits as min(position, max_order_qty, notional capacity).
  In "notional" mode that exceeds the ledger's floor(notional cap / bid) share cap
  for any appreciated position (order_size_cap_exceeded). It now uses
  RiskLimits.effective_max_order_qty, which is unchanged in "fixed" mode. The mover
  requires "notional" mode so recovery chunks are whole shares.
- Exits: no sell on a halted quote; a pre-wire refusal is retried after 1 s without
  being charged (own bound); a latched force grants one fresh budget; exhaustion
  before a force latches a book-wide flatten. After a force, the native loop hands
  the residual to recovery when every held leg is blocked or no exit fills for three
  exit timeouts, instead of idling until the sell window ends.
- sell_limit_price never prices above the bid rounded down to the tick. The new
  native test then showed a 2-decimal instrument cannot carry the sub-penny fill
  below 1 USD (cumulative_fill_precision_requires_reconciliation), so every mover
  instrument is registered at 4 decimals; limits keep the 0.01 tick at or above 1 USD.

Kept: a trial that needed a forced recovery stays needs_attention (the engine's
sticky rule); `recover` clears it and now passes on a multi-session ledger. Not
taken: an immediate hand-off for an unsellable leg before any force, because
recovery stops at its first failed symbol and could leave sellable legs unsold.

Evidence class SYN only: pure tests, native LiveNode tests against the synthetic
port and mutation checks. No broker order was sent.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s mid-pass

Recheck findings on 5438e5b (mover trial mode). Each fix has SYN tests that fail on
the original code and pass now; each piece was also reverted alone in a scratch copy
and caught by its tests.

- MAJOR: the ledger's max_order_notional_usd (200 in config-mover.json) bounded exit
  sells as well as entries, so once one share of a leg was worth more than 200 USD no
  engine path could sell it (book blocked, forced recovery and `recover` refused it)
  and the trial ended needs_attention holding the position. The mover now has its own
  required entry cap, mover.max_entry_notional_usd (200), used for sizing, the entry
  headroom skip and buy quantities, and MoverController refuses a larger buy before the
  ledger reserves it (mover_entry_notional_cap_exceeded), so entries are unchanged. The
  ledger cap is 2000 (the gross cap, the most RiskLimits allows) and config load
  requires it to be at least EXIT_HEADROOM_FACTOR (10) x the entry cap
  (mover_ledger_order_cap_below_exit_headroom). A leg sells whole through a tenfold
  rise and in whole-share chunks until one share exceeds the ledger cap (about 20x
  from an entered share, past the ledger's own gross-cap halt). Past that the book
  blocks it as exit_share_exceeds_ledger_order_cap and recovery_mover lists it under
  unsellable_positions (share_exceeds_ledger_order_cap), surfaced in the trial
  receipt. The factor is a code constant, keep-but-compare against the historical
  distribution of movers' largest rise within the hold.
- MINOR: MoverBook.evaluate read the force once before its per-symbol loop, so a
  book-wide force latched by _exit mid-loop (exit_orders_exhausted or
  exit_refusals_exhausted) missed later legs; a waiting entry after it could still
  buy. Cancels and exits now run first and read the force per leg; a latch during
  that pass sends every leg (also outside the evaluated symbols) through again; entries
  run last under the current force. The latching evaluation cancels open buys,
  flattens every leg and sends no buy (so no buy is sent and canceled in one batch).

Shared engine modules are unchanged (safety.py, recovery.py, runner.py). The mover
ledger pins its limits (persisted_risk_limits_differ); no mover ledger existed on this
host. README-mover.md documents the two caps, the exit headroom and the pin.

Evidence class SYN only: pure tests, native LiveNode tests against the synthetic port
(including the paper and recover commands with a fake preflight) and mutation checks.
No broker order was sent and no credential was read.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… the pre-market mechanics rule

The pre-pass force is read before the hard-flatten latch, so a one-symbol evaluation at the flatten
time also flattens every other leg (regression test fails without it). config-mover.json uses
08:00|G20|V250000|any with X2, the pre-market mechanics rule of the mover study's paper plan.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
mover_scan.engine_scan formats every number with at most 9 decimal places, the limit of the engine's
scan parser (mover._scan_decimal); a 12-place volatility input was refused as scan_regime_invalid.
Checked end to end: the scanner's engine file passes mover_runner check and a synthetic trial through
the real LiveNode (5 entries, 5 X2 exits, flat, cash delta = realized P&L).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-24T08:01:34.178503Z 28c2e69 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 28c2e6950f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tests/test_adaptive_paper_mover.py
Comment thread blueprints/us-equities/adaptive-paper/source-hashes.json
Comment thread blueprints/us-equities/adaptive-paper/mover_runner.py
Comment thread blueprints/us-equities/adaptive-paper/mover.py
Comment thread blueprints/us-equities/adaptive-paper/mover.py Outdated
Comment thread blueprints/us-equities/adaptive-paper/mover.py
Comment thread blueprints/us-equities/adaptive-paper/mover_runner.py Outdated
Comment thread blueprints/us-equities/adaptive-paper/mover_runner.py Outdated
- Keep the order journal when the native loop raises: run_mover records any exception once its
  node task exists and still returns the book's legs, orders, events and native counts (status
  needs_attention, bounded error_type/error_reason, a mover_loop_error event). A periodic
  reconciliation that finds an external order after fills no longer yields a receipt with zero
  symbols and entries.
- Refuse scan rows below the protocol's 1.00 USD minimum price (scan_symbol_below_min_price), and
  apply the scanner's own gain tolerance (G - 1e-9 as a ratio, 1e-7 percentage points at nine
  decimals) so a boundary candidate the scanner fired is accepted.
- Require a positive max_leverage at or above the highest scheduled rung
  (mover_rung_exceeds_max_leverage): entries are sized at the full rung.
- Refuse an X1 config that could never reach 15:58: a session-close latch (controller close less
  cleanup_seconds) or a trial end at or before it (mover_x1_preempted_by_session_close,
  mover_x1_preempted_by_trial_end).
- check --assume-fresh and synthetic --allow-stale-scan take the scan time from load_scan's
  guarded parser (now=None), so a malformed scan is refused with its reason instead of raised; an
  out-of-range scan_time is scan_time_invalid.
- Keep config-mover.json at the 08:00|G20|V1000000|any rule the tests and README pin; regenerate
  source-hashes.json and manifests/evidence.json.

Each behaviour change has a test that fails before and passes after (SYN fixtures only; no broker,
network or credentials). The adaptive-paper suite passes on the pinned native runtime (732 tests,
1 skipped) and on system python3 (732, 198 native skips); evidence_manifest --check and
validate.py pass.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
X1 fires two minutes before the session's regular close from the engine calendar (15:58, or 12:58
on a scheduled early close; study clarification C26). plan_timing refuses a plan whose hard flatten
comes at or before X1 (mover_x1_unreachable): the independent recheck showed a regular-session X1
config with cleanup_seconds 119 and the default 120 s flatten reserve could never reach X1 at the
latest start the preflight allows. Tests cover the early close and the refusal. 732 adaptive-paper
tests OK on both runtimes; synthetic trial through the LiveNode passed (5 entries, 5 X2 exits,
flat, reconciled).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@seathatflowsinourveins
seathatflowsinourveins merged commit 27b1eeb into main Sep 24, 2026
22 checks passed
@seathatflowsinourveins
seathatflowsinourveins deleted the claude/mover-paper-e2e-20260924 branch September 24, 2026 08:58
seathatflowsinourveins added a commit that referenced this pull request Oct 12, 2026
Fix the decision record of the compounding rule text,
docs/decisions/2026-10-11-compounding-rule-text.md. The six rule
surfaces do not change.

- Approval: the owner approved section 1's proposal, the first
  sentence, at 22:01Z. The second sentence was presented as an optional
  clause and the owner's answer did not single it out; the command
  center (CC) included it under the owner's delegation, because it
  restates the owner's own no-dates direction. The owner was told at
  23:44Z, with the option to drop it. The record cites the ruling's
  addendum-01 and the SHA256 of each sentence.
- #172: restated with its recorded supersession at 22:18:17Z, under
  which guard bypasses are follow-ups (research-queue row 23). #1014
  landed as ccff876 with only the one-letter POINTER_VARIABLE suffix.
- PR triage: 3 fold receivers (#878, #927, #958) merged without their
  4 donors (#804, #915, #870, #829), citing the triage's addendum-01.
- What the second sentence leaves in force (F1 of the trading lane's
  read of us-equities-trading #87): it forbids a date as the trigger
  that starts work, and every time bound the contract states as a fact
  or protocol window stays.
- Checks: the budget test's sentence now says what the test does (the
  first GPT read of this PR):
  test_growth_in_any_loaded_file_crosses_the_fixed_budget fills the
  available headroom in each loaded file in turn, verifies that
  equality with the ceiling passes, then appends a two-byte UTF-8
  character and verifies rejection.
- manifests/evidence.json: the record re-registered with register_file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant