PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
source/market_admission.cpp
Go to the documentation of this file.
6#include <algorithm>
7#include <cerrno>
8#include <exception>
9#include <stdexcept>
10#include <type_traits>
11#include <utility>
12
13namespace pineforge::admission {
14inline namespace market_admission_v2 {
15namespace {
16bool same(const ReviewReceipt& a, const ReviewReceipt& b) {
17 return a.sequence == b.sequence && a.checkpoint == b.checkpoint &&
18 a.bar == b.bar && a.target_command == b.target_command;
19}
20bool same(const SizingRevision& a, const SizingRevision& b) {
21 return a.sequence == b.sequence && a.cause_fill == b.cause_fill &&
22 a.bar == b.bar && a.target_command == b.target_command;
23}
24bool known_checkpoint(Checkpoint checkpoint) {
25 return checkpoint == Checkpoint::DefaultGross ||
26 checkpoint == Checkpoint::ExplicitPair || checkpoint == Checkpoint::TerminalGross;
27}
28void require_origin(const std::shared_ptr<const CommandObservation>& origin,
29 uint64_t target, uint64_t event, int bar) {
30 if (!origin || target != origin->command || event <= origin->command ||
31 bar < origin->bar)
32 throw coded<std::invalid_argument>(RunFailureCode::engine_invariant, {},
33 "admission receipt requires its exact earlier command");
34}
35}
36void Draft::bind(std::shared_ptr<const CommandObservation> observation) {
37 if(observation_ || !observation || observation->command==0)
39 "market draft requires one original command observation");
40 observation_=std::move(observation);
41}
43 require_origin(observation_, receipt.target_command, receipt.sequence, receipt.bar);
44 if (!known_checkpoint(receipt.checkpoint))
46 "review requires a known checkpoint");
47 if (review_) {
48 if (same(*review_, receipt)) return;
50 "first admission review cannot be replaced");
51 }
52 review_ = receipt;
53}
55 require_origin(observation_, receipt.target_command, receipt.sequence, receipt.bar);
56 if (receipt.cause_fill == 0)
58 "sizing revision requires committed cause");
59 if (sizing_revision_) {
60 if (same(*sizing_revision_, receipt)) return;
61 if (receipt.sequence <= sizing_revision_->sequence ||
62 receipt.bar < sizing_revision_->bar || receipt.cause_fill < sizing_revision_->cause_fill)
64 "sizing revision cannot rewrite or precede its latest cause");
65 }
66 sizing_revision_ = receipt;
67}
68uint64_t sequence(const Event& event) {
69 return std::visit([](const auto& value)->uint64_t {
70 using T=std::decay_t<decltype(value)>;
71 if constexpr(std::is_same_v<T,CommandEvent>) {
72 if(!value.observation) {
74 "command event requires an observation");
75 }
76 return value.observation->command;
77 }
78 else return value.receipt.sequence;
79 },event);
80}
81struct Journal::CodecState final : detail::ScratchReadback {
83 mutable std::vector<std::byte> page;
84 std::uint64_t count = 0;
85 // One write-behind abandonment slot. It joins the next successful record
86 // transaction, so abandoning a failed append cannot modify that append's
87 // previously committed relation.
88 std::uint64_t abandoned = 0;
89 std::uint64_t failed_sequence = 0;
90 mutable bool snapshot_valid = false;
92
93 explicit CodecState(detail::CommandScratchStore& source) : store(source), page(source.page_size()) {}
94 static void check(detail::ScratchStatus status, const detail::ScratchReadback* reader) {
95 if (status == detail::ScratchStatus::ok) return;
96 if (status == detail::ScratchStatus::io_error) {
97 if (reader) detail::rethrow_carried_scratch_failure(reader->carried_failure());
98 detail::raise_scratch_io_error(EIO, "admission journal codec I/O");
99 }
100 throw coded<std::logic_error>(RunFailureCode::engine_invariant, {},
101 "admission journal codec invalid storage");
102 }
103 detail::ScratchBuffer buffer() const { return {page.data(),page.size()}; }
104 void retire_through(std::uint64_t sequence) {
105 auto status = store.begin();
106 check(status, this);
108 while ((status = store.first(handle)) == detail::ScratchStatus::ok) {
109 std::array<std::byte,8> key{};
110 status = store.read(handle,detail::ScratchField::key,0,{key.data(),key.size()});
111 if (status != detail::ScratchStatus::ok) break;
112 std::uint64_t id = 0;
113 for (unsigned i=0;i!=8;++i)
114 id |= std::uint64_t(std::to_integer<unsigned>(key[i])) << (8*i);
115 if (id > sequence) break; // trailing abandoned IDs remain consumed
116 status = store.erase(handle);
117 if (status != detail::ScratchStatus::ok) break;
118 }
120 check(detail::journal_codec::finish(store,status), this);
121 }
122 std::size_t page_size() const noexcept override { return store.page_size(); }
123 std::uint64_t record_count() const noexcept override { return count; }
124 const detail::ReportScratchIoError* carried_failure() const noexcept override {
125 return store.carried_failure();
126 }
128 detail::ScratchRecordInfo& info) const noexcept override {
129 detail::ScratchRecordInfo actual;
130 const auto status = store.point(handle,actual);
131 if (status != detail::ScratchStatus::ok) return status;
132 if (!actual.value_bytes) return detail::ScratchStatus::not_found;
133 info = actual;
134 return status;
135 }
137 std::uint64_t offset, detail::ScratchBuffer out) const noexcept override {
138 detail::ScratchRecordInfo info;
139 const auto status = point(handle,info);
140 return status == detail::ScratchStatus::ok ? store.read(handle,field,offset,out) : status;
141 }
142 detail::ScratchStatus visit(detail::ScratchHandle handle, detail::ScratchBuffer buffer,
143 detail::ScratchChunkVisitor visitor, void* context) const noexcept override {
144 if (!buffer.data || buffer.size != page_size() || !visitor)
146 detail::ScratchRecordInfo info;
147 const auto status = point(handle,info);
148 return status == detail::ScratchStatus::ok ? store.visit(handle,buffer,visitor,context) : status;
149 }
150 detail::ScratchStatus for_each(detail::ScratchBuffer buffer,
151 detail::ScratchRecordVisitor visitor, void* context) const noexcept override {
152 if (!visitor) return detail::ScratchStatus::programming_error;
153 struct Walk {
154 detail::ScratchRecordVisitor visitor;
155 void* context;
156 static bool row(void* self,const detail::ScratchRecordView& value) noexcept {
157 auto& walk = *static_cast<Walk*>(self);
158 return !value.info.value_bytes || walk.visitor(walk.context,value);
159 }
160 } walk{visitor,context};
161 return store.for_each(buffer,&Walk::row,&walk);
162 }
163};
164
165Journal::Journal() noexcept = default;
166Journal::~Journal() = default;
167
168void bind_journal_codec(Journal& journal, detail::CommandScratchStore& store) {
169 if (store.status() == detail::ScratchStatus::io_error)
170 detail::rethrow_carried_scratch_failure(store.carried_failure());
171 if (journal.next_sequence_ != 1 || journal.event_count() || journal.has_unfinished_events()
172 || journal.codec_ || store.status() != detail::ScratchStatus::ok
173 || store.record_count() || store.transaction_active())
175 "journal codec requires an empty committed store");
176 journal.codec_ = std::make_unique<Journal::CodecState>(store);
177}
178
180 if (!journal.codec_)
182 "journal codec is not bound");
183 return *journal.codec_;
184}
185
187 const detail::ScratchByteSource& source) noexcept {
188 using Status = detail::ScratchStatus;
189 namespace jc = detail::journal_codec;
190 if (!journal.codec_) return Status::programming_error;
191 auto& codec = *journal.codec_;
192 if (codec.failure != Status::ok) return codec.failure;
193 JournalRecordSummary summary;
194 auto status = scan_journal_record(source,codec.buffer(),summary);
195 if (status != Status::ok) return status;
196 if (!summary.sequence || summary.sequence >= journal.next_sequence_
197 || summary.sequence <= journal.retired_prefix_.last_sequence
198 || summary.sequence == codec.abandoned)
199 return Status::programming_error;
201 status = jc::find(codec.store,summary.sequence,existing);
202 if (status == Status::ok) return Status::programming_error; // completed or abandoned
203 if (status != Status::not_found) return status;
204 status = codec.store.begin();
205 if (status != Status::ok) return status;
207 if (codec.abandoned && codec.abandoned > journal.retired_prefix_.last_sequence) {
208 const jc::WordSource abandoned(codec.abandoned);
209 const detail::ScratchSpanSource empty({nullptr,0});
210 status = codec.store.put(abandoned,empty,handle);
211 }
212 if (status == Status::ok) {
213 const jc::WordSource key(summary.sequence);
214 status = codec.store.put(key,source,handle);
215 }
216 status = jc::finish(codec.store,status);
217 if (status == Status::ok) {
218 codec.abandoned = 0;
219 codec.failed_sequence = 0;
220 ++codec.count;
221 --journal.outstanding_count_;
222 codec.snapshot_valid = false;
223 std::vector<Event>().swap(journal.events_);
224 if (summary.sequence <= journal.folded_window_.last_sequence) journal.fold_valid_ = false;
225 if (summary.rejected && journal.rejection_gate_.valid
226 && summary.sequence > journal.rejection_gate_.sequence)
227 journal.rejection_gate_ = {summary.sequence,summary.rejected_bar,true};
228 } else codec.failed_sequence = summary.sequence;
229 return status;
230}
231
232std::size_t Journal::event_count() const {
233 return retired_prefix_.count + (codec_ ? static_cast<std::size_t>(codec_->count) : events_.size());
234}
235
236const std::vector<Event>& Journal::events() const {
237 if (!codec_ || codec_->snapshot_valid) return events_;
238 std::vector<Event> result;
240 auto status = codec_->store.first(handle);
241 while (status == detail::ScratchStatus::ok) {
243 CodecState::check(codec_->store.point(handle,info), codec_.get());
244 if (info.value_bytes) {
245 detail::journal_codec::StoreSource source(codec_->store,handle,info.value_bytes);
246 Event event;
247 CodecState::check(decode_journal_record(source,codec_->buffer(),event), codec_.get());
248 result.push_back(std::move(event));
249 }
250 status = codec_->store.next(handle,handle);
251 }
252 if (status != detail::ScratchStatus::end) CodecState::check(status, codec_.get());
253 events_.swap(result);
254 codec_->snapshot_valid = true;
255 return events_;
256}
257
259 // A destructive filter supplies no prefix proof of its own. A validated
260 // nonzero retired prefix may seed its surviving tail, but never a zero
261 // restart or a rewrite of the command gate's last rejected bar.
262 if (filtered_ && retired_prefix_.count == 0)
264 "admission filter cannot rewrite folded lifetime history");
265 if (codec_) CodecState::check(codec_->failure, codec_.get());
266 if (fold_valid_ && !filtered_ && folded_window_.count == event_count())
267 return folded_window_;
268 Prefix result = fold_valid_ && !filtered_ ? folded_window_ : retired_prefix_;
269 auto gate = rejection_gate_.valid ? rejection_gate_
270 : RejectionGate{retired_prefix_.last_sequence,retired_prefix_.last_rejected_command_bar,true};
271 auto retained_rejected_bar = retired_prefix_.last_rejected_command_bar;
272 const auto append = [&](const detail::ScratchByteSource& source, detail::ScratchBuffer page) {
273 JournalRecordSummary summary;
274 std::optional<detail::ReportScratchIoError> failure;
275 const auto status = fold_validated_journal_record(source,page,summary,result.digest,
276 result.last_sequence,&failure);
277 if (status == detail::ScratchStatus::io_error && failure)
279 CodecState::check(status, codec_.get());
280 if (summary.sequence <= result.last_sequence) return;
281 if (result.count == std::numeric_limits<std::size_t>::max())
283 "admission event count exhausted");
284 ++result.count;
285 result.last_sequence = summary.sequence;
286 if (summary.rejected) {
288 if (summary.sequence > gate.sequence)
289 gate = {summary.sequence,summary.rejected_bar,true};
290 // Prove the old gate at its folded frontier independently of any
291 // later appends; a new rejection cannot repair a discarded cause.
292 if (summary.sequence <= folded_window_.last_sequence)
293 retained_rejected_bar = summary.rejected_bar;
294 }
295 };
296 if (codec_) {
298 // Inclusive seek permits one frontier key check, never a payload scan
299 // of the prefix. Keys are the sequences validated at publication.
301 auto status = codec_->store.lower_bound(frontier,handle);
302 while (status == detail::ScratchStatus::ok) {
304 CodecState::check(codec_->store.point(handle,info), codec_.get());
305 std::array<std::byte,8> key{};
306 CodecState::check(codec_->store.read(handle,detail::ScratchField::key,0,
307 {key.data(),key.size()}), codec_.get());
308 std::uint64_t id = 0;
309 for (unsigned i=0;i!=8;++i)
310 id |= std::uint64_t(std::to_integer<unsigned>(key[i])) << (8*i);
311 if (id > result.last_sequence && info.value_bytes) {
312 detail::journal_codec::StoreSource source(codec_->store,handle,info.value_bytes);
313 append(source,codec_->buffer());
314 }
315 status = codec_->store.next(handle,handle);
316 }
317 if (status != detail::ScratchStatus::end) CodecState::check(status, codec_.get());
318 } else {
319 std::array<std::byte,512> page{};
320 const auto first = std::upper_bound(events_.begin(),events_.end(),result.last_sequence,
321 [](uint64_t id,const auto& event){return id < sequence(event);});
322 for (auto at = first; at != events_.end(); ++at) {
323 const auto source = encode_journal_record(*at);
324 append(source,{page.data(),page.size()});
325 }
326 }
327 if (filtered_ && retained_rejected_bar != folded_window_.last_rejected_command_bar)
329 "admission filter cannot rewrite the rejection gate");
330 folded_window_ = result;
331 fold_valid_ = true;
332 filtered_ = false;
333 rejection_gate_ = gate;
334 return result;
335}
336
338 if (filtered_ && fold_valid_) return folded_window_.last_rejected_command_bar;
339 if (codec_) CodecState::check(codec_->failure, codec_.get());
340 if (!rejection_gate_.valid) return folded_window().last_rejected_command_bar;
341 return rejection_gate_.bar;
342}
343
345 if (other.active_allocations_ != 0)
347 "cannot copy admission journal with active captures");
348 next_sequence_ = other.next_sequence_;
349 outstanding_sequences_ = other.outstanding_sequences_;
350 if (other.codec_)
351 for (auto id = other.retired_prefix_.last_sequence + 1; id < other.next_sequence_; ++id) {
353 const auto status = detail::journal_codec::find(other.codec_->store,id,info);
354 if (status == detail::ScratchStatus::not_found) {
355 if (id != other.codec_->abandoned) outstanding_sequences_.push_back(id);
356 }
357 else CodecState::check(status, other.codec_.get());
358 }
359 events_ = other.events();
360 outstanding_count_ = other.outstanding_count_;
361 retired_prefix_ = other.retired_prefix_;
362 folded_window_ = other.folded_window_;
363 fold_valid_ = other.fold_valid_;
364 filtered_ = other.filtered_;
365 rejection_gate_ = other.rejection_gate_;
366 retain_full_observation_history_ = other.retain_full_observation_history_;
367}
369 if (this == &other) return *this;
370 if (active_allocations_ != 0)
372 "cannot replace admission journal with active captures");
373 Journal copy(other);
374 std::swap(next_sequence_, copy.next_sequence_);
375 codec_.swap(copy.codec_);
376 std::swap(outstanding_count_, copy.outstanding_count_);
377 outstanding_sequences_.swap(copy.outstanding_sequences_);
378 events_.swap(copy.events_);
379 std::swap(retired_prefix_, copy.retired_prefix_);
380 std::swap(folded_window_, copy.folded_window_);
381 std::swap(fold_valid_, copy.fold_valid_);
382 std::swap(filtered_, copy.filtered_);
383 std::swap(rejection_gate_, copy.rejection_gate_);
384 std::swap(retain_full_observation_history_, copy.retain_full_observation_history_);
385 return *this;
386}
388 if (other.active_allocations_ != 0)
390 "cannot move admission journal with active captures");
391 next_sequence_ = other.next_sequence_;
392 codec_ = std::move(other.codec_);
393 outstanding_count_ = std::exchange(other.outstanding_count_,0);
394 outstanding_sequences_ = std::move(other.outstanding_sequences_);
395 events_ = std::move(other.events_);
396 retired_prefix_ = other.retired_prefix_;
397 folded_window_ = other.folded_window_;
398 fold_valid_ = other.fold_valid_;
399 filtered_ = other.filtered_;
400 rejection_gate_ = other.rejection_gate_;
401 retain_full_observation_history_ = other.retain_full_observation_history_;
402 other.outstanding_sequences_.clear();
403 other.events_.clear();
404 other.next_sequence_ = 1;
405 other.retired_prefix_ = {};
406 other.folded_window_ = {};
407 other.fold_valid_ = false;
408 other.filtered_ = false;
409 other.rejection_gate_ = {};
410 other.retain_full_observation_history_ = false;
411}
413 if (this == &other) return *this;
414 if (active_allocations_ != 0)
416 "cannot replace admission journal with active captures");
417 Journal moved(std::move(other));
418 std::swap(next_sequence_, moved.next_sequence_);
419 codec_.swap(moved.codec_);
420 std::swap(outstanding_count_, moved.outstanding_count_);
421 outstanding_sequences_.swap(moved.outstanding_sequences_);
422 events_.swap(moved.events_);
423 std::swap(retired_prefix_, moved.retired_prefix_);
424 std::swap(folded_window_, moved.folded_window_);
425 std::swap(fold_valid_, moved.fold_valid_);
426 std::swap(filtered_, moved.filtered_);
427 std::swap(rejection_gate_, moved.rejection_gate_);
428 std::swap(retain_full_observation_history_, moved.retain_full_observation_history_);
429 return *this;
430}
432 if(next_sequence_==std::numeric_limits<uint64_t>::max()) {
434 "admission event sequence exhausted");
435 }
436 if (codec_) CodecState::check(codec_->failure, codec_.get());
437 else outstanding_sequences_.push_back(next_sequence_);
438 ++outstanding_count_;
439 return next_sequence_++;
440}
442 const auto id = next_sequence();
443 ++active_allocations_;
444 return Allocation(*this, id);
445}
446void Journal::abandon(uint64_t id) noexcept {
447 if (codec_) {
448 if (!id || id >= next_sequence_ || id <= retired_prefix_.last_sequence) return;
449 if (id == codec_->abandoned) return;
451 auto status = id == codec_->failed_sequence ? detail::ScratchStatus::not_found
452 : detail::journal_codec::find(codec_->store,id,info);
453 if (status == detail::ScratchStatus::ok) return;
454 if (status == detail::ScratchStatus::not_found) {
455 if (codec_->abandoned && codec_->abandoned > retired_prefix_.last_sequence) {
456 const detail::ScratchSpanSource empty({nullptr,0});
457 status = detail::journal_codec::put(codec_->store,codec_->abandoned,empty);
458 if (status != detail::ScratchStatus::ok) { codec_->failure = status; return; }
459 }
460 codec_->abandoned = id;
461 codec_->failed_sequence = 0;
462 --outstanding_count_;
463 return;
464 }
465 codec_->failure = status; // destructor cannot throw; next boundary reports it
466 return;
467 }
468 const auto at = std::find(outstanding_sequences_.begin(), outstanding_sequences_.end(), id);
469 if (at != outstanding_sequences_.end()) {
470 outstanding_sequences_.erase(at);
471 --outstanding_count_;
472 }
473}
475 : journal_(other.journal_), sequence_(other.sequence_) {
476 other.journal_ = nullptr;
477 other.sequence_ = 0;
478}
479void Journal::release_allocation(uint64_t sequence) noexcept {
480 abandon(sequence);
481 --active_allocations_;
482}
483Allocation::~Allocation() noexcept { if (journal_) journal_->release_allocation(sequence_); }
485 const auto id=sequence(event);
486 if (codec_) {
487 if (!id || id >= next_sequence_ || id <= retired_prefix_.last_sequence)
489 "admission event has no outstanding allocation");
490 if (id == codec_->abandoned)
492 "admission event has no outstanding allocation");
494 const auto found = detail::journal_codec::find(codec_->store,id,info);
495 if (found == detail::ScratchStatus::ok)
497 "admission event has no outstanding allocation");
498 if (found != detail::ScratchStatus::not_found) CodecState::check(found, codec_.get());
499 const auto source = encode_journal_record(event);
500 CodecState::check(append_journal_record(*this,source), codec_.get());
501 return;
502 }
503 const auto pending = std::find(outstanding_sequences_.begin(), outstanding_sequences_.end(), id);
504 if (pending == outstanding_sequences_.end())
506 "admission event has no outstanding allocation");
507 const auto* command = std::get_if<CommandEvent>(&event);
508 const bool rejected = command && rejected_command(command->outcome);
509 const int rejected_bar = rejected ? command->observation->bar : -1;
510 auto at=std::lower_bound(events_.begin(),events_.end(),id,[](const auto& e,uint64_t n){return sequence(e)<n;});
511 events_.insert(at,std::move(event));
512 // Consume only after insertion succeeds. Allocation failure leaves a caller
513 // free to retry this still-outstanding event; retain never restores an ID.
514 outstanding_sequences_.erase(pending);
515 --outstanding_count_;
516 if (id <= folded_window_.last_sequence) fold_valid_ = false;
517 if (rejected && rejection_gate_.valid && id > rejection_gate_.sequence)
518 rejection_gate_ = {id,rejected_bar,true};
519}
520void Journal::retain(const std::vector<uint64_t>& retained) {
521 if (codec_) {
522 auto& store = codec_->store;
523 auto status = store.begin();
524 CodecState::check(status, codec_.get());
525 auto removed = std::uint64_t{0};
527 status = store.first(handle);
528 while (status == detail::ScratchStatus::ok) {
530 const auto following = store.next(handle,next);
531 if (following != detail::ScratchStatus::ok && following != detail::ScratchStatus::end) {
532 status = following; break;
533 }
535 status = store.point(handle,info);
536 if (status != detail::ScratchStatus::ok) break;
537 if (info.value_bytes) {
538 std::array<std::byte,8> key{};
539 status = store.read(handle,detail::ScratchField::key,0,{key.data(),key.size()});
540 if (status != detail::ScratchStatus::ok) break;
541 std::uint64_t id = 0;
542 for (unsigned i=0;i!=8;++i)
543 id |= std::uint64_t(std::to_integer<unsigned>(key[i])) << (8*i);
544 if (std::find(retained.begin(),retained.end(),id)==retained.end()) {
545 const detail::journal_codec::WordSource probe(id);
546 const detail::ScratchSpanSource empty({nullptr,0});
548 status = store.put(probe,empty,same);
549 if (status != detail::ScratchStatus::ok) break;
550 ++removed;
551 }
552 }
553 handle = next; status = following;
554 }
556 CodecState::check(detail::journal_codec::finish(store,status), codec_.get());
557 if (removed) {
558 codec_->count -= removed;
559 codec_->snapshot_valid = false;
560 std::vector<Event>().swap(events_);
561 filtered_ = filtered_ || fold_valid_;
562 rejection_gate_.valid = false;
563 }
564 return;
565 }
566 const auto before = events_.size();
567 events_.erase(std::remove_if(events_.begin(),events_.end(),[&](const auto& e){
568 return std::find(retained.begin(),retained.end(),sequence(e))==retained.end();
569 }),events_.end());
570 if (events_.size() != before) {
571 if (fold_valid_) filtered_ = true;
572 rejection_gate_.valid = false;
573 }
574}
578 "cannot reset admission journal with unfinished events");
579 if (codec_) {
580 CodecState::check(codec_->failure, codec_.get());
581 CodecState::check(detail::journal_codec::clear(codec_->store), codec_.get());
582 codec_->count = 0; codec_->snapshot_valid = false;
583 codec_->abandoned = codec_->failed_sequence = 0;
584 }
585 std::vector<Event>().swap(events_);
586 std::vector<uint64_t>().swap(outstanding_sequences_);
587 next_sequence_=1;retired_prefix_={};folded_window_={};fold_valid_=false;filtered_=false;
588 rejection_gate_ = {};
589}
591 if (next_sequence_ != 1 || has_unfinished_events() || !events_.empty()
592 || retired_prefix_.count != 0)
594 "full admission observation requires a fresh journal");
595 retain_full_observation_history_ = true;
596}
597void Journal::retire_prefix(std::size_t count, uint64_t digest, uint64_t last_sequence) {
600 "cannot retire admission journal with unfinished events");
601 const auto expected = folded_window();
602 if (count != expected.count || last_sequence != expected.last_sequence
603 || digest != expected.digest || count != event_count())
605 "admission retirement requires the exact folded window");
606 if (retain_full_observation_history_) return;
607 if (codec_) {
608 codec_->retire_through(expected.last_sequence);
609 codec_->count = 0; codec_->snapshot_valid = false;
610 if (codec_->abandoned <= expected.last_sequence) codec_->abandoned = 0;
611 }
612 retired_prefix_ = expected;
613 // Clear destroys all variant payloads, including before/book/review vectors
614 // and shared observations. Capacity is bounded by the largest live bar.
615 std::vector<Event>().swap(events_);
616}
617CommandCapture::CommandCapture(Allocation allocation,CommandObservation input,std::vector<BookObservation> before,
618 std::function<void(CommandEvent)> complete):allocation_(std::move(allocation)),input_(std::move(input)),before_(std::move(before)),complete_(std::move(complete)) {
619 if (input_.command != allocation_.sequence())
621 "command capture requires its allocated event");
622}
624 // Never run a potentially throwing completion while another exception is
625 // unwinding. The Allocation member abandons this unfinished event instead.
626 if (std::uncaught_exceptions() != 0) return;
627 complete_({std::make_shared<const CommandObservation>(input_),outcome_,0,std::move(before_),{}});
628}
629void CommandCapture::bind(Draft& draft,std::optional<SizingObservation> sizing,double equity,double price) {
630 input_.original_sizing=std::move(sizing);input_.explicit_equity=equity;input_.explicit_price=price;
631 draft.bind(std::make_shared<const CommandObservation>(input_));
633}
635 std::function<void(ReviewEvent)> complete)
636 : allocation_(std::move(allocation)),event_(std::move(event)),complete_(std::move(complete)) {
637 if (event_.receipt.sequence != allocation_.sequence() || event_.receipt.target_command != 0 ||
638 !known_checkpoint(event_.receipt.checkpoint))
640 "review capture requires an allocated batch checkpoint");
641}
643 if (std::uncaught_exceptions() != 0) return;
644 complete_(std::move(event_));
645}
647 const auto& origin = draft.observation();
648 if (!origin || std::none_of(event_.reviewed.begin(), event_.reviewed.end(), [&](const auto& order) {
649 return order.draft.observation() && order.draft.observation()->command == origin->command;
650 }))
652 "draft does not belong to this admission review");
653 auto receipt = event_.receipt;
654 receipt.target_command = origin->command;
655 require_origin(origin, receipt.target_command, receipt.sequence, receipt.bar);
656 return receipt;
657}
658
659namespace {
660struct Reflect {
661 const FieldVisitor& visit;
662 template<class T>void field(const std::string& path,const char* name,const T& value) const {
663 if constexpr(std::is_same_v<T,bool>)visit({path+"."+name,uint64_t(value?1:0)});
664 else if constexpr(std::is_enum_v<T>||std::is_same_v<T,int>)visit({path+"."+name,int64_t(value)});
665 else visit({path+"."+name,value});
666 }
667 void birth(const OrderBirth& o,const std::string& p)const {
668 field(p,"cause",o.cause());field(p,"bar",o.bar());field(p,"timestamp",o.timestamp());
669 field(p,"cursor_domain",o.cursor().domain());field(p,"cursor_position",o.cursor().position());
670 field(p,"cursor_index",o.cursor().index());field(p,"cursor_count",o.cursor().count());
671 field(p,"cursor_price",o.cursor_price());field(p,"first_fill",o.first_fill());
672 field(p,"last_fill",o.last_fill());field(p,"evaluation_ordinal",o.evaluation_ordinal());
673 }
674 void config(const Configuration& o,const std::string& p)const {
675#define F(name) field(p,#name,o.name)
676 F(process_on_close);F(calc_on_fills);F(magnifier);F(fill_recalculation);F(scheduler);
677 F(slippage);F(pyramiding);F(default_quantity_type);F(default_quantity_value);
678 F(long_margin);F(short_margin);F(commission_value);F(commission_type);F(pointvalue);F(fx);F(quantity_step);F(mintick);F(risk_direction);F(loss_days_limit);
679 F(drawdown_limit);F(intraday_loss_limit);F(position_limit);F(fill_cap_active);F(risk_halted);
680#undef F
681 }
682 void sizing(const SizingObservation& o,const std::string& p)const {
683#define F(name) field(p,#name,o.name)
684 F(quantity);F(equity);F(price);F(mark);F(fx);
685#undef F
686 }
687 void command(const CommandObservation& o,const std::string& p)const {
688#define F(name) field(p,#name,o.name)
689 F(command);F(kind);birth(o.birth,p+".birth");F(id);F(requested_quantity);F(quantity_type);F(buy);
690 field(p+".prices","limit",o.prices.limit);field(p+".prices","stop",o.prices.stop);
691 F(oca_name);F(oca_type);config(o.configuration,p+".configuration");F(bar);F(placement_side);F(placement_cycle);
692 F(prior_close_quantity);F(held_quantity);F(held_entries);F(realized_equity);F(placement_equity);F(signal_close);F(quantized_fixed_quantity);
693 field(p,"original_sizing_present",o.original_sizing.has_value());
694 if(o.original_sizing)sizing(*o.original_sizing,p+".original_sizing");
695 F(explicit_equity);F(explicit_price);
696#undef F
697 }
698 void review(const ReviewReceipt& o,const std::string& p)const {
699 field(p,"sequence",o.sequence);field(p,"checkpoint",o.checkpoint);field(p,"bar",o.bar);
700 field(p,"target_command",o.target_command);
701 }
702 void revision(const SizingRevision& o,const std::string& p)const {
703 field(p,"sequence",o.sequence);field(p,"cause_fill",o.cause_fill);field(p,"bar",o.bar);
704 field(p,"target_command",o.target_command);
705 }
706 void draft(const Draft& o,const std::string& p)const {
707 field(p,"observation_present",bool(o.observation()));
708 if(o.observation())command(*o.observation(),p+".observation");
709 field(p,"review_present",o.review().has_value());if(o.review())review(*o.review(),p+".review");
710 field(p,"sizing_revision_present",o.sizing_revision().has_value());if(o.sizing_revision())revision(*o.sizing_revision(),p+".sizing_revision");
711 }
712 void book(const BookObservation& o,const std::string& p)const {
713 field(p,"incarnation",o.incarnation);field(p,"priority",o.priority);field(p,"bar",o.bar);
714 field(p,"type",o.type);field(p,"placement_side",o.placement_side);field(p,"buy",o.buy);field(p,"id",o.id);
715 field(p,"oca_name",o.oca_name);field(p,"oca_type",o.oca_type);birth(o.birth,p+".birth");
716 field(p+".prices","limit",o.prices.limit);field(p+".prices","stop",o.prices.stop);
717 field(p+".prices","trail_points",o.prices.trail_points);field(p+".prices","trail_price",o.prices.trail_price);
718 field(p+".prices","trail_offset",o.prices.trail_offset);draft(o.draft,p+".draft");
719 }
720 void resolution(const InstructionResolution& o,const std::string& p)const {
721 field(p,"incarnation",o.incarnation);field(p,"kind",o.kind);field(p,"peer_incarnation",o.peer_incarnation);
722 field(p,"own_priority",o.own_priority);field(p,"peer_priority",o.peer_priority);field(p,"transaction_quantity",o.transaction_quantity);
723 }
724 template<class T,class Fn>void array(const std::vector<T>& values,const std::string& p,Fn fn)const {
725 field(p,"size",uint64_t(values.size()));for(size_t i=0;i<values.size();++i)fn(values[i],p+"["+std::to_string(i)+"]");
726 }
727 void event(const Event& value,const std::string& p)const {
728 field(p,"kind",uint64_t(value.index()));
729 std::visit([&](const auto& o){using T=std::decay_t<decltype(o)>;
730 if constexpr(std::is_same_v<T,CommandEvent>){
731 command(*o.observation,p+".observation");field(p,"outcome",o.outcome);field(p,"admitted_incarnation",o.admitted_incarnation);
732 array(o.before,p+".before",[&](const auto& x,const auto& q){book(x,q);});
733 array(o.removed,p+".removed",[&](auto x,const auto& q){field(q,"incarnation",x);});
734 }else if constexpr(std::is_same_v<T,ReviewEvent>){
735 review(o.receipt,p+".receipt");config(o.configuration,p+".configuration");field(p,"open_price",o.open_price);field(p,"position_side",o.position_side);field(p,"position_cycle",o.position_cycle);array(o.book,p+".book",[&](const auto& x,const auto& q){book(x,q);});array(o.reviewed,p+".reviewed",[&](const auto& x,const auto& q){book(x,q);});
736 array(o.resolutions,p+".resolutions",[&](const auto& x,const auto& q){resolution(x,q);});
737 array(o.causes,p+".causes",[&](auto x,const auto& q){field(q,"sequence",x);});
738 }else{
739 revision(o.receipt,p+".receipt");field(p,"incarnation",o.incarnation);
740 sizing(o.before,p+".before");sizing(o.after,p+".after");
741 field(p,"affordability_equity_before",o.affordability_equity_before);field(p,"affordability_equity_after",o.affordability_equity_after);
742 }
743 },value);
744 }
745};
746}
747void reflect(const Draft& value,const std::string& path,const FieldVisitor& visit){Reflect{visit}.draft(value,path);}
748void reflect(const Event& value,const std::string& path,const FieldVisitor& visit){Reflect{visit}.event(value,path);}
749void Journal::reflect(const std::string& path,const FieldVisitor& visit)const {
750 Reflect r{visit};r.field(path,"next_sequence",next_sequence_);
751 r.field(path,"active_allocations",active_allocations_);
752 if (codec_) {
753 r.field(path+".outstanding_sequences","size",outstanding_count_);
754 std::size_t rank = 0;
755 for (auto id = retired_prefix_.last_sequence + 1; id < next_sequence_; ++id) {
756 if (id == codec_->abandoned) continue;
758 const auto status = detail::journal_codec::find(codec_->store,id,info);
760 r.field(path+".outstanding_sequences["+std::to_string(rank++)+"]","sequence",id);
761 else CodecState::check(status, codec_.get());
762 }
763 } else r.array(outstanding_sequences_,path+".outstanding_sequences",[&](auto sequence,const auto& p){r.field(p,"sequence",sequence);});
764 r.array(events(),path+".events",[&](const auto& event,const auto& p){r.event(event,p);});
765 // These are the original event fold and its derived lookup index, not new
766 // broker hash bytes. The v4 count/digest already covers their observations.
767 if (retired_prefix_.count != 0) {
768 r.field(path+".base","count",uint64_t(retired_prefix_.count));
769 r.field(path+".base","digest",retired_prefix_.digest);
770 r.field(path+".base","last_sequence",retired_prefix_.last_sequence);
771 r.field(path+".base","last_rejected_command_bar",retired_prefix_.last_rejected_command_bar);
772 }
773}
774std::vector<Field> fields(const Draft& draft) {
775 std::vector<Field> result;reflect(draft,"draft",[&](const auto& field){result.push_back(field);});return result;
776}
777namespace {
778template<class T>T read_field(const std::vector<Field>& fields,const std::string& path) {
779 for(const auto& field:fields)if(field.path==path)return std::get<T>(field.value);
780 return T{}; // absent optional payload; the presence leaf is always reflected
781}
782}
783uint64_t read_unsigned(const std::vector<Field>& f,const std::string& p){return read_field<uint64_t>(f,p);}
784int64_t read_integer(const std::vector<Field>& f,const std::string& p){return read_field<int64_t>(f,p);}
785double read_double(const std::vector<Field>& f,const std::string& p){return read_field<double>(f,p);}
786std::string read_string(const std::vector<Field>& f,const std::string& p){return read_field<std::string>(f,p);}
787} // inline namespace market_admission_v2
788} // namespace pineforge::admission
#define F(name)
CommandCapture(Allocation allocation, CommandObservation input, std::vector< BookObservation > before, std::function< void(CommandEvent)> complete)
void bind(Draft &draft, std::optional< SizingObservation > sizing, double explicit_equity, double explicit_price)
const std::shared_ptr< const CommandObservation > & observation() const
void bind(std::shared_ptr< const CommandObservation > observation)
friend detail::ScratchStatus append_journal_record(Journal &, const detail::ScratchByteSource &) noexcept
void reflect(const std::string &path, const FieldVisitor &visit) const
friend void bind_journal_codec(Journal &, detail::CommandScratchStore &)
void retire_prefix(std::size_t count, uint64_t digest, uint64_t last_sequence)
void retain(const std::vector< uint64_t > &sequences)
ReviewCapture(Allocation allocation, ReviewEvent event, std::function< void(ReviewEvent)> complete)
Base with a code: throw coded<std::invalid_argument>(code, {args}, text).
JournalRecordSource encode_journal_record(const Event &) noexcept
uint64_t read_unsigned(const std::vector< Field > &fields, const std::string &path)
std::string read_string(const std::vector< Field > &fields, const std::string &path)
int64_t read_integer(const std::vector< Field > &fields, const std::string &path)
detail::ScratchStatus scan_journal_record(const detail::ScratchByteSource &source, detail::ScratchBuffer page, JournalRecordSummary &summary) noexcept
std::vector< Field > fields(const Draft &draft)
std::function< void(const Field &)> FieldVisitor
detail::ScratchStatus decode_journal_record(const detail::ScratchByteSource &, detail::ScratchBuffer, Event &)
double read_double(const std::vector< Field > &fields, const std::string &path)
const detail::ScratchReadback & journal_readback(const Journal &)
void reflect(const Draft &value, const std::string &path, const FieldVisitor &visit)
std::variant< CommandEvent, ReviewEvent, SizingEvent > Event
detail::ScratchStatus fold_validated_journal_record(const detail::ScratchByteSource &source, detail::ScratchBuffer page, JournalRecordSummary &summary, std::uint64_t &digest, std::uint64_t after_sequence, std::optional< detail::ReportScratchIoError > *failure) noexcept
detail::ScratchStatus append_journal_record(Journal &, const detail::ScratchByteSource &) noexcept
ScratchStatus find(CommandScratchStore &store, std::uint64_t key, ScratchRecordInfo &out) noexcept
ScratchStatus finish(CommandScratchStore &store, ScratchStatus status) noexcept
ScratchStatus clear(CommandScratchStore &store) noexcept
ScratchStatus put(CommandScratchStore &store, std::uint64_t key, const ScratchByteSource &value) noexcept
bool(*)(void *, const ScratchChunkView &) noexcept ScratchChunkVisitor
std::uint64_t ScratchHandle
constexpr ScratchHandle scratch_no_handle
void rethrow_carried_scratch_failure(const ReportScratchIoError *failure)
void raise_scratch_io_error(int error, const char *operation)
bool(*)(void *, const ScratchRecordView &) noexcept ScratchRecordVisitor
int b(int64_t c)
Definition color.hpp:44