PineForge v0.13.1-379-g9b50973
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
native_bracket_strategy.cpp
Go to the documentation of this file.
1// Pine-free native example: a market entry with two anchored bracket legs on
2// an instrument price grid (R5 lane L7b).
3//
4// The legs are placed BEFORE the entry has a price. Each carries a
5// FromOwnerFill anchor spelled in ticks; when the entry's fill arms them the
6// kernel materializes the level as fill + offset, snapped onto the run's tick
7// ladder with NativeAnchorRounding::Directional (a sell limit up, a sell stop
8// down), offers it once to resolve_anchored_level, and writes the installed
9// level into the ArmedEvent. PendingUntilArmed keeps the legs out of the
10// working enumeration until that arm, exactly like a broker that shows no
11// working child before the parent fills.
12//
13// c++ -std=c++17 native_bracket_strategy.cpp -lpineforge -o native_bracket
14//
15// Nothing here is PineScript: no codegen, no `src/source`, no `src/compat`.
16
18
19#include <cstdio>
20#include <iostream>
21#include <optional>
22#include <variant>
23
24namespace {
25
26namespace no = pineforge::native_order;
27namespace tk = pineforge::native_toolkit;
28
29class BracketExample : public pineforge::NativeStrategyHost {
30 int bars_ = 0;
31 no::RequestHandle entry_;
32 tk::BracketReceipt legs_;
33
34 void on_native_run_begin() override { bars_ = 0; }
35
36 void on_native_bar(const pineforge::Bar&,
37 const pineforge::NativeDecisionContext&) override {
38 ++bars_;
39 if (bars_ != 1) return;
40 // The entry: two units at the next open.
41 const auto placed = submit({no::Transact{2.0}, "entry", "bracket"});
42 if (placed.status != no::SubmitStatus::Accepted || !placed.handle) return;
43 entry_ = *placed.handle;
44
45 // The legs: relative to the fill, in ticks, level unknown until then.
46 no::Request take_profit{no::Reduce{no::OwnerOpenedUnits{}}, "take-profit", "bracket"};
47 take_profit.trigger = no::Limit{0.0};
48 take_profit.anchor = no::FromOwnerFill{+12.0, /*ticks=*/true};
49 no::Request stop_loss{no::Reduce{no::OwnerOpenedUnits{}}, "stop-loss", "bracket"};
50 stop_loss.trigger = no::Stop{0.0};
51 stop_loss.anchor = no::FromOwnerFill{-8.0, /*ticks=*/true};
52
53 tk::BracketSpec bracket;
54 bracket.parent = entry_;
55 bracket.take_profit = take_profit;
56 bracket.stop_loss = stop_loss;
57 bracket.anchor_rounding = no::NativeAnchorRounding::Directional;
58 bracket.visibility = no::NativeArmVisibility::PendingUntilArmed;
59 legs_ = tk::submit_bracket(*this, bracket);
60
61 // Before the fill the legs are live but not working orders: only the
62 // entry is enumerated.
63 std::cout << "working before the fill: " << native_working_requests().size() << '\n';
64 }
65
66 // The one policy point: the kernel level is offered once per leg. This
67 // host keeps it (nullopt); a host with its own trigger projection would
68 // return the level it wants installed instead.
69 std::optional<double> resolve_anchored_level(
70 const pineforge::NativeAnchoredLevelView& view) const override {
71 std::printf("arm: %s leg of owner %llu, fill %.4f, offset %+.4f, kernel level %.4f\n",
72 view.trigger == pineforge::NativeAnchoredTrigger::Limit ? "limit" : "stop",
73 static_cast<unsigned long long>(view.owner.incarnation),
74 view.owner_fill_price, view.offset, view.kernel_level);
75 return std::nullopt;
76 }
77
78 void on_native_applied(const no::ExecutionAppliedEvent& event,
79 const pineforge::NativeDecisionContext&) override {
80 if (event.definition && event.definition->handle == entry_) {
81 // The fill armed both legs: from here on they are working orders.
82 std::cout << "working after the fill: " << native_working_requests().size() << '\n';
83 }
84 }
85};
86
89 spec.identity.session_key = "native-bracket-example";
90 spec.identity.run_number = 1;
91 spec.input_tf = "5";
92 spec.script_tf = "5";
93 spec.ticker = "MOCK";
94 spec.tickerid = "TEST:MOCK";
95 spec.type = "crypto";
96 spec.currency = "USDT";
97 spec.basecurrency = "ETH";
98 spec.timezone = "UTC";
99 spec.session = "24x7";
100 spec.initial_capital = 10000.0;
101 spec.point_value = 1.0;
102 spec.account_fx = 1.0;
103 // A quarter-point ladder: the legs snap onto it at the arm.
104 spec.price_tick = 0.25;
105 spec.fee_kind = pineforge::NativeFeeKind::Percent;
106 spec.fee_value = 0.0;
107 return spec;
108}
109
110// open, high, low, close, volume, timestamp (Unix milliseconds). The entry
111// fills at 100.10 (off the ladder); the legs arm at 103.25 (100.10 + 3.00
112// rounded up onto the ladder) and 98.00 (100.10 - 2.00 rounded down), and the
113// rise in the third bar takes the profit.
114const pineforge::Bar kBars[] = {
115 {100.00, 100.50, 99.75, 100.10, 4.0, 0},
116 {100.10, 101.00, 100.00, 100.90, 4.0, 300000},
117 {100.90, 103.60, 100.80, 103.20, 4.0, 600000},
118 {103.20, 103.40, 102.90, 103.00, 4.0, 900000},
119};
120constexpr int kBarCount = 4;
121
122} // namespace
123
124int main() {
125 BracketExample host;
126 if (host.configure_native(make_spec()).status
127 != pineforge::NativeSetupStatus::Applied) {
128 std::cerr << "configure: " << host.last_error() << '\n';
129 return 1;
130 }
131
132 host.run(kBars, kBarCount);
133 if (host.native_state().kind != pineforge::NativeLifecycleKind::Completed) {
134 std::cerr << "run: " << host.last_error() << '\n';
135 return 1;
136 }
137
138 // The ArmedEvent carries the materialized level: read it back.
139 for (const auto& event : host.native_events(0)) {
140 if (!event.command) continue;
141 const auto* armed = std::get_if<no::ArmedEvent>(&*event.command);
142 if (!armed || !armed->definition) continue;
143 const auto& request = armed->definition->request;
144 if (const auto* limit = std::get_if<no::Limit>(&request.trigger)) {
145 std::cout << "armed " << request.label << " at " << limit->price << '\n';
146 } else if (const auto* stop = std::get_if<no::Stop>(&request.trigger)) {
147 std::cout << "armed " << request.label << " at " << stop->price << '\n';
148 }
149 }
150
151 std::cout << "closed trades: " << host.trade_count() << '\n';
152 for (int i = 0; i < host.trade_count(); ++i) {
153 const auto& trade = host.get_trade(i);
154 std::cout << " " << (trade.is_long ? "long " : "short")
155 << " qty=" << trade.qty
156 << " entry=" << trade.entry_price
157 << " exit=" << trade.exit_price
158 << " pnl=" << trade.pnl << '\n';
159 }
160 return host.trade_count() > 0 ? 0 : 1;
161}
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)
@ kBarCount
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.