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