Repository navigation
Name the timeframes and the rule when a run's timeframes do not fit - #841
luisleo526 wants to merge 18 commits into
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]>
…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]>
A run whose input, script or intrabar timeframes do not fit was refused with "Pine adapter produced invalid native run spec field 29" (or 4), and an args object that carried only the option name. The text read like an internal failure, the number was an internal enum value, and neither named the timeframes or the pairing that failed. The refusal now says which timeframes are involved, why they do not fit and what to pass instead, for example "input timeframe '2' cannot build script timeframe '15' (15 is not a multiple of 2); pass bars whose timeframe divides the script timeframe '15', for example 1-minute bars." The args keep the option name and add input_tf, script_tf, intrabar_tf when the bar magnifier is on, and a reason: not_divisible, script_finer_than_input, intrabar_not_divisible or script_finer_than_intrabar. The code stays run_options_rejected, which runs are refused or accepted does not change, and every other refusal keeps its text and args. The catalog declares the two new optional arguments and the new message templates; the generated registry, the catalog diff and the failure-code tables are regenerated by their scripts. The bar magnifier page says that passing the intrabar timeframe itself as the input is refused when it does not divide the chart timeframe. The allocation-bound manifest lists the refusal helper's three new groups (its failure arguments, rule text and message text; ceiling 1418 -> 1421, pinned count 1602 -> 1605) and its site inventory is re-emitted. 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 #813. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…l text The release lands #829, #840 and #839 first; this branch carries their heads, so that taking main after their squash merges changes nothing in this tree. The allocation-bound ceiling and the pinned count add the groups of both sides (1418 -> 1422, 62 -> 66), and 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]>
…l text The branches 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]>
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 1 advisory finding(s) below merit a look before merge.
Not checked: tests were not run; formal verification proved 0 of 8 changed function(s) (8 not verified).
Formal verification. PR-changed functions: 0/8 verified (0 proven, 0 may-equivalent, 0 distinguished) · 8 not verified (8 unsupported).
Not verified on this run: timezone\_classify (unsupported), tz\_file\_actual\_path (unsupported), tz\_file\_is\_tzif (unsupported), tzdir\_canonical (unsupported), tzdir\_realpath\_dir (unsupported), close (unsupported), project (unsupported), resolve\_script\_timezone (unsupported).
Graphify review — findings
Fails the run with an engine error when a timezone database lookup hits descriptor or memory exhaustion, an I/O error, or a missing database, instead of reporting a wrong timezone name. Floors strategy.close(qty = ...) and positive strategy.exit(qty = ...) quantities to the declared quantity step to match TradingView; a close that floors to zero or exceeds the entry's position closes the whole position, and a sub-step exit covers the whole position. Rewrites the refusal for non-fitting input, script or intrabar timeframes to name the timeframes and the rule, with the same data in the structured error args, so the run_options_rejected message templates now have to resolve every placeholder they use.
Worth a look
- Undeclared
{example}placeholder in refusal messages —docker/run_failure_codes.json:1480· Escalate · medium- unverified hypothesis — survived the adversarial refute pass; needs human review
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 — 1827 functions depend on the 1397 functions this change touches.
Health — this change adds coupling hotspots:
- worse:
tf_change()— 28 callers, 5 callees - worse:
crosses_boundary()— 7 callers, 8 callees - worse:
interval_containing()— 10 callers, 5 callees - worse:
build_native_day_partition()— 9 callers, 5 callees - worse:
configure()— 7 callers, 6 callees - worse:
timezone_classify()— 4 callers, 9 callees - worse:
session_day_at()— 8 callers, 4 callees - worse:
timeframe_time_close()— 2 callers, 12 callees - …and 13 more — each is listed as a finding
Verification — 1827 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: 1622 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
1427 of 1427 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 1377 more
non-code file(s) changed (
CHANGELOG.md,ci/cr150-source-bounds.yaml,ci/cr150-source-sites.json,docker/run_failure_codes.json,docker/run_failure_codes_diff.json…) → 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/coverage.md,docs/design/BARS-BIN-INPUT.md,docs/design/arena-bound.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 timezone\_classify.
The verifier did not have enough to check timezone\_classify, 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: signature arity changed (1 -> 2 positional params)
Could not verify: Could not verify tz\_file\_actual\_path.
The verifier did not have enough to check tz\_file\_actual\_path, 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: signature arity changed (1 -> 2 positional params)
Could not verify: Could not verify tz\_file\_is\_tzif.
The verifier did not have enough to check tz\_file\_is\_tzif, 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: signature arity changed (1 -> 2 positional params)
Could not verify: Could not verify tzdir\_canonical.
The verifier did not have enough to check tzdir\_canonical, 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: signature arity changed (0 -> 1 positional params)
Could not verify: Could not verify tzdir\_realpath\_dir.
The verifier did not have enough to check tzdir\_realpath\_dir, 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: signature arity changed (2 -> 3 positional params)
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
Could not verify: Could not verify project.
The verifier did not have enough to check project, 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
Could not verify: Could not verify resolve\_script\_timezone.
The verifier did not have enough to check resolve\_script\_timezone, 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 parameter type(s)
· 11 grounded finding(s) anchored inline below; 10 more finding(s) on lines outside this diff (see the check run).
| // one, observational descriptor and optional acceleration callers retain one | ||
| // attempt per interrupted call and nullopt for failures. Both modes accumulate | ||
| // partial headers. Argument validation requests typed failures. | ||
| std::optional<std::string> tz_file_actual_path(std::string_view name, |
There was a problem hiding this comment.
tz_file_actual_path()
high coupling complexity (Ca·Ce = 12).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| } | ||
|
|
||
| TzClass timezone_classify(std::string_view tz) { | ||
| TzClass timezone_classify(std::string_view tz, bool report_failure = false) { |
There was a problem hiding this comment.
timezone_classify()
fans out to 9 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return status; | ||
| } | ||
|
|
||
| void expect_refusal(const Refusal& row) { |
There was a problem hiding this comment.
expect_refusal()
fans out to 8 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| row.name, "timeframe refusal exposes no internal field"); | ||
| } | ||
|
|
||
| void expect_minute_input_accepted() { |
There was a problem hiding this comment.
expect_minute_input_accepted()
fans out to 7 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| bool escaped = false, report = false; | ||
| }; | ||
|
|
||
| Outcome run(const char* zone = valid_zone) { |
There was a problem hiding this comment.
run()
high coupling complexity (Ca·Ce = 15).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| #endif | ||
|
|
||
| #if defined(__GLIBC__) | ||
| void missing_directory_rows(const Outcome& clean) { |
There was a problem hiding this comment.
missing_directory_rows()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| // Other platforms compile the clean-run and invalid-name controls below. | ||
| #endif | ||
|
|
||
| int main() { |
There was a problem hiding this comment.
main()
fans out to 17 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| CHECK(read_attempts == 0 && !read_fired); | ||
| } | ||
|
|
||
| void setter_failure_survives_rendering_failure() { |
There was a problem hiding this comment.
setter_failure_survives_rendering_failure()
fans out to 14 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| CHECK(report_bytes(following.value) == report_bytes(expected.value)); | ||
| } | ||
|
|
||
| void rendering_failure_keeps_setter_code() { |
There was a problem hiding this comment.
rendering_failure_keeps_setter_code()
fans out to 9 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| std::size_t configure_allocation = 0; | ||
| }; | ||
|
|
||
| SpanOutcome complete_span_run(std::size_t first, std::size_t span) { |
There was a problem hiding this comment.
complete_span_run()
fans out to 8 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
Engine main took the 1.12.0 release, a CI change and the close quantity step fix. This branch still carries the earlier commits of that fix, while main has it as one squashed commit, so the merge from their common base reports conflicts in the files both touched. Each is resolved to main's version plus this branch's own change; apart from the files named below, the merged tree adds exactly this branch's change to main. - docs/design/native-feature-parity.md, row P8: main's wording, with the line numbers of the merged sources. - docker/run_failure_codes_diff.json: regenerated with scripts/gen_run_failure_catalog_diff.py --write against the 1.12.0 catalog, not merged by hand. Co-Authored-By: Claude Opus 5.5 <[email protected]>
One commit on main 2bbdd48 with the tree of the five members merged in order: #849 (5c9bfc4), #846 (fdbe3b4), #845 (434cdc5), #847 (e04ccd2), #842 (c33724c). #842 carries #841: #841's three commits are ancestors of #842's head. == #849 == Every CI round, post-merge included, now takes 15 minutes or less. GitHub CI keeps smoke legs on every configuration; the full profiles leave GitHub CI for the lab's verify, which every merge already requires on the exact head tree (pineforge/verify). Measured: post-merge run 43205609467 took 123.5 minutes (32 of 49 jobs over 15 minutes); each full-profile leg rebuilt the engine from a cold compiler cache in 11-40 minutes on a standard 4-vCPU runner, and 27 single CTest rows ran over 15 minutes on their own. PR run 43242369382 took 75.3 minutes, 73.9 of them in build-arm's unsharded ci_verify.py debug step. The parity subset step ran 13.0 and 13.75 minutes in those two runs: after the runtime it built its 54 strategy libraries one at a time (6 min 53 s), because targets named one by one on a build command line are built serially by a Makefile generator. Run 43308686886 of the new ci-post.yml took 9 min 39 s end to end, queue included; its parity subset job took 6.8 minutes. - ci-post.yml: authz, changes, build-x86-gcc133, preflight (fast tier), smoke and smoke-arm (Release and Debug on ubuntu-24.04 and ubuntu-24.04-arm: pineforge_pr_smoke and ctest -L '^fast$'), sanitizers (the same smoke under ASan and UBSan), corpus-parity-subset and the build aggregate. Removed: preflight-slow, the shard matrices and their aggregates, the native-live call, the full corpus-parity call and the cold_native_cache input. - gke-pr.yml: build-arm runs the Debug smoke on ubuntu-24.04-arm. - corpus-parity.yml: no schedule; workflow_dispatch and workflow_call stay. - scripts/check_corpus_parity.sh --subset and CMakeLists.txt: the subset's strategy libraries build as the dependencies of one aggregate target (pineforge_corpus_parity_subset), JOBS at a time; a target the corpus does not define stops the generation. - scripts/ci_preflight.py pins the new shape; scripts/ci_release_gate.py requires the four smoke check names; the tests follow. - docs/ci.md ("Where the full suites run"), docs/reference/ci-runner-routing.md, AGENTS.md and CLAUDE.md: where each cut suite now runs, the suites the lab does not run yet and the interim rule for them, and the release rule. Doc reverts, each replaced by the new shape's text in docs/ci.md: - 41f08d2: "Complete Release, Debug, native, kernel and parity profiles plus the required x86 build" (the ci-post.yml row of the trigger map). - f85ce18: "Full parity and its post-merge subset" (the corpus-parity.yml row of the trigger map). - 3a0d150: "Scheduled and manual runs execute every profile." and "Schedule, dispatch, post CI callers" (the tiers paragraph and the corpus-parity.yml row of the workflow table). - 8154993: "Dispatch and post CI callers on `main`" (the native-live.yml row of the workflow table). - 3ea5a0d: "All three build jobs refuse an OS mismatch.", "Every shard builds the full tree and runs `--ctest-shard I/N` at the runner's core count." and "Each build refuses an OS mismatch before checkout." (the post build jobs are smoke legs now; smoke and smoke-arm refuse a machine mismatch). - e7fa559: "Release uses four `build` shards, Debug two `build-debug` shards, and kernel two shards; sanitizers uses four, all on the pinned hosted images." (no post shard matrix remains). - "`preflight-slow` runs exactly those stages with `--tier slow`" and "Toolchain setup and the hash-pinned pytest install stay in the slow job." (no GitHub job runs the slow tier; the lab's preflight step runs every tier). - "the caller's `build` aggregate still requires the reusable workflow to succeed" (ci-post.yml no longer calls native-live.yml). - "retains the subset through `corpus-parity.yml` (`mode: subset`) alongside its full-parity call" (there is no full-parity call). - "The GKE Linux arm64 Debug leg runs after merge; a red result there blocks the next release cut under the same fix-or-revert rule." and "The GKE Linux arm64 Debug leg also runs before merge when `arm_relevant=true`; a red post-merge result blocks the next release cut under the same rule." (the leg is a smoke now; the full arm64 Debug profile left GitHub CI). - "the full-profile commands in `ci-post.yml` carry no explicit `--exclude-label` arguments" and "PR CI keeps the Release smoke selected by CTest label `fast`." (the commands run in the lab's verify; GitHub CI keeps the smoke on every configuration). - "Pull requests run the Ubuntu amd64 Release smoke; post-merge runs Ubuntu amd64 and arm64 Release/Debug." and "Both architectures use the same full-profile commands." (the `build`, `build-arm` row of the job table). - docs/reference/ci-runner-routing.md: "Post-merge `ci-post.yml` runs the full profiles on x86 and arm64, plus native and parity callers; there is no macOS cell." and "the post-merge legs (`ci-post.yml`) run their full profile populations, and a documentation-only PR runs admission, preflight and the docs build while skipping proof legs." == #849 commits == --- ci: keep every CI round under 15 minutes (smoke legs in GitHub CI) Every CI round, post-merge included, now takes 15 minutes or less. GitHub CI keeps smoke legs on every configuration; the full profiles leave GitHub CI for the lab's verify, which every merge already requires on the exact head tree (pineforge/verify). Measured: post-merge run 43205609467 took 123.5 minutes (32 of 49 jobs over 15 minutes); each full-profile leg rebuilt the engine from a cold compiler cache in 11-40 minutes on a standard 4-vCPU runner, and 27 single CTest rows ran over 15 minutes on their own. PR run 43242369382 took 75.3 minutes, 73.9 of them in build-arm's unsharded ci_verify.py debug step. The parity subset step ran 13.0 and 13.75 minutes in those two runs: after the runtime it built its 54 strategy libraries one at a time (6 min 53 s), because targets named one by one on a build command line are built serially by a Makefile generator. - ci-post.yml: authz, changes, build-x86-gcc133, preflight (fast tier), smoke and smoke-arm (Release and Debug on ubuntu-24.04 and ubuntu-24.04-arm: pineforge_pr_smoke and ctest -L '^fast$'), sanitizers (the same smoke under ASan and UBSan), corpus-parity-subset and the build aggregate. Removed: preflight-slow, the shard matrices and their aggregates, the native-live call, the full corpus-parity call and the cold_native_cache input. - gke-pr.yml: build-arm runs the Debug smoke on ubuntu-24.04-arm. - corpus-parity.yml: no schedule; workflow_dispatch and workflow_call stay. - scripts/check_corpus_parity.sh --subset and CMakeLists.txt: the subset's strategy libraries build as the dependencies of one aggregate target (pineforge_corpus_parity_subset), JOBS at a time; a target the corpus does not define stops the generation. - scripts/ci_preflight.py pins the new shape; scripts/ci_release_gate.py requires the four smoke check names; the tests follow. - docs/ci.md ("Where the full suites run"), AGENTS.md and CLAUDE.md: where each cut suite now runs, the suites the lab does not run yet and the interim rule for them, and the release rule. Doc reverts, each replaced by the new shape's text in docs/ci.md: - 41f08d2: "Complete Release, Debug, native, kernel and parity profiles plus the required x86 build" (the ci-post.yml row of the trigger map). - f85ce18: "Full parity and its post-merge subset" (the corpus-parity.yml row of the trigger map). - 3a0d150: "Scheduled and manual runs execute every profile." and "Schedule, dispatch, post CI callers" (the tiers paragraph and the corpus-parity.yml row of the workflow table). - 8154993: "Dispatch and post CI callers on `main`" (the native-live.yml row of the workflow table). - 3ea5a0d: "All three build jobs refuse an OS mismatch.", "Every shard builds the full tree and runs `--ctest-shard I/N` at the runner's core count." and "Each build refuses an OS mismatch before checkout." (the post build jobs are smoke legs now; smoke and smoke-arm refuse a machine mismatch). - e7fa559: "Release uses four `build` shards, Debug two `build-debug` shards, and kernel two shards; sanitizers uses four, all on the pinned hosted images." (no post shard matrix remains). --- docs: bring the CI pages in line with the smoke-leg post-merge workflow The change that moved the full profiles out of GitHub CI left sentences in docs/ci.md and docs/reference/ci-runner-routing.md that still described the old post-merge workflow. They now say what runs: smoke legs in ci-post.yml, the full profiles and the full parity in the lab's verify, no preflight-slow job, and the measured time of the parity subset job (6.8 minutes in run 43308686886, against 13.5 and 14.3 before its strategy libraries built in parallel). docs/ci.md keeps its line count. Two comments in scripts/test_ci_preflight.py call a CI leg a leg. Doc reverts, each replaced by a sentence about the new shape: - "`preflight-slow` runs exactly those stages with `--tier slow`" and "Toolchain setup and the hash-pinned pytest install stay in the slow job." (no GitHub job runs the slow tier; the lab's preflight step runs every tier). - "the caller's `build` aggregate still requires the reusable workflow to succeed" (ci-post.yml no longer calls native-live.yml). - "retains the subset through `corpus-parity.yml` (`mode: subset`) alongside its full-parity call" (there is no full-parity call). - "The GKE Linux arm64 Debug leg runs after merge; a red result there blocks the next release cut under the same fix-or-revert rule." and "The GKE Linux arm64 Debug leg also runs before merge when `arm_relevant=true`; a red post-merge result blocks the next release cut under the same rule." (the leg is a smoke now; the full arm64 Debug profile left GitHub CI). - "the full-profile commands in `ci-post.yml` carry no explicit `--exclude-label` arguments" and "PR CI keeps the Release smoke selected by CTest label `fast`." (the commands run in the lab's verify; GitHub CI keeps the smoke on every configuration). - "Pull requests run the Ubuntu amd64 Release smoke; post-merge runs Ubuntu amd64 and arm64 Release/Debug." and "Both architectures use the same full-profile commands." (the `build`, `build-arm` row of the job table). - docs/reference/ci-runner-routing.md: "Post-merge `ci-post.yml` runs the full profiles on x86 and arm64, plus native and parity callers; there is no macOS cell." and "the post-merge legs (`ci-post.yml`) run their full profile populations, and a documentation-only PR runs admission, preflight and the docs build while skipping proof legs." == #846 commits == --- Document that hosted x86 pull-request jobs run only the fast set Say in docs/ci.md that the hosted x86 pull-request jobs build and build-debug run only the CTest rows labelled fast, and that the maintainers' full ci_verify.py run, carried by pineforge/verify, is the only full x86 test gate. No existing sentence is removed or rewritten. --- Keep docs/ci.md at its pinned line count The fast-set sentence added six physical lines to docs/ci.md, and two assertions in scripts/test_ci_preflight.py pin that page at 2133 lines. Join the sentence onto the line it follows, so the page has 2133 lines again. No word changes and no other line moves. == #845 commits == --- Give ta.pvt no value on its first bar TradingView's price-volume trend adds a term that needs the previous bar's close, so its first bar has no value; the engine returned 0 there. The first valid bar now returns na and the cumulative sum starts from zero as before, so every later value is unchanged. Invalid inputs and a zero previous close behave as before, recompute follows compute, and the other volume accumulators are not changed. --- Test that ta.pvt has no value on its first valid bar A class test with an independent forward sum on four invented bar sequences (rising, falling and flat bars; leading missing closes; missing volume; a zero previous close) checks that the first valid bar has no value, that every later value equals the forward sum, and that recompute agrees with compute, including a recompute before any compute. The two older checks that pinned the first bar to zero now expect no value. --- Note the ta.pvt first-bar change in the changelog The first bar of ta.pvt has no value, as on TradingView; it returned 0 there before. Later values are unchanged. --- List the ta.pvt changelog entry beside the other ta entry The entry moves from the first position of the Unreleased list to the position after the ta.percentile_nearest_rank entry. Its words are unchanged. Other open changes add their entries at the first position, so this one no longer meets them there. --- Give ta.obv no value on its first bar TradingView's on-balance volume adds a term that needs the previous bar's close, so its first bar has no value; the engine returned 0 there. The first valid bar now returns na and does the same state updates as before, so the running sum starts from zero and every later value is unchanged. Invalid inputs behave as before and recompute follows compute. The other volume accumulators are not changed: on their first bar they already give TradingView's value. --- Test that ta.obv has no value on its first valid bar A class test with an independent forward sum on four invented bar sequences (rising, falling and equal closes; leading missing closes; a missing volume; zero volume and a zero close) checks that the first valid bar has no value, that every later value equals the forward sum, and that recompute agrees with compute, including a recompute before any compute and recomputes with other inputs than compute saw. One such recompute row is added for ta.pvt as well. The two older checks that pinned the first bar of ta.obv to zero now expect no value. --- Note the ta.obv first-bar change in the changelog The entry for ta.pvt now names ta.obv as well: both have no value on the first bar of their series, as on TradingView, and every later value is unchanged. --- Point the private-fixture evidence at the moved test list lines The two first-bar tests add two lines to tests/CMakeLists.txt, so the line that tests/private_fixture_reads.evidence cites moved from 3473 to 3475, and the line its reason names moved from 3490 to 3492. Only those two numbers change. == #847 commits == --- test: exercise source portability guard failure boundaries Add direct-call, physical-line and fail-closed input discriminators before the portability scanner repairs. The test-only revision deliberately exposes the existing spelling and form-feed defects in the following remote RED run. --- fix: detect direct sync calls using physical source lines Recognize bare sync calls alongside global calls while excluding members. Keep reachability and reported call positions on the same newline basis. == #842 commits == --- Build an N-day chart on the year-restart N-day grid 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. --- Re-anchor the line citations of the published pages (digits only) 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. --- Floor an explicit close quantity to the quantity step 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. --- Re-anchor the line citations of the published pages (digits only) 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). --- Fail the run when a timezone lookup cannot be carried out 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. --- Test the void setter record under consecutive allocation failures 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). --- Name the timeframes and the rule when a run's timeframes do not fit A run whose input, script or intrabar timeframes do not fit was refused with "Pine adapter produced invalid native run spec field 29" (or 4), and an args object that carried only the option name. The text read like an internal failure, the number was an internal enum value, and neither named the timeframes or the pairing that failed. The refusal now says which timeframes are involved, why they do not fit and what to pass instead, for example "input timeframe '2' cannot build script timeframe '15' (15 is not a multiple of 2); pass bars whose timeframe divides the script timeframe '15', for example 1-minute bars." The args keep the option name and add input_tf, script_tf, intrabar_tf when the bar magnifier is on, and a reason: not_divisible, script_finer_than_input, intrabar_not_divisible or script_finer_than_intrabar. The code stays run_options_rejected, which runs are refused or accepted does not change, and every other refusal keeps its text and args. The catalog declares the two new optional arguments and the new message templates; the generated registry, the catalog diff and the failure-code tables are regenerated by their scripts. The bar magnifier page says that passing the intrabar timeframe itself as the input is refused when it does not divide the chart timeframe. The allocation-bound manifest lists the refusal helper's three new groups (its failure arguments, rule text and message text; ceiling 1418 -> 1421, pinned count 1602 -> 1605) and its site inventory is re-emitted. 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 #813. --- Record a warning for a request.security timeframe that is not a multiple of the feed's A request.security call whose requested timeframe is not a multiple of the timeframe of the feed that evaluates it (75 minutes on a 60-minute feed) is accepted, and the script then reads the feed's own bars instead of the requested timeframe's. The run now records one named warning for each such site, request_timeframe_not_multiple, with the feed and requested timeframes and the site identity, and exposes the warnings through the new strategy_get_run_warnings accessor as one canonical JSON array. The accessor is additive: no struct is added and no existing symbol or layout changes (ABI 5, stream layout 4). It answers "[]" when the run recorded no warning, before any run, and after a later attempt that was refused before it began; a live stream's warnings stay readable. If collecting the warnings fails it answers a fixed run_warnings_unavailable element, never an empty list. Values, trades, run acceptance, the failure accessors and the state hashes are unchanged. Compatible pairings, lower-timeframe-array sites and sites with no loaded bars stay silent. The two canonical-JSON appenders of the run-failure writer move unchanged into a private header, so the warnings use the same writer. The Docker report and scripts/run_strategy.py read the optional getter after the report is assembled and before the strategy handle is released, and show a nonempty list as diagnostics.warnings. A library without the getter, or a run without warnings, produces the same report bytes as before. Unknown warning codes and their arguments are kept, malformed elements are dropped, and the warnings stay out of sink records and fingerprints. scripts/run_strategy.py also returns the list, prints one summary line and includes it in a requested trace JSON. The new tests run both entry points against a copy of the stdout stub that adds a canned warning, a fallback warning and selected-window support, and against a build of the same stub without the getter. The allocation-bound manifest lists the ten new groups of the warning records and their strings (ceiling 1421 -> 1431, pinned count 1605 -> 1615), points the entry of the moved appenders at the private header, and its site inventory is re-emitted. The lines added by this change 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 #718. --- Keep the run warnings getter read-only on the current C boundary The void C entry point wrapper now takes the strategy handle and records an escaped exception on it as the handle's failure. The run warnings getter was written against the earlier one-argument wrapper and no longer compiled. It now passes no handle, so it still records nothing on the handle, as before; the function it calls cannot throw. Co-authored-by: Claude Opus 5.5 <[email protected]>
What was wrong
A run whose input, script or intrabar timeframes do not fit was refused with "Pine adapter produced invalid native run spec field 29" (or 4) and an args object that carried only the option name. The text read like an internal failure, the number was an internal enum value, and neither named the timeframes or the pairing that failed.
What changes
input_tfandscript_tf;intrabar_tfwhen the bar magnifier is on;reason:not_divisible,script_finer_than_input,intrabar_not_divisibleorscript_finer_than_intrabar.run_options_rejected. Which runs are refused or accepted does not change, and every other refusal keeps its text and args.Scope in plain words
There is no new failure code, function, field or macro, and only the text and args of these refusals change. Runs whose timeframes fit are unchanged.
Tests
tests/test_run_refusal_timeframes.cpp(new, registered bytests/cr150.d/run-refusal-timeframes.cmake) runs the generated batch entry point's checked boundary.Release
The catalog gains two optional arguments and new message templates (additive). There is one
CHANGELOG.mdbullet in the unreleased section.Squash commit message
🤖 Generated with Claude Code