PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
trade_ledger.hpp
Go to the documentation of this file.
1#pragma once
2#include <array>
3#include <cstddef>
4#include <cstdint>
5#include <functional>
6#include <iterator>
7#include <memory>
8#include <string>
9#include <string_view>
10
11namespace pineforge {
12struct CacheCapacity;
13
14struct Trade;
15namespace detail { struct TradeLedgerStorageProbe; }
16
17// The engine-side additions stay in this existing epoch (DESIGN 6:1344-1347).
18inline namespace engine_script_run_v19 {
19
20// Exact global row space. base() is the publication/fold boundary; an
21// unsealed exit-time group can have arbitrary length and cannot be retired.
22// A kResidentRows descriptor window and an equally sized fixed byte tail of
23// the string payload stay in memory. All four string payloads and all older
24// mutable/sealed descriptors use the existing run-owned positional scratch
25// files. A separate, budgeted read cache retains numeric records and up to
26// kCacheStringBytes of decoded string bytes per row. Wider rows retain their
27// numeric records in the cache and use scratch only for string reads.
28// Owning Trade values still belong to readers.
29//
30// Reads own their values. No reference or contiguous pointer into an
31// evictable page escapes. Iteration visits [base(),size()) as before, now
32// by value; read()/visit()/previous() also read the sealed history, except
33// when a sink run explicitly admits physical history retirement.
35public:
36 using size_type = std::size_t;
38 // The fixed descriptor width is part of the ledger I/O contract. The
39 // implementation asserts it against State::Record after that private
40 // storage type is complete.
41 static constexpr size_type kRecordBytes = 168;
42 static constexpr size_type kResidentRows = 64;
43 // One knob for this cache kind: ledger_rows. Each entry contains a
44 // 168-byte numeric/extent record, a global key and generation/flags,
45 // and exactly 128 inline string bytes (all four strings combined).
46 // The string-validity flag uses the generation word. On 64-bit targets:
47 // 168 + 16 + 128 = 312 bytes, with no additional alignment padding.
48 // Byte allowances include both entries and the private page directory.
49 // No per-entry heap allocation or growing offset/string directory.
50 static constexpr size_type kCacheStringBytes = 128;
51 static constexpr size_type kLedgerEntryBytes =
53 static constexpr size_type kCachePageSlots = 256;
54 static constexpr size_type kDefaultCacheRows = 65536;
55 static constexpr size_type kMaxCacheRows = 1048576;
56 static constexpr size_type cache_capacity_bytes(size_type rows) noexcept {
57 return rows * kLedgerEntryBytes
58 + (rows / kCachePageSlots + (rows % kCachePageSlots != 0)) * sizeof(void*);
59 }
60 // Sole byte-to-row conversion, shared by the host and standalone setters.
62 // A report-sink run has the same declared constant cap as the default:
63 // 20,447,232 entry bytes + 2,048 table bytes + sizeof(TradeLedger)+sizeof(State).
64 // Pages allocate lazily up to this cap, clipping the final page to fit.
66 using Visitor = std::function<bool(size_type, const Trade&)>;
67
69 public:
70 using iterator_category = std::input_iterator_tag;
72 using difference_type = std::ptrdiff_t;
74 using pointer = void;
75 const_iterator() = default;
77 const_iterator& operator++() { ++index_; return *this; }
78 const_iterator operator++(int) { auto old = *this; ++*this; return old; }
79 bool operator==(const const_iterator& other) const {
80 return ledger_ == other.ledger_ && index_ == other.index_;
81 }
82 bool operator!=(const const_iterator& other) const { return !(*this == other); }
83 private:
84 friend class TradeLedger;
85 const_iterator(const TradeLedger* ledger, size_type index)
86 : ledger_(ledger), index_(index) {}
87 const TradeLedger* ledger_ = nullptr;
88 size_type index_ = 0;
89 };
91
94 explicit TradeLedger(CacheCapacity capacity);
100
101 // Bind before the first spill. No mapping or in-memory offset directory.
102 void set_scratch_directory(const std::string&);
103 const std::string& scratch_directory() const;
104 // Configure before the first append/spill; clear() preserves the budget.
105 // The constructor accepts the same optional row budget. Zero bypasses
106 // the decoded cache. Optional table/page allocation failure selects
107 // backing reads for this ledger without retrying the failed allocation.
108 // Valid rows: [0,1048576]; invalid/late settings throw a coded
109 // std::invalid_argument (setting_rejected, reason value_above_maximum
110 // or frozen). Default: 65,536 rows; maximum: 1,048,576 rows.
111 // On 64-bit targets, entries plus the page directory cost 20,449,280 B
112 // at default and 327,188,480 B at maximum, plus the fixed base.
114 // Standalone inclusive byte allowance for entries plus owning pointers.
115 // Selects the largest row cap that fits, without the legacy row ceiling;
116 // allowances below one entry plus one pointer bypass. The host policy
117 // uses this same conversion, with the existing row-setting ceiling.
118 // Frozen after the first append/spill.
121 // Accounted storage: sizeof(TradeLedger)+sizeof(State)+rows*kLedgerEntryBytes
122 // + ceil(rows/256)*sizeof(void*). Excludes allocator overhead, scratch/OS pages,
123 // configured directory strings, returned owning Trades and the explicit
124 // legacy materialize() map. Validates the same row range as the setter.
126 // Actual allocation: fixed base + ceil(cache_rows()/256) owning pointers
127 // after first read + min(256,cache_rows()-256*p)*kLedgerEntryBytes per page p.
128 // Slot mapping stays fixed; page growth preserves entries. Writes only
129 // fill an existing cache; retirement frees pages outside retained rows.
130 // The fixed base includes the zero-row read workspace. The directory
131 // and allocated entry pages each enter the common cache accounting once.
133 enum class CacheBypassReason : std::uint32_t { none, allocation_failed };
138 // One allocation failure is reported once and remains sticky through
139 // reads, appends, replacements and policy changes. clear() starts afresh.
140 bool cache_allocation_failed() const noexcept;
141 CacheStats read_cache_stats() const noexcept;
142 // Occupied decoded entries only; separate from resident_size()'s legacy
143 // descriptor/materialization accounting.
145 void clear();
149 // Cached fixed descriptors plus explicitly materialized legacy values.
150 // No retained string payload belongs to the normal descriptor window.
153 bool empty() const;
154
155 // read()/at()/[] span the retained source, including sealed rows on a
156 // non-retiring ledger. An explicitly retiring ledger refuses reads below
157 // base() with a coded std::logic_error (engine_invariant).
158 // Writers still reject sealed rows below the publication boundary.
159 // Hits allocate only the owning strings already required by read().
160 // Direct-mapped eviction above cache_rows(), or oversized strings, retains
161 // the existing scratch path; a history loop can still cost O(T).
162 Trade read(size_type global) const;
163 Trade operator[](size_type global) const;
164 Trade at(size_type global) const;
165 Trade back() const;
166 // Same retained-row read contract as at(). numeric_at() returns every
167 // numeric field and flag with empty strings, reading only the descriptor.
168 // entry_id_at() returns only that string and shares the field read cache.
170 std::string entry_id_at(size_type global) const;
171 // Pine field reads use the same check(global) as at(). A numeric
172 // hit reads the Record from its private cache entry without
173 // constructing a Trade or owning string. A string hit constructs only
174 // the requested string from its packed bytes.
175 // Misses retain the numeric record and all strings fitting the allowance.
176 double pnl_at(size_type global) const;
177 double pnl_pct_at(size_type global) const;
178 double commission_at(size_type global) const;
179 std::int32_t entry_bar_index_at(size_type global) const;
180 std::int32_t exit_bar_index_at(size_type global) const;
181 std::uint64_t entry_incarnation_at(size_type global) const;
182 double entry_price_at(size_type global) const;
183 double exit_price_at(size_type global) const;
184 std::int64_t entry_time_at(size_type global) const;
185 std::int64_t exit_time_at(size_type global) const;
186 double qty_at(size_type global) const;
187 double max_runup_at(size_type global) const;
188 double max_drawdown_at(size_type global) const;
189 bool is_long_at(size_type global) const;
190 std::string entry_comment_at(size_type global) const;
191 std::string exit_comment_at(size_type global) const;
192 std::string exit_id_at(size_type global) const;
193 // Same checked retained-row contract as the owning text getters. Compare
194 // exact bytes (including NUL); empty fields equal an empty rhs. Return a
195 // plain bool: no pointer escapes. Warm reads allocate no owning strings;
196 // oversized text uses fixed stack chunks and consumes the whole extent,
197 // even on mismatch, so later I/O errors propagate. As with existing const
198 // reads, all access to the same ledger requires external synchronization.
199 bool entry_id_equals(size_type global, std::string_view rhs) const;
200 bool exit_id_equals(size_type global, std::string_view rhs) const;
201 bool entry_comment_equals(size_type global, std::string_view rhs) const;
202 bool exit_comment_equals(size_type global, std::string_view rhs) const;
203 void replace(size_type global, const Trade&);
204 void modify(size_type global, const std::function<void(Trade&)>&);
205
206 /// Append at the logical global end (3.4:613-616).
207 void push_back(const Trade& trade);
208 void push_back(Trade&& trade);
209
210 // A rewind can cross the resident window, but not the published base.
211 void resize(size_type global_end);
212
215 void visit(size_type first, size_type end, const Visitor&) const;
216 // Initialize cursor=size(); each success decrements it and returns that
217 // global index's exact row, newest resident first and then scratch.
218 // Independent cursors may interleave and retain the returned values.
219 bool previous(size_type& cursor, Trade& value) const;
220
221 // Explicit legacy borrowed-pointer materialization. Only the C string
222 // ABI uses this non-flat compatibility boundary; execution, folds and
223 // report streaming must use read()/visit() instead.
224 const Trade& materialize(size_type global) const;
225
226 // Publication still requires a sink and streamable policy, returned
227 // applied notifications, finalized phase/money folds and old exit times.
228 // Keep the last exit-time block, its predecessor and a short-seed opening
229 // that can still be relabelled. Write T and advance the exact row, money,
230 // metric and phase seeds before advancing this publication boundary.
231 // Those predicates may pin every row; physical eviction is independent.
232 // Historical bytes remain readable unless the engine admitted retirement
233 // from an explicit no-history generated-module capability.
234 void seal(size_type global_end);
235
236 // Signed-index comparisons: an index outside [0,size()) compares as empty.
237 // Valid indices keep the checked retained-row contract of *_equals.
238 bool entry_id_matches(int index, std::string_view rhs) const;
239 bool exit_id_matches(int index, std::string_view rhs) const;
240 bool entry_comment_matches(int index, std::string_view rhs) const;
241 bool exit_comment_matches(int index, std::string_view rhs) const;
242
243private:
244 friend class BacktestEngine;
246 // Run-begin policy and non-public storage census; neither changes this
247 // installed object's layout or the logical trade/hash representation.
248 void set_history_retirement(bool);
249 void check_history_read(size_type) const;
250 std::array<std::uint64_t, 2> scratch_storage_bytes() const;
251 struct State;
252 std::unique_ptr<State> state_;
253};
254
255} // inline namespace engine_script_run_v19
256} // namespace pineforge
void replace(size_type global, const Trade &)
std::int32_t exit_bar_index_at(size_type global) const
void visit(size_type first, size_type end, const Visitor &) const
std::string exit_comment_at(size_type global) const
void set_scratch_directory(const std::string &)
void set_cache_capacity(CacheCapacity capacity)
std::string entry_comment_at(size_type global) const
double pnl_at(size_type global) const
double pnl_pct_at(size_type global) const
double max_runup_at(size_type global) const
std::int64_t entry_time_at(size_type global) const
const Trade & materialize(size_type global) const
static size_type resident_cache_bytes(size_type rows)
std::uint64_t entry_incarnation_at(size_type global) const
bool previous(size_type &cursor, Trade &value) const
bool exit_id_equals(size_type global, std::string_view rhs) const
std::int32_t entry_bar_index_at(size_type global) const
static constexpr size_type cache_capacity_bytes(size_type rows) noexcept
bool exit_id_matches(int index, std::string_view rhs) const
bool exit_comment_matches(int index, std::string_view rhs) const
bool entry_comment_matches(int index, std::string_view rhs) const
double commission_at(size_type global) const
void modify(size_type global, const std::function< void(Trade &)> &)
CacheStats read_cache_stats() const noexcept
double entry_price_at(size_type global) const
double max_drawdown_at(size_type global) const
Trade numeric_at(size_type global) const
std::string exit_id_at(size_type global) const
double exit_price_at(size_type global) const
void push_back(const Trade &trade)
Append at the logical global end (3.4:613-616).
Trade read(size_type global) const
bool exit_comment_equals(size_type global, std::string_view rhs) const
bool is_long_at(size_type global) const
bool entry_id_equals(size_type global, std::string_view rhs) const
std::string entry_id_at(size_type global) const
bool entry_comment_equals(size_type global, std::string_view rhs) const
bool entry_id_matches(int index, std::string_view rhs) const
std::int64_t exit_time_at(size_type global) const
std::function< bool(size_type, const Trade &)> Visitor
double qty_at(size_type global) const
TradeLedger & operator=(const TradeLedger &)
static size_type cache_rows_for_capacity(CacheCapacity capacity) noexcept
const std::string & scratch_directory() const
TradeLedger & operator=(TradeLedger &&)