PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
pine_position_tail_summary.hpp
Go to the documentation of this file.
1#pragma once
2
4
9
10#include <algorithm>
11#include <cmath>
12#include <cstddef>
13#include <cstdint>
14#include <limits>
15#include <string>
16#include <unordered_map>
17#include <unordered_set>
18
20
22
23// The adapter's existing test-TU seam supplies PositionTailDiagnostics.
24// The default instantiation has no diagnostic branches, increments, clock
25// reads or callbacks. Neither choice depends on NDEBUG or a fixture value.
27 static constexpr bool enabled = false;
28};
30 static constexpr bool enabled = true;
31};
32
33template<class Diagnostics = PositionTailNoDiagnostics>
35 PositionTailGuard reason = PositionTailGuard::None;
36 PositionTailGuard value() const noexcept { return reason; }
38 if constexpr (Diagnostics::enabled)
39 if (reason == PositionTailGuard::None) reason = value;
40 }
41};
42template<>
44 constexpr PositionTailGuard value() const noexcept { return PositionTailGuard::None; }
45 void reject(PositionTailGuard) const noexcept {}
46};
47
48// A proof, not a differently rounded floating-point accumulator. Each term
49// is a nonnegative integer times 2^quantum; the numerator of the WHOLE
50// selected sum must be strictly below 2^53. Every subset/partial sum is then
51// exactly representable, including the original newest-first left fold.
52// Reducing the quantum checks integer shifts BEFORE doing them. No rounded
53// subtotal, cancellation or subtraction of a retired origin is used.
55 static constexpr std::uint64_t maximum = (UINT64_C(1) << 53) - 1;
56 std::uint64_t magnitude = 0;
57 std::int16_t quantum = 0;
58 bool valid = true;
59 PositionTailGuard reason = PositionTailGuard::None; // occupies existing padding
60
61 template<class Diagnostics = PositionTailNoDiagnostics>
62 void add(double value, PositionTailRejection<Diagnostics>& rejection) noexcept {
63 if (!valid) return;
64 if (!std::isfinite(value) || std::signbit(value)) {
65 rejection.reject(PositionTailGuard::InvalidQuantity);
66 valid = false;
67 return;
68 }
69 if (value == 0.0) return;
70 int exponent = 0;
71 const double fraction = std::frexp(value, &exponent);
72 auto integer = static_cast<std::uint64_t>(std::ldexp(fraction, 53));
73 exponent -= 53;
74 while ((integer & 1) == 0) {
75 integer >>= 1;
76 ++exponent;
77 }
79 term.magnitude = integer;
80 term.quantum = static_cast<std::int16_t>(exponent);
81 merge(term, rejection);
82 }
83
84 void add(double value) noexcept {
86 add(value, rejection);
87 }
88
89 template<class Diagnostics>
90 void merge(const PositionTailExact& other,
91 PositionTailRejection<Diagnostics>& rejection) noexcept {
92 if (!valid || !other.valid) {
93 rejection.reject(PositionTailGuard::InvalidCertificate);
94 valid = false;
95 return;
96 }
97 if (other.magnitude == 0) return;
98 if (magnitude == 0) {
99 *this = other;
100 return;
101 }
102 const int common = std::min(quantum, other.quantum);
103 std::uint64_t left = magnitude, right = other.magnitude;
104 const auto shift = [&](std::uint64_t& value, int bits) {
105 if (bits >= 53 || value > (maximum >> bits)) {
106 if constexpr (Diagnostics::enabled)
107 rejection.reject(bits >= 53 ? PositionTailGuard::QuantumShift
108 : PositionTailGuard::MagnitudeShift);
109 return false;
110 }
111 value <<= bits;
112 return true;
113 };
114 if (!shift(left, quantum - common) || !shift(right, other.quantum - common)
115 || right > maximum - left) {
116 rejection.reject(PositionTailGuard::MagnitudeOverflow);
117 valid = false;
118 return;
119 }
120 magnitude = left + right;
121 quantum = static_cast<std::int16_t>(common);
122 }
123
124 template<class Diagnostics>
125 bool read(double& result, PositionTailRejection<Diagnostics>& rejection) const noexcept {
126 if (!valid) {
127 rejection.reject(PositionTailGuard::InvalidCertificate);
128 return false;
129 }
130 const double value = std::ldexp(static_cast<double>(magnitude), quantum);
131 if (!std::isfinite(value)) {
132 rejection.reject(PositionTailGuard::NonfiniteResult);
133 return false;
134 }
135 result = value;
136 return true;
137 }
138
139 bool read(double& result) const noexcept {
140 PositionTailRejection<> rejection;
141 return read(result, rejection);
142 }
143};
144static_assert(sizeof(PositionTailExact) == 16, "compact dyadic certificate");
145static_assert(std::numeric_limits<double>::is_iec559
146 && std::numeric_limits<double>::digits == 53, "binary64 proof");
147
149 struct Origin {
150 PositionTailExact completed;
151 std::uint32_t visited = 0;
152 bool is_long = false;
153 };
154 using Origins = std::unordered_map<std::uint64_t, Origin>;
155 static_assert(sizeof(Origins::value_type) == 32, "32 bytes per origin, excluding map overhead");
156 struct Margins {
157 PositionTailExact side[2];
158 };
159 static_assert(sizeof(Margins) == 32, "unchanged disk summary width");
160
161public:
163
164 void set_directory(const std::string& directory) { margins_.set_directory(directory); }
165
166 // This cache can be discarded at any point: uncertified/missing facts
167 // ALWAYS select replay. It owns no logical broker transition. Reset
168 // rebuilds reachable equal-time rows from the current row source rather
169 // than assuming an empty position tail at an opening/reversal.
170 template<class Read, class Diagnostics = PositionTailNoDiagnostics>
171 void begin_position(std::int64_t barrier, std::size_t base, std::size_t end, Read&& read,
172 Diagnostics diagnostics = {}) {
173 if constexpr (Diagnostics::enabled) {
175 unsafe_reason_ = PositionTailGuard::None;
176 }
178 margins_.clear();
179 all_margins_ = {};
180 high_closed_origin_ = 0;
181 barrier_ = barrier;
182 observed_end_ = end;
183 active_ = true;
184 safe_ = true;
185 have_time_ = false;
186 std::int64_t newer_time = 0;
187 for (std::size_t index = end; index > base;) {
188 const Trade& row = read(--index);
190 if constexpr (Diagnostics::enabled) {
191 ++work.host_rows;
193 }
194 if (row.exit_time < barrier) break; // strict barrier, equality survives
195 if (have_time_ && row.exit_time > newer_time) {
196 invalidate(PositionTailGuard::NonmonotoneTime, diagnostics);
197 break;
198 }
199 if (!have_time_) last_time_ = row.exit_time;
200 have_time_ = true;
201 newer_time = row.exit_time;
202 accumulate(row, diagnostics);
203 }
204 }
205
206 template<class Read, class Diagnostics = PositionTailNoDiagnostics>
207 void observe(std::size_t base, std::size_t end, Read&& read, Diagnostics diagnostics = {}) {
208 if constexpr (Diagnostics::enabled) ++work.sync_updates;
209 if (!active_ || !safe_) return;
210 if (end < observed_end_ || observed_end_ < base) {
211 if constexpr (Diagnostics::enabled)
212 unsafe_reason_ = end < observed_end_ ? PositionTailGuard::PublicationRewind
213 : PositionTailGuard::PublicationGap;
214 invalidate(PositionTailGuard::Invalidated, diagnostics);
215 return;
216 }
217 while (observed_end_ < end) {
218 const Trade& row = read(observed_end_++);
220 if constexpr (Diagnostics::enabled) {
221 ++work.host_rows;
223 }
224 if (row.exit_time < barrier_ || (have_time_ && row.exit_time < last_time_)) {
225 if constexpr (Diagnostics::enabled)
226 unsafe_reason_ = row.exit_time < barrier_ ? PositionTailGuard::BeforeBarrier
227 : PositionTailGuard::NonmonotoneTime;
228 invalidate(PositionTailGuard::Invalidated, diagnostics);
229 return;
230 }
231 last_time_ = row.exit_time;
232 have_time_ = true;
233 accumulate(row, diagnostics);
234 }
235 }
236
237 template<class Diagnostics = PositionTailNoDiagnostics>
238 void open_origin(std::uint64_t incarnation, bool is_long, Diagnostics = {}) {
239 if constexpr (Diagnostics::enabled) ++work.open_updates;
240 if (!safe_ || !active_) return;
241 auto found = origins_.find(incarnation);
242 if (found == origins_.end()) {
243 Origin row;
244 row.is_long = is_long;
245 // A genuinely new incarnation cannot have earlier completed
246 // rows. An old/reactivated incarnation whose subtotal was
247 // retired is uncertain and must replay instead of starting at 0.
248 row.completed.valid = incarnation > high_closed_origin_;
249 if constexpr (Diagnostics::enabled)
250 if (!row.completed.valid)
251 row.completed.reason = PositionTailGuard::ReactivatedOrigin;
252 origins_.emplace(incarnation, row);
253 } else if (found->second.is_long != is_long) {
254 if constexpr (Diagnostics::enabled)
255 if (found->second.completed.reason == PositionTailGuard::None)
256 found->second.completed.reason = PositionTailGuard::SideMismatch;
257 found->second.completed.valid = false;
258 }
259 }
260
261 void retire_origin(std::uint64_t incarnation) {
262 origins_.erase(incarnation);
263 if (origins_.empty()) {
265 } else if (origins_.bucket_count() / 4 > origins_.size()) {
266 origins_.rehash(origins_.size());
267 }
268 }
269
274 std::size_t resident_origins() const noexcept { return origins_.size(); }
275
276 // Nonempty live maps pin their table rows. The adapter updates this
277 // derived index at the same insert/erase/clear seams as those maps.
278 // Querying only these rows avoids scanning resident historical cohorts
279 // whose rosters are pinned for unrelated source bookkeeping.
280 void live_cohort(const void* row, bool nonempty) {
281 if (nonempty) {
282 if (active_ && safe_) live_cohorts_.insert(row);
283 } else {
284 live_cohorts_.erase(row);
285 if (live_cohorts_.empty())
287 else if (live_cohorts_.bucket_count() / 4 > live_cohorts_.size())
288 live_cohorts_.rehash(live_cohorts_.size());
289 }
290 }
291 template<class Visit>
292 void for_each_live_cohort(Visit&& visit) const {
293 for (const auto* row : live_cohorts_) visit(row);
294 }
295
296 template<class Diagnostics = PositionTailNoDiagnostics>
297 void invalidate(PositionTailGuard reason = PositionTailGuard::Invalidated, Diagnostics = {}) {
298 if constexpr (Diagnostics::enabled) {
300 if (unsafe_reason_ == PositionTailGuard::None
301 || unsafe_reason_ == PositionTailGuard::Inactive)
302 unsafe_reason_ = reason;
303 }
304 safe_ = false;
306 margins_.clear();
307 all_margins_ = {};
308 }
309
310 template<class Select, class Diagnostics = PositionTailNoDiagnostics>
311 bool ancestry(bool is_long, Select&& select, double& result, Diagnostics diagnostics = {}) {
312 ++work.queries;
313 if (!safe_ || !active_) return finish(false, unsafe_reason_, diagnostics);
314 if (++query_generation_ == 0) {
315 for (auto& row : origins_) row.second.visited = 0;
316 ++query_generation_;
317 }
318 PositionTailExact total;
319 PositionTailRejection<Diagnostics> rejection;
320 select([&](std::uint64_t incarnation) {
322 const auto found = origins_.find(incarnation);
323 if (found == origins_.end()) {
324 // No row for a fresh incarnation means an exact zero.
325 // Missing older origins are deliberately uncertified.
326 if (incarnation <= high_closed_origin_) {
327 rejection.reject(PositionTailGuard::MissingOrigin);
328 total.valid = false;
329 }
330 return;
331 }
332 auto& row = found->second;
333 if (row.visited == query_generation_) return; // duplicate cohort membership
334 row.visited = query_generation_;
335 if (!row.completed.valid) {
336 if constexpr (Diagnostics::enabled)
337 rejection.reject(row.completed.reason == PositionTailGuard::None
338 ? PositionTailGuard::InvalidCertificate : row.completed.reason);
339 total.valid = false;
340 } else if (row.is_long == is_long) total.merge(row.completed, rejection);
341 });
342 const bool certified = total.read(result, rejection);
343 return finish(certified, rejection.value(), diagnostics);
344 }
345
346 template<class Diagnostics = PositionTailNoDiagnostics>
347 bool margin(bool is_long, const std::string* named_entry, double& result,
348 Diagnostics diagnostics = {}) {
349 ++work.queries;
350 if (!safe_ || !active_) return finish(false, unsafe_reason_, diagnostics);
351 const auto side = is_long ? 1 : 0;
353 if (!named_entry) {
354 if constexpr (Diagnostics::enabled)
355 rejection.reject(all_margins_.side[side].reason);
356 const bool certified = all_margins_.side[side].read(result, rejection);
357 return finish(certified, rejection.value(), diagnostics);
358 }
359 const auto offset = margins_.find(*named_entry);
360 if (!offset) {
361 result = 0.0;
362 return finish(true, PositionTailGuard::None, diagnostics);
363 }
364 const auto value = margins_.get(*offset);
365 if constexpr (Diagnostics::enabled) rejection.reject(value.side[side].reason);
366 const bool certified = value.side[side].read(result, rejection);
367 return finish(certified, rejection.value(), diagnostics);
368 }
369
370private:
371 template<class Diagnostics>
372 bool finish(bool certified, PositionTailGuard reason, Diagnostics) noexcept {
373 if (certified) ++work.certified_queries;
374 else ++work.fallback_queries;
375 if constexpr (Diagnostics::enabled) {
376 work.guard_reason = certified ? PositionTailGuard::None : reason;
377 ++work.guard_reasons[static_cast<std::size_t>(work.guard_reason)];
378 }
379 return certified;
380 }
381
382 template<class Diagnostics>
383 void accumulate(const Trade& trade, Diagnostics) {
384 high_closed_origin_ = std::max(high_closed_origin_, trade.entry_incarnation);
385 const auto origin = origins_.find(trade.entry_incarnation);
386 if (origin != origins_.end()) {
387 PositionTailRejection<Diagnostics> rejection;
388 if constexpr (Diagnostics::enabled) rejection.reason = origin->second.completed.reason;
389 if (origin->second.is_long != trade.is_long) {
390 rejection.reject(PositionTailGuard::SideMismatch);
391 origin->second.completed.valid = false;
392 }
393 origin->second.completed.add(trade.qty, rejection);
394 if constexpr (Diagnostics::enabled)
395 origin->second.completed.reason = rejection.reason;
396 }
397 if (trade.exit_id != kMarginCallLabel) return;
398 const auto side = trade.is_long ? 1 : 0;
399 PositionTailRejection<Diagnostics> all_rejection, named_rejection;
400 if constexpr (Diagnostics::enabled)
401 all_rejection.reason = all_margins_.side[side].reason;
402 all_margins_.side[side].add(trade.qty, all_rejection);
403 if constexpr (Diagnostics::enabled)
404 all_margins_.side[side].reason = all_rejection.reason;
405 const auto inserted = margins_.insert(trade.entry_id, Margins{});
406 auto value = margins_.get(inserted.first);
407 if constexpr (Diagnostics::enabled) named_rejection.reason = value.side[side].reason;
408 value.side[side].add(trade.qty, named_rejection);
409 if constexpr (Diagnostics::enabled) value.side[side].reason = named_rejection.reason;
410 margins_.set(inserted.first, value);
411 }
412
413 Origins origins_;
414 std::unordered_set<const void*> live_cohorts_;
415 // Historical margin ids must stay queryable even after their last live
416 // origin drains. Only fixed tree metadata lives in RAM; values/ids are
417 // in the existing external table, without a resident directory/cache.
418 ::pineforge::detail::DiskIdTable<Margins> margins_;
419 Margins all_margins_{};
420 std::uint64_t high_closed_origin_ = 0;
421 std::size_t observed_end_ = 0;
422 std::int64_t barrier_ = 0, last_time_ = 0;
423 std::uint32_t query_generation_ = 0;
424 bool active_ = false, safe_ = false, have_time_ = false;
425 PositionTailGuard unsafe_reason_ = PositionTailGuard::Inactive;
426};
427
428// The fallback is deliberately the old ordered fold over the row source.
429// The predicate and the additions have the same evaluation/order as L18:
430// resident newest-to-oldest, then external newest-to-oldest, first strict
431// timestamp barrier terminates the entire walk. It remains O(history).
432template<class Read, class Include, class Diagnostics = PositionTailNoDiagnostics>
433double replay_position_tail(std::size_t end, std::size_t base, Read&& read,
434 const PositionTailSpill* spill, std::int64_t barrier,
435 Include&& include, const std::string& from_entry,
436 PineExecutionAdapter::PositionTailWork& work, Diagnostics = {}) {
437 double result = 0.0;
438 for (std::size_t index = end; index > base;) {
439 const Trade& trade = read(--index);
440 ++work.replay_rows;
441 if constexpr (Diagnostics::enabled) {
442 ++work.host_rows;
443 ++work.replay_host_rows;
444 }
445 if (trade.exit_time < barrier) return result;
446 if (include(trade.is_long, trade.entry_incarnation, trade.exit_id == kMarginCallLabel,
447 [&] { return trade.entry_id == from_entry; })) result += trade.qty;
448 }
449 if (spill) {
450 auto cursor = spill->size_bytes();
451 PositionTailSpill::Row trade;
452 while (spill->previous(cursor, trade)) {
453 ++work.replay_rows;
454 if constexpr (Diagnostics::enabled) ++work.spill_rows;
455 if (trade.exit_time < barrier) break;
456 if (include(trade.is_long, trade.entry_incarnation, trade.margin_call,
457 [&] { return spill->entry_id_equals(trade, from_entry); })) result += trade.qty;
458 }
459 }
460 return result;
461}
462
463} // namespace pineforge::source::detail
std::optional< Offset > find(const std::string &key) const
bool previous(std::uint64_t &cursor, Row &row) const
bool ancestry(bool is_long, Select &&select, double &result, Diagnostics diagnostics={})
void begin_position(std::int64_t barrier, std::size_t base, std::size_t end, Read &&read, Diagnostics diagnostics={})
void invalidate(PositionTailGuard reason=PositionTailGuard::Invalidated, Diagnostics={})
void observe(std::size_t base, std::size_t end, Read &&read, Diagnostics diagnostics={})
void open_origin(std::uint64_t incarnation, bool is_long, Diagnostics={})
bool margin(bool is_long, const std::string *named_entry, double &result, Diagnostics diagnostics={})
void clear_exposure_storage(Container &container)
double replay_position_tail(std::size_t end, std::size_t base, Read &&read, const PositionTailSpill *spill, std::int64_t barrier, Include &&include, const std::string &from_entry, PineExecutionAdapter::PositionTailWork &work, Diagnostics={})
PineExecutionAdapter::PositionTailGuard PositionTailGuard
constexpr char kMarginCallLabel[]
uint64_t entry_incarnation
Definition engine.hpp:213
int64_t exit_time
Definition engine.hpp:183
std::string exit_id
Definition engine.hpp:195
std::uint64_t guard_reasons[static_cast< std::size_t >(PositionTailGuard::Count)]
void merge(const PositionTailExact &other, PositionTailRejection< Diagnostics > &rejection) noexcept
void add(double value, PositionTailRejection< Diagnostics > &rejection) noexcept
bool read(double &result, PositionTailRejection< Diagnostics > &rejection) const noexcept