Repository navigation
Add SeriesPercentileNearestRank for a series length of ta.percentile_nearest_rank - #767
Conversation
TradingView accepts a series length for ta.percentile_nearest_rank. A call site that runs several times in one bar (a call in a loop) appends its source to its own history on every call, and the answer is the nearest-rank percentile of the last `length` appended values, na until that many values exist. On BINANCE:BTCUSDT 1h bars from 2024-01-01 to 2024-03-01 a loop over the lengths 2, 3 and 4 gives 184133.20 on bar 1440 with its first value on bar 1, where three separate calls give 183579.02 with their first value on bar 3. The new test drives pineforge::source::SeriesPercentileNearestRank, which does not exist on this commit, so it does not build here. Its expected values come from a model written in the test (an insertion-ranked window over the appended values), and the rows that hold a constant length also compare the class with ta::PercentileNearestRank, which is independent of it. Rows: the loop of three calls per bar over 1,441 bars; constant lengths with several percentages, once on finite sources (against the model and the constant-length class) and once with na sources (against the constant-length class, whose ordering of na among the other values depends on the order in which they arrived and left); a change of length and the return to the old one with na in the history; lengths that change from call to call; the error texts and codes for an na, zero or negative length and for a window past the 15,000 bar limit; recompute; and the bounded storage. The CMake row shifts the lines that tests/private_fixture_reads.evidence cites; the cited numbers are updated by reading the file. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…nearest_rank pineforge::source::SeriesPercentileNearestRank is the run-time class for a call of ta.percentile_nearest_rank whose length is not a constant: every compute() appends its source to the call site's own ring (kMaxBarsBack + 1 values, no more), even when the site runs several times in one bar, and answers the nearest-rank percentile of the last `length` appended values, na until that many exist. While the length stays the same a call pushes into one ta::PercentileWindow, the window the constant-length class ta::PercentileNearestRank uses, so for a constant length the two classes are the same procedure, including the position of na values among the others (which depends on the order in which they arrived and left, so a replay in time order would differ). After a change of length the window is rebuilt by pushing the last values of the ring in time order. recompute() replaces the value the previous call appended and repushes. The re-execution of a bar that holds a call site that runs more than once is not modelled, as with ta::PercentileNearestRank. An na, zero or negative length stops the run with the texts and codes of the other series-length classes, and a window reaching more than 15,000 bars back stops it with the same historical-buffer limit text; that text now comes from one helper that SeriesWindowExtreme uses as well, unchanged. The nearest-rank selection is the one of ta::PercentileNearestRank: its helper moves out of an unnamed namespace and is declared in ta.hpp, so that both classes share one definition. The source allocation manifest has entries for the ring, the corrected identities of the std::to_string sites that the shared limit helper moved, and the unreviewed ceiling for the two new entries. Co-Authored-By: Claude Sonnet 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 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: update (unsupported).
Graphify review — findings
Adds runtime series-length support to ta.percentile_nearest_rank via SeriesPercentileNearestRank, which keeps a per-call-site ring of kMaxBarsBack + 1 source values, appends on every call (including repeated calls within a bar), and returns na until length values exist. While the length is constant it ranks through a ta::PercentileWindow that follows the constant-length na ordering, and on a length change it rebuilds that window by replaying retained values in time order. Registers the three new storage sites as unreviewed in the CR150 bounds manifest and adds the changelog entry, while the new test's reference model and warm-up check carry the proof of na placement and length-change behaviour.
Worth a look
- Reference model ranks na values order-dependently —
tests/test_series_percentile_nearest_rank.cpp:52· Escalate · medium- NOT verified (no proof, no reproducing execution) — 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 — 1289 functions depend on the 1036 functions this change touches.
Health — this change adds coupling hotspots:
- new:
constant_lengths_and_percentages()— 1 callers, 6 callees - new:
memory_is_bounded()— 1 callers, 6 callees - new:
main()— 0 callers, 7 callees
Verification — 1289 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: 1278 function(s) in the blast radius were not formally verified this run
Formal verification
Could not verify: Could not verify update.
The verifier did not have enough to check update, 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
· 3 grounded finding(s) anchored inline below.
| require(first_value_bar == 1, "loop first value is on bar 1"); | ||
| } | ||
|
|
||
| void constant_lengths_and_percentages() { |
There was a problem hiding this comment.
constant_lengths_and_percentages()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return out.usage(); | ||
| } | ||
|
|
||
| void memory_is_bounded() { |
There was a problem hiding this comment.
memory_is_bounded()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
|
|
||
| } // namespace | ||
|
|
||
| int main() { |
There was a problem hiding this comment.
main()
fans out to 7 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
Plain merge of main into this branch. Main now has the run option series_origin_ms. The only conflict is the line-pinned citation row of tests/private_fixture_reads.evidence: that change adds eight lines to tests/CMakeLists.txt above the cited block and this branch adds one row above it, so every cited line moved by nine from the earlier main's; each cited line was re-read from the merged tests/CMakeLists.txt. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…source-layer lengths The paragraph of docs/coverage.md that lists which classes a series length re-windows now also says what a series length does for ta.percentile_nearest_rank: the call site appends its source at every call and the answer ranks the last `length` appended values (SeriesPercentileNearestRank). The sentence on which codegen release lowers such calls stays where it was and covers the calls listed before it; the percentile sentence follows it and says that only the codegen release that adds the lowering lowers that call. The paragraph also states the two shapes no TradingView tape pins: a call inside an `if`, and the replay of the retained values when the length changes. The five lines added move the lines the manifest comment cites in the same page (:895-896 to :900-901); the comment is updated. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Plain merge of main into this branch. Main now has #771 (ci: admit a hosted form for the release jobs with protected credentials), which changes scripts/ci_preflight.py and scripts/test_ci_preflight.py. This branch changes neither file, so the merge has no conflict and the resulting tree is this branch's previous tree plus exactly that change. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Summary
ta.percentile_nearest_rankwith a series length (a loop counter, or any value computed per bar) now runs. A new class,pineforge::source::SeriesPercentileNearestRank, keeps one buffer per call site: every call of the site appends its source value, and the answer is the nearest-rank percentile of the lastlengthappended values,nauntil that many values exist. That is TradingView's rule as the report in pineforge-codegen-oss#295 (item 1) shows it: a loop that calls the function with the lengths 2, 3 and 4 on one call site appends three values per bar, so its result differs from three separate calls (the figures below are the ones reported in the issue for TradingView: the loop gives 184133.20 on bar 1440 with its first value on bar 1; three separate calls give 183579.02 with their first value on bar 3).What changed
SeriesPercentileNearestRankininclude/pineforge/source/pine_ta_length.hppandsrc/source/pine_ta_length.cpp: a ring ofkMaxBarsBack + 1values that everycompute()appends to, and ata::PercentileWindowover the lastlengthvalues. While the length stays constant the class is the constant-length path (it reusesta::PercentileWindowandta::nearest_rank_of), so it equalsta::PercentileNearestRank,nahandling included. When the length changes, the window is rebuilt by replaying the retained values in time order.recompute()replaces the value the previous call of the site appended.na, zero or negative length, and a window that reaches more than 15000 bars back. The throw for the 15000 bound thatSeriesWindowExtreme::updatewrote inline is now the helpermax_bars_back_exceeded, whichupdateand the new class both call (same message, same fields).ta::nearest_rank_ofleft its anonymous namespace insrc/ta_misc.cppand is declared ininclude/pineforge/ta.hpp, so the class can call it.CHANGELOG.mdhas one bullet. The source allocation manifest gets three entries for the new class (its ring, the growth of the ring and its window; ceiling 1403 to 1406), its site inventory the records the gate proposes, and the count pin moves by three.Tests
tests/test_series_percentile_nearest_rank.cpp(public, synthetic data): the loop shape (lengths 2, 3 and 4 on one site over 1,441 bars, first value on bar 1), constant lengths and percentages against an independent model and againstta::PercentileNearestRank(also withnavalues), a length change over a retained history withnavalues, changing lengths, the typed errors with their exact texts (na, 0 and -3; a window of 15001 values answersna, one of 15002 stops),recompute()against a fresh site, and the storage bound (the ring stays atkMaxBarsBack + 1values after 100,000 appends). The expected values come from a model inside the test (an insertion-sorted copy of the lastlengthappended values andceil(pct / 100 * n)), not from the engine.What is not covered
navalues in the window follows the constant-length class (it depends on the order of the values); no tape says what TradingView does withnain this function.ifappends only when it runs; no tape pins that shape. A re-executed bar that contains a call site running more than once is not modelled, as for the constant-length class.ta::nearest_rank_ofis now declared in a public header.🤖 Generated with Claude Code