PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
native_toolkit.hpp
Go to the documentation of this file.
1#pragma once
2
3// Header-only conveniences over the public native request surface. Nothing
4// here is kernel behaviour: every function composes submit / replace / cancel
5// and the owner, group and anchor values a host could write out by hand, so a
6// host that prefers the primitives loses nothing by ignoring this header. No
7// Pine, no source layer, no engine internals.
8
11
12#include <cstddef>
13#include <cstdint>
14#include <map>
15#include <optional>
16#include <utility>
17
19inline namespace native_toolkit_v1 {
20
21// One filled entry and the exit legs that live off it. Each present leg is
22// submitted as the parent's `WaitForApplied` child in a single OCA group, so
23// a leg cannot rest before the entry has a fill and a completed leg ends its
24// siblings. The legs carry the caller's own intent, trigger and anchor; the
25// builder owns only the owner and group fields, plus the two anchored-leg
26// knobs below, both defaulting to "leave the leg as written".
27//
28// Legs are submitted in take_profit, stop_loss, trail order. The group id is
29// the parent's incarnation: every leg of one entry shares one group, and two
30// brackets built on the same parent deliberately join that same group.
31//
32// anchor_rounding, when set, is written onto every present leg whose anchor
33// is FromOwnerFill (a leg with an absolute level is untouched); unset keeps
34// each leg's own rounding. visibility is written into the owner relation the
35// builder creates for every leg: PendingUntilArmed keeps the legs out of
36// native_working_requests() until the parent's fill arms them.
39 std::optional<native_order::Request> take_profit;
40 std::optional<native_order::Request> stop_loss;
41 std::optional<native_order::Request> trail;
42 native_order::GroupEffect sibling_effect = native_order::GroupEffect::Cancel;
43 std::optional<native_order::NativeAnchorRounding> anchor_rounding;
44 native_order::NativeArmVisibility visibility = native_order::NativeArmVisibility::Working;
45};
46
47// Sibling cohorts, fixed so a hand-written twin can reproduce the shape.
48enum class BracketLeg : std::int64_t {
51 Trail = 3,
52};
53
54// What became of one leg. An empty handle is four different things, and a
55// host that cannot tell them apart runs on with a leg it believes it placed:
56//
57// NotRequested the spec left this leg unset; nothing was submitted.
58// NotSubmitted the spec asked for the leg, but `spec.parent` was never
59// allocated, so there is no fill to wait on and the builder
60// sent nothing. The kernel never saw the leg and gave no
61// verdict; this is the builder's own refusal, not a rejection.
62// Accepted the kernel accepted the leg.
63// Rejected the kernel refused the leg, with its own reason.
64enum class BracketLegState : std::uint8_t {
69};
70
71// One leg's submit outcome. `result` is the kernel's own SubmitResult,
72// verbatim and whole (status, event ordinal, handle, RequestRejectReason):
73// the toolkit adds no refusal vocabulary of its own, and `state` is that
74// status widened by the two cases a submit never produces because no submit
75// happened. It is present exactly when the leg reached the kernel, so for
76// Accepted and Rejected and for neither of the others.
79 std::optional<native_order::SubmitResult> result;
80
81 bool requested() const noexcept { return state != BracketLegState::NotRequested; }
82 bool accepted() const noexcept { return state == BracketLegState::Accepted; }
83 bool rejected() const noexcept { return state == BracketLegState::Rejected; }
84
85 // The handle the leg rests under, empty unless the kernel accepted it.
86 std::optional<native_order::RequestHandle> handle() const {
87 if (!accepted() || !result) return std::nullopt;
88 return result->handle;
89 }
90 // Why the kernel refused the leg, empty unless it refused one.
91 std::optional<native_order::RequestRejectReason> reason() const {
92 if (!rejected() || !result) return std::nullopt;
93 return result->reason;
94 }
95};
96
97// What the builder did with each leg. The three optional handles are the
98// original surface and keep their meaning exactly: each holds the handle its
99// leg was accepted under and is empty otherwise. They cannot say why it is
100// empty -- a leg the caller never asked for, a leg the builder could not
101// submit and a leg the kernel refused are all the same empty optional -- so
102// every leg also carries its outcome, and a host reads the refusal and its
103// reason there.
106 std::optional<native_order::RequestHandle> take_profit;
107 std::optional<native_order::RequestHandle> stop_loss;
108 std::optional<native_order::RequestHandle> trail;
112
113 const BracketLegOutcome& outcome(BracketLeg which) const noexcept {
114 switch (which) {
117 case BracketLeg::Trail: break;
118 }
119 return trail_outcome;
120 }
121
122 // The one question a host must ask before it treats the bracket as
123 // placed: is every leg I asked for resting? A spec with no legs at all is
124 // trivially true.
125 bool every_requested_leg_accepted() const noexcept {
128 const BracketLegOutcome& leg = outcome(which);
129 if (leg.requested() && !leg.accepted()) return false;
130 }
131 return true;
132 }
133};
134
135// The owner/group pair the builder writes onto every leg, plus the two
136// anchored-leg knobs: the visibility goes into the owner relation, the
137// rounding (when given) onto a FromOwnerFill anchor only.
139 const native_order::Request& leg,
140 const native_order::RequestHandle& parent,
141 BracketLeg which,
142 native_order::GroupEffect sibling_effect,
143 native_order::NativeArmVisibility visibility = native_order::NativeArmVisibility::Working,
144 std::optional<native_order::NativeAnchorRounding> anchor_rounding = std::nullopt) {
145 native_order::Request out = leg;
146 out.owner = native_order::WaitForApplied{parent, visibility};
147 out.group = native_order::Member{parent.incarnation, static_cast<std::int64_t>(which),
148 sibling_effect};
149 if (anchor_rounding) {
150 if (auto* anchor = std::get_if<native_order::FromOwnerFill>(&out.anchor)) {
151 anchor->rounding = *anchor_rounding;
152 }
153 }
154 return out;
155}
156
157// Submits the present legs and reports what became of each. A parent handle
158// that was never allocated has no fill to wait for, so nothing is submitted
159// for it and every requested leg reads NotSubmitted.
161 BracketReceipt receipt;
162 receipt.parent = spec.parent;
163 const auto place = [&](const std::optional<native_order::Request>& leg, BracketLeg which,
164 BracketLegOutcome& outcome)
165 -> std::optional<native_order::RequestHandle> {
166 if (!leg) return std::nullopt;
167 if (spec.parent.incarnation == 0) {
168 outcome.state = BracketLegState::NotSubmitted;
169 return std::nullopt;
170 }
171 auto result = host.submit(bracket_leg(*leg, spec.parent, which, spec.sibling_effect,
172 spec.visibility, spec.anchor_rounding));
173 const bool accepted = result.status == native_order::SubmitStatus::Accepted;
174 outcome.state = accepted ? BracketLegState::Accepted : BracketLegState::Rejected;
175 outcome.result = std::move(result);
176 return outcome.handle();
177 };
178 receipt.take_profit =
179 place(spec.take_profit, BracketLeg::TakeProfit, receipt.take_profit_outcome);
180 receipt.stop_loss = place(spec.stop_loss, BracketLeg::StopLoss, receipt.stop_loss_outcome);
181 receipt.trail = place(spec.trail, BracketLeg::Trail, receipt.trail_outcome);
182 return receipt;
183}
184
185// What the book did for one key. `submit_or_replace` may issue TWO commands
186// for one call -- a replace the kernel answers NotWorking, then a fresh
187// submit -- and the single optional handle it returns names neither of them:
188// a host that reads a handle back believes it re-priced its named order when
189// it may in fact hold a brand new request, born later, behind everything
190// already resting, and an empty handle is a refused replacement and a refused
191// submit at once, with no reason attached to either.
192//
193// This word names the command whose verdict the book acted on, and each half
194// of it is that command's own kernel status:
195//
196// Replaced ReplaceStatus::Replaced with a successor; the key now
197// holds it and no submit happened.
198// ReplaceRejected ReplaceStatus::ReplaceRejected; nothing was submitted,
199// the previous request is untouched and still bound.
200// SubmitAccepted SubmitStatus::Accepted for the fresh submit; the key
201// holds the new handle.
202// SubmitRejected SubmitStatus::Rejected for it; the key is left unbound.
203//
204// ReplaceStatus::NotWorking and ReplaceStatus::InvalidHandle are not here
205// because the book does not stop on them: it forgets the key and submits, so
206// the deciding verdict is the submit's. The abandoned replace is still
207// reported, whole, in OrderBookOutcome::replace.
208enum class OrderBookAction : std::uint8_t {
213};
214
215// One submit_or_replace outcome. `replace` and `submit` are the kernel's own
216// ReplaceResult and SubmitResult verbatim and whole -- status, event ordinal,
217// successor/handle, RequestRejectReason -- and each is present exactly when
218// that command reached the kernel. The toolkit invents no refusal vocabulary
219// of its own: `action` is the only word it adds, and it is the disjoint union
220// of the two kernel statuses restricted to the four the book stops on.
221//
222// At least one of the two is always present: submit_or_replace always
223// commands the host, so every outcome carries the verdict that decided it.
226 std::optional<native_order::ReplaceResult> replace;
227 std::optional<native_order::SubmitResult> submit;
228
229 // The key is bound to a live request this call produced.
230 bool bound() const noexcept {
233 }
234 // The key's own request was amended in place; no new request was born.
235 bool replaced() const noexcept { return action == OrderBookAction::Replaced; }
236 // The kernel refused the call, on whichever command decided it.
237 bool refused() const noexcept { return !bound(); }
238
239 // Exactly what submit_or_replace returns: the handle the key now holds,
240 // empty when the kernel refused.
241 std::optional<native_order::RequestHandle> handle() const {
242 if (action == OrderBookAction::Replaced && replace) return replace->successor;
243 if (action == OrderBookAction::SubmitAccepted && submit) return submit->handle;
244 return std::nullopt;
245 }
246 // Why the kernel refused, empty unless it refused.
247 std::optional<native_order::RequestRejectReason> reason() const {
248 if (action == OrderBookAction::ReplaceRejected && replace) return replace->reason;
249 if (action == OrderBookAction::SubmitRejected && submit) return submit->reason;
250 return std::nullopt;
251 }
252 // The timeline ordinal of the command that decided the call.
253 std::uint64_t event_ordinal() const noexcept {
254 switch (action) {
257 return replace ? replace->event_ordinal : 0;
260 break;
261 }
262 return submit ? submit->event_ordinal : 0;
263 }
264};
265
266// What the book did for one cancel. `result` is the kernel's own CancelResult
267// verbatim and whole, present exactly when a cancel reached the kernel. It is
268// empty in the two cases the book answers alone -- and cancel(key) reports
269// both of them as CancelStatus::NotWorking, a verdict the kernel never gave:
270//
271// known == false the key was not in the book; nothing was commanded.
272// known == true the key was bound, but its request is neither an
273// unarmed pending child nor in the working enumeration,
274// so the book forgot it and commanded nothing.
275//
276// Either way the key is unbound when the call returns.
278 bool known = false;
279 std::optional<native_order::CancelResult> result;
280
281 // A cancel reached the kernel and `result` is its answer.
282 bool commanded() const noexcept { return result.has_value(); }
283 // The one-word answer cancel(key) returns, unchanged: the kernel's own
284 // status when it was commanded, NotWorking when the book answered alone.
286 return result ? result->status : native_order::CancelStatus::NotWorking;
287 }
288 // The cancellation's timeline ordinal, 0 when nothing was commanded.
289 std::uint64_t event_ordinal() const noexcept { return result ? result->event_ordinal : 0; }
290};
291
292// A host-side key to live-handle index: the bookkeeping a strategy would
293// otherwise write to re-price "its" order by name. It owns no engine state,
294// only the handles the host returned. Key must be ordered (a std::string id
295// or an integral id are the usual choices).
296//
297// This book is for a host that wants to REACH one named order again --
298// replace it, re-price it, cancel it, ask whether it is still live. It is not
299// the only way to address an order by name: a host that only ever withdraws
300// them in bulk needs no book at all, because
301// NativeStrategyHost::cancel_where(text, NativeRequestField::Label) cancels
302// exactly the live requests whose Request::label is that text (and
303// NativeRequestField::Comment does the same for the comment) in one call,
304// walking the live book once. The trade is the usual one: the predicate
305// keeps nothing in step and reads the truth the kernel already holds; this
306// book is O(log n) to a handle and survives the request's own terminal
307// states only as the key it forgets.
308template <class Key>
310public:
311 explicit OrderBook(NativeStrategyHost& host) noexcept : host_(&host) {}
312
313 std::optional<native_order::RequestHandle> handle(const Key& key) const {
314 const auto found = entries_.find(key);
315 if (found == entries_.end()) return std::nullopt;
316 return found->second.handle;
317 }
318 bool contains(const Key& key) const { return entries_.find(key) != entries_.end(); }
319 std::size_t size() const noexcept { return entries_.size(); }
320 bool empty() const noexcept { return entries_.empty(); }
321
322 // Replaces the key's request while it is still working, otherwise submits
323 // a fresh one under that key. A replacement the host rejects leaves the
324 // previous request working and still bound to the key; a rejected submit
325 // leaves the key unbound.
326 //
327 // A key bound to a PendingUntilArmed child is not in the working
328 // enumeration before its arm, so the book does not ask the enumeration
329 // whether it is working: it replaces first and treats NotWorking as
330 // "gone, submit afresh". Every key bound to a Working request keeps the
331 // enumeration check, so its command footprint is unchanged.
332 //
333 // This spelling answers the handle alone, which cannot say which command
334 // produced it nor why it is empty; submit_or_replace_outcome() is the
335 // same call reporting the kernel's own results. It is the handle of that
336 // outcome, exactly.
337 std::optional<native_order::RequestHandle> submit_or_replace(
338 const Key& key, const native_order::Request& request,
339 native_order::ReplaceOptions options = {}) {
340 return submit_or_replace_outcome(key, request, options).handle();
341 }
342
343 // submit_or_replace with the kernel's verdicts attached: which command
344 // decided the call, the ReplaceResult of a replace that reached the
345 // kernel (including one abandoned as NotWorking), the SubmitResult of a
346 // submit that did, and with them the refusal reason and the ordinal the
347 // handle alone cannot carry. Same commands, same order, same book state.
349 const Key& key, const native_order::Request& request,
350 native_order::ReplaceOptions options = {}) {
351 OrderBookOutcome outcome;
352 const auto found = entries_.find(key);
353 if (found != entries_.end()) {
354 if (found->second.pending_until_armed || working(found->second.handle)) {
355 const auto result = host_->replace(found->second.handle, request, options);
356 outcome.replace = result;
357 if (result.status == native_order::ReplaceStatus::Replaced && result.successor) {
358 found->second = Entry{*result.successor, pending_until_armed(request)};
360 return outcome;
361 }
362 if (result.status == native_order::ReplaceStatus::ReplaceRejected) {
364 return outcome;
365 }
366 }
367 entries_.erase(found);
368 }
369 const auto submitted = host_->submit(request);
370 outcome.submit = submitted;
371 if (submitted.status != native_order::SubmitStatus::Accepted || !submitted.handle) {
373 return outcome;
374 }
375 entries_.emplace(key, Entry{*submitted.handle, pending_until_armed(request)});
377 return outcome;
378 }
379
380 // Cancels the key's request and forgets the key. An unknown key is not a
381 // command: nothing reaches the host. A key bound to a PendingUntilArmed
382 // child is cancelled by handle without consulting the enumeration.
383 //
384 // The one word this returns is CancelStatus::NotWorking for both cases in
385 // which nothing was commanded, which is the book speaking in the kernel's
386 // vocabulary; cancel_outcome() is the same call saying who spoke.
387 native_order::CancelStatus cancel(const Key& key) { return cancel_outcome(key).status(); }
388
389 // cancel with the kernel's verdict attached: whether the key was bound at
390 // all, and the CancelResult of a cancel that reached the kernel. Same
391 // commands, same book state.
393 OrderBookCancel outcome;
394 const auto found = entries_.find(key);
395 if (found == entries_.end()) return outcome;
396 outcome.known = true;
397 const auto target = found->second;
398 entries_.erase(found);
399 if (!target.pending_until_armed && !working(target.handle)) return outcome;
400 outcome.result = host_->cancel(target.handle);
401 return outcome;
402 }
403
404 // Drops the key without commanding the host.
405 void forget(const Key& key) { entries_.erase(key); }
406
407private:
408 struct Entry {
410 bool pending_until_armed = false;
411 };
412
413 static bool pending_until_armed(const native_order::Request& request) noexcept {
414 const auto* wait = std::get_if<native_order::WaitForApplied>(&request.owner);
415 return wait != nullptr
416 && wait->visibility == native_order::NativeArmVisibility::PendingUntilArmed;
417 }
418
419 bool working(const native_order::RequestHandle& target) const {
420 for (const auto& row : host_->native_working_requests()) {
421 if (row.definition && row.definition->handle == target) return true;
422 }
423 return false;
424 }
425
426 NativeStrategyHost* host_ = nullptr;
427 std::map<Key, Entry> entries_;
428};
429
430} // inline namespace native_toolkit_v1
431} // namespace pineforge::native_toolkit
The public native host: an abstract subclass of BacktestEngine with no PineScript on it.
std::vector< NativeWorkingRequest > native_working_requests() const
Working-book snapshot and bulk cancellation.
native_order::ReplaceResult replace(const native_order::RequestHandle &target, const native_order::Request &request)
Replace one live request: validate first, then retire that incarnation and birth a successor with a n...
native_order::CancelStatus cancel(const Key &key)
std::optional< native_order::RequestHandle > submit_or_replace(const Key &key, const native_order::Request &request, native_order::ReplaceOptions options={})
std::optional< native_order::RequestHandle > handle(const Key &key) const
OrderBookOutcome submit_or_replace_outcome(const Key &key, const native_order::Request &request, native_order::ReplaceOptions options={})
NativeArmVisibility
Whether an owner-related request that arms is a working order before its arm.
BracketReceipt submit_bracket(NativeStrategyHost &host, const BracketSpec &spec)
native_order::Request bracket_leg(const native_order::Request &leg, const native_order::RequestHandle &parent, BracketLeg which, native_order::GroupEffect sibling_effect, native_order::NativeArmVisibility visibility=native_order::NativeArmVisibility::Working, std::optional< native_order::NativeAnchorRounding > anchor_rounding=std::nullopt)
Replacement behaviour that is not expressible in the successor request.
Aggregate field order keeps market construction: Request{Transact{1.0}, "buy", "comment"}...
The one owner relation that arms (the ArmedEvent).
std::optional< native_order::RequestHandle > handle() const
std::optional< native_order::RequestRejectReason > reason() const
std::optional< native_order::RequestHandle > trail
std::optional< native_order::RequestHandle > stop_loss
const BracketLegOutcome & outcome(BracketLeg which) const noexcept
std::optional< native_order::RequestHandle > take_profit
std::optional< native_order::Request > take_profit
std::optional< native_order::NativeAnchorRounding > anchor_rounding
std::optional< native_order::CancelResult > result
native_order::CancelStatus status() const noexcept
std::optional< native_order::RequestHandle > handle() const
std::optional< native_order::RequestRejectReason > reason() const
std::optional< native_order::ReplaceResult > replace