37 std::optional<pineforge::NativeTrailState> armed_state;
38 std::optional<pineforge::NativeTrailState> last_state;
39 std::optional<double> stored_trail_offset;
40 std::size_t working_after_trail = 0;
41 std::size_t cancelled_by_comment = 0;
42 std::optional<no::RequestHandle> trail;
43 std::optional<no::RequestHandle> re_entry;
44 bool re_entry_accepted_at_submit =
false;
48 std::optional<no::RequestHandle> entry_;
50 void on_native_run_begin()
override { bars_ = 0; }
52 void on_native_bar(
const pineforge::Bar&,
53 const pineforge::NativeDecisionContext&)
override {
56 entry_ =
submit({no::Transact{2.0},
"entry",
"trail-risk"}).handle;
58 no::Request far{no::Transact{1.0},
"far-limit",
"parked"};
59 far.trigger = no::Limit{90.0};
65 no::Request request{no::Reduce{no::ExplicitUnits{2.0}},
"trail",
"trail-risk"};
68 spelled.arm_price = 102.0;
69 spelled.ticks = no::TrailTicks{8.0};
70 request.trigger = spelled;
71 trail =
submit(request).handle;
74 const auto working = native_working_requests();
75 working_after_trail = working.size();
76 for (
const auto& row : working) {
77 if (trail && row.definition->handle == *trail) {
78 if (
const auto* live = std::get_if<no::Trail>(&row.definition->request.trigger)) {
79 stored_trail_offset = live->offset;
86 if (
const auto state = trail_state(*trail); state && state->activated) {
87 if (!armed_state) armed_state = state;
91 if (bars_ == 6) cancelled_by_comment = cancel_where(
"parked");
94 void on_native_applied(
const no::ExecutionAppliedEvent& event,
95 const pineforge::NativeDecisionContext&)
override {
96 if (trail && event.handle() == *trail) {
99 const auto placed =
submit({no::Transact{1.0},
"re-entry",
"trail-risk"});
100 re_entry_accepted_at_submit = placed.status == no::SubmitStatus::Accepted;
101 re_entry = placed.handle;
114 spec.
type =
"crypto";
123 spec.
fee_kind = pineforge::NativeFeeKind::Percent;
128 risk.
action = pineforge::NativeRiskAction::BlockOpenings;
133constexpr std::int64_t kQuarter = 15LL * 60LL * 1000LL;
140 { 99.75, 100.25, 99.50, 100.00, 10.0, 0 * kQuarter},
141 {100.00, 101.00, 99.75, 100.75, 10.0, 1 * kQuarter},
142 {100.75, 103.00, 100.50, 102.75, 10.0, 2 * kQuarter},
143 {102.75, 106.00, 102.50, 105.75, 10.0, 3 * kQuarter},
144 {105.75, 105.75, 101.00, 101.50, 10.0, 4 * kQuarter},
145 {101.50, 102.00, 101.00, 101.75, 10.0, 5 * kQuarter},
149const char* limit_name(no::RiskLimitKind kind) {
151 case no::RiskLimitKind::MaxDrawdown:
return "MaxDrawdown";
152 case no::RiskLimitKind::MaxIntradayLoss:
return "MaxIntradayLoss";
153 case no::RiskLimitKind::MaxConsecutiveLossDays:
return "MaxConsecutiveLossDays";
154 case no::RiskLimitKind::MaxFillsPerDay:
return "MaxFillsPerDay";
162 TrailRiskExample host;
163 if (host.configure_native(
make_spec()).status
164 != pineforge::NativeSetupStatus::Applied) {
165 std::cerr <<
"configure: " << host.last_error() <<
'\n';
170 if (host.native_state().kind != pineforge::NativeLifecycleKind::Completed) {
171 std::cerr <<
"run: " << host.last_error() <<
'\n';
176 std::printf(
"working requests after the trail was placed: %zu; stored trail offset %.2f "
177 "(8 ticks x 0.25, resolved at acceptance)\n",
178 host.working_after_trail,
179 host.stored_trail_offset ? *host.stored_trail_offset : -1.0);
180 if (host.working_after_trail != 2 || !host.stored_trail_offset || *host.stored_trail_offset != 2.0) {
181 std::cerr <<
"expected the far limit and the trail working, with the ticks resolved to 2.00\n";
184 if (!host.armed_state || !host.last_state) {
185 std::cerr <<
"expected the trail to arm and track\n";
188 std::printf(
"trail armed: best=%.2f level=%.2f; last tracked: best=%.2f level=%.2f\n",
189 host.armed_state->best_price, host.armed_state->current_level,
190 host.last_state->best_price, host.last_state->current_level);
191 if (host.armed_state->best_price != 103.0 || host.armed_state->current_level != 101.0
192 || host.last_state->best_price != 106.0 || host.last_state->current_level != 104.0) {
193 std::cerr <<
"expected the level to ride 2.00 behind the running best\n";
196 std::printf(
"cancel_where(\"parked\") cancelled %zu request(s)\n", host.cancelled_by_comment);
197 if (host.cancelled_by_comment != 1) {
198 std::cerr <<
"expected the far limit to be cancelled by its comment\n";
203 int refused_on_risk = 0;
204 std::optional<no::NativeRiskEvent> tripped;
205 for (
const auto& event : host.native_events(0)) {
206 if (!event.command)
continue;
207 if (
const auto* rejected = std::get_if<no::MatchRejectedEvent>(&*event.command)) {
208 if (host.re_entry && rejected->handle() == *host.re_entry
209 && rejected->reason == no::MatchRejectReason::RiskLimit) {
213 if (
const auto* risk = std::get_if<no::NativeRiskEvent>(&*event.command)) {
214 if (!tripped) tripped = *risk;
217 const auto ledger = host.native_risk_state();
218 std::printf(
"risk ledger: blocked=%d fills_today=%llu reason=%s\n",
219 ledger.blocked ? 1 : 0,
static_cast<unsigned long long>(ledger.fills_today),
220 ledger.reason ? limit_name(*ledger.reason) :
"-");
222 std::printf(
"risk event: %s limit=%.0f observed=%.0f\n",
223 limit_name(tripped->kind), tripped->limit, tripped->observed);
225 std::printf(
"re-entry: %s at submit, refused on the risk block at %d candidate(s), final position %.2f\n",
226 host.re_entry_accepted_at_submit ?
"accepted" :
"rejected", refused_on_risk,
227 host.physical_position().signed_units);
228 if (!ledger.blocked || ledger.fills_today != 2 || !ledger.reason
229 || *ledger.reason != no::RiskLimitKind::MaxFillsPerDay || !tripped
230 || !host.re_entry_accepted_at_submit || refused_on_risk == 0
231 || host.physical_position().signed_units != 0.0) {
232 std::cerr <<
"expected max_fills_per_day to block the re-entry after two fills\n";
236 std::cout <<
"closed trades: " << host.trade_count() <<
'\n';
237 for (
int i = 0; i < host.trade_count(); ++i) {
238 const auto& trade = host.get_trade(i);
239 std::cout <<
" " << (trade.is_long ?
"long " :
"short")
240 <<
" qty=" << trade.qty
241 <<
" entry=" << trade.entry_price
242 <<
" exit=" << trade.exit_price
243 <<
" pnl=" << trade.pnl
244 <<
" exit_id=" << trade.exit_id <<
'\n';
246 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)
Generic account risk limits (L9), entirely opt-in: a spec that leaves NativeRunSpec::risk unset evalu...
std::optional< std::uint32_t > max_fills_per_day
One complete setup value, staged/copied by NativeStrategyHost before it is applied at begin.
std::optional< NativeRiskLimits > risk
Opt-in generic account risk limits.
native_order::RunIdentity identity