PineForge v0.13.1-379-g9b50973
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
native_htf_strategy.cpp
Go to the documentation of this file.
1// Pine-free native example: higher-timeframe series for a bare host (R5
2// lanes L6, L6c and L6d).
3//
4// A host reads a coarser series of its own symbol -- Pine's
5// request.security(syminfo.tickerid, "60", close) -- by declaring it. The
6// kernel aggregates the accepted 15-minute input into hourly buckets and
7// * pushes each completed bucket to on_native_timeframe_bar, riding on the
8// input bar that completed it, before that input is aggregated, matched
9// or calculated;
10// * answers native_series_bar(i) with the latest delivered bucket from
11// inside every callback (the pull side, used here to trade on it).
12// Two series are declared here, from on_native_run_begin
13// (declare_timeframe_subscriptions, for a host whose series are known only
14// at begin; NativeRunSpec::subscriptions is the declaration-time spelling):
15// * index 0, "60" with gaps = false: the delivered bucket stands until the
16// next delivery replaces it;
17// * index 1, "60" with gaps = true: the series is cleared on every input bar
18// it delivers nothing on, so native_series_bar(1) answers nullopt there
19// -- the empty that stands for na.
20// The input has a hole: the second hour is missing its :45 bar, so its bucket
21// cannot close on its own last input and is sealed lazily by the next hour's
22// first bar (NativeTimeframeBarContext::completion == LazyComplete, delivered
23// at 02:00). The other two buckets close on their own fourth bar (Confirmed).
24// The trailing hour has one bar only and is never delivered: a bucket still
25// open at the end of the input is not a bucket.
26//
27// c++ -std=c++17 native_htf_strategy.cpp -lpineforge_kernel -o htf
28//
29// Nothing here is PineScript: no codegen, no `src/source`, no `src/compat`.
30
32
33#include <cstdio>
34#include <iostream>
35#include <optional>
36#include <vector>
37
38namespace {
39
40namespace no = pineforge::native_order;
41
42const char* completion_name(pineforge::NativeCompletionKind kind) {
43 switch (kind) {
44 case pineforge::NativeCompletionKind::Confirmed: return "Confirmed";
45 case pineforge::NativeCompletionKind::LazyComplete: return "LazyComplete";
46 case pineforge::NativeCompletionKind::SessionShortened: return "SessionShortened";
47 case pineforge::NativeCompletionKind::PartialFinalized: return "PartialFinalized";
48 }
49 return "?";
50}
51
52class HtfExample : public pineforge::NativeStrategyHost {
53public:
54 struct Delivery {
55 std::size_t subscription;
56 pineforge::Bar bucket;
58 std::int64_t delivered_at_ms;
59 };
60 std::vector<Delivery> deliveries;
61 bool declared = false;
62 int bars = 0;
63 int series0_present = 0; // gaps = false: stands until replaced
64 int series1_present = 0; // gaps = true: only on its own delivery bars
65
66private:
67 std::optional<std::int64_t> acted_on_bucket_;
68
69 // L6c: the series are declared here, before the kernel registers them.
70 void on_native_run_begin() override {
71 bars = 0;
72 deliveries.clear();
73 acted_on_bucket_.reset();
74 pineforge::NativeTimeframeSubscription hourly;
75 hourly.tf = "60";
76 pineforge::NativeTimeframeSubscription hourly_gaps = hourly;
77 hourly_gaps.gaps = true;
78 declared = declare_timeframe_subscriptions({hourly, hourly_gaps});
79 }
80
81 // The push side: one call per completed bucket, per series.
82 void on_native_timeframe_bar(const pineforge::Bar& bucket,
83 const pineforge::NativeTimeframeBarContext& context) override {
84 deliveries.push_back({context.subscription, bucket, context.completion,
85 context.delivered_at_ms});
86 }
87
88 // The pull side: trade on the hourly close, once per hourly bucket.
89 void on_native_bar(const pineforge::Bar&,
90 const pineforge::NativeDecisionContext&) override {
91 ++bars;
92 if (native_series_bar(0)) ++series0_present;
93 if (native_series_bar(1)) ++series1_present;
94 const auto hour = native_series_bar(0);
95 if (!hour || (acted_on_bucket_ && *acted_on_bucket_ == hour->timestamp)) return;
96 acted_on_bucket_ = hour->timestamp;
97 const bool flat = physical_position().signed_units == 0.0;
98 if (hour->close > hour->open && flat) {
99 submit({no::Transact{1.0}, "hour-up", "htf"});
100 } else if (hour->close < hour->open && !flat) {
101 submit({no::Flatten{}, "hour-down", "htf"});
102 }
103 }
104};
105
108 spec.identity.session_key = "native-htf-example";
109 spec.identity.run_number = 1;
110 spec.input_tf = "15";
111 spec.script_tf = "15";
112 spec.ticker = "MOCK";
113 spec.tickerid = "TEST:MOCK";
114 spec.type = "crypto";
115 spec.currency = "USDT";
116 spec.basecurrency = "ETH";
117 spec.timezone = "UTC";
118 spec.session = "24x7";
119 spec.initial_capital = 10000.0;
120 spec.point_value = 1.0;
121 spec.account_fx = 1.0;
122 spec.price_tick = 0.25;
123 spec.fee_kind = pineforge::NativeFeeKind::Percent;
124 spec.fee_value = 0.0;
125 // spec.subscriptions stays empty: this host declares at begin instead.
126 return spec;
127}
128
129constexpr std::int64_t kQuarter = 15LL * 60LL * 1000LL;
130
131// open, high, low, close, volume, timestamp (Unix milliseconds).
132// Hour 0 rises (bucket closes up), hour 1 falls and is missing its 01:45 bar,
133// hour 2 rises again, hour 3 has one bar and is never delivered.
134const pineforge::Bar kBars[] = {
135 {100.00, 100.50, 99.75, 100.25, 4.0, 0 * kQuarter}, // 00:00
136 {100.25, 100.75, 100.00, 100.50, 4.0, 1 * kQuarter}, // 00:15
137 {100.50, 101.00, 100.25, 100.75, 4.0, 2 * kQuarter}, // 00:30
138 {100.75, 101.25, 100.50, 101.00, 4.0, 3 * kQuarter}, // 00:45 -> bucket 0 Confirmed
139 {101.00, 101.25, 100.25, 100.50, 4.0, 4 * kQuarter}, // 01:00 (long fills here)
140 {100.50, 100.75, 99.75, 100.00, 4.0, 5 * kQuarter}, // 01:15
141 {100.00, 100.25, 99.25, 99.50, 4.0, 6 * kQuarter}, // 01:30 (01:45 is missing)
142 { 99.50, 100.00, 99.00, 99.75, 4.0, 8 * kQuarter}, // 02:00 -> bucket 1 LazyComplete
143 { 99.75, 100.25, 99.50, 100.00, 4.0, 9 * kQuarter}, // 02:15 (flatten fills here)
144 {100.00, 100.50, 99.75, 100.25, 4.0, 10 * kQuarter}, // 02:30
145 {100.25, 100.75, 100.00, 100.50, 4.0, 11 * kQuarter}, // 02:45 -> bucket 2 Confirmed
146 {100.50, 101.00, 100.25, 100.75, 4.0, 12 * kQuarter}, // 03:00 (long fills here; hour 3 stays open)
147};
148constexpr int kBarCount = 12;
149
150} // namespace
151
152int main() {
153 HtfExample host;
154 if (host.configure_native(make_spec()).status
155 != pineforge::NativeSetupStatus::Applied) {
156 std::cerr << "configure: " << host.last_error() << '\n';
157 return 1;
158 }
159
160 host.run(kBars, kBarCount);
161 if (host.native_state().kind != pineforge::NativeLifecycleKind::Completed) {
162 std::cerr << "run: " << host.last_error() << '\n';
163 return 1;
164 }
165 if (!host.declared) {
166 std::cerr << "declare_timeframe_subscriptions refused the list at begin\n";
167 return 1;
168 }
169
170 // --- the push side: three buckets per series, in input order ------------
171 std::printf("deliveries: %zu\n", host.deliveries.size());
172 for (const auto& d : host.deliveries) {
173 std::printf(" series %zu: hour opened %02lld:%02lld o=%.2f h=%.2f l=%.2f c=%.2f %s, delivered on the %02lld:%02lld bar\n",
174 d.subscription,
175 static_cast<long long>(d.bucket.timestamp / 3600000),
176 static_cast<long long>(d.bucket.timestamp / 60000 % 60),
177 d.bucket.open, d.bucket.high, d.bucket.low, d.bucket.close,
178 completion_name(d.completion),
179 static_cast<long long>(d.delivered_at_ms / 3600000),
180 static_cast<long long>(d.delivered_at_ms / 60000 % 60));
181 }
182 std::vector<HtfExample::Delivery> series0;
183 for (const auto& d : host.deliveries) if (d.subscription == 0) series0.push_back(d);
184 if (host.deliveries.size() != 6 || series0.size() != 3) {
185 std::cerr << "expected three hourly buckets on each of the two series\n";
186 return 1;
187 }
188 const bool shapes_ok =
189 series0[0].completion == pineforge::NativeCompletionKind::Confirmed
190 && series0[0].bucket.open == 100.00 && series0[0].bucket.close == 101.00
191 && series0[0].delivered_at_ms == 3 * kQuarter
192 && series0[1].completion == pineforge::NativeCompletionKind::LazyComplete
193 && series0[1].bucket.open == 101.00 && series0[1].bucket.close == 99.50
194 && series0[1].delivered_at_ms == 8 * kQuarter
195 && series0[2].completion == pineforge::NativeCompletionKind::Confirmed
196 && series0[2].bucket.open == 99.50 && series0[2].bucket.close == 100.50
197 && series0[2].delivered_at_ms == 11 * kQuarter;
198 if (!shapes_ok) {
199 std::cerr << "unexpected bucket shapes, completions or delivery bars\n";
200 return 1;
201 }
202
203 // --- the pull side: gaps = false stands, gaps = true clears -------------
204 std::printf("native_series_bar inside on_native_bar: series 0 (gaps off) present on %d of %d bars, "
205 "series 1 (gaps on) present on %d of %d bars\n",
206 host.series0_present, host.bars, host.series1_present, host.bars);
207 if (host.bars != kBarCount || host.series0_present != 9 || host.series1_present != 3) {
208 std::cerr << "expected the gaps-off series from its first delivery on, the gaps-on series on delivery bars only\n";
209 return 1;
210 }
211
212 std::cout << "closed trades: " << host.trade_count() << '\n';
213 for (int i = 0; i < host.trade_count(); ++i) {
214 const auto& trade = host.get_trade(i);
215 std::cout << " " << (trade.is_long ? "long " : "short")
216 << " qty=" << trade.qty
217 << " entry=" << trade.entry_price
218 << " exit=" << trade.exit_price
219 << " pnl=" << trade.pnl << '\n';
220 }
221 return host.trade_count() > 0 ? 0 : 1;
222}
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
NativeCompletionKind
How a script interval or a higher-timeframe bucket was closed.
int main()
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.