PineForge v0.13.1-379-g9b50973
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
native_auxiliary_feed_strategy.cpp
Go to the documentation of this file.
1// Pine-free native example: the auxiliary finer feed (R5 gap lane N7).
2//
3// A declared series is normally built from the run's own input, so it can
4// never be finer than that input: a "5" series under a "15" input has nothing
5// to aggregate. NativeRunSpec::auxiliary_feed is the bars the input does not
6// have — the run's OWN symbol at a strictly finer timeframe — and a
7// subscription whose NativeSeriesSource is AuxiliaryFeed is built from them
8// instead. Pine has no spelling for this: request.security cannot go finer
9// than the chart, and request.security_lower_tf returns an intrabar array
10// rather than a series. It is what a host does when its decision feed is
11// coarse and its signal is not.
12//
13// This host runs 15-minute inputs over a 60-minute (one hour) auxiliary feed
14// of 1-minute bars and declares one "5" series on it. What the example proves,
15// each check independent of the feature it checks:
16// * the very same subscription is REFUSED when no feed is declared
17// (NativeRunSpecError::SubscriptionWithoutAuxiliaryFeed), so the feed is
18// what makes it legal;
19// * every delivered bucket equals the hand aggregation of its five minutes;
20// * routing is by time: the three buckets that opened inside an input's
21// period ride on that input, oldest first, BEFORE its calculation, so the
22// bar the host decides on already sees the last of them;
23// * native_series_bar(0) inside on_native_bar is that last delivered bucket.
24// The strategy then trades on it: long on a five-minute bucket that closed up,
25// flat on one that closed down — a decision three times finer than the bar the
26// kernel calculates on.
27//
28// A stream learns its feed live instead: append_auxiliary_bars(bars, n) adds
29// the later bars of a realtime feed to the declared one (the C spelling is
30// strategy_native_append_auxiliary_bars_v1). That path is pinned by
31// tests/test_native_auxiliary_feed_stream.cpp; this host is batch.
32//
33// c++ -std=c++17 native_auxiliary_feed_strategy.cpp -lpineforge_kernel -o aux_feed
34//
35// Nothing here is PineScript: no codegen, no `src/source`, no `src/compat`.
36
38
39#include <cmath>
40#include <cstdio>
41#include <iostream>
42#include <optional>
43#include <vector>
44
45namespace {
46
47namespace no = pineforge::native_order;
48
49constexpr std::int64_t kMinute = 60LL * 1000LL;
50constexpr int kMinutes = 60; // one hour of feed
51constexpr int kInputs = 4; // 4 x 15 minutes
52constexpr int kBuckets = 12; // 12 x 5 minutes
53
54bool same(double a, double b) {
55 return std::isfinite(a) && std::isfinite(b) && std::abs(a - b) <= 1e-9;
56}
57
58// One minute of tape. The close walks up for twenty minutes, down for twenty
59// and up again for twenty, so the five-minute buckets alternate direction in a
60// shape the reader can check by eye: buckets 0-3 up, 4-7 down, 8-11 up.
61std::vector<pineforge::Bar> minute_bars() {
62 std::vector<pineforge::Bar> bars;
63 bars.reserve(kMinutes);
64 double close = 100.0;
65 for (int k = 0; k < kMinutes; ++k) {
66 const double step = (k < 20 || k >= 40) ? 0.10 : -0.10;
67 const double open = close;
68 close = open + step;
69 pineforge::Bar bar{};
70 bar.open = open;
71 bar.close = close;
72 bar.high = (open > close ? open : close) + 0.05;
73 bar.low = (open < close ? open : close) - 0.05;
74 bar.volume = 1.0 + (k % 4);
75 bar.timestamp = static_cast<std::int64_t>(k) * kMinute;
76 bars.push_back(bar);
77 }
78 return bars;
79}
80
81// The oracle every expected bucket is derived from: one contiguous group of
82// feed bars, labelled by the group's first timestamp.
83pineforge::Bar hand_aggregate(const std::vector<pineforge::Bar>& bars,
84 std::size_t from, std::size_t count) {
85 pineforge::Bar out = bars[from];
86 for (std::size_t i = 1; i < count; ++i) {
87 const pineforge::Bar& next = bars[from + i];
88 if (next.high > out.high) out.high = next.high;
89 if (next.low < out.low) out.low = next.low;
90 out.close = next.close;
91 out.volume += next.volume;
92 }
93 return out;
94}
95
96bool bars_equal(const pineforge::Bar& got, const pineforge::Bar& want) {
97 return got.timestamp == want.timestamp && same(got.open, want.open)
98 && same(got.high, want.high) && same(got.low, want.low)
99 && same(got.close, want.close) && same(got.volume, want.volume);
100}
101
102class AuxiliaryFeedExample : public pineforge::NativeStrategyHost {
103public:
104 struct Delivery {
105 std::size_t subscription;
106 pineforge::Bar bucket;
107 int bars_calculated_before; // 0-based index of the input it rode on
108 bool accessor_matches; // native_series_bar answered this bucket
109 };
110 std::vector<Delivery> deliveries;
111 std::vector<std::optional<pineforge::Bar>> series_at_bar;
112 int bars = 0;
113
114private:
115 std::optional<std::int64_t> acted_on_bucket_;
116
117 void on_native_run_begin() override {
118 bars = 0;
119 deliveries.clear();
120 series_at_bar.clear();
121 acted_on_bucket_.reset();
122 }
123
124 // The push side: one call per completed five-minute bucket.
125 void on_native_timeframe_bar(const pineforge::Bar& bucket,
126 const pineforge::NativeTimeframeBarContext& context) override {
127 const auto pulled = native_series_bar(context.subscription);
128 deliveries.push_back({context.subscription, bucket, bars,
129 pulled.has_value() && bars_equal(*pulled, bucket)});
130 }
131
132 // The pull side: decide on the finest bucket the feed has completed.
133 void on_native_bar(const pineforge::Bar&,
134 const pineforge::NativeDecisionContext&) override {
135 ++bars;
136 const auto bucket = native_series_bar(0);
137 series_at_bar.push_back(bucket);
138 if (!bucket || (acted_on_bucket_ && *acted_on_bucket_ == bucket->timestamp)) return;
139 acted_on_bucket_ = bucket->timestamp;
140 const bool flat = physical_position().signed_units == 0.0;
141 if (bucket->close > bucket->open && flat) {
142 submit({no::Transact{1.0}, "bucket-up", "aux"});
143 } else if (bucket->close < bucket->open && !flat) {
144 submit({no::Flatten{}, "bucket-down", "aux"});
145 }
146 }
147};
148
151 spec.identity.session_key = "native-auxiliary-feed-example";
152 spec.identity.run_number = 1;
153 spec.input_tf = "15";
154 spec.script_tf = "15";
155 spec.ticker = "MOCK";
156 spec.tickerid = "TEST:MOCK";
157 spec.type = "crypto";
158 spec.currency = "USDT";
159 spec.basecurrency = "ETH";
160 spec.timezone = "UTC";
161 spec.session = "24x7";
162 spec.initial_capital = 10000.0;
163 spec.point_value = 1.0;
164 spec.account_fx = 1.0;
165 spec.price_tick = 0.01;
166 spec.fee_kind = pineforge::NativeFeeKind::Percent;
167 spec.fee_value = 0.0;
168
170 five.tf = "5";
171 five.source = pineforge::NativeSeriesSource::AuxiliaryFeed;
172 spec.subscriptions.push_back(five);
173 return spec;
174}
175
176} // namespace
177
178int main() {
179 const std::vector<pineforge::Bar> minutes = minute_bars();
180
181 // The 15-minute input is the same tape at the run's own timeframe.
182 std::vector<pineforge::Bar> inputs;
183 inputs.reserve(kInputs);
184 for (int i = 0; i < kInputs; ++i) {
185 inputs.push_back(hand_aggregate(minutes, static_cast<std::size_t>(i) * 15, 15));
186 }
187
188 // 1. Without the feed the subscription has nothing to aggregate, and the
189 // run spec says so by name rather than silently delivering nothing.
190 {
192 const auto refused = pineforge::validate_native_run_spec(without);
193 if (refused.error != pineforge::NativeRunSpecError::SubscriptionWithoutAuxiliaryFeed) {
194 std::cerr << "a finer series without a feed must be refused by name, got error "
195 << static_cast<int>(refused.error) << '\n';
196 return 1;
197 }
198 std::printf("without a feed: refused, NativeRunSpecError::SubscriptionWithoutAuxiliaryFeed\n");
199 }
200
203 feed.tf = "1";
204 feed.bars = minutes;
205 spec.auxiliary_feed = feed;
206
207 AuxiliaryFeedExample host;
208 if (host.configure_native(spec).status != pineforge::NativeSetupStatus::Applied) {
209 std::cerr << "configure: " << host.last_error() << '\n';
210 return 1;
211 }
212 host.run(inputs.data(), static_cast<int>(inputs.size()));
213 if (host.native_state().kind != pineforge::NativeLifecycleKind::Completed) {
214 std::cerr << "run: " << host.last_error() << '\n';
215 return 1;
216 }
217
218 // 2. Twelve buckets, each the hand aggregation of its own five minutes.
219 if (host.deliveries.size() != static_cast<std::size_t>(kBuckets) || host.bars != kInputs) {
220 std::cerr << "expected " << kBuckets << " buckets over " << kInputs
221 << " inputs, got " << host.deliveries.size() << " over " << host.bars << '\n';
222 return 1;
223 }
224 for (int j = 0; j < kBuckets; ++j) {
225 const auto& delivery = host.deliveries[static_cast<std::size_t>(j)];
226 const pineforge::Bar want = hand_aggregate(minutes, static_cast<std::size_t>(j) * 5, 5);
227 if (delivery.subscription != 0 || !bars_equal(delivery.bucket, want)) {
228 std::cerr << "bucket " << j << " is not the aggregation of its five minutes\n";
229 return 1;
230 }
231 // 3. Routing by time: buckets 3i, 3i+1, 3i+2 opened inside input i's
232 // period, so they ride on it, oldest first, before its calculation.
233 if (delivery.bars_calculated_before != j / 3 || !delivery.accessor_matches) {
234 std::cerr << "bucket " << j << " rode on input " << delivery.bars_calculated_before
235 << " (expected " << (j / 3) << ") or the pull side disagreed\n";
236 return 1;
237 }
238 }
239
240 std::printf("buckets: %zu over %d inputs, three per input, oldest first\n",
241 host.deliveries.size(), host.bars);
242 for (int j = 0; j < kBuckets; ++j) {
243 const auto& d = host.deliveries[static_cast<std::size_t>(j)];
244 std::printf(" %02lld:%02lld o=%.2f h=%.2f l=%.2f c=%.2f %s, on input %d\n",
245 static_cast<long long>(d.bucket.timestamp / 3600000),
246 static_cast<long long>(d.bucket.timestamp / kMinute % 60),
247 d.bucket.open, d.bucket.high, d.bucket.low, d.bucket.close,
248 d.bucket.close > d.bucket.open ? "up " : "down",
249 d.bars_calculated_before);
250 }
251
252 // 4. The series the host decided on is the last bucket delivered on that
253 // input: buckets 2, 5, 8 and 11.
254 if (host.series_at_bar.size() != static_cast<std::size_t>(kInputs)) {
255 std::cerr << "expected one series reading per input bar\n";
256 return 1;
257 }
258 for (int i = 0; i < kInputs; ++i) {
259 const auto& seen = host.series_at_bar[static_cast<std::size_t>(i)];
260 const pineforge::Bar want = hand_aggregate(minutes, static_cast<std::size_t>(i) * 15 + 10, 5);
261 if (!seen || !bars_equal(*seen, want)) {
262 std::cerr << "native_series_bar(0) on input " << i
263 << " is not the last bucket that rode on it\n";
264 return 1;
265 }
266 }
267 std::printf("native_series_bar(0) on each input bar: the third bucket of that input\n");
268
269 std::cout << "closed trades: " << host.trade_count() << '\n';
270 for (int i = 0; i < host.trade_count(); ++i) {
271 const auto& trade = host.get_trade(i);
272 std::cout << " " << (trade.is_long ? "long " : "short")
273 << " qty=" << trade.qty
274 << " entry=" << trade.entry_price
275 << " exit=" << trade.exit_price
276 << " pnl=" << trade.pnl << '\n';
277 }
278 return host.trade_count() > 0 ? 0 : 1;
279}
The public native host: an abstract subclass of BacktestEngine with no PineScript on it.
static pf_native_run_spec_v1 make_spec(void)
NativeRunSpecValidation validate_native_run_spec(const NativeRunSpec &spec) noexcept
Complete validation, with deterministic first-error field order.
static void submit(struct host_state *state, uint32_t intent, double value, uint32_t trigger, double price, const char *label)
double open
Definition bar.hpp:7
double close
Definition bar.hpp:7
double low
Definition bar.hpp:7
double volume
Definition bar.hpp:7
int64_t timestamp
Definition bar.hpp:8
double high
Definition bar.hpp:7
An auxiliary feed of the run's OWN symbol at a timeframe strictly finer than the input: the bars the ...
One complete setup value, staged/copied by NativeStrategyHost before it is applied at begin.
std::vector< NativeTimeframeSubscription > subscriptions
Declared higher-timeframe series.
std::optional< NativeAuxiliaryFeed > auxiliary_feed
Opt-in auxiliary finer feed.