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"});
94 spec.
type =
"futures";
103 spec.
fee_kind = pineforge::NativeFeeKind::Percent;
110constexpr std::int64_t kQuarter = 15LL * 60LL * 1000LL;
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},
137 std::vector<Fill> expected;
141bool on_ladder(
double price) {
return price == std::round(price / kTick) * kTick; }
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';
151 if (
host.native_state().kind != pineforge::NativeLifecycleKind::Completed) {
152 std::cerr << mode.name <<
" run: " <<
host.last_error() <<
'\n';
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});
165 for (
int i = 0; i <
host.trade_count(); ++i) net +=
host.get_trade(i).pnl;
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);
171 std::printf(
" | trades=%d net=%+.2f\n",
host.trade_count(), net);
173 if (fills.size() != mode.expected.size()) {
174 std::cerr << mode.name <<
": expected " << mode.expected.size() <<
" fills, got "
175 << fills.size() <<
'\n';
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';
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";
193 if (std::fabs(net - mode.expected_net) > 1e-9) {
194 std::cerr << mode.name <<
": expected net " << mode.expected_net <<
", got " << net <<
'\n';
197 closed_trades +=
host.trade_count();
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}},
222 {
"QuantizeFills/Directional", NativePriceGrid::QuantizeFills, NativeGridRounding::Directional,
223 {{
"entry", 100.10, 100.25}, {
"target", 100.75, 100.75}, {
"protect", 99.40, 99.25}},
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}},
232 int closed_trades = 0;
233 for (
const auto& mode : modes) {
234 if (!
run_mode(mode, closed_trades))
return 1;
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;
251 std::cout <<
"closed trades: " << closed_trades <<
'\n';
252 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
NativeEventRetention event_retention
What native_events() can still return; see NativeEventRetention.