PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
pine_ta_length.cpp
Go to the documentation of this file.
1/*
2 * pine_ta_length.cpp — ta.* calls whose length is not a constant: checked
3 * simple lengths, series-length window extremes, the first-execution
4 * supertrend factor. The TradingView rules and their tapes are documented in
5 * <pineforge/source/pine_ta_length.hpp>.
6 */
7
9#include <pineforge/na.hpp>
11
12#include <algorithm>
13#include <climits>
14#include <cmath>
15#include <cstdio>
16#include <stdexcept>
17#include <string>
18
19namespace pineforge {
20namespace source {
21
22namespace {
23
24// "Error on bar N: " as TradingView prefixes a runtime error, when an
25// evaluation context names the bar.
26std::string bar_prefix() {
27 const ta::BarContext& ctx = ta::bar_context();
28 if (!ctx.installed) return std::string();
29 return "Error on bar " + std::to_string(ctx.bar_index) + ": ";
30}
31
32[[noreturn]] void length_not_positive(long long value, const char* function,
33 const char* argument) {
34 throw coded<std::runtime_error>(
36 {{"function", function}, {"argument", argument}, {"rule", "must_be_positive"}},
37 bar_prefix() + "Invalid value of the '" + argument +
38 "' argument (" + std::to_string(value) + ") in the '" +
39 function + "' function. It must be > 0.");
40}
41
42[[noreturn]] void length_na(const char* function, const char* argument) {
43 throw coded<std::runtime_error>(
45 {{"function", function}, {"argument", argument}, {"rule", "must_not_be_na"}},
46 bar_prefix() + "Invalid value of the '" + argument +
47 "' argument in the '" + function +
48 "' function. It must not be na");
49}
50
51// A Pine int length carried as a double: na stays na, anything else
52// truncates toward zero (Pine's int()), saturating far outside any window.
53bool length_value(double value, long long& out) {
54 if (is_na(value)) return false;
55 const double t = std::trunc(value);
56 if (t > 1e15) out = static_cast<long long>(1e15);
57 else if (t < -1e15) out = static_cast<long long>(-1e15);
58 else out = static_cast<long long>(t);
59 return true;
60}
61
62[[noreturn]] void factor_not_positive(double factor) {
63 char text[64];
64 if (factor == std::trunc(factor) && std::fabs(factor) < 1e15) {
65 std::snprintf(text, sizeof text, "%lld", static_cast<long long>(factor));
66 } else {
67 std::snprintf(text, sizeof text, "%.15g", factor);
68 }
69 throw coded<std::runtime_error>(
71 {{"function", "supertrend"}, {"argument", "factor"}, {"rule", "must_be_positive"}},
72 bar_prefix() + "Invalid value of the 'factor' argument (" + text +
73 ") in the 'supertrend' function. It must be > 0.");
74}
75
76} // namespace
77
78// --- simple lengths ---
79
80int simple_ta_length(double value, const char* function, const char* argument) {
81 long long length = 0;
82 if (!length_value(value, length)) length_na(function, argument);
83 if (length <= 0) length_not_positive(length, function, argument);
84 return length > INT_MAX ? INT_MAX : static_cast<int>(length);
85}
86
87int simple_ta_length(int value, const char* function, const char* argument) {
88 if (is_na(value)) length_na(function, argument);
89 if (value <= 0) length_not_positive(value, function, argument);
90 return value;
91}
92
93int simple_ta_length(std::int64_t value, const char* function, const char* argument) {
94 if (is_na(value)) length_na(function, argument);
95 if (value <= 0) length_not_positive(value, function, argument);
96 return value > INT_MAX ? INT_MAX : static_cast<int>(value);
97}
98
99// --- HeldHistory ---
100
101void HeldHistory::push(double value) {
102 if (tail_.size() == kChunk) {
103 frozen_.push_back(std::make_shared<const std::vector<double>>(std::move(tail_)));
104 tail_ = std::vector<double>();
105 }
106 if (tail_.capacity() == 0) tail_.reserve(kChunk);
107 tail_.push_back(value);
108 ++count_;
109}
110
111void HeldHistory::drop_front_chunks(std::size_t keep) {
112 std::size_t drop = 0;
113 while (drop < frozen_.size() &&
114 count_ - static_cast<long long>(kChunk) >= static_cast<long long>(keep)) {
115 count_ -= static_cast<long long>(kChunk);
116 first_bar_ += static_cast<long long>(kChunk);
117 ++drop;
118 }
119 if (drop) frozen_.erase(frozen_.begin(), frozen_.begin() + static_cast<long>(drop));
120}
121
122void HeldHistory::write(long long bar, double value, std::size_t keep) {
123 if (count_ == 0) {
124 first_bar_ = bar;
125 push(value);
126 return;
127 }
128 const long long last = last_bar();
129 if (bar < last) return; // an earlier bar is never rewritten
130 if (bar == last) {
131 tail_.back() = value;
132 return;
133 }
134 // Skipped bars hold the last value. Only the `keep` newest bars can ever
135 // be read, so a longer gap restarts the history at its readable part.
136 const double held = tail_.back();
137 long long gap = bar - last - 1;
138 if (gap >= static_cast<long long>(keep)) {
139 frozen_.clear();
140 tail_ = std::vector<double>();
141 count_ = 0;
142 first_bar_ = bar - static_cast<long long>(keep);
143 gap = static_cast<long long>(keep);
144 }
145 for (long long i = 0; i < gap; ++i) push(held);
146 push(value);
147 drop_front_chunks(keep);
148}
149
150double HeldHistory::at(long long bar) const {
151 const long long i = bar - first_bar_;
152 const std::size_t chunk = static_cast<std::size_t>(i) / kChunk;
153 const std::size_t offset = static_cast<std::size_t>(i) % kChunk;
154 if (chunk < frozen_.size()) return (*frozen_[chunk])[offset];
155 return tail_[offset];
156}
157
158// --- SeriesWindowExtreme ---
159
161 bool advance, bool want_max,
162 const char* function) {
163 long long bar;
164 long long origin;
165 const ta::BarContext& ctx = ta::bar_context();
166 if (ctx.installed) {
167 bar = ctx.bar_index;
168 origin = ctx.origin;
169 } else {
170 // No context: every compute() is its own bar and recompute() rewrites
171 // the current one, as ta::ExtremeRing counts.
172 if (advance || !has_bar_) ++own_bar_;
173 bar = own_bar_;
174 origin = 0;
175 }
176
177 if (length <= 0) length_not_positive(length, function, "length");
178 if (length - 1 > kMaxBarsBack) {
181 {{"limit", "max_bars_back"}, {"max", kMaxBarsBack}},
182 bar_prefix() +
183 "The script attempts to reference historical data that is too far from "
184 "the current bar (" + std::to_string(length - 1) +
185 " bars back). The historical buffer's limit is " +
186 std::to_string(kMaxBarsBack) + " bars.");
187 }
188
189 // Every call of one bar re-applies from the state the bar found.
190 if (!has_bar_ || bar != current_bar_) {
191 saved_state_ = state_;
192 current_bar_ = bar;
193 has_bar_ = true;
194 } else {
195 state_ = saved_state_;
196 }
197 BarState& s = state_;
198 if (!s.started) first_exec_bar_ = bar;
199 history_.write(bar, src, static_cast<std::size_t>(kMaxBarsBack) + 1);
200
201 const auto better = [want_max](double a, double b) { return want_max ? a > b : a < b; };
202 // A member of the window: 1 = a value, 0 = na (resets the running
203 // extremum), -1 = before the context's first bar (does not exist).
204 const auto member = [&](long long at, double& value) -> int {
205 if (at < origin) return -1;
206 if (at < first_exec_bar_ || at < history_.first_bar()) return 0;
207 value = history_.at(at);
208 return is_na(value) ? 0 : 1;
209 };
210 // Walks window offsets [from, to] oldest first into `running`.
211 const auto walk = [&](long long from_k, long long to_k, Cache& running) {
212 for (long long k = from_k; k >= to_k; --k) {
213 double v = 0.0;
214 const int kind = member(bar - k, v);
215 if (kind < 0) continue;
216 if (kind == 0) {
217 running.valid = false;
218 continue;
219 }
220 if (!running.valid || better(v, running.value)) {
221 running.valid = true;
222 running.value = v;
223 running.bar = bar - k;
224 }
225 }
226 };
227 const auto rescan = [&]() {
228 Cache fresh;
229 walk(length - 1, 0, fresh);
230 return fresh;
231 };
232
233 Cache answer; // an answer this call gives without caching it
234 bool na_source = false;
235 if (is_na(src)) {
236 s.cache.valid = false;
237 s.started = true;
238 na_source = true;
239 } else if (!s.started) {
240 s.cache = rescan();
241 s.started = true;
242 } else if (length > s.prev_length) {
243 if (first_exec_bar_ > origin && length > s.max_length) s.cache.valid = false;
244 if (!s.cache.valid || bar - s.cache.bar < length) {
245 walk(length - 1, s.prev_length, s.cache);
246 }
247 if (!s.cache.valid) {
248 answer.valid = true;
249 answer.value = src;
250 answer.bar = bar;
251 } else if (better(src, s.cache.value)) {
252 s.cache.value = src;
253 s.cache.bar = bar;
254 } else if (bar - s.cache.bar >= length) {
255 s.cache = rescan();
256 }
257 } else {
258 if (!s.cache.valid) {
259 s.cache = rescan();
260 } else if (better(src, s.cache.value)) {
261 s.cache.value = src;
262 s.cache.bar = bar;
263 } else if (bar - s.cache.bar >= length) {
264 s.cache = rescan();
265 }
266 }
267 s.prev_length = length;
268 s.max_length = std::max(s.max_length, length);
269
270 if (bar - origin < length - 1) return Result{na<double>(), na<double>()};
271 const Cache& result = answer.valid ? answer : s.cache;
272 if (na_source || !result.valid) return Result{na<double>(), 0.0};
273 return Result{result.value, -static_cast<double>(bar - result.bar)};
274}
275
276namespace {
277
278long long series_length(double length, const char* function) {
279 long long value = 0;
280 // TradingView reports an na length as 0 (RE10001).
281 if (!length_value(length, value)) length_not_positive(0, function, "length");
282 return value;
283}
284
285} // namespace
286
287double SeriesHighest::compute(double src, double length) {
288 return core_.update(src, series_length(length, "highest"), true, true, "highest").value;
289}
290double SeriesHighest::recompute(double src, double length) {
291 return core_.update(src, series_length(length, "highest"), false, true, "highest").value;
292}
293double SeriesLowest::compute(double src, double length) {
294 return core_.update(src, series_length(length, "lowest"), true, false, "lowest").value;
295}
296double SeriesLowest::recompute(double src, double length) {
297 return core_.update(src, series_length(length, "lowest"), false, false, "lowest").value;
298}
299double SeriesHighestBars::compute(double src, double length) {
300 return core_.update(src, series_length(length, "highestbars"), true, true, "highestbars")
301 .offset;
302}
303double SeriesHighestBars::recompute(double src, double length) {
304 return core_.update(src, series_length(length, "highestbars"), false, true, "highestbars")
305 .offset;
306}
307double SeriesLowestBars::compute(double src, double length) {
308 return core_.update(src, series_length(length, "lowestbars"), true, false, "lowestbars")
309 .offset;
310}
311double SeriesLowestBars::recompute(double src, double length) {
312 return core_.update(src, series_length(length, "lowestbars"), false, false, "lowestbars")
313 .offset;
314}
315
316// --- PineSupertrend ---
317
318ta::SupertrendResult PineSupertrend::step(double high, double low, double close, bool again) {
319 if (is_na(high) || is_na(low) || is_na(close)) {
320 if (again) atr_->recompute(high, low, close);
321 else atr_->compute(high, low, close);
323 }
324 const double atr = again ? atr_->recompute(high, low, close) : atr_->compute(high, low, close);
325 const double src = (high + low) / 2.0;
326 double upper = src + factor_ * atr;
327 double lower = src - factor_ * atr;
328 const double prev_lower = is_na(state_.prev_lower) ? 0.0 : state_.prev_lower;
329 const double prev_upper = is_na(state_.prev_upper) ? 0.0 : state_.prev_upper;
330 const double close1 = state_.prev_close;
331 // Comparisons with na are false, as Pine's are.
332 lower = (lower > prev_lower || close1 < prev_lower) ? lower : prev_lower;
333 upper = (upper < prev_upper || close1 > prev_upper) ? upper : prev_upper;
334 double direction;
335 if (is_na(state_.prev_atr)) {
336 direction = 1.0;
337 } else if (state_.prev_st == prev_upper) {
338 direction = close > upper ? -1.0 : 1.0;
339 } else {
340 direction = close < lower ? 1.0 : -1.0;
341 }
342 const double line = direction == -1.0 ? lower : upper;
343 state_.prev_upper = upper;
344 state_.prev_lower = lower;
345 state_.prev_st = line;
346 state_.prev_atr = atr;
347 state_.prev_close = close;
348 return ta::SupertrendResult{line, direction};
349}
350
351ta::SupertrendResult PineSupertrend::compute(double factor, double atr_period, double high,
352 double low, double close) {
353 if (!latched_) {
354 atr_.emplace(simple_ta_length(atr_period, "supertrend", "atrPeriod"));
355 // Only the latched factor is checked: an na one is accepted (and read
356 // as na), zero or a negative one stops the run on the first execution
357 // (tv/edge st-factor-zero / -neg: RE10001 on bar 0); a later 0 or -2
358 // is never read (st-factor-zero-later, 60/60 equal to factor 3).
359 if (!is_na(factor) && factor <= 0.0) factor_not_positive(factor);
360 factor_ = factor;
361 latched_ = true;
362 }
363 saved_state_ = state_;
364 return step(high, low, close, false);
365}
366
367ta::SupertrendResult PineSupertrend::recompute(double factor, double atr_period, double high,
368 double low, double close) {
369 if (!latched_) return compute(factor, atr_period, high, low, close);
370 state_ = saved_state_;
371 return step(high, low, close, true);
372}
373
374} // namespace source
375} // namespace pineforge
Base with a code: throw coded<std::invalid_argument>(code, {args}, text).
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
int t(int64_t c)
Definition color.hpp:45
int simple_ta_length(double value, const char *function, const char *argument)
BarContext & bar_context()
double na< double >()
Definition na.hpp:17
bool is_na(const Line &h)
Definition drawing.hpp:58