41constexpr double kTick = 0.25;
47 void on_native_run_begin()
override { bars_ = 0; }
49 void on_native_bar(
const pineforge::Bar&,
50 const pineforge::NativeDecisionContext&)
override {
54 submit({no::Transact{2.0},
"entry",
"grid"});
60 no::Request target{no::Reduce{no::ExplicitUnits{1.0}},
"target",
"grid"};
61 target.trigger = no::Limit{100.75};
63 no::Request protect{no::Reduce{no::ExplicitUnits{1.0}},
"protect",
"grid"};
64 protect.trigger = no::Stop{99.50};
70 no::Request breakout{no::Transact{1.0},
"breakout",
"grid"};
71 breakout.trigger = no::Stop{99.75};
75 if (bars_ == 5 && physical_position().signed_units > 0.0) {
76 submit({no::Flatten{},
"breakout-exit",
"grid"});
90 spec.
type =
"futures";
99 spec.
fee_kind = pineforge::NativeFeeKind::Percent;
106constexpr std::int64_t kQuarter = 15LL * 60LL * 1000LL;
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},
133 std::vector<Fill> expected;
137bool on_ladder(
double price) {
return price == std::round(price / kTick) * kTick; }
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';
147 if (host.native_state().kind != pineforge::NativeLifecycleKind::Completed) {
148 std::cerr << mode.name <<
" run: " << host.last_error() <<
'\n';
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});
161 for (
int i = 0; i < host.trade_count(); ++i) net += host.get_trade(i).pnl;
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);
167 std::printf(
" | trades=%d net=%+.2f\n", host.trade_count(), net);
169 if (fills.size() != mode.expected.size()) {
170 std::cerr << mode.name <<
": expected " << mode.expected.size() <<
" fills, got "
171 << fills.size() <<
'\n';
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';
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";
189 if (std::fabs(net - mode.expected_net) > 1e-9) {
190 std::cerr << mode.name <<
": expected net " << mode.expected_net <<
", got " << net <<
'\n';
193 closed_trades += host.trade_count();
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}},
218 {
"QuantizeFills/Directional", NativePriceGrid::QuantizeFills, NativeGridRounding::Directional,
219 {{
"entry", 100.10, 100.25}, {
"target", 100.75, 100.75}, {
"protect", 99.40, 99.25}},
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}},
228 int closed_trades = 0;
229 for (
const auto& mode : modes) {
230 if (!
run_mode(mode, closed_trades))
return 1;
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;
247 std::cout <<
"closed trades: " << closed_trades <<
'\n';
248 return closed_trades > 0 ? 0 : 1;
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)
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.
native_order::RunIdentity identity
NativePriceGrid price_grid
Opt-in instrument grid.
NativeGridRounding grid_rounding