PineForge v0.13.1-379-g9b50973
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
native_calc_on_fills_strategy.cpp
Go to the documentation of this file.
1// Pine-free native example: calculation timing (R5 lane L5).
2//
3// Two opt-in run-spec fields change WHEN the host calculates and WHAT its
4// bar-open callback may see. Neither moves a fill.
5// * calculation = BarCloseAndFills: besides the one calculation at every
6// script bar's close, the kernel recalculates once at the cursor of each
7// applied execution, bounded by max_recalculations_per_point. The extra
8// calculation arrives in on_native_recalculate with reason OrderFill and
9// the applied event as its cause. This host's entry is a resting limit
10// that fills mid-bar, on the leg from the open down to the low; the
11// recalculation at that fill reads the bar so far and scales into the
12// position right there. The scale-in follows the ordinary birth rule --
13// matched from the next eligible point on, the next bar's open -- one
14// full bar earlier than a close-only host, which could submit it only at
15// this bar's close.
16// * open_bar_view = OpenOnly: on_native_bar_open receives H = L = C = open
17// and volume 0, so a host deciding at the open cannot read the bar's
18// high and low ahead of time. current_partial_bar() is the lookahead-free
19// bar so far at every mid-bar callback: it never runs ahead of the
20// cursor, so at a fill inside a leg it holds the points already reached
21// (here the open alone) and never the later high.
22// The BarClose calculation is untouched: it still reaches on_native_bar (via
23// on_native_recalculate's default) exactly once per script bar.
24//
25// c++ -std=c++17 native_calc_on_fills_strategy.cpp -lpineforge_kernel -o calc_on_fills
26//
27// Nothing here is PineScript: no codegen, no `src/source`, no `src/compat`.
28
30
31#include <cstdio>
32#include <iostream>
33#include <optional>
34
35namespace {
36
37namespace no = pineforge::native_order;
38
39class CalcOnFillsExample : public pineforge::NativeStrategyHost {
40public:
41 int close_calculations = 0;
42 int fill_recalculations = 0;
43 int bar_opens = 0;
44 int masked_bar_opens = 0;
45 std::optional<pineforge::Bar> partial_at_entry_fill;
46 std::optional<double> scale_in_fill_price;
47
48private:
49 int bars_ = 0;
50 bool scaled_in_ = false;
51 std::optional<no::RequestHandle> entry_;
52 std::optional<no::RequestHandle> scale_in_;
53
54 void on_native_run_begin() override {
55 bars_ = 0;
56 scaled_in_ = false;
57 entry_.reset();
58 scale_in_.reset();
59 }
60
61 // OpenOnly: this callback sees the open only, on every bar.
62 void on_native_bar_open(const pineforge::Bar& bar,
63 const pineforge::NativeDecisionContext&) override {
64 ++bar_opens;
65 if (bar.high == bar.open && bar.low == bar.open && bar.close == bar.open
66 && bar.volume == 0.0) {
67 ++masked_bar_opens;
68 }
69 }
70
71 // The bar's own close calculation: reason BarClose, forwarded here by
72 // on_native_recalculate's default.
73 void on_native_bar(const pineforge::Bar&,
74 const pineforge::NativeDecisionContext&) override {
75 ++bars_;
76 ++close_calculations;
77 if (bars_ == 1) {
78 // A resting buy limit below the close: it fills at the next bar's
79 // low waypoint, mid-bar, which is where the recalculation runs.
80 no::Request entry{no::Transact{1.0}, "entry", "calc-on-fills"};
81 entry.trigger = no::Limit{99.50};
82 entry_ = submit(entry).handle;
83 } else if (bars_ == 4) {
84 submit({no::Flatten{}, "flat", "calc-on-fills"});
85 }
86 }
87
88 // Every calculation arrives here first; OrderFill is the one the spec
89 // opted into, at the applied execution's cursor, with that event as cause.
90 void on_native_recalculate(const pineforge::Bar& bar,
91 const pineforge::NativeDecisionContext& ctx,
93 const no::ExecutionAppliedEvent* cause) override {
94 if (reason != pineforge::NativeCalculationReason::OrderFill) {
96 return;
97 }
98 ++fill_recalculations;
99 const auto partial = current_partial_bar(); // the bar so far, at this cursor
100 if (cause && entry_ && cause->handle() == *entry_ && !scaled_in_) {
101 partial_at_entry_fill = partial;
102 scaled_in_ = true;
103 // Scale in at the fill, not at the next close. Born at this
104 // mid-bar cursor, the request is matched from the next eligible
105 // point on: the next bar's open.
106 scale_in_ = submit({no::Transact{1.0}, "scale-in", "calc-on-fills"}).handle;
107 }
108 if (cause && scale_in_ && cause->handle() == *scale_in_) {
109 scale_in_fill_price = cause->resolved_price;
110 }
111 if (partial) {
112 std::printf(" fill recalculation #%d for %s (filled at %.2f, bar t=%lld): "
113 "bar so far o=%.2f h=%.2f l=%.2f c=%.2f v=%.0f\n",
114 fill_recalculations, cause ? cause->request().label.c_str() : "?",
115 cause ? cause->resolved_price : 0.0,
116 cause ? static_cast<long long>(cause->effective_time_ms()) : -1LL,
117 partial->open, partial->high, partial->low, partial->close, partial->volume);
118 }
119 }
120};
121
124 spec.identity.session_key = "native-calc-on-fills-example";
125 spec.identity.run_number = 1;
126 spec.input_tf = "5";
127 spec.script_tf = "5";
128 spec.ticker = "MOCK";
129 spec.tickerid = "TEST:MOCK";
130 spec.type = "crypto";
131 spec.currency = "USDT";
132 spec.basecurrency = "ETH";
133 spec.timezone = "UTC";
134 spec.session = "24x7";
135 spec.initial_capital = 10000.0;
136 spec.point_value = 1.0;
137 spec.account_fx = 1.0;
138 spec.price_tick = 0.01;
139 spec.fee_kind = pineforge::NativeFeeKind::Percent;
140 spec.fee_value = 0.0;
141 // Calculation timing: recalculate at each fill, and hand the bar-open
142 // callback the open only.
143 spec.calculation = pineforge::NativeCalculationTrigger::BarCloseAndFills;
145 spec.open_bar_view = pineforge::NativeOpenBarView::OpenOnly;
146 return spec;
147}
148
149// open, high, low, close, volume, timestamp (Unix milliseconds). The limit
150// entry fills at the second bar's low waypoint (99.50); the scale-in born at
151// that fill's recalculation fills at the third bar's open (101.50); the
152// flatten submitted at the fourth close fills at the fifth bar's open.
153const pineforge::Bar kBars[] = {
154 { 99.50, 100.50, 99.00, 100.00, 4.0, 0},
155 {100.00, 102.00, 99.50, 101.50, 4.0, 300000},
156 {101.50, 103.00, 101.00, 102.50, 4.0, 600000},
157 {102.50, 104.00, 102.00, 103.50, 4.0, 900000},
158 {103.50, 105.00, 103.00, 104.50, 4.0, 1200000},
159};
160constexpr int kBarCount = 5;
161
162} // namespace
163
164int main() {
165 CalcOnFillsExample host;
166 if (host.configure_native(make_spec()).status
167 != pineforge::NativeSetupStatus::Applied) {
168 std::cerr << "configure: " << host.last_error() << '\n';
169 return 1;
170 }
171
172 host.run(kBars, kBarCount);
173 if (host.native_state().kind != pineforge::NativeLifecycleKind::Completed) {
174 std::cerr << "run: " << host.last_error() << '\n';
175 return 1;
176 }
177
178 std::printf("bar-open view: %d of %d bars arrived masked (H = L = C = open, volume 0)\n",
179 host.masked_bar_opens, host.bar_opens);
180 if (host.bar_opens != kBarCount || host.masked_bar_opens != kBarCount) {
181 std::cerr << "expected OpenOnly to mask every bar-open view\n";
182 return 1;
183 }
184
185 std::printf("close calculations: %d; fill recalculations: %d "
186 "(kernel count %llu, skipped %llu)\n",
187 host.close_calculations, host.fill_recalculations,
188 static_cast<unsigned long long>(host.native_recalculation_count()),
189 static_cast<unsigned long long>(host.native_recalculations_skipped()));
190 if (host.close_calculations != kBarCount || host.fill_recalculations < 2) {
191 std::cerr << "expected one close calculation per bar and a recalculation per fill\n";
192 return 1;
193 }
194 if (!host.partial_at_entry_fill) {
195 std::cerr << "expected a bar so far at the entry's fill recalculation\n";
196 return 1;
197 }
198 const auto& partial = *host.partial_at_entry_fill;
199 std::printf("bar so far at the entry fill: o=%.2f h=%.2f l=%.2f c=%.2f v=%.0f "
200 "(the complete bar is o=100.00 h=102.00 l=99.50 c=101.50)\n",
201 partial.open, partial.high, partial.low, partial.close, partial.volume);
202 // Lookahead-free: nothing past the cursor -- not the later high of 102.00,
203 // and never a low below the leg's own destination.
204 if (partial.open != 100.0 || partial.high != 100.0 || partial.low < 99.50
205 || partial.close > 100.0 || partial.volume != 0.0) {
206 std::cerr << "expected the bar so far to hold the open leg only, without the later high\n";
207 return 1;
208 }
209 if (!host.scale_in_fill_price) {
210 std::cerr << "expected the scale-in born at the fill recalculation to fill\n";
211 return 1;
212 }
213 std::printf("scale-in born at that recalculation filled at %.2f, the next bar's open\n",
214 *host.scale_in_fill_price);
215 if (*host.scale_in_fill_price != 101.50) {
216 std::cerr << "expected the scale-in at the next eligible point, the next bar's open\n";
217 return 1;
218 }
219
220 std::cout << "closed trades: " << host.trade_count() << '\n';
221 for (int i = 0; i < host.trade_count(); ++i) {
222 const auto& trade = host.get_trade(i);
223 std::cout << " " << (trade.is_long ? "long " : "short")
224 << " qty=" << trade.qty
225 << " entry=" << trade.entry_price
226 << " exit=" << trade.exit_price
227 << " pnl=" << trade.pnl << '\n';
228 }
229 return host.trade_count() > 0 ? 0 : 1;
230}
The public native host: an abstract subclass of BacktestEngine with no PineScript on it.
virtual void on_native_recalculate(const Bar &bar, const NativeDecisionContext &ctx, NativeCalculationReason reason, const native_order::ExecutionAppliedEvent *cause)
EVERY calculation of the run arrives here first, including the script bar's own close calculation (re...
static const pf_bar_t kBars[]
static pf_native_run_spec_v1 make_spec(void)
@ kBarCount
NativeCalculationReason
Why the kernel is asking the host to calculate.
static void submit(struct host_state *state, uint32_t intent, double value, uint32_t trigger, double price, const char *label)
double open
Definition bar.hpp:7
double close
Definition bar.hpp:7
double low
Definition bar.hpp:7
double volume
Definition bar.hpp:7
double high
Definition bar.hpp:7
One complete setup value, staged/copied by NativeStrategyHost before it is applied at begin.
NativeCalculationTrigger calculation
Calculation timing.