45const char* leg_name(tk::BracketLeg which) {
47 case tk::BracketLeg::TakeProfit:
return "take-profit";
48 case tk::BracketLeg::StopLoss:
return "stop-loss";
49 case tk::BracketLeg::Trail:
break;
54const char* reject_name(no::RequestRejectReason reason) {
56 case no::RequestRejectReason::InvalidQuantity:
return "InvalidQuantity";
57 case no::RequestRejectReason::OffGrid:
return "OffGrid";
58 case no::RequestRejectReason::InvalidTrigger:
return "InvalidTrigger";
59 case no::RequestRejectReason::InvalidCapacity:
return "InvalidCapacity";
60 case no::RequestRejectReason::InvalidOwner:
return "InvalidOwner";
61 case no::RequestRejectReason::InvalidQuantityBasis:
return "InvalidQuantityBasis";
62 case no::RequestRejectReason::InvalidGroup:
return "InvalidGroup";
63 case no::RequestRejectReason::PlacementAdmission:
break;
65 return "PlacementAdmission";
68const char* action_name(tk::OrderBookAction action) {
70 case tk::OrderBookAction::Replaced:
return "replaced in place";
71 case tk::OrderBookAction::ReplaceRejected:
return "replacement REFUSED";
72 case tk::OrderBookAction::SubmitAccepted:
return "submitted fresh";
73 case tk::OrderBookAction::SubmitRejected:
break;
75 return "submit REFUSED";
83void report_key(
const char* key,
const tk::OrderBookOutcome& outcome) {
84 std::printf(
"key %s: %s", key, action_name(outcome.action));
85 if (
const auto handle = outcome.handle()) {
86 std::printf(
", request %llu",
static_cast<unsigned long long>(handle->incarnation));
88 if (
const auto reason = outcome.reason()) {
89 std::printf(
" (%s)", reject_name(*reason));
91 std::printf(
", ordinal %llu\n",
static_cast<unsigned long long>(outcome.event_ordinal()));
99void report_legs(
const tk::BracketReceipt& legs) {
100 for (
const tk::BracketLeg which : {tk::BracketLeg::TakeProfit, tk::BracketLeg::StopLoss,
101 tk::BracketLeg::Trail}) {
102 const tk::BracketLegOutcome& leg = legs.outcome(which);
104 case tk::BracketLegState::NotRequested:
105 std::printf(
"leg %s: not requested\n", leg_name(which));
107 case tk::BracketLegState::NotSubmitted:
108 std::printf(
"leg %s: not submitted (the parent was never allocated)\n",
111 case tk::BracketLegState::Accepted:
112 std::printf(
"leg %s: accepted as request %llu\n", leg_name(which),
113 static_cast<unsigned long long>(leg.handle()->incarnation));
115 case tk::BracketLegState::Rejected:
116 std::printf(
"leg %s: REFUSED (%s)\n", leg_name(which),
117 leg.reason() ? reject_name(*leg.reason()) :
"no reason given");
127 std::size_t working_before_fill = 0;
128 std::size_t working_after_fill = 0;
129 mutable std::optional<double> offered_take_profit;
130 mutable std::optional<double> offered_stop_loss;
131 mutable std::optional<double> offered_trail_arm;
135 no::RequestHandle entry_;
136 tk::BracketReceipt legs_;
137 std::optional<tk::OrderBook<std::string>> book_;
139 void on_native_run_begin()
override {
141 book_.emplace(*
this);
146 static no::Request ladder(
double price,
const char* label) {
147 no::Request
request{no::Transact{1.0}, label,
"ladder"};
154 void work_the_book() {
157 const auto opened = book_->submit_or_replace_outcome(
"ladder", ladder(90.0,
"bid-1"));
158 report_key(
"ladder", opened);
159 assert(opened.action == tk::OrderBookAction::SubmitAccepted);
160 assert(opened.bound() && !opened.replace.has_value());
166 const auto repriced =
167 book_->submit_or_replace_outcome(
"ladder", ladder(91.0,
"bid-2"));
168 report_key(
"ladder", repriced);
169 assert(repriced.replaced());
170 assert(repriced.replace && !repriced.submit.has_value());
176 no::Request zero{no::Reduce{no::ExplicitUnits{0.0}},
"bid-bad",
"ladder"};
177 zero.trigger = no::Limit{91.0};
178 const auto refused = book_->submit_or_replace_outcome(
"ladder", zero);
179 report_key(
"ladder", refused);
180 assert(refused.action == tk::OrderBookAction::ReplaceRejected);
181 assert(refused.reason() == no::RequestRejectReason::InvalidQuantity);
182 assert(!refused.handle().has_value() && !refused.submit.has_value());
183 assert(book_->handle(
"ladder") == repriced.handle());
187 const auto withdrawn = book_->cancel_outcome(
"ladder");
188 std::printf(
"key ladder: cancel commanded=%d status=%s\n",
189 withdrawn.commanded() ? 1 : 0,
190 withdrawn.status() == no::CancelStatus::Cancelled ?
"Cancelled"
192 assert(withdrawn.known && withdrawn.commanded());
193 assert(withdrawn.status() == no::CancelStatus::Cancelled);
195 const auto never = book_->cancel_outcome(
"no-such-key");
196 assert(!never.known && !never.commanded());
197 assert(never.status() == no::CancelStatus::NotWorking);
205 void on_native_bar(
const pineforge::Bar&,
206 const pineforge::NativeDecisionContext&)
override {
213 const auto placed =
submit({no::Transact{2.0},
"entry",
"bracket"});
214 if (placed.status != no::SubmitStatus::Accepted || !placed.handle)
return;
215 entry_ = *placed.handle;
218 no::Request take_profit{no::Reduce{no::OwnerOpenedUnits{}},
"take-profit",
"bracket"};
219 take_profit.trigger = no::Limit{0.0};
220 take_profit.anchor = no::FromOwnerFill{+12.0,
true};
221 no::Request stop_loss{no::Reduce{no::OwnerOpenedUnits{}},
"stop-loss",
"bracket"};
222 stop_loss.trigger = no::Stop{0.0};
223 stop_loss.anchor = no::FromOwnerFill{-8.0,
true};
228 ride.ticks = no::TrailTicks{4.0};
229 no::Request trail{no::Reduce{no::OwnerOpenedUnits{}},
"trail",
"bracket"};
230 trail.trigger = ride;
231 trail.anchor = no::FromOwnerFill{+6.0,
true};
233 tk::BracketSpec bracket;
234 bracket.parent = entry_;
235 bracket.take_profit = take_profit;
236 bracket.stop_loss = stop_loss;
237 bracket.trail = trail;
238 bracket.anchor_rounding = no::NativeAnchorRounding::Directional;
239 bracket.visibility = no::NativeArmVisibility::PendingUntilArmed;
240 legs_ = tk::submit_bracket(*
this, bracket);
247 assert(legs_.every_requested_leg_accepted());
248 assert(legs_.outcome(tk::BracketLeg::TakeProfit).state
249 == tk::BracketLegState::Accepted);
250 assert(legs_.outcome(tk::BracketLeg::StopLoss).state == tk::BracketLegState::Accepted);
251 assert(legs_.outcome(tk::BracketLeg::TakeProfit).handle() == legs_.take_profit);
252 assert(!legs_.outcome(tk::BracketLeg::TakeProfit).reason().has_value());
257 assert(legs_.outcome(tk::BracketLeg::Trail).state == tk::BracketLegState::Accepted);
258 assert(legs_.outcome(tk::BracketLeg::Trail).result.has_value());
259 assert(legs_.trail.has_value() && legs_.outcome(tk::BracketLeg::Trail).handle() == legs_.trail);
263 working_before_fill = native_working_requests().size();
264 std::cout <<
"working before the fill: " << working_before_fill <<
'\n';
265 assert(working_before_fill == 1);
271 std::optional<double> resolve_anchored_level(
272 const pineforge::NativeAnchoredLevelView& view)
const override {
273 const char* trigger =
"stop";
274 if (view.
trigger == pineforge::NativeAnchoredTrigger::Limit) {
277 }
else if (view.
trigger == pineforge::NativeAnchoredTrigger::TrailArm) {
278 trigger =
"trail arm";
283 std::printf(
"arm: %s leg of owner %llu, fill %.4f, offset %+.4f, kernel level %.4f\n",
289 void on_native_applied(
const no::ExecutionAppliedEvent& event,
290 const pineforge::NativeDecisionContext&)
override {
291 if (event.definition && event.definition->handle == entry_) {
294 working_after_fill = native_working_requests().size();
295 std::cout <<
"working after the fill: " << working_after_fill <<
'\n';
296 assert(working_after_fill == 3);
313 spec.
type =
"crypto";
323 spec.
fee_kind = pineforge::NativeFeeKind::Percent;
335 {100.00, 100.50, 99.75, 100.10, 4.0, 0},
336 {100.10, 101.00, 100.00, 100.90, 4.0, 300000},
337 {100.90, 103.60, 100.80, 103.20, 4.0, 600000},
338 {103.20, 103.40, 102.90, 103.00, 4.0, 900000},
346 if (host.configure_native(
make_spec()).status
347 != pineforge::NativeSetupStatus::Applied) {
348 std::cerr <<
"configure: " << host.last_error() <<
'\n';
353 if (host.native_state().kind != pineforge::NativeLifecycleKind::Completed) {
354 std::cerr <<
"run: " << host.last_error() <<
'\n';
360 std::optional<double> take_profit_level;
361 std::optional<double> stop_loss_level;
362 std::optional<double> trail_arm_level;
363 int group_cancels = 0;
364 for (
const auto& event : host.native_events(0)) {
365 if (!event.command)
continue;
366 if (
const auto* cancelled = std::get_if<no::CancelledEvent>(&*event.command)) {
369 const std::string& label = cancelled->request().label;
370 if (label ==
"stop-loss" || label ==
"trail") {
371 std::cout <<
"cancelled " << label <<
" (reason "
372 << (cancelled->reason == no::CancelReason::Group ?
"Group" :
"other")
374 assert(cancelled->reason == no::CancelReason::Group);
379 const auto* armed = std::get_if<no::ArmedEvent>(&*event.command);
380 if (!armed || !armed->definition)
continue;
381 const auto& request = armed->definition->request;
382 if (
const auto* limit = std::get_if<no::Limit>(&request.trigger)) {
383 std::cout <<
"armed " << request.label <<
" at " << limit->price <<
'\n';
384 take_profit_level = limit->price;
385 }
else if (
const auto* stop = std::get_if<no::Stop>(&request.trigger)) {
386 std::cout <<
"armed " << request.label <<
" at " << stop->price <<
'\n';
387 stop_loss_level = stop->price;
388 }
else if (
const auto* trail = std::get_if<no::Trail>(&request.trigger)) {
391 std::cout <<
"armed " << request.label <<
" at "
392 << trail->arm_price.value_or(-1.0) <<
", riding " << trail->offset
393 <<
" behind its best\n";
394 trail_arm_level = trail->arm_price;
395 assert(trail->offset == 1.0);
401 const auto on_ladder = [](
double level) {
402 const double ticks = level / 0.25;
403 return ticks == std::floor(ticks);
405 assert(take_profit_level == 103.25 && host.offered_take_profit == take_profit_level);
406 assert(stop_loss_level == 98.00 && host.offered_stop_loss == stop_loss_level);
407 assert(trail_arm_level == 101.75 && host.offered_trail_arm == trail_arm_level);
408 assert(on_ladder(*take_profit_level) && on_ladder(*stop_loss_level)
409 && on_ladder(*trail_arm_level));
412 assert(host.working_before_fill == 1 && host.working_after_fill == 3);
413 assert(group_cancels == 2);
414 assert(host.trade_count() == 1 && host.get_trade(0).exit_id ==
"take-profit"
415 && host.get_trade(0).exit_price == 103.25);
418 std::cout <<
"armed tp=" << *take_profit_level <<
" sl=" << *stop_loss_level
419 <<
" trail=" << *trail_arm_level <<
" working " << host.working_before_fill
420 <<
" -> " << host.working_after_fill <<
" oca cancels: " << group_cancels
421 <<
" closed trades: " << host.trade_count() <<
'\n';
422 for (
int i = 0; i < host.trade_count(); ++i) {
423 const auto& trade = host.get_trade(i);
424 std::cout <<
" " << (trade.is_long ?
"long " :
"short")
425 <<
" qty=" << trade.qty
426 <<
" entry=" << trade.entry_price
427 <<
" exit=" << trade.exit_price
428 <<
" pnl=" << trade.pnl <<
'\n';
430 return host.trade_count() > 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)
static void submit(struct host_state *state, uint32_t intent, double value, uint32_t trigger, double price, const char *label)
NativeAnchoredTrigger trigger
native_order::RequestHandle owner
One complete setup value, staged/copied by NativeStrategyHost before it is applied at begin.
native_order::RunIdentity identity
NativeEventRetention event_retention
What native_events() can still return; see NativeEventRetention.