Repository navigation
Test strategy.risk.max_drawdown at closing fills, as TradingView does - #814
Conversation
…tten at the breach strategy.risk.max_drawdown was measured once per bar close, against a peak made of bar-close equity, and a breach only refused later entries: it never closed a position, and it kept refusing entries that TradingView lets through. What TradingView does, measured on the tapes listed below (fixed quantities, no commission, a limit of 0.5 percent of equity, initial capital 10000). The drawdown is tested at an order fill that closes units, never between fills, on the equity at that fill's price with the open positions marked in; a close that realises a profit is tested too. The peak is the start equity raised by that marked equity at fills that close units, so the open profit of the units still held counts; the equity at an opening fill or at a bar close does not raise it. A breach closes every open position at that fill's price with the exit text "Close Position (Max Drawdown)" and an empty exit id, and later entries do not happen (to the end of the next session on the tapes). The closing leg of a reversing entry is such a fill: the new position is closed at once. A same-side entry fill is not tested, so it fills after an untested breach, and so does a later same-side entry of the same session. What the engine does besides, which no tape shows: the breach also cancels the pending orders (a later entry is refused anyway, so no tape separates the cancel from the entry block), and the block on later entries lasts to the end of the run. TradingView tapes (sha256): marked equity at a fill and no test between fills 5beeac719db4c7b6d84c31bf94f8bd9ac80cecaac8483e9900ec0b9532f36fd5, a flatten of every open position ee278ee1d5069aae226c93ae9287f1b1129a9e6ddb1d8e528f378e9b98260009, the marked book of a FIFO close 5eb4683a5e0f5688d6a46520f577380b2c40c59d92b460617da6fe22f0154bb9, a reversing entry 8a5bd165a195e74ecf72ce6336a369b7892828acef6ee5dccf662be38440dc4a, a same-side entry 0acb1158cea715b0e6cb5c6a838d997c6baf358c79df2433ad21a12fece399ac, the peak at bar closes 566c1d58849f9f1660274618e85c5d7f41dcee96e849c6ae411c0b27cca0ab09, a later entry of the same session 43dbd0bbe9a12b4c7252879895e295c6d20902b1f27129da13fbbac1c62180ae, a profit close a61368e0a2277f8939d4e338dca17545be75cbc6c86dac4b9c17bbcbf18a2f04, the peak at an opening fill 47ed90bb5e9edb0199e36bd3ef64165c93769ba36904dc7834b026991c29c701, the peak raised by a closing fill 6c8004381906ce42c022be9b7214d6a583f0fd23e64bb73220be1888a5646ed9, the open profit counted in the peak 8996c131d435ddb85b3dce0931a4fc720f6d81e341fbf07c34acfde6505f511e. update_risk_state keeps the consecutive-loss-days latch and the bar-close observation and the threshold overflow check as they were (the broker state hash of a run without a drawdown limit is unchanged); only the drawdown latch is removed. The new fill-time peak is folded into the broker state hash only under a drawdown limit. tests/test_adapter_drawdown_fill_halt.cpp holds eleven scenarios derived from the tapes. tests/test_adapter_risk_relower.cpp: the cadence scenario keeps its result and its comment now names the tapes; the scenario of a halted strategy that still reverses is re-pinned (the reversing entry's new position is closed at once and the later entry is refused). tests/test_engine_risk_l4a.cpp: four same-side lots now fill, the close_all closes four, and one entry after the breaching close_all is refused; the scenario is renamed. tests/twin_parity_ledger.md and tests/twin_parity_inventory.json follow the lines and the assertion text of that file (the inventory pins the digest of the rewritten twin's assertions), and the two sha256 pins of those files in scripts/check_twin_parity.py are updated with them. Refs #804. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
|
Not ready to merge: the x86_64 layout pins in tests/test_abi_layout_probe.cpp need updating for the new field; a follow-up commit will come on this branch. |
…oves The fill-time peak of the drawdown rule is one double in the risk state, so PineExecutionAdapter and PineStrategyHost each grow by 8 bytes and the three adapter members that follow that field move by 8 bytes. The layout probe pins all of these on Linux x86_64 with GNU libstdc++ (two static assertions and runtime pins), and the release profile's build checks them. The new values are the ones the probe printed when built and run on Linux x86_64: sizeof PineExecutionAdapter 90360 (was 90352), sizeof PineStrategyHost 109264 (was 109256), and the offsets of pine_view_ 89080 (was 89072), retired_row_scratch_ 89088 (was 89080) and staged_scopes_ 89760 (was 89752). No other pin of the probe moves. Refs #804. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
There was a problem hiding this comment.
Graphify reviewed this change.
No issues found by static checks — no coupling regressions or blocking issues in the code graph. That is not the same as safe to merge: see what was not checked.
Not checked: tests were not run; no formal proof of the changed code.
Graphify review — findings
Changes strategy.risk.max_drawdown so it is tested at order fills that close units, not once per bar close. Drawdown is measured as the peak equity at such fills minus the equity at the current fill (open positions included). A breach latches the halt, closes every open position at that fill's price with the exit text "Close Position (Max Drawdown)", cancels pending orders, and blocks later entries. The old bar-close check that only refused new entries is removed. The new fill-peak state is reset per run and hashed only when a drawdown limit is set, so runs without one keep their digests. The forced close is classified as a risk-limit close cause, and the MG10-b reversal expectation is rewritten to one reversal followed by the drawdown close, with a new test covering the fill-halt behaviour.
Review partial — this diff was larger than one review pass covers, so later files were not reviewed; some findings may be missing.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 2354 functions depend on the 2225 functions this change touches.
Health — this change adds coupling hotspots:
- worse:
find()— 10 callers, 4 callees - new:
main()— 0 callers, 11 callees
Verification — 2354 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 2315 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
1401 of 1401 test file(s) selected (100%) via static blast radius.
Escalated to a full run for safety (
noncode-change,uncovered-code) — the selection is not trustworthy on its own (see below). CI should run the whole suite.
benchmarks/tests/test_aggregate.py— full-run-safetybenchmarks/tests/test_check_provenance.py— full-run-safetybenchmarks/tests/test_compare.py— full-run-safetybenchmarks/tests/test_public_names.py— full-run-safetybenchmarks/tests/test_run_all.py— full-run-safetybenchmarks/tests/test_run_strategy_feeds.py— full-run-safetydocker/pineforge_sink_finalize_test.py— full-run-safetydocker/pineforge_sink_replay_rq_test.py— full-run-safetydocker/pineforge_sink_replay_test.py— full-run-safetydocker/pineforge_sink_to_json_test.py— full-run-safetydocker/quota_columns_test.py— full-run-safetydocker/quota_convert_test.py— full-run-safetydocker/quota_finalize_test.py— full-run-safetydocker/replay_rq_engine_test.py— full-run-safetydocker/run_json_artifact_window_test.py— full-run-safetydocker/run_json_bin_test.py— full-run-safetydocker/run_json_bounded_transport_test.py— full-run-safetydocker/run_json_bulk_equity_fuzz_test.py— full-run-safetydocker/run_json_bulk_equity_test.py— full-run-safetydocker/run_json_codes_test.py— full-run-safetydocker/run_json_converter_metadata_test.py— full-run-safetydocker/run_json_converter_session_test.py— full-run-safetydocker/run_json_diagnostics_test.py— full-run-safetydocker/run_json_equity_scan_test.py— full-run-safetydocker/run_json_fused_test.py— full-run-safetydocker/run_json_harnesses_test.py— full-run-safetydocker/run_json_metrics_only_test.py— full-run-safetydocker/run_json_misc2_test.py— full-run-safetydocker/run_json_misc_test.py— full-run-safetydocker/run_json_pfbar_test.py— full-run-safetydocker/run_json_provenance_test.py— full-run-safetydocker/run_json_quota_supervisor_test.py— full-run-safetydocker/run_json_replay_test.py— full-run-safetydocker/run_json_selected_sink_test.py— full-run-safetydocker/run_json_sink_lexical_test.py— full-run-safetydocker/run_json_sink_summary_test.py— full-run-safetydocker/run_json_sink_test.py— full-run-safetydocker/run_json_sink_vectors_test.py— full-run-safetydocker/run_json_spool_work_test.py— full-run-safetydocker/run_json_stringio_test.py— full-run-safetydocker/run_json_symbol_feeds_test.py— full-run-safetydocker/run_json_syminfo_test.py— full-run-safetydocker/run_json_trade_scan_test.py— full-run-safetydocker/run_json_window_budget_test.py— full-run-safetydocker/run_json_wrapper_work_test.py— full-run-safetyinclude/pineforge/native_run_spec.hpp— full-run-safetyscripts/derive_corpus_feeds_self_test.py— full-run-safetyscripts/fingerprint_self_test.py— full-run-safetyscripts/run_corpus_self_test.py— full-run-safetyscripts/test_abi_receipt_skips.py— full-run-safety- … and 1351 more
non-code file(s) changed (
CHANGELOG.md,tests/CMakeLists.txt,tests/twin_parity_inventory.json,tests/twin_parity_ledger.md) → running the full suite for safety (a code graph can't see config/fixture/data deps)
changed code file(s) with no mapped test (
CHANGELOG.md,src/source/pine_state_hash.cpp,src/source/pine_strategy_host.cpp,tests/adapter_risk_relower_expectations.inc,tests/twin_parity_ledger.md) — a coverage gap or a missing link — running the full suite rather than only the selected tests
Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.
· 1 grounded finding(s) anchored inline below; 1 more finding(s) on lines outside this diff (see the check run).
|
|
||
| } // namespace | ||
|
|
||
| int main() { |
There was a problem hiding this comment.
main()
fans out to 11 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
Refs #804
Limits of this change (put under the description of the change)
What it changes:
strategy.risk.max_drawdownis now tested at the order fills that close units, on the marked equity at that fill's price (open positions included), against a peak that starts at the equity of the first fill and is raised by the marked equity at fills that close units. A breach closes every open position at that fill's price (exit text "Close Position (Max Drawdown)", empty exit id), cancels the pending orders and blocks later entries. Before, the drawdown was measured once per bar close against a peak made of bar-close equity, and a breach only refused new entries: it closed nothing, and it also refused entries that TradingView lets through. The bar-close measurement and the overflow check of an enormous percentage are kept as they were; only the latch that refused entries at the bar close is removed. The closing leg of a reversing entry is a closing fill, so the new position of the reversal is closed at once when its closing leg breaches the limit. A same-side entry fill is not a test.What was measured against TradingView (NASDAQ:AAPL 15-minute bars of two sessions, fixed quantities, no commission, no
process_orders_on_close, a limit of 0.5 percent of equity, initial capital 10000,pyramiding3):Each point is a test scenario on synthetic bars in
tests/test_adapter_drawdown_fill_halt.cpp, with the arithmetic in its comment and the digest of its tape; the two pinned scenarios oftests/test_adapter_risk_relower.cppand the scenario oftests/test_engine_risk_l4a.cppthat this behaviour changes name the tape next to the edit. Each scenario fails on main; the scenarios of points 8 and 9 also fail when the peak is never raised, and the scenario of point 9 also fails when the peak is raised by realised equity only.What it does not change and does not claim:
process_orders_on_close, the bar magnifier andcalc_on_order_fills; margin-call fills as closing fills; stop and limit fills inside a bar; a short as the first position; the money (non-percent) form of the limit; a commission (the peak starts at the equity of the first fill, which a commission lowers); other symbols and timeframes; other limit values.Known items: this change adds lines to
src/source/pine_adapter.cpp, so the documentation-anchor check (scripts/check_doc_anchors.py) reports the anchors below them on this pull request; they are re-anchored once, by one commit on the release branch after the last member pull request. The same commit corrects the statements that describe the previous rule:docs/design/native-feature-parity.mdrow 541 (a drawdown measured once per script bar at the close mark byupdate_risk_stateagainst a peak of close marks; a halted strategy that still reverses) and its row 106 anddocs/coverage.mdline 264, which nameupdate_risk_stateas the enforcement point.🤖 Generated with Claude Code