Repository navigation
Floor an explicit close quantity to the quantity step - #839
Conversation
A strategy whose chart timeframe is N days (2D, 3D, 5D ...) built from daily bars ran on the days-since-1970 grid, and the chart's bar count, last_bar_index, barstate.islast and open-trade row came from the wrong bars. TradingView builds an N-day chart from N consecutive trading dates counted from the first trading date of each calendar year (the last bucket of a year is cut at its end), and its last_bar_index, barstate.islast and open-trade row follow that chart's own last bar. The engine already had that partition for request.security; this change installs it for the chart itself. The scheduler creates the existing partition source for an aggregated chart with a day count of two or more and hands it to the execution consumer, whose script bucket cursor, interval lookup, chart close and chart-time offsets read it. Only this chart path drops a leading bucket that opens before the input range and seals the final historical bucket. The calendar gains an interval overload that takes the partition's adjacent opens. The epoch-key paths, request.security, the D, W and M charts, intraday charts and runs whose input and chart timeframes are equal do not change, and a run without a partition adds nothing to the state hash. A day count whose seconds do not fit the legacy int conversion is calendar-only. Limits: a live stream keeps the previous chart path, and a selected window on a generated N-day chart partition is refused with the existing coded stop window_mode_unsupported (capability selected_window_chart_timeframe), because the window planner reconstructs the nominal calendar. Time offsets, exchange calendars and intraday input are not measured against TradingView. tests/test_n_day_chart_timeframe.cpp holds the rows on synthetic daily bars: the 3D grid, the last bar index, barstate.islast, the open row at the end, the 2D and 5D grids, the stream and refusal rows, the time offsets and the controls that must not move. It is registered through tests/cr150.d/n-day-chart-timeframe.cmake. scripts/check_native_cpp_versions.py lists the new reads of the retained input bars in the scheduler. TradingView tapes (sha256): 3D chart 1b2ef41a6f1ba4096e6659e12d5ef3f03c8bf2a51e4bb0d8331d814d9aade262, 2D chart ea679d91cbc0cd078c80604e2c73b3c9bb3104ff8d1989695da6abecb8b9e174, 5D chart 5342be10bdda8185b12b2c6a95b9b26101334cd1f58663ceb1f28c42197cb5dc, last_bar_index 05617c83d33cbd8be7a0d64356dc8f40e5b8d2ab20baa1b74b08b8aa66a07d25, barstate.islast 4ae8e0a932b7da6ffb4e0bcd5de9ee4d514cd6b78dbbb3fa4207aba1a2c6420d. Refs #811. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The N-day chart commit moved code in src/native_execution_consumer.cpp, src/native_execution_consumer.hpp, src/source/pine_strategy_host.cpp, src/source/pine_scheduler_native.cpp and src/timeframe.cpp, so the file:line citations of the published pages that point into them went stale, and scripts/check_doc_anchors.py reported 212 bad anchors. This commit changes digits only: the line numbers inside file:line citations of nine pages (145 changed lines, 233 changed numbers) and nothing else. No sentence, word, content pin or table is edited. The numbers were moved with the repository's own tools: scripts/check_doc_anchors.py --fix (the cited symbol sits on exactly one line) and scripts/reanchor_doc_citations.py --ref 360c4ab (a line map from the commit before the N-day chart change, where the pages were correct). The last six citations, which claim no symbol, were carried across the same line map by hand: the cited code line is the same text at its new number. scripts/check_doc_anchors.py reports no bad anchor on the result, and scripts/check_doc_reverts.py reports nothing, since a re-anchor is not an edit. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Main brings the memory-failure, timezone and setter-error changes. The conflicts were the Unreleased list in CHANGELOG.md, where both sets of bullets are kept unchanged, and the line citations of the published pages, which take main's lines here; the citations are re-anchored in the next commit by the anchors gate's own fix. Co-Authored-By: Claude Opus 5.5 <[email protected]>
strategy.close(id, qty = X) with an X that is not a multiple of the declared quantity step (qty_step, or mincontract when no qty_step is declared) placed no closing order: the quantity reached the native order layer unchanged, and that layer refuses explicit units that are off the grid. TradingView floors such a quantity to the step and closes that much; a quantity that floors to zero, or is larger than the position, closes the whole position. An explicit close quantity now goes through the same floor as an explicit entry quantity, including its handling of a quantity a hair under a step multiple, before the order is made. A quantity that floors to zero closes the whole exposure of the id when there is any. An explicit strategy.exit quantity is floored the same way before its units are reserved; one below a single step is ignored, so the exit covers the whole position (with the default qty_percent), as TradingView does. Runs that declare no quantity step are unchanged. The new test runs the adapter on synthetic bars with a step of 0.00001 and pins the quantities TradingView closed on BTCUSDT 1-hour bars: a half close of 0.03683 takes 0.01841 and leaves 0.01842, a close of 0.000004 or 0.123456 takes the whole position, a limit exit of 0.000004 or 0.123456 takes the whole position at its limit, and a limit exit of 0.018415, a short side and a reducing order floor the same way. One more row checks that a close quantity a hair under a step multiple (0.018409999999999) closes 0.0184, the same as an entry of that quantity opens, and leaves 0.01843. The lines added to src/source/pine_adapter.cpp move the lines the documentation cites below them; the anchors that follow them are re-pointed by the number of lines added (digits only, no sentence changes). Refs #716. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Merging main moved code in src/native_execution_consumer.cpp, src/source/pine_strategy_host.cpp, src/source/pine_scheduler_native.cpp and the other files the N-day chart change touches, so the file:line citations of the published pages that point into them went stale, and scripts/check_doc_anchors.py reported 208 bad anchors on the merge. This commit changes digits only: the line numbers inside file:line citations of nine pages (140 changed lines) and nothing else. No sentence, word, content pin or table is edited. The numbers were moved with the repository's own tools: scripts/check_doc_anchors.py --fix (the cited symbol sits on exactly one line) and scripts/reanchor_doc_citations.py --ref e163980 (a line map from main, where the pages are correct). The last six citations, which claim no symbol, were carried across by hand: the cited code line is the same text at its new number (one of them, the cursor constructor, gained a parameter on its line). Co-Authored-By: Claude Opus 5.5 <[email protected]>
The check that accepts a timezone name written in a script looks the name up in the timezone database with realpath, open and read, and it treated every failure of those calls as "no such zone". When the process had no file descriptor or memory left, or a read failed, a valid name such as America/New_York was refused with pine_timezone_error (class strategy, not retryable) and TradingView's text for an incorrect timezone; a database directory that could not be resolved refused every zone name the same way. The lookup now tells an absent name from a lookup that could not be carried out. ENOENT, ENOTDIR, ELOOP, ENAMETOOLONG and EISDIR still mean that the name denotes no zone file and keep pine_timezone_error. ENOMEM raises out_of_memory. Every other error raises engine_unclassified_error with the errno kept and the failed call named in the message. When the database directory itself cannot be resolved, a missing directory included, the run fails the same way and the message names the directory. In this check an interrupted call is tried up to three times, and a header read that returns fewer than four bytes is continued. Only the argument check of script timezones raises, through the internal timezone_lookup_accepted; the installed native_calendar::timezone_accepted keeps its false result, so its callers, the selected-run admission among them, refuse as before. A name that is also a complete POSIX definition, such as EST5EDT, stays accepted when its optional file lookup fails. A failed lookup is not remembered as an accepted name. tests/test_timezone_lookup_failure.cpp interposes realpath, open and read to fail each call of the lookup with each class of error, and checks the failure code, the errno and the call named in the message, that the name is looked up again afterwards and a clean run then books the same trades, that one interrupted call does not change the run, the file header (wrong magic, end of file, a read split in two), the database directory, the selected-run admission, and a TZDIR that is not a directory, where UTC offsets and POSIX definitions still run. Documentation sentences this change removes or rewrites: - docs/coverage.md, the pine_str_format_time row: "The message is TradingView's text with the engine's bar prefix, `Error on bar N: `, or `Error before the first bar: ` when no bar is being executed, and the arguments carry `bar_index`." It now begins "The error message for a refused name is", after a new sentence on the lookup failures. The allocation-bound manifest lists one new group, the message string of the lookup refusal in the native calendar (ceiling 1418 -> 1419, pinned count 1356 + 63 + 184), and its site inventory is re-emitted. Co-Authored-By: Claude Opus 5.5 <[email protected]>
A failed void C setter records its failure in the handle's run consumer. If a handle had no consumer, recording would first allocate one, and a failure of that allocation would leave only the status, without the code or the text. That state cannot be reached: the engine's only constructor binds the consumer, and the native host can be neither copied nor moved. These tests pin that, and show that the record survives a second allocation failure, without a change to the engine. - Every host type of the test starts with its consumer: storing a selected-window descriptor allocates nothing and does not return the allocation refusal -4. - When a setter's own string and then the rendering of the record's bar prefix are both refused, the setter's out_of_memory record is read without any allocation and its text falls back to "setter failed"; the next run start fails once with that record and publishes no report, and the start after it produces the report of a clean run. A setter refusal that carries its own code keeps that code when the rendering fails. - A sweep refuses two, and then three, consecutive allocations starting at every allocation index of a configured run (the setters, preparation, the bar callbacks and the report). Each injected run ends in a typed setup refusal before the first bar, in out_of_memory after it, or in a report byte-identical to the clean run, and the sweep prints how many refusals each run received: a pair at 256 indexes (Debug 258), a triple at 37 (Debug 39); the other 234 end at the first refusal. tests/fail_alloc_seam.hpp gains arm_span(first, count), which refuses count consecutive allocation attempts, and refusals(), the number of refusals delivered; arm(nth) is arm_span(nth, 1). Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main gained the CI job graph documentation (AGENTS.md and CLAUDE.md); this change touches neither file. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main brings the description of the current CI job graph in AGENTS.md and CLAUDE.md. Neither file is touched by this branch, so the merge has no conflicts and changes nothing else. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main gained the CI job graph documentation (AGENTS.md and CLAUDE.md); these changes touch neither file. Co-Authored-By: Claude Opus 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; formal verification proved 0 of 1 changed function(s) (1 not verified).
Formal verification. PR-changed functions: 0/1 verified (0 proven, 0 may-equivalent, 0 distinguished) · 1 not verified (1 unsupported).
Not verified on this run: close (unsupported).
Graphify review — findings
Fixes strategy.close(qty = ...) and strategy.exit(qty = ...) sizing to follow TradingView: explicit quantities now floor to the declared quantity step. A close quantity that floors to zero or exceeds the entry id's position closes that whole position, and an exit quantity below one step is ignored, so the exit covers the full position at the default qty_percent = 100. The changelog notes the behaviour was measured against TradingView on a single instrument and lot, and lists the cases not measured: close_entries_rule = "ANY", pyramided entry ids, whole-unit steps, and sub-step exits with qty_percent below 100. The ADR and design docs only have refreshed line-number references, and a new test file covers the close and exit quantity step cases.
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 — 899 functions depend on the 813 functions this change touches.
Health — grade B; 10 existing hotspot(s) in the area this change touches (pre-existing, not introduced here):
consume_matched_request— 2 callers, 43 callees (high)source_trigger_threshold()— 12 callers, 7 callees (high)match_path— 2 callers, 35 callees (high)execute_current— 2 callers, 24 callees (high)invoke_callback— 4 callers, 12 callees (high)deliver_confirmed_script— 2 callers, 23 callees (high)pump_batch— 4 callers, 11 callees (high)directional_tick()— 10 callers, 4 callees (high)- …and 2 more
Verification — 899 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: 851 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
1424 of 1424 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 1374 more
non-code file(s) changed (
CHANGELOG.md,docs/adr/0001-kernel-adapter-boundary.md,docs/design/arena-bound.md,docs/design/native-feature-parity.md,docs/native-refactor-progress.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,docs/adr/0001-kernel-adapter-boundary.md,docs/design/arena-bound.md,docs/design/native-feature-parity.md,docs/native-refactor-progress.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.
Formal verification
Could not verify: Could not verify close.
The verifier did not have enough to check close, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unsupported: member function — receiver/self (`this`) not synthesizable from the simple-type literal pools — abstaining rather than guessing a receiver
…follow-ups The release lands #829 first; this branch carries that change's head, so that taking main after its squash merge changes nothing in this tree. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…tep fix The release lands #829 and #840 first; this branch carries their heads, so that taking main after their squash merges changes nothing in this tree. The cited line numbers that both sides moved are combined (digits only). Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main now carries #829's squash merge, whose tree equals the #829 head this branch already merged, so this merge changes nothing in the tree. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…tep fix The branch below this one took engine main after #829's squash merge, whose tree equals the #829 head this branch already carries, so this merge changes nothing in the tree. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The explicit strategy.exit quantity floor sent every quantity that floors to zero to percentage sizing. TradingView was measured only with the default qty_percent = 100, where a quantity below one step leaves the exit covering the whole position. With a qty_percent below 100 (not measured) the fallback sized the exit from the percentage instead of the script's quantity: on a one-lot position an explicit 0.5 with qty_percent 50 armed the whole lot, against the existing control in tests/test_integer_lot_percent_exit_min_step_l4b.cpp that keeps 0.5. Now a qty_percent of 100 or more, or none, takes the fallback; below 100 a sub-step quantity is kept as the script gives it, as before the floor. Quantities of one step or more still floor to the step. The changelog entry and the SZ11 row of docs/pages/pine-to-native.md now say the same. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Engine main took the change this branch was stacked on (#840) as one squash commit whose tree equals the #840 head this branch already carries, so this merge changes nothing in the tree. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Engine main took a documentation change (#843) that rewrites two changelog entries and says "work item" in row P8 of docs/design/native-feature-parity.md. This branch edits the same P8 line, where it moves the set_risk_direction anchor to pine_adapter.cpp:27363. The merge keeps both edits on that line; every other path merges without a conflict. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Engine main took the 1.12.0 release commit, which changes VERSION and docker/run_failure_codes_diff.json. This branch changes neither file, so the merge has no conflict and adds only those two files. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Engine main took a CI change that keeps the output of the pull request smoke test. It edits .github/workflows/gke-pr.yml and scripts/test_ci_preflight.py. This branch changes neither file, so the merge has no conflict and adds only that change. Co-Authored-By: Claude Fable 5.2 <[email protected]>
strategy.close(id, qty = X) with an X that is not a multiple of the
declared quantity step (qty_step, or mincontract when no qty_step is
declared) placed no closing order: the quantity reached the native order
layer unchanged, and that layer refuses explicit units that are off the
grid. TradingView floors such a quantity to the step and closes that
much; a quantity that floors to zero, or is larger than the position,
closes the whole position.
An explicit close quantity now goes through the same floor as an explicit
entry quantity, including its handling of a quantity a hair under a step
multiple, before the order is made. A quantity that floors to zero closes
the whole exposure of the id when there is any. An explicit strategy.exit
quantity is floored the same way before its units are reserved; one below
a single step is ignored unless qty_percent is below 100, so with the
default qty_percent the exit covers the whole position, as TradingView
does. With qty_percent below 100 (not measured against TradingView) a
sub-step exit quantity is kept as given, as before; the existing control
in tests/test_integer_lot_percent_exit_min_step_l4b.cpp pins that. Runs
that declare no quantity step are unchanged.
The new test runs the adapter on synthetic bars with a step of 0.00001
and pins the quantities TradingView closed on BTCUSDT 1-hour bars: a
half close of 0.03683 takes 0.01841 and leaves 0.01842, a close of
0.000004 or 0.123456 takes the whole position, a limit exit of 0.000004
or 0.123456 takes the whole position at its limit, and a limit exit of
0.018415, a short side and a reducing order floor the same way. One more
row checks that a close quantity a hair under a step multiple
(0.018409999999999) closes 0.0184, the same as an entry of that quantity
opens, and leaves 0.01843.
The lines added to src/source/pine_adapter.cpp move the lines the
documentation cites below them; the anchors that follow them are
re-pointed by the number of lines added (digits only, no sentence
changes).
Refs #716.
🤖 Generated with Claude Code