PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
pine_ta_length.hpp
Go to the documentation of this file.
1#pragma once
2// TradingView's ta.* calls whose length (or factor) is not a constant.
3//
4// A Pine length argument is qualified const, input, simple or series.
5// TradingView compiles a series length only for the functions that declare
6// it (ta.highest, ta.lowest, ta.highestbars, ta.lowestbars, ta.sma, ...):
7// ta.rma, ta.ema, ta.rsi, ta.atr, ta.dmi, ta.macd, ta.kc, ta.hma, ta.tsi,
8// ta.rci and ta.supertrend's atrPeriod refuse it at compile time (CE10123
9// "An argument of series int type was used but a simple int is expected").
10// Pinned with `lab tv` compile probes on 52 calls (K-TA-DYNLEN scratch
11// tv/acc). The generated code reaches this header only for a length the
12// constructor cannot take: a const or input length keeps the fixed-length
13// classes of <pineforge/ta.hpp> and their constructor.
14//
15// * A simple length is fixed for the whole run: TradingView answers exactly
16// the const-length call (rec-simple: ta.rsi/ema/rma/atr, chart and a
17// request.security("5") payload, 150/150 bars on BINANCE:BTCUSDT 15 and
18// NYSE:F 15; win-qual: a sparse ta.lowest / ta.highestbars / ta.sma with a
19// simple length equals the const one on 46/46 calls, never-written ring
20// slots reading 0 included). FirstCallBound builds the const-length class
21// from the arguments of the call site's first execution.
22// * A series length re-windows every call: SeriesHighest, SeriesLowest,
23// SeriesHighestBars and SeriesLowestBars implement the rule documented at
24// SeriesWindowExtreme.
25// * ta.supertrend accepts a series factor but uses the factor of its first
26// execution for the whole run (PineSupertrend).
27//
28// A length of 0, a negative length or an na length stops the run, as it
29// stops TradingView's (the texts below are TradingView's own).
30#include <pineforge/ta.hpp>
31#include <pineforge/na.hpp>
32
33#include <cstddef>
34#include <cstdint>
35#include <memory>
36#include <optional>
37#include <utility>
38#include <vector>
39
40namespace pineforge {
41namespace source {
42
43// The integer a simple-qualified length argument carries, checked the way
44// TradingView checks it on the call's first execution: na stops the run with
45// "Invalid value of the '<argument>' argument in the '<function>' function.
46// It must not be na" (RE10003), zero or a negative value with "Invalid value
47// of the '<argument>' argument (<value>) in the '<function>' function. It
48// must be > 0." (RE10001) -- pinned on ta.rsi / ta.ema with a simple length
49// of 0, -3 and na (tv/edge). A float value truncates toward zero, as Pine's
50// int() does.
51int simple_ta_length(double value, const char* function, const char* argument);
52int simple_ta_length(int value, const char* function, const char* argument);
53int simple_ta_length(std::int64_t value, const char* function, const char* argument);
54
55// A number argument of the classes below as the double they take: an int or
56// int64 na (the integer sentinel, not a NaN) stays na.
57inline double ta_number(double value) { return value; }
58inline double ta_number(int value) {
59 return is_na(value) ? na<double>() : static_cast<double>(value);
60}
61inline double ta_number(std::int64_t value) {
62 return is_na(value) ? na<double>() : static_cast<double>(value);
63}
64
65// A fixed-length TA object built on the first execution of its call site
66// from that execution's arguments (a simple length is the same on every
67// bar, so the first execution's value is the run's value). `make` returns
68// the object; later calls reuse it and never evaluate `make` again.
69// compute(make, args...) / recompute(make, args...) bind, then forward.
70template <class T>
72 std::optional<T> object_;
73
74public:
75 template <class Make>
76 T& bind(Make&& make) {
77 if (!object_) object_.emplace(std::forward<Make>(make)());
78 return *object_;
79 }
80 bool bound() const { return object_.has_value(); }
81
82 template <class Make, class... Args>
83 decltype(auto) compute(Make&& make, Args&&... args) {
84 return bind(std::forward<Make>(make)).compute(std::forward<Args>(args)...);
85 }
86 template <class Make, class... Args>
87 decltype(auto) recompute(Make&& make, Args&&... args) {
88 return bind(std::forward<Make>(make)).recompute(std::forward<Args>(args)...);
89 }
90};
91
92// The history a series-length window reads: one value per bar of the
93// evaluation context, from the call site's first execution on. A bar the site
94// executes holds its source; a bar it skips holds the value of the last
95// execution before it. The last kMaxBarsBack + 1 bars are kept. Full chunks
96// are shared between copies until written, so an in-memory checkpoint copies
97// at most the current chunk. Numeric scratch backing is also copy-on-write;
98// its resident cache size is independent of the retained lookback.
100public:
101 static constexpr std::size_t kChunk = 1024;
102
105 : values_(cache) {}
106 bool empty() const { return count_ == 0; }
107 long long first_bar() const { return first_bar_; }
108 long long last_bar() const { return first_bar_ + count_ - 1; }
109 // Appends bars up to and including `bar`, each skipped bar holding the
110 // last value, then writes `value` for `bar`.
111 void write(long long bar, double value, std::size_t keep);
112 // The value held for `bar`; the caller keeps `bar` in
113 // [first_bar(), last_bar()].
114 double at(long long bar) const;
115 WindowCacheCapacity cache_capacity() const { return values_.cache_capacity(); }
116 WindowCacheStats cache_stats() const { return values_.cache_stats(); }
117
118private:
119 void ensure_slots(std::size_t keep);
120 std::size_t slot(std::size_t offset) const;
121 void push(double value);
122 void drop_front_chunks(std::size_t keep);
123
125 std::size_t head_ = 0;
126 long long first_bar_ = 0; // the bar at head_
127 long long count_ = 0; // bars held
128};
129
130// ta.highest / ta.lowest / ta.highestbars / ta.lowestbars with a series
131// length. Fitted with no mismatch on 5,647 recorded calls of 35 call sites
132// (lab tv, K-TA-DYNLEN scratch tv/val: win-every, win-cond, win-lazy,
133// win-first, win-late on BINANCE:BTCUSDT 15 and NYSE:F 15, win-synth on a
134// synthetic source; the reference model is tv/series_extreme_model.py):
135//
136// * The window is the last `length` BARS of the site's own source history:
137// a bar the site did not execute holds its last executed source (a call
138// inside an `if` or on the right of a lazy `and`), a bar before the site's
139// first execution is na, a bar before the context's first bar does not
140// exist. The ring of the const-length classes (never-written slots read 0,
141// stale slots alias) does not apply.
142// * The answer is na (and so is the *Bars offset) while bar_index <
143// length - 1. After that an na answer carries the offset 0.
144// * The extremum is read through a cached {value, bar}. A call whose length
145// exceeds the previous call's first walks the slots the longer window adds,
146// oldest first, into the cache; when the site first executed after the
147// context's first bar and the length exceeds every earlier length, the
148// cache is dropped before that walk. Then the source: a strictly better
149// source takes the cache (ties keep the older member); an invalid cache
150// answers the source for this call without storing it; an aged cache
151// (bar - cached bar >= length) rescans the window oldest first. An na
152// member met by a walk or a rescan resets the running extremum (the answer
153// is the extremum over the members newer than it); an na source answers na
154// and invalidates the cache.
155// * A length of 0, a negative length or an na length stops the run
156// (RE10001, na reported as 0); a window reaching more than 15000 bars back
157// stops it too (RE10008) -- pinned on ta.lowest / ta.highestbars / ta.sma
158// (tv/edge), and 4990, 5001 and 9000-bar windows answered in full.
160public:
161 static constexpr long long kMaxBarsBack = 15000;
162
165 : history_(cache) {}
166 struct Result {
167 double value; // na: warm-up, or no member
168 double offset; // -bars back of the extremum; na during warm-up
169 };
170 Result update(double src, long long length, bool advance, bool want_max,
171 const char* function);
172
173private:
174 struct Cache {
175 bool valid = false;
176 double value = 0.0;
177 long long bar = 0;
178 };
179 struct BarState {
180 Cache cache;
181 long long prev_length = 0;
182 long long max_length = 0;
183 bool started = false;
184 };
185
186 HeldHistory history_;
187 BarState state_;
188 BarState saved_state_; // the state before the current bar
189 long long current_bar_ = 0;
190 bool has_bar_ = false;
191 long long first_exec_bar_ = 0;
192 long long own_bar_ = -1; // cadence when no context is installed
193};
194
197
198public:
199 SeriesHighest() = default;
200 explicit SeriesHighest(WindowCacheCapacity cache) : core_(cache) {}
201 double compute(double src, double length);
202 double recompute(double src, double length);
203};
204
207
208public:
209 SeriesLowest() = default;
210 explicit SeriesLowest(WindowCacheCapacity cache) : core_(cache) {}
211 double compute(double src, double length);
212 double recompute(double src, double length);
213};
214
217
218public:
219 SeriesHighestBars() = default;
220 explicit SeriesHighestBars(WindowCacheCapacity cache) : core_(cache) {}
221 double compute(double src, double length);
222 double recompute(double src, double length);
223};
224
227
228public:
229 SeriesLowestBars() = default;
230 explicit SeriesLowestBars(WindowCacheCapacity cache) : core_(cache) {}
231 double compute(double src, double length);
232 double recompute(double src, double length);
233};
234
235// ta.supertrend(factor, atrPeriod) as TradingView computes it: the Pine
236// reference implementation (hl2 +/- factor * ta.atr(atrPeriod), the bands
237// ratcheted against nz(band[1]) and close[1], direction 1 while atr[1] is na)
238// with the factor and atrPeriod of the call site's FIRST execution for the
239// whole run. TradingView accepts a series factor but never reads it again:
240// with factors 1, 1.75, 2, 4, 4.5 and 5.5 after a first bar at 3 the line and
241// direction equal ta.supertrend(3, 10) on 330/330 bars of BINANCE:BTCUSDT 15
242// and NYSE:F 15 (tv/val st-factor); a factor of 2 on bar 0 only reads as 2 on
243// 112/112 bars; an na first factor reads as na: the lower band sticks at 0 and
244// the answer alternates 0 / na with direction -1 / 1 once atr[1] is valid,
245// 120/120 (st-latch A and B). The warm-up is the reference's too: bar 0
246// answers the line 0, bars 1 .. atrPeriod - 1 na, direction 1 throughout.
247// close[1] and the true range read the site's own previous execution (the
248// chart's for a site executed every bar).
250public:
251 ta::SupertrendResult compute(double factor, double atr_period, double high, double low,
252 double close);
253 ta::SupertrendResult recompute(double factor, double atr_period, double high, double low,
254 double close);
255
256private:
257 struct State {
258 double prev_upper = na<double>();
259 double prev_lower = na<double>();
260 double prev_st = na<double>();
261 double prev_atr = na<double>();
262 double prev_close = na<double>();
263 };
264 ta::SupertrendResult step(double high, double low, double close, bool again);
265
266 bool latched_ = false;
267 double factor_ = na<double>();
268 std::optional<ta::ATR> atr_;
269 State state_;
270 State saved_state_;
271};
272
273} // namespace source
274} // namespace pineforge
275
276// Feature macro: the classes above exist.
277#define PINEFORGE_HAS_TA_SERIES_LENGTH 1
decltype(auto) recompute(Make &&make, Args &&... args)
decltype(auto) compute(Make &&make, Args &&... args)
WindowCacheCapacity cache_capacity() const
HeldHistory(WindowCacheCapacity cache)
double at(long long bar) const
WindowCacheStats cache_stats() const
void write(long long bar, double value, std::size_t keep)
static constexpr std::size_t kChunk
ta::SupertrendResult recompute(double factor, double atr_period, double high, double low, double close)
ta::SupertrendResult compute(double factor, double atr_period, double high, double low, double close)
double recompute(double src, double length)
SeriesHighestBars(WindowCacheCapacity cache)
double compute(double src, double length)
double compute(double src, double length)
double recompute(double src, double length)
SeriesHighest(WindowCacheCapacity cache)
double recompute(double src, double length)
double compute(double src, double length)
SeriesLowestBars(WindowCacheCapacity cache)
SeriesLowest(WindowCacheCapacity cache)
double compute(double src, double length)
double recompute(double src, double length)
Result update(double src, long long length, bool advance, bool want_max, const char *function)
SeriesWindowExtreme(WindowCacheCapacity cache)
static constexpr long long kMaxBarsBack
double ta_number(double value)
int simple_ta_length(double value, const char *function, const char *argument)
double na< double >()
Definition na.hpp:64
bool is_na(const Line &h)
Definition drawing.hpp:58
WindowCacheCapacity & window_cache_capacity()
Definition series.hpp:31