PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
pine_position_tail.cpp
Go to the documentation of this file.
3#include "../read_through_window.hpp"
4
8
9#include <algorithm>
10#include <array>
11#include <cstring>
12#include <limits>
13#include <stdexcept>
14
16namespace {
17
18static_assert(sizeof(double) == 8 && std::numeric_limits<double>::is_iec559,
19 "position-tail scratch records preserve binary64 quantities");
20
21[[noreturn]] void invalid_record() {
22 throw coded<std::logic_error>(RunFailureCode::engine_invariant, {},
23 "invalid Pine position-tail spill record");
24}
25
26} // namespace
27
28struct PositionTailSpill::State {
29 PositionTailBinding::File file;
30 std::uint64_t end = 0;
31 std::uint64_t rows = 0;
32 mutable ::pineforge::detail::ReadThroughWindow cache;
33 mutable std::array<unsigned char, kPageBytes> transfer{};
34
35 State(const std::string& directory, std::size_t cache_bytes)
36 : file(directory),
37 cache(cache_bytes ? std::min(cache_bytes, kPageBytes) : kPageBytes,
38 cache_bytes, this, read_backing) {}
39 State(const std::string& directory, CacheCapacity capacity)
40 : file(directory), cache(kPageBytes, capacity, this, read_backing) {}
41
42 static void read_backing(void* context, std::uint64_t offset,
43 void* destination, std::size_t size) {
44 static_cast<State*>(context)->file.read(offset, destination, size);
45 }
46
47 void write(std::uint64_t offset, const void* data, std::size_t size) {
48 const auto* bytes = static_cast<const unsigned char*>(data);
49 for (std::size_t done = 0; done < size;) {
50 // Also bounds each syscall for identifiers larger than a window.
51 const std::size_t count = std::min(size - done, kWindowBytes);
52 file.write(offset + done, bytes + done, count);
53 done += count;
54 }
55 }
56
57 void read(std::uint64_t offset, void* data, std::size_t size) const {
58 if (offset > end || size > end - offset) invalid_record();
59 auto* bytes = static_cast<unsigned char*>(data);
60 const auto page_size = cache.page_size();
61 // Copy backwards, including footers and ids crossing page boundaries.
62 // Request only the existing prefix of the final backing page.
63 std::size_t left = size;
64 while (left != 0) {
65 const std::uint64_t last = offset + left - 1;
66 const auto page = last / page_size;
67 const auto begin = page * page_size;
68 const auto prefix = static_cast<std::size_t>(
69 std::min<std::uint64_t>(page_size, end - begin));
70 cache.get(page, transfer.data(), prefix);
71 const std::size_t count = static_cast<std::size_t>(
72 std::min<std::uint64_t>(left, last - begin + 1));
73 std::memcpy(bytes + left - count, transfer.data() + last - begin + 1 - count, count);
74 left -= count;
75 }
76 }
77};
78
79PositionTailSpill::PositionTailSpill(const std::string& scratch_directory,
80 std::size_t cache_bytes)
81 : state_(std::make_unique<State>(scratch_directory, cache_bytes)) {}
82
83PositionTailSpill::PositionTailSpill(const std::string& scratch_directory,
84 CacheCapacity capacity)
85 : state_(std::make_unique<State>(scratch_directory, capacity)) {}
86
88
89void PositionTailSpill::append(const Trade& trade) {
90 const auto length = trade.entry_id.size();
91 const auto maximum = static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max());
92 if (length > std::numeric_limits<std::uint32_t>::max()
93 || state_->end > maximum - kFooterBytes
94 || length > maximum - kFooterBytes - state_->end
95 || state_->rows == std::numeric_limits<std::uint64_t>::max()) {
97 "Pine position-tail spill offset exhausted");
98 }
99 std::array<unsigned char, kFooterBytes> footer{};
100 const auto id_size = static_cast<std::uint32_t>(length);
101 std::memcpy(footer.data(), &trade.exit_time, 8);
102 std::memcpy(footer.data() + 8, &trade.qty, 8);
103 std::memcpy(footer.data() + 16, &trade.entry_incarnation, 8);
104 footer[24] = trade.is_long ? 1 : 0;
105 footer[25] = trade.exit_id == kMarginCallLabel ? 1 : 0;
106 std::memcpy(footer.data() + 26, &id_size, 4);
107 state_->cache.invalidate();
108 state_->write(state_->end, trade.entry_id.data(), length);
109 state_->write(state_->end + length, footer.data(), footer.size());
110 // Publish only after both writes. A failed append leaves the logical
111 // end intact, so the caller never drops the row whose write failed.
112 state_->end += static_cast<std::uint64_t>(length) + kFooterBytes;
113 ++state_->rows;
114}
115
117 state_->cache.invalidate();
118 state_->file.clear();
119 state_->end = 0;
120 state_->rows = 0;
121}
122
123std::uint64_t PositionTailSpill::size_bytes() const noexcept { return state_->end; }
124std::uint64_t PositionTailSpill::row_count() const noexcept { return state_->rows; }
125std::size_t PositionTailSpill::cache_capacity_bytes() const noexcept {
126 return state_->cache.payload_bytes();
127}
128std::uint64_t PositionTailSpill::backing_read_count() const noexcept {
129 return state_->file.read_count();
130}
131std::uint64_t PositionTailSpill::backing_read_bytes() const noexcept {
132 return state_->file.read_bytes();
133}
135 return state_->cache.stats();
136}
137std::size_t PositionTailSpill::cache_control_bytes() const noexcept {
138 return sizeof(PositionTailSpill) + sizeof(State) - sizeof(state_->transfer);
139}
140
141bool PositionTailSpill::previous(std::uint64_t& cursor, Row& row) const {
142 if (cursor > state_->end) invalid_record();
143 if (cursor == 0) return false;
144 if (cursor < kFooterBytes) invalid_record();
145 std::array<unsigned char, kFooterBytes> footer{};
146 state_->read(cursor - kFooterBytes, footer.data(), footer.size());
147 Row next;
148 std::memcpy(&next.exit_time, footer.data(), 8);
149 std::memcpy(&next.qty, footer.data() + 8, 8);
150 std::memcpy(&next.entry_incarnation, footer.data() + 16, 8);
151 if (footer[24] > 1 || footer[25] > 1) invalid_record();
152 next.is_long = footer[24] != 0;
153 next.margin_call = footer[25] != 0;
154 std::memcpy(&next.entry_id_size, footer.data() + 26, 4);
155 if (next.entry_id_size > cursor - kFooterBytes) invalid_record();
156 next.entry_id_offset = cursor - kFooterBytes - next.entry_id_size;
157 cursor = next.entry_id_offset;
158 row = next;
159 return true;
160}
161
162bool PositionTailSpill::entry_id_equals(const Row& row, std::string_view entry_id) const {
163 if (entry_id.size() != row.entry_id_size) return false;
164 if (row.entry_id_offset > state_->end
165 || row.entry_id_size > state_->end - row.entry_id_offset) invalid_record();
166 // Compare in fixed-sized pieces; no owning copy of a spilled id.
167 std::array<unsigned char, 256> bytes{};
168 std::size_t left = entry_id.size();
169 while (left != 0) {
170 const std::size_t count = std::min(left, bytes.size());
171 const std::size_t begin = left - count;
172 state_->read(row.entry_id_offset + begin, bytes.data(), count);
173 if (std::memcmp(bytes.data(), entry_id.data() + begin, count) != 0) return false;
174 left = begin;
175 }
176 return true;
177}
178
179} // namespace pineforge::source::detail
Base with a code: throw coded<std::invalid_argument>(code, {args}, text).
std::uint64_t backing_read_count() const noexcept
::pineforge::detail::ReadThroughCacheStats cache_stats() const noexcept
PositionTailSpill(const std::string &scratch_directory, std::size_t cache_bytes=kWindowBytes)
bool previous(std::uint64_t &cursor, Row &row) const
std::uint64_t backing_read_bytes() const noexcept
bool entry_id_equals(const Row &row, std::string_view entry_id) const
std::uint64_t page(std::uint64_t bytes)
constexpr char kMarginCallLabel[]
uint64_t entry_incarnation
Definition engine.hpp:219
int64_t exit_time
Definition engine.hpp:189
std::string entry_id
Definition engine.hpp:198
std::string exit_id
Definition engine.hpp:201