PineForge v0.13.1-379-g9b50973
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
84 spec.identity.session_key = key;
85 spec.identity.run_number = 1;
86 spec.input_tf = "15";
87 spec.script_tf = "15";
88 spec.ticker = "MOCK";
89 spec.tickerid = "TEST:MOCK";
90 spec.type = "futures";
91 spec.currency = "USD";
92 spec.basecurrency = "";
93 spec.timezone = "UTC";
94 spec.session = "24x7";
95 spec.initial_capital = 10000.0;
96 spec.point_value = 1.0;
97 spec.account_fx = 1.0;
98 spec.price_tick = kTick;
99 spec.fee_kind = pineforge::NativeFeeKind::Percent;
100 spec.fee_value = 0.0;
101 spec.price_grid = grid;
102 spec.grid_rounding = rounding;
103 return spec;
104}
105
106constexpr std::int64_t kQuarter = 15LL * 60LL * 1000LL;
107
108// open, high, low, close, volume, timestamp (Unix milliseconds). Sub-tick
109// prints throughout: 100.10 opens the entry, 100.80 trades through the target,
110// 99.40 gaps under the 99.50 stop, 99.65 is the high that only a quantized
111// path carries to 99.75, and 99.60 opens the breakout's exit.
112const pineforge::Bar kBars[] = {
113 {100.00, 100.20, 99.90, 100.10, 10.0, 0 * kQuarter},
114 {100.10, 100.30, 100.05, 100.20, 10.0, 1 * kQuarter},
115 {100.20, 100.80, 100.15, 100.70, 10.0, 2 * kQuarter},
116 { 99.40, 99.45, 99.20, 99.30, 10.0, 3 * kQuarter},
117 { 99.45, 99.65, 99.35, 99.60, 10.0, 4 * kQuarter},
118 { 99.60, 99.62, 99.30, 99.40, 10.0, 5 * kQuarter},
119};
120constexpr int kBarCount = 6;
121
122struct Fill {
123 std::string label;
124 double raw = 0.0; // the modeled price of the path, never rounded
125 double booked = 0.0; // what the run settled
126};
127
128struct Mode {
129 const char* name;
132 // Hand-computed from the rounding rules above, never read back.
133 std::vector<Fill> expected;
134 double expected_net;
135};
136
137bool on_ladder(double price) { return price == std::round(price / kTick) * kTick; }
138
139bool run_mode(const Mode& mode, int& closed_trades) {
140 PriceGridExample host;
141 if (host.configure_native(make_spec(mode.name, mode.grid, mode.rounding)).status
142 != pineforge::NativeSetupStatus::Applied) {
143 std::cerr << mode.name << " configure: " << host.last_error() << '\n';
144 return false;
145 }
146 host.run(kBars, kBarCount);
147 if (host.native_state().kind != pineforge::NativeLifecycleKind::Completed) {
148 std::cerr << mode.name << " run: " << host.last_error() << '\n';
149 return false;
150 }
151
152 std::vector<Fill> fills;
153 for (const auto& event : host.native_events(0)) {
154 if (!event.command) continue;
155 if (const auto* applied = std::get_if<no::ExecutionAppliedEvent>(&*event.command)) {
156 fills.push_back({applied->request().label, applied->raw_price, applied->resolved_price});
157 }
158 }
159
160 double net = 0.0;
161 for (int i = 0; i < host.trade_count(); ++i) net += host.get_trade(i).pnl;
162
163 std::printf("%-34s", mode.name);
164 for (const auto& fill : fills) {
165 std::printf(" %s %.2f->%.2f", fill.label.c_str(), fill.raw, fill.booked);
166 }
167 std::printf(" | trades=%d net=%+.2f\n", host.trade_count(), net);
168
169 if (fills.size() != mode.expected.size()) {
170 std::cerr << mode.name << ": expected " << mode.expected.size() << " fills, got "
171 << fills.size() << '\n';
172 return false;
173 }
174 for (std::size_t i = 0; i < fills.size(); ++i) {
175 const Fill& want = mode.expected[i];
176 if (fills[i].label != want.label || fills[i].raw != want.raw
177 || fills[i].booked != want.booked) {
178 std::cerr << mode.name << ": fill " << i << " expected " << want.label << ' '
179 << want.raw << "->" << want.booked << '\n';
180 return false;
181 }
182 // Under a grid every booked price is a ladder price; the raw modeled
183 // price is a fact of the path and is never rounded.
184 if (mode.grid != pineforge::NativePriceGrid::None && !on_ladder(fills[i].booked)) {
185 std::cerr << mode.name << ": " << fills[i].label << " booked off the ladder\n";
186 return false;
187 }
188 }
189 if (std::fabs(net - mode.expected_net) > 1e-9) {
190 std::cerr << mode.name << ": expected net " << mode.expected_net << ", got " << net << '\n';
191 return false;
192 }
193 closed_trades += host.trade_count();
194 return true;
195}
196
197} // namespace
198
199int main() {
202
203 // entry: the market fill at bar 1's open, 100.10 — nearest tick 100.00,
204 // adverse (buy) tick 100.25.
205 // target: the sell limit at the ladder price 100.75 — its own fixed point.
206 // protect: the sell stop at 99.50, gapped through by bar 3's open 99.40 —
207 // nearest tick 99.50, adverse (sell) tick 99.25.
208 // breakout: the buy stop at 99.75; bar 4's raw high is 99.65, whose nearest
209 // tick is 99.75 — reached on the quantized path only. Its market exit
210 // at bar 5's open 99.60 books the nearest tick, 99.50.
211 const std::vector<Mode> modes = {
212 {"None", NativePriceGrid::None, NativeGridRounding::HalfUp,
213 {{"entry", 100.10, 100.10}, {"target", 100.75, 100.75}, {"protect", 99.40, 99.40}},
214 (100.75 - 100.10) + (99.40 - 100.10)},
215 {"QuantizeFills/HalfUp", NativePriceGrid::QuantizeFills, NativeGridRounding::HalfUp,
216 {{"entry", 100.10, 100.00}, {"target", 100.75, 100.75}, {"protect", 99.40, 99.50}},
217 0.75 - 0.50},
218 {"QuantizeFills/Directional", NativePriceGrid::QuantizeFills, NativeGridRounding::Directional,
219 {{"entry", 100.10, 100.25}, {"target", 100.75, 100.75}, {"protect", 99.40, 99.25}},
220 0.50 - 1.00},
221 {"QuantizeFillsAndTriggers/HalfUp", NativePriceGrid::QuantizeFillsAndTriggers,
222 NativeGridRounding::HalfUp,
223 {{"entry", 100.10, 100.00}, {"target", 100.75, 100.75}, {"protect", 99.40, 99.50},
224 {"breakout", 99.75, 99.75}, {"breakout-exit", 99.60, 99.50}},
225 0.75 - 0.50 - 0.25},
226 };
227
228 int closed_trades = 0;
229 for (const auto& mode : modes) {
230 if (!run_mode(mode, closed_trades)) return 1;
231 }
232
233 // A quantizing grid without a ladder is refused, not silently disabled.
234 {
235 PriceGridExample host;
236 auto spec = make_spec("no-ladder", NativePriceGrid::QuantizeFills, NativeGridRounding::HalfUp);
237 spec.price_tick = 0.0;
238 const auto setup = host.configure_native(spec);
239 const bool refused = setup.status == pineforge::NativeSetupStatus::Failed
240 && setup.validation.error == pineforge::NativeRunSpecError::GridRequiresPriceTick
241 && setup.validation.field == pineforge::NativeRunSpecField::PriceGrid;
242 std::printf("QuantizeFills with price_tick = 0: %s\n",
243 refused ? "refused (GridRequiresPriceTick on PriceGrid)" : "NOT refused");
244 if (!refused) return 1;
245 }
246
247 std::cout << "closed trades: " << closed_trades << '\n';
248 return closed_trades > 0 ? 0 : 1;
249}
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.