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