PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
pine_intent_state.hpp
Go to the documentation of this file.
1#pragma once
2
3// Selected-window capture: the source layer's record of the last pre-roll
4// evaluation, parked with the consumer for one run. A selected run feeds the
5// bars before the window's start T through the script body with the book
6// inactive. The nine command doors and the six risk setters of PineStrategyHost
7// append here instead of reaching the adapter (the risk setters still apply
8// live as well), and a later step replays the rows at the window's first Open.
9// This header is the representation, the two seam accessors
10// (selected_intents, selected_replay_point) and the capture helpers the host's
11// hooks share. It replays nothing and places nothing.
12//
13// The state is the consumer's mandatory NativeHostState slot, never the
14// optional NativeHostCache: it is allocated outside the kernel, keyed by owner
15// and run, and an allocation failure surfaces where it is adopted. Nothing here
16// grows a class (no installed member, no vtable) and there is no process-global
17// table. A run that is not selected never reaches any of it: every entry checks
18// NativeExecutionConsumer::selected_window_admitted first, with a descriptor on
19// the stack, so OFF allocates nothing and folds nothing.
20// Not installed API.
21
22#include "../native_execution_consumer.hpp"
23
28
29#include <cstdint>
30#include <memory>
31#include <stdexcept>
32#include <string>
33#include <utility>
34#include <variant>
35#include <vector>
36
38
39// The nine command doors of PineStrategyHost (pine_strategy_commands.cpp), one
40// payload each, over the door's own argument types. Strings are owned copies;
41// nothing borrows the script's arguments. The two strategy_close overloads are
42// distinct alternatives: only the second carries the callsite token.
44 std::string id;
45 bool is_long;
47 double stop_price;
48 double qty;
49 std::string comment;
50 std::string oca_name;
53};
55 std::string id;
56 std::string comment;
57 double qty;
60};
62 std::string id;
63 std::string comment;
64 double qty;
67 std::uint64_t callsite_token;
68};
70struct IntentExit {
71 std::string id;
72 std::string from_entry;
74 double stop_price;
79 std::string comment;
80 double qty;
81 std::string oca_name;
83 double loss_ticks;
84};
86 std::string exit_id;
87 std::string from_entry;
88 std::string comment;
89};
91 std::string id;
92};
95 std::string id;
96 bool is_long;
97 double qty;
99 double stop_price;
100 std::string oca_name;
102};
103
104// The six risk setter statements of PineStrategyHost (set_pine_risk_*), over
105// their own argument types. Kept as ordered rows beside the commands because
106// the generated order of statements is arbitrary and the percent flags are
107// sticky in the setters.
115 double value;
117};
128
129// The variant index is the row's tag and is folded into the hash.
130using PineIntentPayload = std::variant<
136
137// One captured call: what the script passed, the configuration the host held
138// at the call, and the live point the call ran at. All owned values.
144
145// The state's kind tag (identity only, never read for its value).
146inline const char kPineIntentStateKind = 0;
147
148// The last pre-roll evaluation of the selected run `run` served by `owner`.
149// Written only while `capturing`, which spans exactly one evaluation of a bar
150// labelled before T. `rows` is that evaluation's alone: every pre-roll
151// evaluation resets it, even a script that calls nothing, and a suppressed
152// pre-roll tail resets it to an empty last-present evaluation. `replay_point`
153// is transient (the replay's own RAII sets and clears it); it is neither
154// hashed nor adopted.
155struct PineIntentState : NativeHostState {
156 PineIntentState(const NativeStrategyHost* owner_host, std::uint64_t native_run) noexcept
157 : NativeHostState(&kPineIntentStateKind), owner(owner_host), run(native_run) {}
158
159 const NativeStrategyHost* owner = nullptr;
160 std::uint64_t run = 0;
161 bool has_evaluation = false;
162 bool capturing = false;
163 bool replayed = false;
164 std::int64_t label_ms = 0;
170 std::vector<PineIntentRow> rows;
172};
173
174// A selected pre-roll call the capture cannot take is the engine's fault, never
175// the script's: it fails the run. It never drops the intent and never replays
176// it from a default point.
177[[noreturn]] inline void intent_outside_bracket() {
179 "selected pre-roll command outside its capture bracket");
180}
181[[noreturn]] inline void intent_without_point() {
183 "selected pre-roll capture without a live callback point");
184}
185[[noreturn]] inline void intent_without_state() {
187 "selected pre-roll evaluation without its intent state");
188}
189[[noreturn]] inline void intent_without_spec() {
191 "selected run begin without a running spec for its intent state");
192}
193
194// The consumer's live frame point: valid inside the callback that asked, and
195// the point a row retains. Read from the consumer itself, never through the
196// adapter's replay-aware read.
198 return NativeExecutionConsumer::bound(host).current_point();
199}
200
201// The intent state of the selected run `host` serves, or null: the run is not
202// selected (OFF, or no consumer yet), is not running, parks no state, or parks
203// one of another kind, owner or native run. A mismatch is never reused.
204// Allocates nothing, so a const or hash read may ask.
207 if (!NativeExecutionConsumer::selected_window_admitted(host, &window)) return nullptr;
208 const NativeExecutionConsumer& consumer = NativeExecutionConsumer::bound(host);
209 if (!consumer.running()) return nullptr;
210 NativeHostState* const parked = consumer.host_state();
211 if (parked == nullptr || parked->kind() != &kPineIntentStateKind) return nullptr;
212 PineIntentState* const state = static_cast<PineIntentState*>(parked);
213 const NativeRunSpec* const spec = consumer.state_spec();
214 if (spec == nullptr || state->owner != &host || state->run != spec->identity.run_number)
215 return nullptr;
216 return state;
217}
218
219// The retained point of the row being replayed, or null: no selected state, or
220// no replay open. The private seam the adapter's read path consumes. Allocates
221// nothing.
223 const NativeStrategyHost& host) noexcept {
224 const PineIntentState* const state = selected_intents(host);
225 return state != nullptr ? state->replay_point : nullptr;
226}
227
228// Parks the state of a selected run, from on_native_run_begin: after the
229// consumer's final pre-host reset, so it belongs to this run alone. Does nothing
230// for a run that is not selected. The state is allocated before it is adopted
231// (the adoption cannot fail), so an allocation failure propagates out of the
232// begin hook and nothing is dropped silently.
235 if (!NativeExecutionConsumer::selected_window_admitted(host, &window)) return;
236 NativeExecutionConsumer& consumer = NativeExecutionConsumer::bound(host);
237 const NativeRunSpec* const spec = consumer.state_spec();
238 if (spec == nullptr) intent_without_spec();
239 consumer.adopt_host_state(
240 std::make_unique<PineIntentState>(&host, spec->identity.run_number));
241}
242
243// One command door's pre-roll capture. False: the call is not a selected
244// pre-roll call (OFF, or a label inside the window), no payload was built, and
245// the door runs exactly as it did. True: the payload built by `make_payload`,
246// this call's configuration and the live point were appended, and the door
247// returns before its legacy trading-window gate and before the adapter. A
248// selected pre-roll call outside a capture bracket, with no state or no live
249// point, fails as engine_invariant.
250template <class MakePayload>
251bool capture_pre_roll_command(const NativeStrategyHost& host, std::int64_t label_ms,
252 const PineStrategyConfig& config, MakePayload&& make_payload) {
254 if (!NativeExecutionConsumer::selected_window_admitted(host, &window)
255 || label_ms >= window.start_ms) {
256 return false;
257 }
258 PineIntentState* const state = selected_intents(host);
259 if (state == nullptr || !state->capturing) intent_outside_bracket();
260 const NativeCurrentPointView* const point = intent_live_point(host);
261 if (point == nullptr) intent_without_point();
262 state->rows.push_back(PineIntentRow{make_payload(), config, *point});
263 return true;
264}
265
266// One risk setter statement's capture: a row while a pre-roll evaluation
267// captures, nothing otherwise. The setter applies live either way.
268template <class Payload>
270 Payload&& payload) {
271 PineIntentState* const state = selected_intents(host);
272 if (state == nullptr || !state->capturing) return;
273 const NativeCurrentPointView* const point = intent_live_point(host);
274 if (point == nullptr) intent_without_point();
275 state->rows.push_back(PineIntentRow{std::forward<Payload>(payload), config, *point});
276}
277
278// The recording target of a source evaluation labelled `label_ms`: the intent
279// state and the live point it runs at. Both are null when the evaluation is not
280// a selected pre-roll one (OFF, or a label inside the window). A selected
281// pre-roll evaluation without its state or its point fails as engine_invariant.
287 std::int64_t label_ms) {
288 PreRollEvaluation found;
290 if (!NativeExecutionConsumer::selected_window_admitted(host, &window)
291 || label_ms >= window.start_ms) {
292 return found;
293 }
294 found.state = selected_intents(host);
295 if (found.state == nullptr) intent_without_state();
296 found.point = intent_live_point(host);
297 if (found.point == nullptr) intent_without_point();
298 return found;
299}
300
301// Resets the buffer to one evaluation labelled `label_ms` that holds no rows:
302// the bracket is closed, the configuration and risk configuration are the
303// current ones at both ends, and the terminal point is the live one. A pre-roll
304// evaluation opens its bracket on top of this; a suppressed tail leaves it as
305// the empty last-present evaluation, so no earlier bar's signal outlives it.
306inline void reset_intent_evaluation(PineIntentState& state, std::int64_t label_ms,
307 const PineStrategyConfig& config,
308 const PineReplayRiskConfig& risk,
309 const NativeCurrentPointView& point) noexcept {
310 state.rows.clear();
311 state.has_evaluation = true;
312 state.capturing = false;
313 state.replayed = false;
314 state.label_ms = label_ms;
315 state.start_config = config;
316 state.end_config = config;
317 state.start_risk = risk;
318 state.end_risk = risk;
319 state.terminal_point = point;
320}
321
322// Closes a capture bracket when its evaluation's scope ends, normally or by
323// exception. Only a flag is written, so the destructor cannot throw.
325public:
326 IntentCaptureScope(const NativeStrategyHost& host, bool opened) noexcept
327 : host_(&host), opened_(opened) {}
331 if (!opened_) return;
332 if (PineIntentState* const state = selected_intents(*host_)) state->capturing = false;
333 }
334
335private:
336 const NativeStrategyHost* host_;
337 bool opened_;
338};
339
340} // namespace pineforge::source::detail
Base with a code: throw coded<std::invalid_argument>(code, {args}, text).
The public native host: an abstract subclass of BacktestEngine with no PineScript on it.
IntentCaptureScope & operator=(const IntentCaptureScope &)=delete
IntentCaptureScope(const IntentCaptureScope &)=delete
IntentCaptureScope(const NativeStrategyHost &host, bool opened) noexcept
PineIntentState * selected_intents(const NativeStrategyHost &host) noexcept
void reset_intent_evaluation(PineIntentState &state, std::int64_t label_ms, const PineStrategyConfig &config, const PineReplayRiskConfig &risk, const NativeCurrentPointView &point) noexcept
const NativeCurrentPointView * selected_replay_point(const NativeStrategyHost &host) noexcept
std::variant< IntentEntry, IntentClose, IntentCloseToken, IntentCloseAll, IntentExit, IntentExitCancelBracket, IntentCancel, IntentCancelAll, IntentOrder, IntentRiskDirection, IntentRiskMaxConsLossDays, IntentRiskMaxDrawdown, IntentRiskMaxIntradayLoss, IntentRiskMaxIntradayFilledOrders, IntentRiskMaxPositionSize > PineIntentPayload
bool capture_pre_roll_command(const NativeStrategyHost &host, std::int64_t label_ms, const PineStrategyConfig &config, MakePayload &&make_payload)
PreRollEvaluation selected_pre_roll_evaluation(const NativeStrategyHost &host, std::int64_t label_ms)
void adopt_selected_intents(NativeStrategyHost &host)
const NativeCurrentPointView * intent_live_point(const NativeStrategyHost &host) noexcept
void capture_risk_row(const NativeStrategyHost &host, const PineStrategyConfig &config, Payload &&payload)
Selected-window configuration, handed to strategy_set_selected_window_v1().
What current_execution_point() answers inside a callback: the point's decision context,...
One complete setup value, staged/copied by NativeStrategyHost before it is applied at begin.
PineIntentState(const NativeStrategyHost *owner_host, std::uint64_t native_run) noexcept
const NativeCurrentPointView * replay_point