Skip to content

Add SeriesPercentileNearestRank for a series length of ta.percentile_nearest_rank - #767

Merged
luisleo526 merged 5 commits into
mainfrom
series-percentile-length
Nov 7, 2026
Merged

luisleo526 merged 5 commits into
mainfrom
series-percentile-length

Conversation

@luisleo526

Copy link
Copy Markdown
Collaborator

Summary

ta.percentile_nearest_rank with 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 last length appended values, na until 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

  • SeriesPercentileNearestRank in include/pineforge/source/pine_ta_length.hpp and src/source/pine_ta_length.cpp: a ring of kMaxBarsBack + 1 values that every compute() appends to, and a ta::PercentileWindow over the last length values. While the length stays constant the class is the constant-length path (it reuses ta::PercentileWindow and ta::nearest_rank_of), so it equals ta::PercentileNearestRank, na handling 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.
  • The failures are the typed ones of the highest/lowest family with the same texts: an na, zero or negative length, and a window that reaches more than 15000 bars back. The throw for the 15000 bound that SeriesWindowExtreme::update wrote inline is now the helper max_bars_back_exceeded, which update and the new class both call (same message, same fields).
  • ta::nearest_rank_of left its anonymous namespace in src/ta_misc.cpp and is declared in include/pineforge/ta.hpp, so the class can call it.
  • CHANGELOG.md has 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 against ta::PercentileNearestRank (also with na values), a length change over a retained history with na values, changing lengths, the typed errors with their exact texts (na, 0 and -3; a window of 15001 values answers na, one of 15002 stops), recompute() against a fresh site, and the storage bound (the ring stays at kMaxBarsBack + 1 values after 100,000 appends). The expected values come from a model inside the test (an insertion-sorted copy of the last length appended values and ceil(pct / 100 * n)), not from the engine.
  • Before the change the test does not build (the class is missing); after it the test and its four neighbouring rows pass. A control that removes the stop for a non-positive length fails the "zero length" row.
  • With the companion code generator change (a series length is lowered to this class instead of refused), the reporter's loop on a one-hour BTCUSDT feed gives 184133.2 on bar 1440 (TradingView: 184133.20) and its first value on bar 1; no other case of the reporter's 29 changes.

What is not covered

  • The rule for a changing length (a replay of the retained values into a new window) has no TradingView tape; only a constant length and the loop shape are pinned, the loop by the figures of the report. The rebuild on a change costs O(length) and a loop whose lengths all differ rebuilds on every call, so its cost grows with the loop bound (not measured).
  • The position of na values in the window follows the constant-length class (it depends on the order of the values); no tape says what TradingView does with na in this function.
  • A call inside an if appends 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.
  • The storage test checks that the storage does not change between 15001 and 100000 appends and that the payload is at least the ring's bytes; it is not a byte-exact bound.
  • ta::nearest_rank_of is now declared in a public header.
  • The engine's whole test suite (the hosted CI runs it) and a parity sweep were not run by the author.

🤖 Generated with Claude Code

luisleo526 and others added 2 commits November 7, 2026 02:10
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]>

@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 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() {

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 — 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() {

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 — memory_is_bounded()

fans out to 6 callees (efferent coupling).

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


} // namespace

int main() {

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 7 callees (efferent coupling).

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

luisleo526 and others added 3 commits November 7, 2026 07:56
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]>
@luisleo526
luisleo526 merged commit be584eb into main Nov 7, 2026
17 checks passed
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