57constexpr double kCash = 1497700.0;
58constexpr double kFx = 150.0;
59constexpr double kFeePercent = 0.1;
60constexpr double kSizingPrice = 212.35;
61constexpr double kEntryPrice = 212.45;
62constexpr double kExitPrice = 213.05;
66bool money(
double value,
double expected) {
67 return std::isfinite(value) && std::fabs(value - expected) <= 1e-9 * std::fabs(expected);
71double leg_fee(
double units,
double price) {
72 return units *
price * kFx * kFeePercent / 100.0;
75no::Sized cash_opening(
bool reserve) {
77 sized.side = no::Side::Long;
78 sized.basis = no::CashValue{kCash};
79 sized.time = no::SizeTime::AtAcceptance;
80 sized.price = no::SizePrice::SignalOnTick;
81 sized.reserve_percent_fee = reserve;
87 explicit FeeReserveExample(
bool reserve) : reserve_(reserve) {}
90 std::optional<double> preview_reserved;
91 std::optional<double> preview_plain;
92 double entry_units = 0.0;
93 double entry_price = 0.0;
94 double entry_ticket = 0.0;
95 double exit_ticket = 0.0;
96 double lot_entry_commission = 0.0;
99 bool reserve_ =
false;
101 std::optional<no::RequestHandle> entry_;
102 std::optional<no::RequestHandle> exit_;
104 void on_native_run_begin()
override { bar_ = -1; }
106 void on_native_bar(
const pineforge::Bar& bar,
107 const pineforge::NativeDecisionContext&)
override {
113 const double equity = native_marked_equity(bar.
close);
114 preview_reserved = native_sized_units(cash_opening(
true), kSizingPrice, equity, kFx);
115 preview_plain = native_sized_units(cash_opening(
false), kSizingPrice, equity, kFx);
116 entry_ =
submit({cash_opening(reserve_),
"entry",
"fee-reserve"}).handle;
117 }
else if (bar_ == 2) {
119 const auto lots = native_open_lots(bar.
close);
120 assert(lots.size() == 1);
121 lot_entry_commission = lots[0].entry_commission;
122 exit_ =
submit({no::Flatten{},
"exit",
"fee-reserve"}).handle;
126 void on_native_applied(
const no::ExecutionAppliedEvent& event,
127 const pineforge::NativeDecisionContext&)
override {
128 if (entry_ && event.handle() == *entry_) {
129 entry_units =
event.opened_units;
130 entry_price =
event.resolved_price;
131 entry_ticket =
event.current_ticket;
132 }
else if (exit_ && event.handle() == *exit_) {
133 exit_ticket =
event.current_ticket;
161constexpr std::int64_t kQuarter = 15LL * 60LL * 1000LL;
165 {212.12, 212.40, 211.95, 212.33, 900.0, 0 * kQuarter},
166 {212.45, 212.70, 212.30, 212.62, 1200.0, 1 * kQuarter},
167 {212.62, 212.95, 212.55, 212.90, 1500.0, 2 * kQuarter},
168 {213.05, 213.35, 212.80, 213.20, 1800.0, 3 * kQuarter},
174 pineforge::Trade trade;
175 int closed_trades = 0;
179Outcome run_once(
bool reserve) {
180 FeeReserveExample host(reserve);
181 const auto setup = host.configure_native(
make_spec(pineforge::NativeFeeKind::Percent,
183 assert(setup.status == pineforge::NativeSetupStatus::Applied);
185 assert(host.native_state().kind == pineforge::NativeLifecycleKind::Completed);
187 const double units = reserve ? 46.0 : 47.0;
188 std::printf(
"%s: preview %g / %g shares (reserve / none); filled %g @ %.2f\n",
189 reserve ?
"reserve" :
"no reserve",
190 host.preview_reserved.value_or(-1.0), host.preview_plain.value_or(-1.0),
191 host.entry_units, host.entry_price);
192 assert(host.preview_reserved == 46.0 && host.preview_plain == 47.0);
193 assert(host.entry_units == units && host.entry_price == kEntryPrice);
197 assert(host.trade_count() == 1);
199 std::printf(
" entry fee %.4f (lot keeps %.4f) exit fee %.4f row commission %.4f "
201 host.entry_ticket, host.lot_entry_commission, host.exit_ticket,
203 assert(money(host.entry_ticket, leg_fee(units, kEntryPrice)));
204 assert(money(host.lot_entry_commission, host.entry_ticket));
205 assert(money(host.exit_ticket, leg_fee(units, kExitPrice)));
206 assert(money(trade.
commission, leg_fee(units, kEntryPrice) + leg_fee(units, kExitPrice)));
207 assert(money(trade.
commission, reserve ? 2935.95 : 2999.775));
210 assert(money(trade.
pnl, (kExitPrice - kEntryPrice) * units * kFx - trade.
commission));
211 assert(money(trade.
pnl, reserve ? 1204.05 : 1230.225));
212 return {trade, host.trade_count()};
220 const double share = kSizingPrice * kFx;
221 const double with_fee = 1.0 + kFeePercent / 100.0;
222 std::printf(
"one share %.2f JPY; 47 shares + fee %.4f, 46 shares + fee %.4f, cash %.2f\n",
223 share, 47.0 * share * with_fee, 46.0 * share * with_fee, kCash);
224 assert(47.0 * share * with_fee > kCash && 46.0 * share * with_fee <= kCash);
226 const Outcome reserved = run_once(
true);
227 const Outcome plain = run_once(
false);
230 assert(reserved.closed_trades == plain.closed_trades);
237 FeeReserveExample cash_ticket(
true);
238 const auto setup = cash_ticket.configure_native(
239 make_spec(pineforge::NativeFeeKind::CashPerExecution, 250.0));
240 assert(setup.status == pineforge::NativeSetupStatus::Applied);
241 const auto flat_fee_units =
242 cash_ticket.native_sized_units(cash_opening(
true), kSizingPrice, kCash, kFx);
243 std::printf(
"CashPerExecution: reserve on -> %g shares\n", flat_fee_units.value_or(-1.0));
244 assert(flat_fee_units == 47.0);
247 std::printf(
"reserve %g shares, none %g commission %.4f / %.4f closed trades: %d per run\n",
249 plain.trade.
commission, reserved.closed_trades);
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)
NativeFeeKind
Encodings coincide with the versioned native-v1 C transport.
static void submit(struct host_state *state, uint32_t intent, double value, uint32_t trigger, double price, const char *label)
One complete setup value, staged/copied by NativeStrategyHost before it is applied at begin.
std::optional< double > quantity_grid
native_order::RunIdentity identity