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};
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;
118 spec.
type =
"crypto";
127 spec.
fee_kind = pineforge::NativeFeeKind::Percent;
132 risk.
action = pineforge::NativeRiskAction::BlockOpenings;
137constexpr std::int64_t kQuarter = 15LL * 60LL * 1000LL;
144 { 99.75, 100.25, 99.50, 100.00, 10.0, 0 * kQuarter},
145 {100.00, 101.00, 99.75, 100.75, 10.0, 1 * kQuarter},
146 {100.75, 103.00, 100.50, 102.75, 10.0, 2 * kQuarter},
147 {102.75, 106.00, 102.50, 105.75, 10.0, 3 * kQuarter},
148 {105.75, 105.75, 101.00, 101.50, 10.0, 4 * kQuarter},
149 {101.50, 102.00, 101.00, 101.75, 10.0, 5 * kQuarter},
153const char* limit_name(no::RiskLimitKind kind) {
155 case no::RiskLimitKind::MaxDrawdown:
return "MaxDrawdown";
156 case no::RiskLimitKind::MaxIntradayLoss:
return "MaxIntradayLoss";
157 case no::RiskLimitKind::MaxConsecutiveLossDays:
return "MaxConsecutiveLossDays";
158 case no::RiskLimitKind::MaxFillsPerDay:
return "MaxFillsPerDay";
166 TrailRiskExample host;
167 if (host.configure_native(
make_spec()).status
168 != pineforge::NativeSetupStatus::Applied) {
169 std::cerr <<
"configure: " << host.last_error() <<
'\n';
174 if (host.native_state().kind != pineforge::NativeLifecycleKind::Completed) {
175 std::cerr <<
"run: " << host.last_error() <<
'\n';
180 std::printf(
"working requests after the trail was placed: %zu; stored trail offset %.2f "
181 "(8 ticks x 0.25, resolved at acceptance)\n",
182 host.working_after_trail,
183 host.stored_trail_offset ? *host.stored_trail_offset : -1.0);
184 if (host.working_after_trail != 2 || !host.stored_trail_offset || *host.stored_trail_offset != 2.0) {
185 std::cerr <<
"expected the far limit and the trail working, with the ticks resolved to 2.00\n";
188 if (!host.armed_state || !host.last_state) {
189 std::cerr <<
"expected the trail to arm and track\n";
192 std::printf(
"trail armed: best=%.2f level=%.2f; last tracked: best=%.2f level=%.2f\n",
193 host.armed_state->best_price, host.armed_state->current_level,
194 host.last_state->best_price, host.last_state->current_level);
195 if (host.armed_state->best_price != 103.0 || host.armed_state->current_level != 101.0
196 || host.last_state->best_price != 106.0 || host.last_state->current_level != 104.0) {
197 std::cerr <<
"expected the level to ride 2.00 behind the running best\n";
200 std::printf(
"cancel_where(\"parked\") cancelled %zu request(s)\n", host.cancelled_by_comment);
201 if (host.cancelled_by_comment != 1) {
202 std::cerr <<
"expected the far limit to be cancelled by its comment\n";
207 int refused_on_risk = 0;
208 std::optional<no::NativeRiskEvent> tripped;
209 for (
const auto& event : host.native_events(0)) {
210 if (!event.command)
continue;
211 if (
const auto* rejected = std::get_if<no::MatchRejectedEvent>(&*event.command)) {
212 if (host.re_entry && rejected->handle() == *host.re_entry
213 && rejected->reason == no::MatchRejectReason::RiskLimit) {
217 if (
const auto* risk = std::get_if<no::NativeRiskEvent>(&*event.command)) {
218 if (!tripped) tripped = *risk;
221 const auto ledger = host.native_risk_state();
222 std::printf(
"risk ledger: blocked=%d fills_today=%llu reason=%s\n",
223 ledger.blocked ? 1 : 0,
static_cast<unsigned long long>(ledger.fills_today),
224 ledger.reason ? limit_name(*ledger.reason) :
"-");
226 std::printf(
"risk event: %s limit=%.0f observed=%.0f\n",
227 limit_name(tripped->kind), tripped->limit, tripped->observed);
229 std::printf(
"re-entry: %s at submit, refused on the risk block at %d candidate(s), final position %.2f\n",
230 host.re_entry_accepted_at_submit ?
"accepted" :
"rejected", refused_on_risk,
231 host.physical_position().signed_units);
232 if (!ledger.blocked || ledger.fills_today != 2 || !ledger.reason
233 || *ledger.reason != no::RiskLimitKind::MaxFillsPerDay || !tripped
234 || !host.re_entry_accepted_at_submit || refused_on_risk == 0
235 || host.physical_position().signed_units != 0.0) {
236 std::cerr <<
"expected max_fills_per_day to block the re-entry after two fills\n";
240 std::cout <<
"closed trades: " << host.trade_count() <<
'\n';
241 for (
int i = 0; i < host.trade_count(); ++i) {
242 const auto& trade = host.get_trade(i);
243 std::cout <<
" " << (trade.is_long ?
"long " :
"short")
244 <<
" qty=" << trade.qty
245 <<
" entry=" << trade.entry_price
246 <<
" exit=" << trade.exit_price
247 <<
" pnl=" << trade.pnl
248 <<
" exit_id=" << trade.exit_id <<
'\n';
250 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
NativeEventRetention event_retention
What native_events() can still return; see NativeEventRetention.