PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
exit_leg_lifecycle.hpp
Go to the documentation of this file.
1#pragma once
2// First standalone lifecycle ABI; aggregate engine layout uses its own epoch.
3#include <array>
4#include <cmath>
5#include <cstdint>
6#include <cstring>
7#include <limits>
8#include <memory>
9#include <optional>
10#include <stdexcept>
11#include <variant>
12#include <type_traits>
13#include <vector>
14
16inline namespace lifecycle_v1 {
17inline double absent() { return std::numeric_limits<double>::quiet_NaN(); }
18enum class Leg : uint8_t { Stop, Limit, Trail };
19// The observation domain a frame comes from. `FillRecalc` is the
20// fill-recalculation re-entry pass -- the host re-runs its script after a fill
21// and observes the rest of the same bar; `MagnifierFillRecalc` is that pass on
22// a magnified sub-bar. Their pre-1.0 spellings `Coof` / `MagnifierCoof` (coof =
23// calc-on-order-fills, the Pine adapter's name for the same pass) were removed
24// for 1.0; the values (1 and 3) and the underlying type are unchanged. See
25// ADR-0001 "Deprecated public spellings".
29enum class Phase : uint8_t { Observation, AfterMargin };
30enum class Fold : uint8_t { Prefix, Continue };
31struct Frame {
32 uint64_t event = 0;
33 int64_t bar = -1;
36};
37struct Target { uint64_t incarnation = 0; int64_t owner = 0; };
38struct Prices {
39 double limit_price = absent();
40 double stop_price = absent();
41 double trail_points = absent();
42 double trail_price = absent();
43 double trail_offset = absent();
44 double profit_ticks = absent();
45 double loss_ticks = absent();
46};
47class Lifecycle;
48struct Definition {
49 uint64_t incarnation() const { return incarnation_; }
50 uint64_t revision() const { return revision_; }
51 bool has_value() const { return value_ != nullptr; }
52 const Prices& prices() const { static const Prices empty{}; return value_ ? *value_ : empty; }
53private:
54 friend class Lifecycle;
55 uint64_t incarnation_ = 0;
56 uint64_t revision_ = 0;
57 std::shared_ptr<const Prices> value_;
58};
59struct Barrier { Frame requested; Target target; uint64_t revision = 0; };
62 double best = absent();
63 double prefix = absent();
64};
65struct Retirement { uint64_t generation; Frame cause; };
71struct Suspension {
73 std::vector<Leg> legs;
74 std::optional<Barrier> hold;
75 std::optional<Definition> revival_definition;
76 std::optional<Replacement> replacement;
77 std::optional<ObservationWindow> window;
78};
79struct BindOwner { int64_t owner; };
80struct Suspend {
81 std::vector<Leg> legs;
82 std::optional<Barrier> hold;
83 std::optional<ObservationWindow> window;
84 std::vector<Leg> retire;
85};
88struct Restore { std::vector<Leg> legs; };
89struct CompleteBarrier { Frame completed; std::optional<Barrier> requested; };
90struct Observe { double high; double low; int direction; Fold fold; };
91struct Cancel { std::vector<Leg> legs; };
102
103// One canonical definition and bounded current lifecycle. No external book,
104// source-rule recognizer or map of historical action outcomes.
106public:
107 const Prices& prices() const { return definition_.prices(); }
108 const Definition& current_definition() const { return definition_; }
109 Definition definition(uint64_t incarnation) const {
110 if (!incarnation || (target_.incarnation && target_.incarnation != incarnation))
111 throw std::logic_error("definition identity mismatch");
112 Definition d = definition_; d.incarnation_ = incarnation; return d;
113 }
114 Target target() const { return target_; }
115 uint64_t revision() const { return revision_; }
116 uint64_t generation(Leg leg) const { return generations_[index(leg)]; }
117 const std::optional<Suspension>& suspension() const { return suspension_; }
118 const std::array<std::optional<Retirement>, 3>& retirements() const { return retired_; }
119 const std::optional<Action>& last_action() const { return last_; }
120 bool suspended(Leg leg) const {
121 if (!suspension_) return false;
122 for (Leg selected : suspension_->legs) if (selected == leg) return true;
123 return false;
124 }
125 bool retired(Leg leg) const { return retired_[index(leg)].has_value(); }
126 bool available(Leg leg, int64_t bar) const {
127 if (suspended(leg) || retired(leg)) return false;
128 return leg != Leg::Trail || !suspension_ || !suspension_->window
129 || suspension_->window->excluded.bar != bar;
130 }
131 bool dormant() const { return suspension_.has_value(); }
132 bool pending_replacement() const { return suspension_ && suspension_->replacement.has_value(); }
133 double original_stop() const {
134 return suspension_ && suspension_->revival_definition
135 ? suspension_->revival_definition->prices().stop_price : absent();
136 }
137 std::optional<Barrier> release_barrier() const {
138 if (!suspension_) return {};
139 if (suspension_->replacement) return suspension_->replacement->release;
140 return suspension_->hold;
141 }
142 int64_t hold_bar() const { return suspension_ && suspension_->hold ? suspension_->hold->requested.bar : -1; }
143 int64_t excluded_bar() const { return suspension_ && suspension_->window ? suspension_->window->excluded.bar : -1; }
144 double trail_best() const { return suspension_ && suspension_->window ? suspension_->window->best : absent(); }
145 double trail_prefix() const { return suspension_ && suspension_->window ? suspension_->window->prefix : absent(); }
146
147 // Construction/fresh copied quantity bindings, not a replayable action.
148 // The owner supplies the new incarnation; no receipt survives a fork.
149 void attach(uint64_t incarnation, int64_t owner) {
150 if (!incarnation || owner < 0 || target_.incarnation) throw std::logic_error("exit lifecycle already attached");
151 target_ = {incarnation, owner}; definition_.incarnation_ = incarnation;
152 }
153 void fork(uint64_t incarnation, int64_t owner) {
154 if (!incarnation || owner < 0 || incarnation == target_.incarnation) throw std::logic_error("invalid exit lifecycle fork");
155 target_ = {incarnation, owner}; definition_.incarnation_ = incarnation;
156 revision_ = 0; last_.reset();
157 // A new quantity-bound instruction has no queue predecessor, even
158 // when its inherited old-stop definition remains causally relevant.
159 if (suspension_ && suspension_->replacement)
160 suspension_->replacement->queue_predecessor = 0;
161 if (suspension_ && suspension_->hold) {
162 suspension_->hold->target = target_; suspension_->hold->revision = revision_;
163 }
164 if (suspension_ && suspension_->replacement) {
165 suspension_->replacement->release.target = target_;
166 suspension_->replacement->release.revision = revision_;
167 }
168 }
169
170 // Trusted local definition construction/materialization. The immutable
171 // Prices object is replaced, so predecessor references cannot diverge.
172 // Each change invalidates outstanding action revisions, even without fills.
173 void set_prices(Prices value) {
174 if (revision_ == UINT64_MAX || definition_.revision_ == UINT64_MAX)
175 throw std::overflow_error("exit definition revision exhausted");
176 auto next = std::make_shared<const Prices>(std::move(value));
177 definition_.value_ = std::move(next); ++definition_.revision_; ++revision_;
178 }
179#define PF_LEG_PRICE_SETTER(name) \
180 double set_##name(double value) { auto next = prices(); next.name = value; set_prices(std::move(next)); return value; }
181 PF_LEG_PRICE_SETTER(limit_price)
182 PF_LEG_PRICE_SETTER(stop_price)
183 PF_LEG_PRICE_SETTER(trail_points)
184 PF_LEG_PRICE_SETTER(trail_price)
185 PF_LEG_PRICE_SETTER(trail_offset)
186 PF_LEG_PRICE_SETTER(profit_ticks)
187 PF_LEG_PRICE_SETTER(loss_ticks)
188#undef PF_LEG_PRICE_SETTER
189
190 Result apply(Target actual, const Action& action) {
191 if (!actual.incarnation || actual.incarnation != target_.incarnation
192 || action.target.incarnation != actual.incarnation) return Result::StaleIdentity;
193 if (actual.owner != target_.owner || action.target.owner != actual.owner) return Result::StaleOwner;
194 if (!valid_payload(action)) return Result::InvalidAction;
195 if (last_ && action.cause.event == last_->cause.event) {
196 if (!equal(action, *last_)) return Result::ConflictingReplay;
197 return revision_ == last_->expected_revision + 1 ? Result::Replay : Result::StaleRevision;
198 }
199 if (!action.cause.event || (last_ && action.cause.event < last_->cause.event)) return Result::ExpiredEvent;
200 if (action.expected_revision != revision_) return Result::StaleRevision;
201 if (revision_ == UINT64_MAX) return Result::Exhausted;
202 Lifecycle next = *this;
203 if (!next.perform(action)) return Result::InvalidAction;
204 ++next.revision_; next.last_ = action;
205 *this = std::move(next);
206 return Result::Applied;
207 }
208
209 // Every stored field is emitted, including the last bounded replay payload.
210 // The same traversal supplies state hashing and exact replay comparison.
211 template<class Sink> void visit(Sink& f) const {
212 f.u(target_.incarnation); f.i(target_.owner); f.u(revision_);
213 visit_definition(f, definition_);
214 for (auto generation : generations_) f.u(generation);
215 for (const auto& retirement : retired_) {
216 f.b(retirement.has_value());
217 if (retirement) { f.u(retirement->generation); visit_frame(f, retirement->cause); }
218 }
219 f.b(suspension_.has_value());
220 if (suspension_) {
221 visit_frame(f, suspension_->cause); visit_legs(f, suspension_->legs);
222 visit_barrier(f, suspension_->hold);
223 f.b(suspension_->revival_definition.has_value());
224 if (suspension_->revival_definition) visit_definition(f, *suspension_->revival_definition);
225 f.b(suspension_->replacement.has_value());
226 if (suspension_->replacement) visit_replacement(f, *suspension_->replacement);
227 visit_window(f, suspension_->window);
228 }
229 f.b(last_.has_value()); if (last_) visit_action(f, *last_);
230 }
231private:
232 Definition definition_;
233 Target target_;
234 uint64_t revision_ = 0;
235 std::array<uint64_t, 3> generations_{{1, 1, 1}};
236 std::array<std::optional<Retirement>, 3> retired_;
237 std::optional<Suspension> suspension_;
238 std::optional<Action> last_;
239
240 static size_t index(Leg leg) { return static_cast<size_t>(leg); }
241 static bool valid_legs(const std::vector<Leg>& legs) {
242 unsigned seen = 0; // transient validation, not stored policy
243 for (Leg leg : legs) { const auto n = index(leg); if (n >= 3 || (seen & (1u << n))) return false; seen |= 1u << n; }
244 return !legs.empty();
245 }
246 static bool valid_frame(const Frame& f) {
247 return static_cast<unsigned>(f.domain) <= static_cast<unsigned>(Domain::RawTicks)
248 && static_cast<unsigned>(f.phase) <= static_cast<unsigned>(Phase::AfterMargin);
249 }
250 static bool valid_payload(const Action& action) {
251 if (!valid_frame(action.cause)) return false;
252 return std::visit([](const auto& op) {
253 using T = std::decay_t<decltype(op)>;
254 if constexpr (std::is_same_v<T, Suspend>)
255 return valid_legs(op.legs) && (op.retire.empty() || valid_legs(op.retire))
256 && (!op.hold || valid_frame(op.hold->requested))
257 && (!op.window || valid_frame(op.window->excluded));
258 else if constexpr (std::is_same_v<T, Restore> || std::is_same_v<T, Cancel>) return valid_legs(op.legs);
259 else if constexpr (std::is_same_v<T, BindOwner>) return op.owner >= 0;
260 else if constexpr (std::is_same_v<T, Observe>) return (op.direction == 1 || op.direction == -1)
261 && (op.fold == Fold::Prefix || op.fold == Fold::Continue);
262 else if constexpr (std::is_same_v<T, CompleteBarrier>) return valid_frame(op.completed)
263 && (!op.requested || valid_frame(op.requested->requested));
264 else if constexpr (std::is_same_v<T, StageReplacement>) return valid_frame(op.relation.release.requested);
265 else return true;
266 }, action.operation);
267 }
268 static bool unissued(const Barrier& b) {
269 return b.target.incarnation == 0 && b.target.owner == 0 && b.revision == 0;
270 }
271 Barrier issue(Barrier b) const {
272 b.target = target_; b.revision = revision_ + 1; return b;
273 }
274 static bool same_barrier(const Barrier& a, const Barrier& b) {
275 return a.target.incarnation == b.target.incarnation && a.target.owner == b.target.owner
276 && a.revision == b.revision && a.requested.event == b.requested.event
277 && a.requested.bar == b.requested.bar && a.requested.domain == b.requested.domain
278 && a.requested.phase == b.requested.phase;
279 }
280 // Event identities share a causal sequence. Bar/phase coordinates can
281 // also be compared when they belong to the same observation domain.
282 static bool not_after(const Frame& occurrence, const Frame& processing) {
283 return occurrence.event <= processing.event
284 && (occurrence.domain != processing.domain
285 || occurrence.bar < processing.bar
286 || (occurrence.bar == processing.bar
287 && static_cast<unsigned>(occurrence.phase)
288 <= static_cast<unsigned>(processing.phase)));
289 }
290 bool restore(const std::vector<Leg>& legs) {
291 if (!valid_legs(legs)) return false;
292 for (Leg leg : legs) if (generations_[index(leg)] == UINT64_MAX) return false;
293 for (Leg leg : legs) {
294 ++generations_[index(leg)]; retired_[index(leg)].reset();
295 if (suspension_) {
296 auto& list = suspension_->legs;
297 for (auto it = list.begin(); it != list.end();) {
298 if (*it == leg) it = list.erase(it); else ++it;
299 }
300 }
301 }
302 if (suspension_ && suspension_->legs.empty()) suspension_.reset();
303 return true;
304 }
305 bool perform(const Action& action) {
306 return std::visit([&](const auto& op) -> bool {
307 using T = std::decay_t<decltype(op)>;
308 if constexpr (std::is_same_v<T, BindOwner>) { target_.owner = op.owner; return true; }
309 else if constexpr (std::is_same_v<T, Suspend>) {
310 if (!valid_legs(op.legs) || (!op.retire.empty() && !valid_legs(op.retire))) return false;
311 if (op.hold && !unissued(*op.hold)) return false;
312 const auto hold = op.hold ? std::optional<Barrier>{issue(*op.hold)} : std::nullopt;
313 suspension_ = Suspension{action.cause, op.legs, hold, {}, {}, op.window};
314 for (Leg leg : op.retire) if (!retired_[index(leg)])
315 retired_[index(leg)] = Retirement{generations_[index(leg)], action.cause};
316 return true;
317 } else if constexpr (std::is_same_v<T, StageReplacement>) {
318 if (!op.relation.revival_definition.incarnation() || !unissued(op.relation.release)) return false;
319 // This component supports one outstanding completion target.
320 // A new explicit Suspend may supersede an episode; staging a
321 // second obligation on an existing hold/replacement is refused.
322 if (suspension_ && (suspension_->hold || suspension_->replacement)) return false;
323 if (!suspension_) suspension_ = Suspension{action.cause, {Leg::Stop, Leg::Limit}, {}, {}, {}, {}};
324 suspension_->revival_definition = op.relation.revival_definition;
325 suspension_->replacement = op.relation;
326 suspension_->replacement->release = issue(op.relation.release);
327 return true;
328 } else if constexpr (std::is_same_v<T, CancelDeferredActivation>) {
329 if (!suspension_) return false;
330 suspension_->replacement.reset(); return true;
331 } else if constexpr (std::is_same_v<T, Restore>) { return restore(op.legs); }
332 else if constexpr (std::is_same_v<T, CompleteBarrier>) {
333 const auto expected = release_barrier();
334 if (!expected || !op.requested || !same_barrier(*expected, *op.requested)) return false;
335 const auto& origin = expected->requested;
336 // Coordinates from different domains are not interchangeable.
337 // Cross-domain completion names the actual old obligation;
338 // the caller selects that conversion, never a permission bit.
339 // The completion occurrence may precede its processing receipt
340 // (e.g. an intervening owner bind), but it cannot come from the
341 // future. Check every comparable pair, including an origin and
342 // receipt separated by a cross-domain completion.
343 if (!not_after(origin, op.completed)
344 || !not_after(op.completed, action.cause)
345 || !not_after(origin, action.cause)) return false;
346 if (suspension_->replacement) return restore({Leg::Stop, Leg::Limit, Leg::Trail});
347 suspension_->hold.reset(); // only the named hold, no activation
348 return true;
349 } else if constexpr (std::is_same_v<T, Observe>) {
350 if (!suspension_ || !suspension_->window || (op.direction != 1 && op.direction != -1)) return false;
351 auto& w = *suspension_->window;
352 // A window excludes its entire originating observation bar.
353 // Other domains require explicit caller selection; Pine also
354 // retains its legacy bar-only filter before issuing Observe.
355 if (action.cause.event <= w.excluded.event
356 || (action.cause.domain == w.excluded.domain
357 && action.cause.bar <= w.excluded.bar)) return false;
358 if (op.fold == Fold::Prefix) w.prefix = w.best;
359 const double price = op.direction > 0 ? op.high : op.low;
360 if (std::isnan(w.best) || (op.direction > 0 ? price > w.best : price < w.best)) w.best = price;
361 return true;
362 } else if constexpr (std::is_same_v<T, Cancel>) {
363 if (!valid_legs(op.legs)) return false;
364 for (Leg leg : op.legs) if (!retired_[index(leg)])
365 retired_[index(leg)] = Retirement{generations_[index(leg)], action.cause};
366 return true;
367 }
368 return false;
369 }, action.operation);
370 }
371 template<class S> static void visit_frame(S& f, const Frame& v) {
372 f.u(v.event); f.i(v.bar); f.u(static_cast<uint64_t>(v.domain)); f.u(static_cast<uint64_t>(v.phase));
373 }
374 template<class S> static void visit_prices(S& f, const Prices& p) {
375 f.d(p.limit_price); f.d(p.stop_price); f.d(p.trail_points); f.d(p.trail_price);
376 f.d(p.trail_offset); f.d(p.profit_ticks); f.d(p.loss_ticks);
377 }
378 template<class S> static void visit_definition(S& f, const Definition& d) {
379 f.u(d.incarnation_); f.u(d.revision_); f.b(d.value_ != nullptr); visit_prices(f, d.prices());
380 }
381 template<class S> static void visit_legs(S& f, const std::vector<Leg>& legs) {
382 f.u(legs.size()); for (Leg leg : legs) f.u(static_cast<uint64_t>(leg));
383 }
384 template<class S> static void visit_barrier_value(S& f, const Barrier& b) {
385 visit_frame(f, b.requested); f.u(b.target.incarnation); f.i(b.target.owner); f.u(b.revision);
386 }
387 template<class S> static void visit_barrier(S& f, const std::optional<Barrier>& b) {
388 f.b(b.has_value()); if (b) visit_barrier_value(f, *b);
389 }
390 template<class S> static void visit_window(S& f, const std::optional<ObservationWindow>& w) {
391 f.b(w.has_value()); if (w) { visit_frame(f, w->excluded); f.d(w->best); f.d(w->prefix); }
392 }
393 template<class S> static void visit_replacement(S& f, const Replacement& r) {
394 f.u(r.queue_predecessor); visit_definition(f, r.revival_definition); visit_barrier_value(f, r.release);
395 }
396 // The broker's ordinary double fold canonicalizes NaN/zero. Replay is
397 // bit-exact, so its bounded receipt payload must preserve those bits too.
398 template<class S> struct ReplaySink {
399 S& sink;
400 void u(uint64_t v) { sink.u(v); }
401 void i(int64_t v) { sink.i(v); }
402 void b(bool v) { sink.b(v); }
403 void d(double v) { uint64_t bits; std::memcpy(&bits, &v, sizeof(bits)); sink.u(bits); }
404 };
405 template<class S> static void visit_action(S& f, const Action& a) {
406 ReplaySink<S> raw{f};
407 visit_action_fields(raw, a);
408 }
409 template<class S> static void visit_action_fields(S& f, const Action& a) {
410 f.u(a.target.incarnation); f.i(a.target.owner); f.u(a.expected_revision); visit_frame(f, a.cause);
411 f.u(a.operation.index());
412 std::visit([&](const auto& op) {
413 using T = std::decay_t<decltype(op)>;
414 if constexpr (std::is_same_v<T, BindOwner>) f.i(op.owner);
415 else if constexpr (std::is_same_v<T, Suspend>) { visit_legs(f, op.legs); visit_barrier(f, op.hold); visit_window(f, op.window); visit_legs(f, op.retire); }
416 else if constexpr (std::is_same_v<T, StageReplacement>) visit_replacement(f, op.relation);
417 else if constexpr (std::is_same_v<T, Restore> || std::is_same_v<T, Cancel>) visit_legs(f, op.legs);
418 else if constexpr (std::is_same_v<T, CompleteBarrier>) { visit_frame(f, op.completed); visit_barrier(f, op.requested); }
419 else if constexpr (std::is_same_v<T, Observe>) { f.d(op.high); f.d(op.low); f.i(op.direction); f.u(static_cast<uint64_t>(op.fold)); }
420 }, a.operation);
421 }
422 struct Exact {
423 std::vector<uint64_t> words;
424 void u(uint64_t v) { words.push_back(v); }
425 void i(int64_t v) { u(static_cast<uint64_t>(v)); }
426 void b(bool v) { u(v ? 1 : 0); }
427 void d(double v) { uint64_t bits; std::memcpy(&bits, &v, sizeof(bits)); u(bits); }
428 };
429 static bool equal(const Action& a, const Action& b) { Exact x, y; visit_action(x, a); visit_action(y, b); return x.words == y.words; }
430};
431} // inline namespace lifecycle_v1
432} // namespace pineforge::exit_legs
const std::array< std::optional< Retirement >, 3 > & retirements() const
Result apply(Target actual, const Action &action)
std::optional< Barrier > release_barrier() const
void attach(uint64_t incarnation, int64_t owner)
const std::optional< Action > & last_action() const
Definition definition(uint64_t incarnation) const
void fork(uint64_t incarnation, int64_t owner)
const std::optional< Suspension > & suspension() const
#define PF_LEG_PRICE_SETTER(name)
std::variant< BindOwner, Suspend, StageReplacement, CancelDeferredActivation, Restore, CompleteBarrier, Observe, Cancel > Operation
int b(int64_t c)
Definition color.hpp:44
int r(int64_t c)
Definition color.hpp:42
std::optional< ObservationWindow > window