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 immutable and shared between copies, so copying the object (the
97// script-state checkpoint does it every bar) costs one chunk at most.
99public:
100 static constexpr std::size_t kChunk = 1024;
101
102 bool empty() const { return count_ == 0; }
103 long long first_bar() const { return first_bar_; }
104 long long last_bar() const { return first_bar_ + count_ - 1; }
105 // Appends bars up to and including `bar`, each skipped bar holding the
106 // last value, then writes `value` for `bar`.
107 void write(long long bar, double value, std::size_t keep);
108 // The value held for `bar`; the caller keeps `bar` in
109 // [first_bar(), last_bar()].
110 double at(long long bar) const;
111
112private:
113 void push(double value);
114 void drop_front_chunks(std::size_t keep);
115
116 std::vector<std::shared_ptr<const std::vector<double>>> frozen_;
117 std::vector<double> tail_;
118 long long first_bar_ = 0; // the bar of frozen_[0][0] (or tail_[0])
119 long long count_ = 0; // bars held
120};
121
122// ta.highest / ta.lowest / ta.highestbars / ta.lowestbars with a series
123// length. Fitted with no mismatch on 5,647 recorded calls of 35 call sites
124// (lab tv, K-TA-DYNLEN scratch tv/val: win-every, win-cond, win-lazy,
125// win-first, win-late on BINANCE:BTCUSDT 15 and NYSE:F 15, win-synth on a
126// synthetic source; the reference model is tv/series_extreme_model.py):
127//
128// * The window is the last `length` BARS of the site's own source history:
129// a bar the site did not execute holds its last executed source (a call
130// inside an `if` or on the right of a lazy `and`), a bar before the site's
131// first execution is na, a bar before the context's first bar does not
132// exist. The ring of the const-length classes (never-written slots read 0,
133// stale slots alias) does not apply.
134// * The answer is na (and so is the *Bars offset) while bar_index <
135// length - 1. After that an na answer carries the offset 0.
136// * The extremum is read through a cached {value, bar}. A call whose length
137// exceeds the previous call's first walks the slots the longer window adds,
138// oldest first, into the cache; when the site first executed after the
139// context's first bar and the length exceeds every earlier length, the
140// cache is dropped before that walk. Then the source: a strictly better
141// source takes the cache (ties keep the older member); an invalid cache
142// answers the source for this call without storing it; an aged cache
143// (bar - cached bar >= length) rescans the window oldest first. An na
144// member met by a walk or a rescan resets the running extremum (the answer
145// is the extremum over the members newer than it); an na source answers na
146// and invalidates the cache.
147// * A length of 0, a negative length or an na length stops the run
148// (RE10001, na reported as 0); a window reaching more than 15000 bars back
149// stops it too (RE10008) -- pinned on ta.lowest / ta.highestbars / ta.sma
150// (tv/edge), and 4990, 5001 and 9000-bar windows answered in full.
152public:
153 static constexpr long long kMaxBarsBack = 15000;
154
155 struct Result {
156 double value; // na: warm-up, or no member
157 double offset; // -bars back of the extremum; na during warm-up
158 };
159 Result update(double src, long long length, bool advance, bool want_max,
160 const char* function);
161
162private:
163 struct Cache {
164 bool valid = false;
165 double value = 0.0;
166 long long bar = 0;
167 };
168 struct BarState {
169 Cache cache;
170 long long prev_length = 0;
171 long long max_length = 0;
172 bool started = false;
173 };
174
175 HeldHistory history_;
176 BarState state_;
177 BarState saved_state_; // the state before the current bar
178 long long current_bar_ = 0;
179 bool has_bar_ = false;
180 long long first_exec_bar_ = 0;
181 long long own_bar_ = -1; // cadence when no context is installed
182};
183
186
187public:
188 double compute(double src, double length);
189 double recompute(double src, double length);
190};
191
194
195public:
196 double compute(double src, double length);
197 double recompute(double src, double length);
198};
199
202
203public:
204 double compute(double src, double length);
205 double recompute(double src, double length);
206};
207
210
211public:
212 double compute(double src, double length);
213 double recompute(double src, double length);
214};
215
216// ta.supertrend(factor, atrPeriod) as TradingView computes it: the Pine
217// reference implementation (hl2 +/- factor * ta.atr(atrPeriod), the bands
218// ratcheted against nz(band[1]) and close[1], direction 1 while atr[1] is na)
219// with the factor and atrPeriod of the call site's FIRST execution for the
220// whole run. TradingView accepts a series factor but never reads it again:
221// with factors 1, 1.75, 2, 4, 4.5 and 5.5 after a first bar at 3 the line and
222// direction equal ta.supertrend(3, 10) on 330/330 bars of BINANCE:BTCUSDT 15
223// and NYSE:F 15 (tv/val st-factor); a factor of 2 on bar 0 only reads as 2 on
224// 112/112 bars; an na first factor reads as na: the lower band sticks at 0 and
225// the answer alternates 0 / na with direction -1 / 1 once atr[1] is valid,
226// 120/120 (st-latch A and B). The warm-up is the reference's too: bar 0
227// answers the line 0, bars 1 .. atrPeriod - 1 na, direction 1 throughout.
228// close[1] and the true range read the site's own previous execution (the
229// chart's for a site executed every bar).
231public:
232 ta::SupertrendResult compute(double factor, double atr_period, double high, double low,
233 double close);
234 ta::SupertrendResult recompute(double factor, double atr_period, double high, double low,
235 double close);
236
237private:
238 struct State {
239 double prev_upper = na<double>();
240 double prev_lower = na<double>();
241 double prev_st = na<double>();
242 double prev_atr = na<double>();
243 double prev_close = na<double>();
244 };
245 ta::SupertrendResult step(double high, double low, double close, bool again);
246
247 bool latched_ = false;
248 double factor_ = na<double>();
249 std::optional<ta::ATR> atr_;
250 State state_;
251 State saved_state_;
252};
253
254} // namespace source
255} // namespace pineforge
256
257// Feature macro: the classes above exist.
258#define PINEFORGE_HAS_TA_SERIES_LENGTH 1
decltype(auto) recompute(Make &&make, Args &&... args)
decltype(auto) compute(Make &&make, Args &&... args)
double at(long long bar) 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)
double compute(double src, double length)
double compute(double src, double length)
double recompute(double src, double length)
double recompute(double src, double length)
double compute(double src, double length)
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)
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:17
bool is_na(const Line &h)
Definition drawing.hpp:58