PineForge v0.13.1-379-g9b50973
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
native_sized_report_strategy.cpp
Go to the documentation of this file.
1// Pine-free native example: kernel-sized openings and a kernel-recorded
2// report (R5 lanes L3 and L2).
3//
4// Sizing: the host names WHAT an opening is worth and the kernel resolves the
5// units.
6// * Sized{CashValue{2500}}: 2500 of account currency, resolved once when
7// the request is accepted (SizeTime::AtAcceptance) against the
8// decision-point price on the instrument's tick ladder
9// (SizePrice::SignalOnTick): 2500 / 100.00 = 25 units.
10// * Sized{EquityFraction{0.5}}: half the marked equity, resolved at the
11// matching candidate (SizeTime::AtMatch) against the price the kernel
12// settles at (SizePrice::Resolved): 0.5 * 10075 / 100.75 = 50 units.
13// A host that owns its whole quantity emits HostSized instead (see
14// native_selected_strategy.cpp); a Sized request needs no override.
15//
16// Reporting: report_policy = KernelRecorded asks the kernel to record one
17// equity point per script bar, so a bare host gets a non-empty equity curve
18// and finite drawdown / run-up metrics without recording anything itself.
19// report_open_position_at_end adds the position still open at run end as one
20// mark-to-market REPORT row at the last close: a report row, never a closed
21// trade, so trade_count() stays what the kernel actually closed.
22//
23// c++ -std=c++17 native_sized_report_strategy.cpp -lpineforge_kernel -o sized_report
24//
25// Nothing here is PineScript: no codegen, no `src/source`, no `src/compat`.
26
28
29#include <cmath>
30#include <cstdio>
31#include <iostream>
32#include <optional>
33
34namespace {
35
36namespace no = pineforge::native_order;
37
38class SizedReportExample : public pineforge::NativeStrategyHost {
39public:
40 struct Filled { double units = 0.0; double price = 0.0; };
41 std::optional<Filled> cash_sized;
42 std::optional<Filled> equity_sized;
43
44private:
45 int bars_ = 0;
46 std::optional<no::RequestHandle> cash_entry_;
47 std::optional<no::RequestHandle> equity_entry_;
48
49 void on_native_run_begin() override {
50 bars_ = 0;
51 cash_entry_.reset();
52 equity_entry_.reset();
53 cash_sized.reset();
54 equity_sized.reset();
55 }
56
57 void on_native_bar(const pineforge::Bar&,
58 const pineforge::NativeDecisionContext&) override {
59 ++bars_;
60 if (bars_ == 1) {
61 // 2500 of exposure, frozen at acceptance against this bar's close
62 // (100.00) on the 0.01 tick ladder: 25 units, known before the fill.
63 no::Sized sized;
64 sized.side = no::Side::Long;
65 sized.basis = no::CashValue{2500.0};
66 sized.time = no::SizeTime::AtAcceptance;
67 sized.price = no::SizePrice::SignalOnTick;
68 cash_entry_ = submit({sized, "cash-sized", "sized-report"}).handle;
69 } else if (bars_ == 3) {
70 submit({no::Flatten{}, "flat", "sized-report"});
71 } else if (bars_ == 4) {
72 // Half of whatever the account is worth when the candidate is
73 // matched, converted at the price the kernel settles at.
74 no::Sized sized;
75 sized.side = no::Side::Long;
76 sized.basis = no::EquityFraction{0.5};
77 sized.time = no::SizeTime::AtMatch;
78 sized.price = no::SizePrice::Resolved;
79 equity_entry_ = submit({sized, "equity-sized", "sized-report"}).handle;
80 }
81 }
82
83 void on_native_applied(const no::ExecutionAppliedEvent& event,
84 const pineforge::NativeDecisionContext&) override {
85 const Filled filled{event.opened_units, event.resolved_price};
86 if (cash_entry_ && event.handle() == *cash_entry_) cash_sized = filled;
87 if (equity_entry_ && event.handle() == *equity_entry_) equity_sized = filled;
88 }
89};
90
93 spec.identity.session_key = "native-sized-report-example";
94 spec.identity.run_number = 1;
95 spec.input_tf = "5";
96 spec.script_tf = "5";
97 spec.ticker = "MOCK";
98 spec.tickerid = "TEST:MOCK";
99 spec.type = "crypto";
100 spec.currency = "USDT";
101 spec.basecurrency = "ETH";
102 spec.timezone = "UTC";
103 spec.session = "24x7";
104 spec.initial_capital = 10000.0;
105 spec.point_value = 1.0;
106 spec.account_fx = 1.0;
107 spec.price_tick = 0.01;
108 spec.fee_kind = pineforge::NativeFeeKind::Percent;
109 spec.fee_value = 0.0;
110 // The report: the kernel records the curve, one point per script bar,
111 // and reports the position still open at the end as a marked row.
112 spec.report_policy = pineforge::NativeReportPolicy::KernelRecorded;
113 spec.report_open_position_at_end = true;
114 return spec;
115}
116
117// open, high, low, close, volume, timestamp (Unix milliseconds).
118const pineforge::Bar kBars[] = {
119 { 99.50, 100.50, 99.00, 100.00, 4.0, 0}, // decision price 100.00
120 {100.00, 102.50, 99.75, 102.00, 4.0, 300000}, // 25 units fill at 100.00
121 {102.00, 103.50, 101.50, 103.00, 4.0, 600000}, // flatten submitted
122 {103.00, 103.75, 100.50, 100.75, 4.0, 900000}, // flat at 103.00: +75
123 {100.75, 102.00, 100.50, 101.75, 4.0, 1200000}, // 50 units fill at 100.75
124};
125constexpr int kBarCount = 5;
126
127// Owning report view: fill_report allocates, free_report releases.
128struct Report {
129 pineforge::ReportC c{};
130 explicit Report(const pineforge::BacktestEngine& engine) { engine.fill_report(&c); }
132 Report(const Report&) = delete;
133 Report& operator=(const Report&) = delete;
134};
135
136bool near(double value, double expected) { return std::fabs(value - expected) < 1e-9; }
137
138} // namespace
139
140int main() {
141 SizedReportExample host;
142 if (host.configure_native(make_spec()).status
143 != pineforge::NativeSetupStatus::Applied) {
144 std::cerr << "configure: " << host.last_error() << '\n';
145 return 1;
146 }
147
148 host.run(kBars, kBarCount);
149 if (host.native_state().kind != pineforge::NativeLifecycleKind::Completed) {
150 std::cerr << "run: " << host.last_error() << '\n';
151 return 1;
152 }
153
154 // --- sizing: the kernel resolved both bases into units -----------------
155 if (!host.cash_sized || !host.equity_sized) {
156 std::cerr << "a Sized entry did not fill\n";
157 return 1;
158 }
159 std::printf("cash-sized: CashValue 2500 at the 100.00 signal -> %.4f units filled at %.2f\n",
160 host.cash_sized->units, host.cash_sized->price);
161 std::printf("equity-sized: EquityFraction 0.5 of 10075 at %.2f -> %.4f units\n",
162 host.equity_sized->price, host.equity_sized->units);
163 if (!near(host.cash_sized->units, 25.0) || !near(host.equity_sized->units, 50.0)) {
164 std::cerr << "unexpected sized units\n";
165 return 1;
166 }
167
168 // --- report: a kernel-recorded curve, one point per script bar ---------
169 Report report(host);
170 std::printf("equity points: %lld (script bars %lld)\n",
171 static_cast<long long>(report.c.equity_curve_len),
172 static_cast<long long>(report.c.script_bars_processed));
173 if (report.c.equity_curve_len != kBarCount
174 || report.c.equity_curve_len != report.c.script_bars_processed) {
175 std::cerr << "expected one equity point per script bar\n";
176 return 1;
177 }
178 for (std::int64_t i = 0; i < report.c.equity_curve_len; ++i) {
179 const auto& point = report.c.equity_curve[i];
180 if (!std::isfinite(point.equity) || !std::isfinite(point.open_profit)) {
181 std::cerr << "non-finite equity point\n";
182 return 1;
183 }
184 std::printf(" t=%lld equity=%.2f open_profit=%.2f\n",
185 static_cast<long long>(point.time_ms), point.equity, point.open_profit);
186 }
187 std::printf("max drawdown %.2f, max run-up %.2f, open P&L at the end %.2f\n",
190 report.c.metrics.equity.open_pl);
191 if (!std::isfinite(report.c.metrics.equity.max_equity_drawdown)
192 || !std::isfinite(report.c.metrics.equity.max_equity_runup)) {
193 std::cerr << "non-finite equity metrics\n";
194 return 1;
195 }
196
197 // --- the open position is a report row, not a closed trade -------------
198 int open_at_end = 0;
199 for (int i = 0; i < report.c.trades_len; ++i) open_at_end += report.c.trades[i].open_at_end;
200 std::printf("report rows: %d (%d closed, %d open at run end, marked at the last close)\n",
201 report.c.trades_len, host.trade_count(), open_at_end);
202 if (report.c.trades_len != host.trade_count() + 1 || open_at_end != 1
203 || report.c.trades_len != host.report_trade_count()) {
204 std::cerr << "expected exactly one range-end report row\n";
205 return 1;
206 }
207
208 std::cout << "closed trades: " << host.trade_count() << '\n';
209 for (int i = 0; i < host.trade_count(); ++i) {
210 const auto& trade = host.get_trade(i);
211 std::cout << " " << (trade.is_long ? "long " : "short")
212 << " qty=" << trade.qty
213 << " entry=" << trade.entry_price
214 << " exit=" << trade.exit_price
215 << " pnl=" << trade.pnl << '\n';
216 }
217 return host.trade_count() > 0 ? 0 : 1;
218}
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)
double max_equity_drawdown
Peak-to-trough equity drop, positive currency magnitude.
Definition pineforge.h:260
double open_pl
Mark-to-market open profit at the final bar.
Definition pineforge.h:311
double max_equity_runup
Trough-to-peak rise where the trough resets on each new equity peak (mirrors the engine's intra-run e...
Definition pineforge.h:263
pf_equity_stats_t equity
Definition pineforge.h:318
pf_metrics_t metrics
Definition engine.hpp:268
int64_t equity_curve_len
Definition engine.hpp:270
pf_equity_point_t * equity_curve
Definition engine.hpp:269
int64_t script_bars_processed
Definition engine.hpp:244
int32_t open_at_end
Definition engine.hpp:213
One complete setup value, staged/copied by NativeStrategyHost before it is applied at begin.
bool report_open_position_at_end
Report a position still open at run end as a mark-to-market closed row at the last close.