From 01a14dafc395dc0c64d8caa8b25f02ed797753c1 Mon Sep 17 00:00:00 2001 From: seathatflowsinourveins <234074349+seathatflowsinourveins@users.noreply.github.com> Date: Sat, 10 Oct 2026 20:21:48 -0400 Subject: [PATCH] fix(trading): make paper qualification proofs optional and mark receipts `runner.py paper` now starts without `--gate-result` and `--snapshot`. Supplying either argument still requests the complete promotion check, so both must then be supplied and pass. This adopts the mover runner's optional-proof selection and its receipt marker (mover_runner.py:703-704 at 03a986ed): the paper receipt's preflight block records `promotion_gate` as "validated" or "not_supplied", so a receipt without proofs never reads like a qualified one. `recover` adds no marker, because the paper receipt that admitted the trial carries it, and `preflight` is unchanged. MainCommandGateWiring drives runner.main(). Its no-proof test failed first with promotion_gate_missing. Its promotion_gate assertions in the two no-proof tests and the valid-proof test failed first against the previous head's runner.py (blob 4951f603), with None != 'not_supplied' and None != 'validated', and pass with this change. README-paper-exploration.md paraphrases the 2026-10-10 exploration direction, states preset and leverage-rung coverage as that direction rather than current behavior, and replaces terms the repository does not define. blueprints/us-equities/data/README.md no longer says `paper` always validates the gate. source-hashes.json was recomputed from the current bytes of the files it lists. The manifests/evidence.json entries were re-registered with scripts/host_receipts.py register_file, per the docs/lanes.md hot-file protocol. Co-Authored-By: Claude Opus 5.5 --- .../README-paper-exploration.md | 78 +++++++++++++++++++ .../us-equities/adaptive-paper/runner.py | 21 +++-- .../adaptive-paper/source-hashes.json | 4 +- blueprints/us-equities/data/README.md | 20 +++-- manifests/evidence.json | 19 +++-- tests/test_adaptive_paper_runner.py | 58 ++++++++++---- 6 files changed, 165 insertions(+), 35 deletions(-) create mode 100644 blueprints/us-equities/adaptive-paper/README-paper-exploration.md diff --git a/blueprints/us-equities/adaptive-paper/README-paper-exploration.md b/blueprints/us-equities/adaptive-paper/README-paper-exploration.md new file mode 100644 index 000000000..6df8437c4 --- /dev/null +++ b/blueprints/us-equities/adaptive-paper/README-paper-exploration.md @@ -0,0 +1,78 @@ +# Exploratory paper runs + +Owner direction, relayed by the CC on 2026-10-10 at 18:39 UTC: +They authorized paper and sim exploration without approval or qualification +prerequisites, covering all factors and leverage modes, including aggressive +presets. The direction is for exploratory paper and simulation runs to cover +every registered preset and the `leverage-schedule-v1-20260922` rungs: 1×, 2× +and 4× intraday, with overnight exposure capped at 2×. That coverage is not in +place yet; [Remaining execution work](#remaining-execution-work) lists what +still blocks it. The frozen qualification trials and their retained records +under `trials/` stay frozen, and `scheduled_trial.sh` still supplies both +proofs. Heavy simulations run one at a time, paced to the host's capacity: none +starts while the host is low on available memory. + +`runner.py paper` can start without `--gate-result` and `--snapshot`. +Supplying either argument requests qualification validation, so both must then +be supplied and the existing status, snapshot hash, row count and check +validation applies. The paper receipt's `preflight` block records +`promotion_gate`: `validated` when either argument requested validation and +`not_supplied` when neither was given. This follows the mover runner's existing +optional-proof behavior and marker. The `preflight` and `recover` commands +retain their existing behavior and add no marker. + +Startup readiness remains separate from qualification. `validate_preflight` +still checks account readiness, cash/equity, broker margin entitlement, +session/window availability, flat startup, asset tradability and benchmark +freshness. Account locking, reconciliation, request budgets, deterministic risk +limits, fee/cash accounting and durable trial state continue through the same +code paths. + +Measured broker-paper acceptance for this change is **NOT_RUN**. CLI tests use +fixture credentials and a synthetic broker observation and stop before native +execution. They establish optional qualification wiring and continued account +readiness checks. + +## Remaining execution work + +- No runner or configuration alias for the exploratory paper runs has been + verified. This change does not select an account or operate the frozen + qualification trials' state. +- The existing config-hash continuity check still raises + `next_trial_config_differs_from_frozen_limits` for a changed configuration on + the same account's next trial. Removing mandatory qualification does not + enable same-account preset/rung rotation. Retain the account's existing state + root and ledger, fee and cash continuity when resolving that limitation. +- The bounded source review of main at `03a986ed` and T22 at `a9e4eeb7` found + no production paper dispatch caller for T22's factor/cohort strategies. T22 + documents that it supplies no paper command. Its native synthetic corpus + receipts measure engineering behavior; broker-paper execution, historical + performance and empirical preset refits remain unmeasured. Each future refit + must have appropriate results and a new preset version. + +## Sources and regression evidence + +Mechanism references are pinned to native-agent-stack main +`03a986ed637349403658f23da694c1833527819d`: + +- [mover_runner.py:703](https://github.com/seathatflowsinourveins/native-agent-stack/blob/03a986ed637349403658f23da694c1833527819d/blueprints/us-equities/adaptive-paper/mover_runner.py#L703) + selects promotion validation only when either optional proof path is supplied; + [mover_runner.py:704](https://github.com/seathatflowsinourveins/native-agent-stack/blob/03a986ed637349403658f23da694c1833527819d/blueprints/us-equities/adaptive-paper/mover_runner.py#L704) + records that selection as the preflight summary's `promotion_gate` marker. +- [runner.py:2310](https://github.com/seathatflowsinourveins/native-agent-stack/blob/03a986ed637349403658f23da694c1833527819d/blueprints/us-equities/adaptive-paper/runner.py#L2310) + shows the prior unconditional paper selection corrected here; + [runner.py:661](https://github.com/seathatflowsinourveins/native-agent-stack/blob/03a986ed637349403658f23da694c1833527819d/blueprints/us-equities/adaptive-paper/runner.py#L661) + keeps qualification dispatch separate from ordinary readiness checks. +- [runner.py:2434](https://github.com/seathatflowsinourveins/native-agent-stack/blob/03a986ed637349403658f23da694c1833527819d/blueprints/us-equities/adaptive-paper/runner.py#L2434) + binds next-trial configuration and the following fee/cash continuity checks. +- [T22 README.md:54](https://github.com/seathatflowsinourveins/native-agent-stack/blob/a9e4eeb7f5ffc4cc1ce67b22746225684a8859ae/blueprints/us-equities/strategies/README.md#L54) + records the absent paper command at that strategy source pin. + +`tests/test_adaptive_paper_runner.py::MainCommandGateWiring` drives the public +CLI. The no-proof test failed first with `promotion_gate_missing`. Its +`promotion_gate` assertions failed first against the previous runner.py, with +`None != 'not_supplied'` for both no-proof tests and `None != 'validated'` for +the valid complete proof. The suite also covers a blocked account without +proof, either proof argument alone, invalid complete proofs and unchanged +recovery and preflight behavior; recovery adds no marker. Run the touched module with the installed combined native +runtime: `python -B -m unittest tests.test_adaptive_paper_runner -v`. diff --git a/blueprints/us-equities/adaptive-paper/runner.py b/blueprints/us-equities/adaptive-paper/runner.py index 6883f85dd..74a303fcf 100644 --- a/blueprints/us-equities/adaptive-paper/runner.py +++ b/blueprints/us-equities/adaptive-paper/runner.py @@ -645,8 +645,9 @@ def validate_preflight(observation, config, *, require_open, allow_existing=Fals allow_existing_positions=None, session_policy=None, mode=None, gate_result_path=None, snapshot_path=None): """``mode="paper"`` additionally requires a passing, hash-matched promotion - gate (see ``_check_promotion_gate``); ``mode=None`` (the default) preserves - prior behavior exactly for existing preflight/recover call sites. + gate (see ``_check_promotion_gate``); ``mode=None`` (the default) skips it + for the preflight/recover call sites and for paper without either proof + argument. ``allow_existing`` (recovery mode, only set by ``--command recover``) relaxes the cash/equity floor, the full-universe/benchmark-quote checks @@ -2223,13 +2224,13 @@ def main(): parser.add_argument("--trial", default="adaptive-20260921") parser.add_argument("--state-root", type=Path, default=DEFAULT_STOP.parent) parser.add_argument("--gate-result", type=Path, default=None, - help="promotion-gate result JSON from " + help="optional qualification result JSON from " "blueprints/us-equities/data/promotion_gate.py; " - "required (status pass, hash-matched) for `paper`") + "supply with --snapshot to validate a frozen qualification") parser.add_argument("--snapshot", type=Path, default=None, help="the exact bars/universe input file the promotion " "gate validated (its hash must match --gate-result's " - "input_sha256); required for `paper`") + "input_sha256); required only with --gate-result") parser.add_argument("--live-dir", type=Path, default=None, help="optional directory for a live record of this run: events.jsonl " "(decisions and intents as they happen) and NautilusTrader's own " @@ -2307,7 +2308,15 @@ def observe_request(kind, **kwargs): save(args.output, summary) print(json.dumps({k: summary[k] for k in ("status", "orders_submitted")}, default=str)) return 0 if summary["status"] == "ready" else 2 - gate_mode = "paper" if args.command == "paper" else None + gate_mode = "paper" if ( + args.command == "paper" + and (args.gate_result is not None or args.snapshot is not None) + ) else None + # The paper receipt's preflight block states whether qualification proofs + # were validated, as mover_runner.py's paper command records it. recover + # adds no marker: the paper receipt that admitted the trial carries it. + if args.command == "paper": + summary["promotion_gate"] = "validated" if gate_mode else "not_supplied" # Rebase review D1: overnight_holds relaxes the flat-account/no-open-orders # gate only to resume a trial this engine itself left "held_overnight". # A first trial (or any other prior phase) still requires a flat account, diff --git a/blueprints/us-equities/adaptive-paper/source-hashes.json b/blueprints/us-equities/adaptive-paper/source-hashes.json index cf39408c1..9b7c6eebb 100644 --- a/blueprints/us-equities/adaptive-paper/source-hashes.json +++ b/blueprints/us-equities/adaptive-paper/source-hashes.json @@ -27,7 +27,7 @@ "blueprints/us-equities/adaptive-paper/recovery.py": "76c1b6e21e39e1c87138ffa9e3dd4323e599d9f59af406807213f71eb3661259", "blueprints/us-equities/adaptive-paper/registry.json": "60da73b985e718bd1dece7a3350f34636bda0fb38e766a44ae71c5a8c532282a", "blueprints/us-equities/adaptive-paper/requirements.txt": "6859f3c7362730fb1e1f023777863dc70650607c90873836b99c4e7d63982216", - "blueprints/us-equities/adaptive-paper/runner.py": "19c079e32397532818a14d2919d7fa83c21e3eaf7f4c9c93be39c0d8a309030e", + "blueprints/us-equities/adaptive-paper/runner.py": "d7f83441269de29ea31d6eef328220aac40b463722a604ec68d3409da06b94d4", "blueprints/us-equities/adaptive-paper/safety.py": "bfec57e021b65aee058c8d2e7c0cbafad46f6203edbe9396eec739c8546b0fd9", "blueprints/us-equities/adaptive-paper/scheduled_trial.sh": "58a5b656875bda629443768cd0c69a4be28cbf90dfc9f5ae8782d7dcf3ac7d76", "blueprints/us-equities/adaptive-paper/selector.py": "28de8ebf66b6e7135979554f73692ede772167682007d8c6ba41da6fb6fb39b3", @@ -53,7 +53,7 @@ "tests/test_adaptive_paper_mover_native.py": "cc12a556b537cbe53f23c10e524dd6b3dfe9b3b09b09326aa4413ed7c19d83c1", "tests/test_adaptive_paper_native.py": "728458624788243abcaf653f6a20a5a6201cfa9ec3328e8d7eb3fd58fdfa94b0", "tests/test_adaptive_paper_recovery.py": "688e23f102249d170bf03d6a8d06196a42472cea049453228e4831a436c602ce", - "tests/test_adaptive_paper_runner.py": "5a1b81190e368c8891babb62f0c58b74eb5c898b4119f3bbfb8e146fd3c4d42f", + "tests/test_adaptive_paper_runner.py": "b3505e592b78beec416433c8d4b15991a34da2a84fb013712028715398d920d6", "tests/test_adaptive_paper_safety.py": "dda093e5428a58ac667ae4d9a620acbf7ee78b8b26db83fccf08b530f1e58c7e", "tests/test_adaptive_paper_selector.py": "27b431ac7911372d050f25d32ac24a3df9d2cf3c5e7dafdebfe51db6cab55936", "tests/test_adaptive_paper_sessions.py": "dbb85e80b045c42dfed2ac90e0653a39b77718352d511b2e919f3311139abc70", diff --git a/blueprints/us-equities/data/README.md b/blueprints/us-equities/data/README.md index a60c8e57e..1a046c017 100644 --- a/blueprints/us-equities/data/README.md +++ b/blueprints/us-equities/data/README.md @@ -161,12 +161,17 @@ are fetched fresh at preflight time, not read from a stored file), and `config.json` itself cannot be the gated "snapshot": `promotion_gate.py` only accepts `.parquet`/`.csv`/`duckdb://` input, so pointing it at a JSON config always fails closed with `unsupported_input_format`. Instead, -`runner.py`'s `main()` registers `--gate-result ` and `--snapshot -` CLI arguments; for the `paper` command (not `preflight`, not -`recover`, which resumes a trial admitted by an earlier `paper` invocation) -it calls `validate_preflight(..., mode="paper", gate_result_path=args.gate_result, -snapshot_path=args.snapshot)`. The gate result must satisfy the full -contract -- every required key present, every named check passing, +`runner.py`'s `main()` registers optional `--gate-result ` and +`--snapshot ` CLI arguments. For the `paper` command (not `preflight`, +not `recover`, which resumes a trial admitted by an earlier `paper` +invocation), supplying either argument requests validation: `main()` calls +`validate_preflight(..., mode="paper", gate_result_path=args.gate_result, +snapshot_path=args.snapshot)`. Without either argument, `paper` runs the same +readiness checks with `mode=None`, as the mover runner does +([README-paper-exploration.md](../adaptive-paper/README-paper-exploration.md)). +The paper receipt's `preflight` block records `promotion_gate` as `validated` +or `not_supplied`. When validation is requested, the gate result must satisfy +the full contract -- every required key present, every named check passing, `row_count > 0`, a top-level `status: "pass"`, and an `input_sha256` matching a `--snapshot` file the gate itself could accept (`.csv`/`.parquet`) -- or the runtime raises `SafetyError` with kind `promotion_gate_missing` (no result @@ -177,7 +182,8 @@ named check reports `status: "fail"` despite a "pass" top-level status), `promotion_gate_empty` (`row_count <= 0` despite a "pass" top-level status), `promotion_gate_failed` (top-level `status != "pass"`), or `promotion_gate_mismatch` (hash does not match the current file). `mode=None` -(the `preflight` and `recover` call sites) is unaffected. `--snapshot` must +(the `preflight` and `recover` call sites, and `paper` without either +argument) skips this check. `--snapshot` must name an actual bars/universe input file that was run through `promotion_gate.py`. [`ingest_snapshot.py`](ingest_snapshot.py) (added in #84) writes one: a bounded, read-only Alpaca daily-bar snapshot of diff --git a/manifests/evidence.json b/manifests/evidence.json index 81edddb4f..baebe5f1e 100644 --- a/manifests/evidence.json +++ b/manifests/evidence.json @@ -12855,6 +12855,11 @@ "sha256": "ae35cb8901695fecade818c8cb78ea0c738db2bbf5d6413d36893d5cca4713d4", "bytes": 14691 }, + { + "path": "blueprints/us-equities/adaptive-paper/README-paper-exploration.md", + "sha256": "b89575f2015573e0aea5480f419319370fb6b1baac767cc1ad4c9ae69978fc09", + "bytes": 5393 + }, { "path": "blueprints/us-equities/adaptive-paper/README-recovery.md", "sha256": "aee5231130eb08d2f3008a1b62ef3d0f302e5527c7ea6843d1f9e8bd820199a2", @@ -13217,8 +13222,8 @@ }, { "path": "blueprints/us-equities/adaptive-paper/runner.py", - "sha256": "19c079e32397532818a14d2919d7fa83c21e3eaf7f4c9c93be39c0d8a309030e", - "bytes": 155497 + "sha256": "d7f83441269de29ea31d6eef328220aac40b463722a604ec68d3409da06b94d4", + "bytes": 155977 }, { "path": "blueprints/us-equities/adaptive-paper/safety.py", @@ -13242,7 +13247,7 @@ }, { "path": "blueprints/us-equities/adaptive-paper/source-hashes.json", - "sha256": "4a78486aec53ccbcce6cffeadae0a246c0b2c96e95a2a6a96da6e38ebfdf69f0", + "sha256": "be95543d4ff7a3b82f86b515947f7bd8c2d6b678789a1f7e43226b5314c19b4b", "bytes": 7312 }, { @@ -14602,8 +14607,8 @@ }, { "path": "blueprints/us-equities/data/README.md", - "sha256": "8810322b8daa2459970f15432bea4c8476a683742327a81320f514ac50f544de", - "bytes": 12108 + "sha256": "535a6945f5e6dd36740ca53a30840a1f81f3bc2d76397fde89ac72bc9d33412f", + "bytes": 12525 }, { "path": "blueprints/us-equities/data/fixtures/bad-gate-result.json", @@ -60087,8 +60092,8 @@ }, { "path": "tests/test_adaptive_paper_runner.py", - "sha256": "5a1b81190e368c8891babb62f0c58b74eb5c898b4119f3bbfb8e146fd3c4d42f", - "bytes": 286409 + "sha256": "b3505e592b78beec416433c8d4b15991a34da2a84fb013712028715398d920d6", + "bytes": 288091 }, { "path": "tests/test_adaptive_paper_safety.py", diff --git a/tests/test_adaptive_paper_runner.py b/tests/test_adaptive_paper_runner.py index 730e84883..767d6aff4 100644 --- a/tests/test_adaptive_paper_runner.py +++ b/tests/test_adaptive_paper_runner.py @@ -2113,14 +2113,11 @@ def test_snapshot_path_that_is_a_directory_raises_promotion_gate_missing(self): class MainCommandGateWiring(unittest.TestCase): - """Regression: review found `python3 runner.py paper ...` bypassed the - promotion gate entirely -- `main()` registered no `--gate-result`/ - `--snapshot` arguments and its `paper`/`recover` call site omitted the - new `mode`/`gate_result_path`/`snapshot_path` kwargs, so `validate_preflight` - always ran with `mode=None`. These tests drive `runner.main()` itself - (mocking only the network-touching `preflight` call and the - native-runtime entry past the gate check) to prove the CLI wiring, not - just the already-covered `validate_preflight` unit behavior.""" + """Paper may omit both qualification arguments; supplying either + requests the complete promotion check. Drive `runner.main()` with + fixture credentials and a network preflight observation, stopping at + native-runtime entry to check CLI behavior independently of the broker. + Direct `validate_preflight` proof validation is covered separately.""" def setUp(self): self.tmp = tempfile.TemporaryDirectory() @@ -2155,12 +2152,24 @@ def _run_main(self, command, extra=()): "gate check raised/returned" without needing the pinned native runtime or an SQLite ledger. Returns `(sentinel, code, raised)`: `code` is `main()`'s return value (`None` if it raised instead), and - `raised` is the exception instance if one propagated out of `main()`.""" + `raised` is the exception instance if one propagated out of `main()`. + `self.preflight_summaries` keeps each summary `public_preflight` + returned; `main()` mutates it in place and a trial receipt carries it + as its `preflight` block.""" sentinel = RuntimeError("reached_post_gate_execution") + self.preflight_summaries = [] + real_public_preflight = runner_module.public_preflight + + def capture_public_preflight(*args, **kwargs): + summary = real_public_preflight(*args, **kwargs) + self.preflight_summaries.append(summary) + return summary + with patch.object(sys, "argv", self._argv(command, extra)), \ patch.object(runner_module, "load_config", return_value=(self.config, None, None)), \ patch.object(runner_module, "credentials", return_value=("fixture-key", "fixture-secret")), \ patch.object(runner_module, "preflight", return_value=self.observation), \ + patch.object(runner_module, "public_preflight", capture_public_preflight), \ patch.object(runner_module, "account_lock_fingerprint", side_effect=sentinel): try: code = runner_module.main() @@ -2168,13 +2177,31 @@ def _run_main(self, command, extra=()): except Exception as exc: # noqa: BLE001 -- deliberately catches the sentinel too return sentinel, None, exc - def test_paper_without_gate_arguments_is_blocked_before_execution_continues(self): + def test_paper_without_gate_arguments_reaches_post_gate_execution(self): + sentinel, code, raised = self._run_main("paper") + self.assertIsNone(code) + self.assertIs(raised, sentinel) + (summary,) = self.preflight_summaries + self.assertEqual(summary.get("promotion_gate"), "not_supplied") + + def test_paper_without_gate_arguments_still_requires_ready_account(self): + self.observation["account"]["trading_blocked"] = True sentinel, code, raised = self._run_main("paper") self.assertIsNone(raised) self.assertEqual(code, 2) result = json.loads(self.output.read_text()) - self.assertEqual(result["status"], "not_started") - self.assertEqual(result["reason"], "promotion_gate_missing") + self.assertEqual(result["reason"], "account_not_ready") + self.assertEqual(result.get("promotion_gate"), "not_supplied") + + def test_paper_with_either_gate_argument_alone_requires_the_complete_pair(self): + gate_path = self.root / "gate-result.json" + gate_path.write_text(json.dumps(_full_gate_result(input_sha256=self.snapshot_hash))) + for argument, path in (("--gate-result", gate_path), ("--snapshot", self.snapshot)): + with self.subTest(argument=argument): + sentinel, code, raised = self._run_main("paper", [argument, str(path)]) + self.assertIsNone(raised) + self.assertEqual(code, 2) + self.assertEqual(json.loads(self.output.read_text())["reason"], "promotion_gate_missing") def test_paper_with_failing_gate_result_is_blocked_before_execution_continues(self): gate_path = self.root / "gate-result.json" @@ -2224,14 +2251,19 @@ def test_paper_with_passing_matched_gate_reaches_post_gate_execution(self): sentinel, code, raised = self._run_main("paper", ["--gate-result", str(gate_path), "--snapshot", str(self.snapshot)]) self.assertIsNone(code) self.assertIs(raised, sentinel) + (summary,) = self.preflight_summaries + self.assertEqual(summary.get("promotion_gate"), "validated") def test_recover_command_is_unaffected_by_missing_gate_arguments(self): """`recover` resumes a trial already admitted by an earlier `paper` - run; it must not additionally require --gate-result/--snapshot.""" + run; it must not additionally require --gate-result/--snapshot, and + it adds no promotion marker: the admitting paper receipt holds it.""" self.observation["positions"] = [{"symbol": "SPY", "qty": "1"}] sentinel, code, raised = self._run_main("recover") self.assertIsNone(code) self.assertIs(raised, sentinel) + (summary,) = self.preflight_summaries + self.assertNotIn("promotion_gate", summary) def test_preflight_command_is_unaffected_by_missing_gate_arguments(self): sentinel, code, raised = self._run_main("preflight")