PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
native_price_grid_strategy.cpp
Go to the documentation of this file.
1// Pine-free native example: the instrument price grid (R5 lanes L8 and L8b).
2//
3// The instrument trades on a 0.25 ladder; the feed is a composite whose prints
4// land between the ticks. NativeRunSpec::price_grid says what the broker model
5// does about that, and the same strategy is run on the same six bars under
6// each answer:
7// * None (the default): price_tick is only the slippage multiplier, every
8// booked price is the raw modeled one;
9// * QuantizeFills + HalfUp: a fill books on the nearest tick (ties away from
10// zero); a resting trigger is still tested against the raw path;
11// * QuantizeFills + Directional: a market fill and a stop book on the
12// adverse tick, a limit on its own favourable side;
13// * QuantizeFillsAndTriggers + HalfUp: a resting trigger is additionally
14// tested against the tick-quantized path, so the breakout stop at 99.75 is
15// reached by a raw high of 99.65 (its nearest tick is 99.75) and by no
16// other mode.
17// A level that already is a ladder price is a fixed point of every mode: the
18// 100.75 target books 100.75 four times. A quantizing grid needs a ladder:
19// price_grid != None with price_tick == 0 is refused at configure time.
20//
21// c++ -std=c++17 native_price_grid_strategy.cpp -lpineforge_kernel -o price_grid
22//
23// Nothing here is PineScript: no codegen, no `src/source`, no `src/compat`.
24// The Pine adapter never declares this field (it keeps TradingView's per-order
25// tick rules on top of None); the grid is a native-host feature by ruling —
26// docs/design/native-feature-parity.md §3.6.
27
29
30#include <cmath>
31#include <cstdio>
32#include <iostream>
33#include <string>
34#include <variant>
35#include <vector>
36
37namespace {
38
39namespace no = pineforge::native_order;
40
41constexpr double kTick = 0.25;
42
43class PriceGridExample : public pineforge::NativeStrategyHost {
44private:
45 int bars_ = 0;
46
47 void on_native_run_begin() override { bars_ = 0; }
48
49 void on_native_bar(const pineforge::Bar&,
50 const pineforge::NativeDecisionContext&) override {
51 ++bars_;
52 if (bars_ == 1) {
53 // Two units at the market: filled at the next bar's open, 100.10 raw.
54 submit({no::Transact{2.0}, "entry", "grid"});
55 return;
56 }
57 if (bars_ == 2) {
58 // Both levels are ladder prices, as a host that knows its
59 // instrument spells them.
60 no::Request target{no::Reduce{no::ExplicitUnits{1.0}}, "target", "grid"};
61 target.trigger = no::Limit{100.75};
62 submit(target);
63 no::Request protect{no::Reduce{no::ExplicitUnits{1.0}}, "protect", "grid"};
64 protect.trigger = no::Stop{99.50};
65 submit(protect);
66 return;
67 }
68 if (bars_ == 4) {
69 // Flat again. A breakout entry one tick above the recent prints.
70 no::Request breakout{no::Transact{1.0}, "breakout", "grid"};
71 breakout.trigger = no::Stop{99.75};
72 submit(breakout);
73 return;
74 }
75 if (bars_ == 5 && physical_position().signed_units > 0.0) {
76 submit({no::Flatten{}, "breakout-exit", "grid"});
77 }
78 }
79};
80
85 spec.identity.run_number = 1;
86 // This example reads its whole event record once the run has ended, so
87 // it keeps it all: the default, Window, keeps only what a host has not
88 // yet acknowledged (native_acknowledge_events).
89 spec.event_retention = pineforge::NativeEventRetention::Full;
90 spec.input_tf = "15";
91 spec.script_tf = "15";
92 spec.ticker = "MOCK";
93 spec.tickerid = "TEST:MOCK";
94 spec.type = "futures";
95 spec.currency = "USD";
96 spec.basecurrency = "";
97 spec.timezone = "UTC";
98 spec.session = "24x7";
99 spec.initial_capital = 10000.0;
100 spec.point_value = 1.0;
101 spec.account_fx = 1.0;
102 spec.price_tick = kTick;
103 spec.fee_kind = pineforge::NativeFeeKind::Percent;
104 spec.fee_value = 0.0;
105 spec.price_grid = grid;
106 spec.grid_rounding = rounding;
107 return spec;
108}
109
110constexpr std::int64_t kQuarter = 15LL * 60LL * 1000LL;
111
112// open, high, low, close, volume, timestamp (Unix milliseconds). Sub-tick
113// prints throughout: 100.10 opens the entry, 100.80 trades through the target,
114// 99.40 gaps under the 99.50 stop, 99.65 is the high that only a quantized
115// path carries to 99.75, and 99.60 opens the breakout's exit.
116const pineforge::Bar kBars[] = {
117 {100.00, 100.20, 99.90, 100.10, 10.0, 0 * kQuarter},
118 {100.10, 100.30, 100.05, 100.20, 10.0, 1 * kQuarter},
119 {100.20, 100.80, 100.15, 100.70, 10.0, 2 * kQuarter},
120 { 99.40, 99.45, 99.20, 99.30, 10.0, 3 * kQuarter},
121 { 99.45, 99.65, 99.35, 99.60, 10.0, 4 * kQuarter},
122 { 99.60, 99.62, 99.30, 99.40, 10.0, 5 * kQuarter},
123};
124constexpr int kBarCount = 6;
125
126struct Fill {
127 std::string label;
128 double raw = 0.0; // the modeled price of the path, never rounded
129 double booked = 0.0; // what the run settled
130};
131
132struct Mode {
133 const char* name;
136 // Hand-computed from the rounding rules above, never read back.
137 std::vector<Fill> expected;
138 double expected_net;
139};
140
141bool on_ladder(double price) { return price == std::round(price / kTick) * kTick; }
142
143bool run_mode(const Mode& mode, int& closed_trades) {
144 PriceGridExample host;
145 if (host.configure_native(make_spec(mode.name, mode.grid, mode.rounding)).status
146 != pineforge::NativeSetupStatus::Applied) {
147 std::cerr << mode.name << " configure: " << host.last_error() << '\n';
148 return false;
149 }
150 host.run(kBars, kBarCount);
151 if (host.native_state().kind != pineforge::NativeLifecycleKind::Completed) {
152 std::cerr << mode.name << " run: " << host.last_error() << '\n';
153 return false;
154 }
155
156 std::vector<Fill> fills;
157 for (const auto& event : host.native_events(0)) {
158 if (!event.command) continue;
159 if (const auto* applied = std::get_if<no::ExecutionAppliedEvent>(&*event.command)) {
160 fills.push_back({applied->request().label, applied->raw_price, applied->resolved_price});
161 }
162 }
163
164 double net = 0.0;
165 for (int i = 0; i < host.trade_count(); ++i) net += host.get_trade(i).pnl;
166
167 std::printf("%-34s", mode.name);
168 for (const auto& fill : fills) {
169 std::printf(" %s %.2f->%.2f", fill.label.c_str(), fill.raw, fill.booked);
170 }
171 std::printf(" | trades=%d net=%+.2f\n", host.trade_count(), net);
172
173 if (fills.size() != mode.expected.size()) {
174 std::cerr << mode.name << ": expected " << mode.expected.size() << " fills, got "
175 << fills.size() << '\n';
176 return false;
177 }
178 for (std::size_t i = 0; i < fills.size(); ++i) {
179 const Fill& want = mode.expected[i];
180 if (fills[i].label != want.label || fills[i].raw != want.raw
181 || fills[i].booked != want.booked) {
182 std::cerr << mode.name << ": fill " << i << " expected " << want.label << ' '
183 << want.raw << "->" << want.booked << '\n';
184 return false;
185 }
186 // Under a grid every booked price is a ladder price; the raw modeled
187 // price is a fact of the path and is never rounded.
188 if (mode.grid != pineforge::NativePriceGrid::None && !on_ladder(fills[i].booked)) {
189 std::cerr << mode.name << ": " << fills[i].label << " booked off the ladder\n";
190 return false;
191 }
192 }
193 if (std::fabs(net - mode.expected_net) > 1e-9) {
194 std::cerr << mode.name << ": expected net " << mode.expected_net << ", got " << net << '\n';
195 return false;
196 }
197 closed_trades += host.trade_count();
198 return true;
199}
200
201} // namespace
202
203int main() {
206
207 // entry: the market fill at bar 1's open, 100.10 — nearest tick 100.00,
208 // adverse (buy) tick 100.25.
209 // target: the sell limit at the ladder price 100.75 — its own fixed point.
210 // protect: the sell stop at 99.50, gapped through by bar 3's open 99.40 —
211 // nearest tick 99.50, adverse (sell) tick 99.25.
212 // breakout: the buy stop at 99.75; bar 4's raw high is 99.65, whose nearest
213 // tick is 99.75 — reached on the quantized path only. Its market exit
214 // at bar 5's open 99.60 books the nearest tick, 99.50.
215 const std::vector<Mode> modes = {
216 {"None", NativePriceGrid::None, NativeGridRounding::HalfUp,
217 {{"entry", 100.10, 100.10}, {"target", 100.75, 100.75}, {"protect", 99.40, 99.40}},
218 (100.75 - 100.10) + (99.40 - 100.10)},
219 {"QuantizeFills/HalfUp", NativePriceGrid::QuantizeFills, NativeGridRounding::HalfUp,
220 {{"entry", 100.10, 100.00}, {"target", 100.75, 100.75}, {"protect", 99.40, 99.50}},
221 0.75 - 0.50},
222 {"QuantizeFills/Directional", NativePriceGrid::QuantizeFills, NativeGridRounding::Directional,
223 {{"entry", 100.10, 100.25}, {"target", 100.75, 100.75}, {"protect", 99.40, 99.25}},
224 0.50 - 1.00},
225 {"QuantizeFillsAndTriggers/HalfUp", NativePriceGrid::QuantizeFillsAndTriggers,
226 NativeGridRounding::HalfUp,
227 {{"entry", 100.10, 100.00}, {"target", 100.75, 100.75}, {"protect", 99.40, 99.50},
228 {"breakout", 99.75, 99.75}, {"breakout-exit", 99.60, 99.50}},
229 0.75 - 0.50 - 0.25},
230 };
231
232 int closed_trades = 0;
233 for (const auto& mode : modes) {
234 if (!run_mode(mode, closed_trades)) return 1;
235 }
236
237 // A quantizing grid without a ladder is refused, not silently disabled.
238 {
239 PriceGridExample host;
240 auto spec = make_spec("no-ladder", NativePriceGrid::QuantizeFills, NativeGridRounding::HalfUp);
241 spec.price_tick = 0.0;
242 const auto setup = host.configure_native(spec);
243 const bool refused = setup.status == pineforge::NativeSetupStatus::Failed
244 && setup.validation.error == pineforge::NativeRunSpecError::GridRequiresPriceTick
245 && setup.validation.field == pineforge::NativeRunSpecField::PriceGrid;
246 std::printf("QuantizeFills with price_tick = 0: %s\n",
247 refused ? "refused (GridRequiresPriceTick on PriceGrid)" : "NOT refused");
248 if (!refused) return 1;
249 }
250
251 std::cout << "closed trades: " << closed_trades << '\n';
252 return closed_trades > 0 ? 0 : 1;
253}
The public native host: an abstract subclass of BacktestEngine with no PineScript on it.
static const pf_bar_t kBars[]
static pf_native_run_spec_v1 make_spec(void)
@ kBarCount
NativePriceGrid
A generic instrument price grid.
NativeGridRounding
HalfUp is the nearest tick with ties away from zero.
static void submit(struct host_state *state, uint32_t intent, double value, uint32_t trigger, double price, const char *label)
static int run_mode(const struct mode *mode)
One complete setup value, staged/copied by NativeStrategyHost before it is applied at begin.
NativePriceGrid price_grid
Opt-in instrument grid.
NativeEventRetention event_retention
What native_events() can still return; see NativeEventRetention.