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