Skip to content

Build an N-day chart on the year-restart N-day grid - #829

Merged
luisleo526 merged 5 commits into
mainfrom
fix/n-day-chart-timeframe-v3
Nov 12, 2026
Merged

luisleo526 merged 5 commits into
mainfrom
fix/n-day-chart-timeframe-v3

Conversation

@luisleo526

Copy link
Copy Markdown
Collaborator

What changes

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.

Scope that was measured, and the limits

Multi-day charts restart their trading-date grid each year; measured on 24x7 daily input for chart bars, last_bar_index, barstate.islast and the open-trade report row. Streams keep the previous chart path; selected windows on generated N-day chart partitions are refused (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

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.

🤖 Generated with Claude Code

🤖 Generated with Claude Code

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]>

@graphify-labs graphify-labs Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 13 changed function(s) (13 not verified).

Formal verification. PR-changed functions: 0/13 verified (0 proven, 0 may-equivalent, 0 distinguished) · 13 not verified (13 unsupported).

Not verified on this run: continuation\_hash (unsupported), final\_script\_session\_closed (unsupported), next (unsupported), prepare\_public\_begin (unsupported), script\_bucket\_completions (unsupported), script\_interval\_resolved (unsupported), seal\_script (unsupported), run\_begin (unsupported), chart\_bar\_close\_ms (unsupported), on\_native\_run\_begin (unsupported), and 3 more.


Graphify review — findings

Adds a host-supplied chart partition so multi-day charts restart their N-day trading-date grid each year, with interval_containing now resolving a script interval from the partition's adjacent opens while the calendar still supplies eligible spans and the next input open. The consumer uses it for script intervals, bucket completions, sealing and the continuation hash; streams and sources without period_bounds fall back to the previous nominal-calendar path, and a leading bucket opening before the partition's input range is dropped without consuming a script index. When no session span overlaps the partition, interval_containing still returns an interval built from the partition's own open and close, so the interval it reports can describe a period that never traded.

Worth a look

  • Empty partitions produce a fabricated interval — src/native_calendar.cpp:1563 · 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 — 2338 functions depend on the 1786 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: native_run_spec_digest() — 17 callers, 3 callees
  • worse: interval_containing() — 10 callers, 5 callees
  • worse: build_native_day_partition() — 9 callers, 5 callees
  • worse: session_day_containing() — 9 callers, 4 callees
  • worse: session_day_at() — 8 callers, 4 callees
  • worse: timeframe_time_close() — 2 callers, 12 callees
  • …and 10 more — each is listed as a finding

Verification — 2338 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: 2307 function(s) in the blast radius were not formally verified this run

Test selection

Test selection

1418 of 1418 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-safety
  • benchmarks/tests/test_check_provenance.py — full-run-safety
  • benchmarks/tests/test_compare.py — full-run-safety
  • benchmarks/tests/test_public_names.py — full-run-safety
  • benchmarks/tests/test_run_all.py — full-run-safety
  • benchmarks/tests/test_run_strategy_feeds.py — full-run-safety
  • docker/pineforge_sink_finalize_test.py — full-run-safety
  • docker/pineforge_sink_replay_rq_test.py — full-run-safety
  • docker/pineforge_sink_replay_test.py — full-run-safety
  • docker/pineforge_sink_to_json_test.py — full-run-safety
  • docker/quota_columns_test.py — full-run-safety
  • docker/quota_convert_test.py — full-run-safety
  • docker/quota_finalize_test.py — full-run-safety
  • docker/replay_rq_engine_test.py — full-run-safety
  • docker/run_json_artifact_window_test.py — full-run-safety
  • docker/run_json_bin_test.py — full-run-safety
  • docker/run_json_bounded_transport_test.py — full-run-safety
  • docker/run_json_bulk_equity_fuzz_test.py — full-run-safety
  • docker/run_json_bulk_equity_test.py — full-run-safety
  • docker/run_json_codes_test.py — full-run-safety
  • docker/run_json_converter_metadata_test.py — full-run-safety
  • docker/run_json_converter_session_test.py — full-run-safety
  • docker/run_json_diagnostics_test.py — full-run-safety
  • docker/run_json_equity_scan_test.py — full-run-safety
  • docker/run_json_fused_test.py — full-run-safety
  • docker/run_json_harnesses_test.py — full-run-safety
  • docker/run_json_metrics_only_test.py — full-run-safety
  • docker/run_json_misc2_test.py — full-run-safety
  • docker/run_json_misc_test.py — full-run-safety
  • docker/run_json_pfbar_test.py — full-run-safety
  • docker/run_json_provenance_test.py — full-run-safety
  • docker/run_json_quota_supervisor_test.py — full-run-safety
  • docker/run_json_replay_test.py — full-run-safety
  • docker/run_json_selected_sink_test.py — full-run-safety
  • docker/run_json_sink_lexical_test.py — full-run-safety
  • docker/run_json_sink_summary_test.py — full-run-safety
  • docker/run_json_sink_test.py — full-run-safety
  • docker/run_json_sink_vectors_test.py — full-run-safety
  • docker/run_json_spool_work_test.py — full-run-safety
  • docker/run_json_stringio_test.py — full-run-safety
  • docker/run_json_symbol_feeds_test.py — full-run-safety
  • docker/run_json_syminfo_test.py — full-run-safety
  • docker/run_json_trade_scan_test.py — full-run-safety
  • docker/run_json_window_budget_test.py — full-run-safety
  • docker/run_json_wrapper_work_test.py — full-run-safety
  • include/pineforge/native_run_spec.hpp — full-run-safety
  • scripts/derive_corpus_feeds_self_test.py — full-run-safety
  • scripts/fingerprint_self_test.py — full-run-safety
  • scripts/run_corpus_self_test.py — full-run-safety
  • scripts/test_abi_receipt_skips.py — full-run-safety
  • … and 1368 more

non-code file(s) changed (CHANGELOG.md, tests/cr150.d/n-day-chart-timeframe.cmake) → 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_scheduler_native.cpp, src/source/pine_strategy_host.cpp) — 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 continuation\_hash.

The verifier did not have enough to check continuation\_hash, 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 final\_script\_session\_closed.

The verifier did not have enough to check final\_script\_session\_closed, 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 next.

The verifier did not have enough to check next, 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 prepare\_public\_begin.

The verifier did not have enough to check prepare\_public\_begin, 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 script\_bucket\_completions.

The verifier did not have enough to check script\_bucket\_completions, 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 script\_interval\_resolved.

The verifier did not have enough to check script\_interval\_resolved, 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 seal\_script.

The verifier did not have enough to check seal\_script, 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 run\_begin.

The verifier did not have enough to check run\_begin, 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 chart\_bar\_close\_ms.

The verifier did not have enough to check chart\_bar\_close\_ms, 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 on\_native\_run\_begin.

The verifier did not have enough to check on\_native\_run\_begin, 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 pine\_time\_offset.

The verifier did not have enough to check pine\_time\_offset, 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 tf\_ratio.

The verifier did not have enough to check tf\_ratio, 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: C++ compile failed: | ^~~~~~~~~~~~~~

Could not verify: Could not verify tf\_to\_seconds.

The verifier did not have enough to check tf\_to\_seconds, 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: C++ compile failed: | ^~~~~~~~~~~~~~~

· 8 grounded finding(s) anchored inline below; 10 more finding(s) on lines outside this diff (see the check run).

Comment thread src/native_calendar.cpp
return core->open_ms;
}

std::optional<NativeInterval> interval_containing(const SessionCalendar& calendar,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — interval_containing()

10 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

assert(trade.exit_price == exit_price);
}

void three_days(const std::vector<Bar>& bars, int row) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — three_days()

fans out to 6 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

}
}

void other_days(const std::vector<Bar>& bars, int n, const std::vector<int>& targets) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — other_days()

fans out to 6 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

}
}

void controls(const std::vector<Bar>& bars) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — controls()

fans out to 6 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

assert(probe.rows.size() == 1);
}

void stream_chart(const std::vector<Bar>& bars) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — stream_chart()

fans out to 7 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

return window;
}

void selected_chart(const std::vector<Bar>& bars) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — selected_chart()

fans out to 9 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

assert(strategy_selected_window_counts_v1(handle, &counts) == -2);
}

void selected_controls(const std::vector<Bar>& bars) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — selected_controls()

fans out to 12 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

}
} // namespace

int main(int argc, char** argv) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — main()

fans out to 9 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

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]>
@luisleo526

Copy link
Copy Markdown
Collaborator Author

One commit on top (655c97e) re-points the documentation line citations that moved with the code in this branch. It changes digits only: a check over the patch reads 145 changed line pairs, 233 changed numbers and 0 changed words, so no sentence is added or altered. The documentation anchor check resolves 2345 of 2345 citations on this commit.

🤖 Generated with Claude Code

luisleo526 and others added 3 commits November 12, 2026 12:42
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]>
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]>
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]>
@luisleo526
luisleo526 merged commit 9aacd54 into main Nov 12, 2026
17 checks passed
luisleo526 added a commit that referenced this pull request Nov 12, 2026
…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]>
luisleo526 added a commit that referenced this pull request Nov 12, 2026
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]>
luisleo526 added a commit that referenced this pull request Nov 12, 2026
…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]>
luisleo526 added a commit that referenced this pull request Nov 12, 2026
…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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant