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
3#include <cstddef>
4#include <cstdint>
5#include <functional>
6#include <iterator>
7#include <memory>
8#include <string>
9
10namespace pineforge {
11
12struct Trade;
13
14// The engine-side additions stay in this existing epoch (DESIGN 6:1344-1347).
15inline namespace engine_script_run_v19 {
16
17// Exact global row space. base() is the publication/fold boundary, NOT a
18// storage boundary: an unsealed exit-time group can have arbitrary length.
19// A kResidentRows descriptor window and an equally sized fixed byte tail of
20// the string payload stay in memory. All four string payloads and all older
21// mutable/sealed descriptors use the existing run-owned positional scratch
22// files. A separate, budgeted read cache retains numeric records and up to
23// kCacheStringBytes of decoded string bytes per row. Wider rows retain their
24// numeric records in the cache and use scratch only for string reads.
25// Owning Trade values still belong to readers.
26//
27// Reads own their values. No reference or contiguous pointer into an
28// evictable page escapes. Iteration visits [base(),size()) as before, now
29// by value; read()/visit()/previous() also read the sealed history.
31public:
32 using size_type = std::size_t;
34 // The fixed descriptor width is part of the ledger I/O contract. The
35 // implementation asserts it against State::Record after that private
36 // storage type is complete.
37 static constexpr size_type kRecordBytes = 168;
38 static constexpr size_type kResidentRows = 64;
39 // One knob for this cache kind: ledger_rows. Each entry contains a
40 // 168-byte numeric/extent record, a global key and validity generation,
41 // and exactly 128 inline string bytes (all four strings combined).
42 // The string-validity flag shares the generation word. On 64-bit targets:
43 // 168 + 16 + 128 = 312 bytes, with no additional alignment padding.
44 // No per-entry heap allocation or growing offset/string directory.
45 static constexpr size_type kCacheStringBytes = 128;
46 static constexpr size_type kLedgerEntryBytes =
48 static constexpr size_type kDefaultCacheRows = 65536;
49 static constexpr size_type kMaxCacheRows = 1048576;
50 // A run with a report sink installed keeps the cache inside the sink's
51 // fixed heap window: 128 x 312 = 39,936 B, under one 64 KiB scratch window.
52 static constexpr size_type kSinkCacheRows = 128;
53 using Visitor = std::function<bool(size_type, const Trade&)>;
54
56 public:
57 using iterator_category = std::input_iterator_tag;
59 using difference_type = std::ptrdiff_t;
61 using pointer = void;
62 const_iterator() = default;
64 const_iterator& operator++() { ++index_; return *this; }
65 const_iterator operator++(int) { auto old = *this; ++*this; return old; }
66 bool operator==(const const_iterator& other) const {
67 return ledger_ == other.ledger_ && index_ == other.index_;
68 }
69 bool operator!=(const const_iterator& other) const { return !(*this == other); }
70 private:
71 friend class TradeLedger;
72 const_iterator(const TradeLedger* ledger, size_type index)
73 : ledger_(ledger), index_(index) {}
74 const TradeLedger* ledger_ = nullptr;
75 size_type index_ = 0;
76 };
78
85
86 // Bind before the first spill. No mapping or in-memory offset directory.
87 void set_scratch_directory(const std::string&);
88 const std::string& scratch_directory() const;
89 // Configure before the first append/spill; clear() preserves the budget.
90 // Valid rows: [1,1048576]; invalid/late settings throw a coded
91 // std::invalid_argument (setting_rejected, reason value_below_minimum,
92 // value_above_maximum or frozen). Default: 65,536 rows, 20,447,232 entry
93 // bytes on 64-bit targets; maximum: 1,048,576 rows, 327,155,712 entry bytes.
96 // Accounted ledger/cache storage: rows*kLedgerEntryBytes + a fixed base
97 // (sizeof ledger + State). Excludes allocator overhead, scratch/OS pages,
98 // configured directory strings, returned owning Trades and the explicit
99 // legacy materialize() map. Validates the same row range as the setter.
101 // Actual allocation: fixed base plus allocated capacity*kLedgerEntryBytes.
102 // Allocated on the first read, at min(cache_rows(), the next power of two
103 // >= max(64,size())). Further growth invalidates entries; capacity stays
104 // bounded by cache_rows() and independent of the number of bars.
106 // Occupied decoded entries only; separate from resident_size()'s legacy
107 // descriptor/materialization accounting.
109 void clear();
113 // Cached fixed descriptors plus explicitly materialized legacy values.
114 // No retained string payload belongs to the normal descriptor window.
117 bool empty() const;
118
119 // read() spans the whole exact source; at()/[] retain the checked
120 // unsealed-window contract for existing settlement callers.
121 // Hits allocate only the owning strings already required by read().
122 // Direct-mapped eviction above cache_rows(), or oversized strings, retains
123 // the existing scratch path; a history loop can still cost O(T).
124 Trade read(size_type global) const;
126 Trade at(size_type global) const;
127 Trade back() const;
128 // Same checked unsealed-row contract as at(). numeric_at() returns every
129 // numeric field and flag with empty strings, reading only the descriptor.
130 // entry_id_at() returns only that string and shares the field read cache.
132 std::string entry_id_at(size_type global) const;
133 // Pine field reads use the same check(global,true) as at(). A numeric
134 // hit loads just its Record member, without constructing/copying a Trade
135 // or any string. A string hit copies only the requested packed bytes.
136 // Misses retain the numeric record and all strings fitting the allowance.
137 double pnl_at(size_type global) const;
138 double pnl_pct_at(size_type global) const;
139 double commission_at(size_type global) const;
140 std::int32_t entry_bar_index_at(size_type global) const;
141 std::int32_t exit_bar_index_at(size_type global) const;
142 std::uint64_t entry_incarnation_at(size_type global) const;
143 double entry_price_at(size_type global) const;
144 double exit_price_at(size_type global) const;
145 std::int64_t entry_time_at(size_type global) const;
146 std::int64_t exit_time_at(size_type global) const;
147 double qty_at(size_type global) const;
148 double max_runup_at(size_type global) const;
149 double max_drawdown_at(size_type global) const;
150 bool is_long_at(size_type global) const;
151 std::string entry_comment_at(size_type global) const;
152 std::string exit_comment_at(size_type global) const;
153 std::string exit_id_at(size_type global) const;
154 void replace(size_type global, const Trade&);
155 void modify(size_type global, const std::function<void(Trade&)>&);
156
157 /// Append at the logical global end (3.4:613-616).
158 void push_back(const Trade& trade);
159 void push_back(Trade&& trade);
160
161 // A rewind can cross the resident window, but not the published base.
162 void resize(size_type global_end);
163
166 void visit(size_type first, size_type end, const Visitor&) const;
167 // Initialize cursor=size(); each success decrements it and returns that
168 // global index's exact row, newest resident first and then scratch.
169 // Independent cursors may interleave and retain the returned values.
170 bool previous(size_type& cursor, Trade& value) const;
171
172 // Explicit legacy borrowed-pointer materialization. Only the C string
173 // ABI uses this non-flat compatibility boundary; execution, folds and
174 // report streaming must use read()/visit() instead.
175 const Trade& materialize(size_type global) const;
176
177 // Publication still requires a sink and streamable policy, returned
178 // applied notifications, finalized phase/money folds and old exit times.
179 // Keep the last exit-time block, its predecessor and a short-seed opening
180 // that can still be relabelled. Write T and advance the exact row, money,
181 // metric and phase seeds before advancing this publication boundary.
182 // Those predicates may pin every row; physical eviction is independent.
183 // Historical bytes remain readable independently of publication.
184 void seal(size_type global_end);
185
186private:
187 struct State;
188 std::unique_ptr<State> state_;
189};
190
191} // inline namespace engine_script_run_v19
192} // namespace pineforge
void replace(size_type global, const Trade &)
Trade operator[](size_type global) const
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 &)
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
std::int32_t entry_bar_index_at(size_type global) const
double commission_at(size_type global) const
void modify(size_type global, const std::function< void(Trade &)> &)
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 is_long_at(size_type global) const
std::string entry_id_at(size_type global) 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 &)
const std::string & scratch_directory() const
TradeLedger & operator=(TradeLedger &&)