PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
metrics_fold.hpp
Go to the documentation of this file.
1#pragma once
2
3// Pure report folds. No engine, point array, scratch region, allocation, or
4// callback is retained here. The caller supplies chart-civil month keys after
5// dispatch, without holding a timezone guard across an engine callback.
7#include <algorithm>
8#include <cmath>
9#include <cstddef>
10#include <cstdint>
11#include <cstring>
12#include <limits>
13#include <string>
14
15namespace pineforge {
16namespace metrics {
17
18// Same whole-second truncation and civil-month convention as the array path.
19// Non-UTC calls take and release the timezone lock within this call; callers
20// must not already hold that lock. The pure folds below also accept keys from
21// an independently verified calendar provider.
22int equity_month_key(std::int64_t time_ms, const std::string& chart_timezone);
23
25 std::size_t count = 0;
26 double sum = 0.0;
27 void fold(double value) { sum += value; ++count; }
28};
29
31 std::int64_t point_count = 0;
32 std::int64_t observation_count = 0;
33 std::int64_t bars_in_market = 0;
36 double max_drawdown = 0.0, max_drawdown_pct = 0.0;
37 double max_runup = 0.0, max_runup_pct = 0.0;
39 // Report-only ordered witness, independent of the broker-state hash.
40 std::uint64_t point_witness = 1469598103934665603ULL;
41 // First failure in the ordered point walk, deferred until finalization:
42 // the array producer checks inputs and the FINAL open profit first.
43 const char* nonfinite_field = nullptr;
44
45 double span_ms() const {
46 return last.time_ms >= first.time_ms
47 ? static_cast<double>(static_cast<std::uint64_t>(last.time_ms)
48 - static_cast<std::uint64_t>(first.time_ms))
49 : -static_cast<double>(static_cast<std::uint64_t>(first.time_ms)
50 - static_cast<std::uint64_t>(last.time_ms));
51 }
52 double span_years() const { return span_ms() / (365.25 * 86400.0 * 1000.0); }
53 bool needs_second_pass() const {
54 return month_returns.count >= 2
55 || (point_count >= 3 && span_years() > 0.0 && bar_returns.count >= 2);
56 }
57 bool same_input(const EquityFoldSnapshot& other) const {
58 return point_count == other.point_count
62 && ((nonfinite_field == nullptr && other.nonfinite_field == nullptr)
64 && std::strcmp(nonfinite_field, other.nonfinite_field) == 0))
65 && bar_returns.count == other.bar_returns.count
66 && month_returns.count == other.month_returns.count
67 && std::memcmp(&bar_returns.sum, &other.bar_returns.sum, sizeof(double)) == 0
68 && std::memcmp(&month_returns.sum, &other.month_returns.sum, sizeof(double)) == 0
69 && std::memcmp(&first, &other.first, sizeof(first)) == 0
70 && std::memcmp(&last, &other.last, sizeof(last)) == 0;
71 }
72};
73
74namespace fold_detail {
75constexpr double nan = std::numeric_limits<double>::quiet_NaN();
76
77// Do not throw while accumulating a prefix. A terminal replacement can remove
78// its held defect, and a completed report has checks preceding this walk.
79// Keep only the first field in each ordered stage; the producer's existing
80// report_result_invalid helper raises it at that stage during finalization.
81inline double defer_nonfinite(double value, const char* field, const char*& first) {
82 if (!std::isfinite(value) && first == nullptr) first = field;
83 return value;
84}
85
86// The state of the array path's two return generators, in their original
87// order. A bucket closes on a KEY CHANGE, including nonmonotonic timestamps.
89 bool have_point = false;
90 bool have_previous_month = false;
91 int current_key = 0;
92 double previous_equity = 0.0;
94
95 template<class BarEmit, class MonthEmit>
96 void fold(const pf_equity_point_t& point, int key, BarEmit bar, MonthEmit month) {
97 if (!have_point) {
98 have_point = true;
99 current_key = key;
101 return;
102 }
103 if (previous_equity > 0.0) bar(point.equity / previous_equity - 1.0);
104 previous_equity = point.equity;
105 if (key != current_key) {
106 finish_month(month);
108 have_previous_month = true;
109 current_key = key;
110 }
112 }
113 template<class MonthEmit>
114 void finish_month(MonthEmit month) const {
117 }
118};
119
120inline void witness_word(std::uint64_t& hash, std::uint64_t word) {
121 for (unsigned i = 0; i < 8; ++i) {
122 hash ^= (word >> (8 * i)) & 255u;
123 hash *= 1099511628211ULL;
124 }
125}
126inline void witness_double(std::uint64_t& hash, double value) {
127 std::uint64_t bits;
128 std::memcpy(&bits, &value, sizeof(bits));
129 witness_word(hash, bits);
130}
131
135 double peak = 0.0, trough = 0.0;
136
137 void fold(const pf_equity_point_t& point, int key, bool observation, bool exposed) {
138 auto& s = values;
139 if (s.point_count == 0) {
140 s.first = point;
141 peak = trough = point.equity; // FIRST point, never initial capital
142 }
143 ++s.point_count;
144 if (observation) ++s.observation_count;
145 if (observation && exposed) ++s.bars_in_market;
146 s.last = point;
147 const auto check = [&s](double value, const char* field) {
148 return defer_nonfinite(value, field, s.nonfinite_field);
149 };
150 const double eq = check(point.equity, "equity_curve.equity");
151 check(point.open_profit, "equity_curve.open_profit");
152 if (eq > peak) { peak = eq; trough = eq; }
153 if (eq < trough) trough = eq;
154 const double dd = check(peak - eq, "metrics.equity.max_equity_drawdown");
155 if (dd > s.max_drawdown) {
156 s.max_drawdown = dd;
157 s.max_drawdown_pct = peak > 0.0
158 ? check(dd / peak * 100.0, "metrics.equity.max_equity_drawdown_pct") : nan;
159 }
160 const double ru = check(eq - trough, "metrics.equity.max_equity_runup");
161 if (ru > s.max_runup) {
162 s.max_runup = ru;
163 s.max_runup_pct = trough > 0.0
164 ? check(ru / trough * 100.0, "metrics.equity.max_equity_runup_pct") : nan;
165 }
166 returns.fold(point, key,
167 [&](double r) { s.bar_returns.fold(r); },
168 [&](double r) { s.month_returns.fold(r); });
169 witness_word(s.point_witness, static_cast<std::uint64_t>(point.time_ms));
170 witness_double(s.point_witness, point.equity);
171 witness_double(s.point_witness, point.open_profit);
172 witness_word(s.point_witness, static_cast<std::uint64_t>(key));
173 witness_word(s.point_witness, observation ? 1u : 0u);
174 witness_word(s.point_witness, observation && exposed ? 1u : 0u);
175 }
176};
177} // namespace fold_detail
178
179// Prefix excludes the held point. Replacing that point therefore requires no
180// subtraction, inverse sum, or regrouping, even across a civil-month boundary.
181// first_pass() closes the held month in a value copy and is repeatable.
183public:
184 void fold(const pf_equity_point_t& point, int month_key,
185 bool observation = true, bool exposed = false) {
186 if (have_held_)
187 prefix_.fold(held_, held_key_, held_observation_, held_exposed_);
188 held_ = point;
189 held_key_ = month_key;
190 held_observation_ = observation;
191 held_exposed_ = exposed;
192 have_held_ = true;
193 }
194 // Returns false on an empty fold; it never invents an observation.
195 bool replace_last(const pf_equity_point_t& point, int month_key) {
196 if (!have_held_) return false;
197 held_ = point;
198 held_key_ = month_key;
199 return true;
200 }
201 const pf_equity_point_t* held_point() const { return have_held_ ? &held_ : nullptr; }
202 int held_month_key() const { return held_key_; }
203 bool held_observation() const { return held_observation_; }
204 bool held_exposed() const { return held_exposed_; }
206 auto result = prefix_;
207 if (have_held_)
208 result.fold(held_, held_key_, held_observation_, held_exposed_);
209 result.returns.finish_month(
210 [&](double r) { result.values.month_returns.fold(r); });
211 return result.values;
212 }
213
214private:
216 pf_equity_point_t held_{};
217 int held_key_ = 0;
218 bool held_observation_ = false, held_exposed_ = false, have_held_ = false;
219};
220
225 double net_profit; // Report row-order sum, including presented terminal rows.
226};
227
232 const char* nonfinite_field = nullptr;
233 bool complete = false;
234 bool matches(const EquityFoldSnapshot& first) const {
235 return complete && expected.same_input(first) && replayed.same_input(first);
236 }
237};
238
239namespace fold_detail {
241 std::size_t expected_count = 0, count = 0;
242 double mean = 0.0, rf = 0.0, ann = 0.0;
243 double var = 0.0, down = 0.0;
244 const char* sharpe_field = nullptr;
245 const char* sortino_field = nullptr;
246 const char* nonfinite_field = nullptr;
247 bool enabled = false;
248 double check(double value, const char* field) {
249 return defer_nonfinite(value, field, nonfinite_field);
250 }
251 void prepare(const EquityReturnSummary& first, double risk_free, double annualization,
252 bool allowed, const char* sharpe_name, const char* sortino_name) {
253 sharpe_field = sharpe_name;
254 sortino_field = sortino_name;
255 expected_count = first.count;
256 enabled = allowed && first.count >= 2;
257 if (!enabled) return;
258 rf = check(risk_free, sharpe_field);
259 ann = check(annualization, sharpe_field);
260 check(first.sum, sharpe_field);
261 mean = check(first.sum / static_cast<double>(first.count), sharpe_field);
262 }
263 void fold(double value) {
264 if (!enabled) return;
265 check(value, sharpe_field);
266 const double delta = check(value - mean, sharpe_field);
267 var = check(var + delta * delta, sharpe_field);
268 const double d = std::min(0.0, check(value - rf, sortino_field));
269 down = check(down + d * d, sortino_field);
270 ++count;
271 }
272 bool complete() const { return !enabled || count == expected_count; }
273 void finish(double& sharpe, double& sortino) {
274 sharpe = sortino = nan;
275 if (!enabled || !complete()) return;
276 const double sd = check(std::sqrt(var / static_cast<double>(expected_count - 1)),
278 const double dd = check(std::sqrt(down / static_cast<double>(expected_count)),
280 if (sd > 0.0) sharpe = check((mean - rf) / sd * ann, sharpe_field);
281 if (dd > 0.0) sortino = check((mean - rf) / dd * ann, sortino_field);
282 }
283};
287 void fold(const pf_equity_point_t& point, int key) {
288 returns.fold(point, key, [&](double r) { bars.fold(r); },
289 [&](double r) { months.fold(r); });
290 }
291};
292} // namespace fold_detail
293
294// A deterministic replay feeds finalized points in their original order.
295// This class does not execute an engine, retain an input, or invoke observers.
296// It also holds its last point so a replay can apply terminal/presentation
297// rewrites before finish(). Both return series share this single walk.
299public:
300 explicit EquityMetricsSecondPass(const EquityFoldSnapshot& first) : expected_(first) {
301 prefix_.months.prepare(first.month_returns, 0.02 / 12.0, std::sqrt(12.0), true,
302 "metrics.equity.sharpe_monthly", "metrics.equity.sortino_monthly");
303 const double span = first.span_years();
304 if (first.point_count >= 3 && span > 0.0) {
305 const double bars_per_year = prefix_.bars.check(
306 static_cast<double>(first.point_count - 1) / span, "metrics.equity.sharpe_bar");
307 prefix_.bars.prepare(first.bar_returns, 0.02 / bars_per_year,
308 std::sqrt(bars_per_year), true,
309 "metrics.equity.sharpe_bar", "metrics.equity.sortino_bar");
310 }
311 }
312 void fold(const pf_equity_point_t& point, int month_key,
313 bool observation = true, bool exposed = false) {
314 if (const auto* held = replay_.held_point())
315 prefix_.fold(*held, replay_.held_month_key());
316 replay_.fold(point, month_key, observation, exposed);
317 }
318 bool replace_last(const pf_equity_point_t& point, int month_key) {
319 return replay_.replace_last(point, month_key);
320 }
322 auto final = prefix_;
323 if (const auto* held = replay_.held_point())
324 final.fold(*held, replay_.held_month_key());
325 final.returns.finish_month([&](double r) { final.months.fold(r); });
326 EquityRatioResult result;
327 result.expected = expected_;
328 result.replayed = replay_.first_pass();
329 result.complete = expected_.same_input(result.replayed)
330 && final.bars.complete() && final.months.complete();
331 if (result.complete) {
332 final.months.finish(result.sharpe_monthly, result.sortino_monthly);
333 final.bars.finish(result.sharpe_bar, result.sortino_bar);
334 // Monthly ratios precede per-bar ratios in the array producer,
335 // regardless of when either defect was encountered during replay.
336 result.nonfinite_field = final.months.nonfinite_field
337 ? final.months.nonfinite_field : final.bars.nonfinite_field;
338 }
339 return result;
340 }
341
342private:
343 EquityFoldSnapshot expected_;
344 EquityMetricsFold replay_;
346};
347
349 const EquityFoldSnapshot& first, const EquityMetricInputs& inputs,
350 const EquityRatioResult* ratios = nullptr);
351
352// The trade blocks come from the existing fixed accumulators. Passing no
353// second pass explicitly produces first-pass fields only; callers publish
354// readiness separately (needs_second_pass), never describe pending ratios as
355// a completed summary.
357 const EquityFoldSnapshot& first, const EquityMetricInputs& inputs,
358 const pf_trade_stats_t& all, const pf_trade_stats_t& longs,
359 const pf_trade_stats_t& shorts, const EquityRatioResult* ratios = nullptr) {
360 pf_metrics_t result{};
361 result.all = all;
362 result.longs = longs;
363 result.shorts = shorts;
364 result.equity = finalize_equity_metrics(first, inputs, ratios);
365 return result;
366}
367
368} // namespace metrics
369} // namespace pineforge
bool replace_last(const pf_equity_point_t &point, int month_key)
const pf_equity_point_t * held_point() const
void fold(const pf_equity_point_t &point, int month_key, bool observation=true, bool exposed=false)
EquityFoldSnapshot first_pass() const
void fold(const pf_equity_point_t &point, int month_key, bool observation=true, bool exposed=false)
EquityMetricsSecondPass(const EquityFoldSnapshot &first)
bool replace_last(const pf_equity_point_t &point, int month_key)
void witness_word(std::uint64_t &hash, std::uint64_t word)
void witness_double(std::uint64_t &hash, double value)
double defer_nonfinite(double value, const char *field, const char *&first)
int equity_month_key(std::int64_t time_ms, const std::string &chart_timezone)
pf_metrics_t finalize_metrics(const EquityFoldSnapshot &first, const EquityMetricInputs &inputs, const pf_trade_stats_t &all, const pf_trade_stats_t &longs, const pf_trade_stats_t &shorts, const EquityRatioResult *ratios=nullptr)
pf_equity_stats_t finalize_equity_metrics(const EquityFoldSnapshot &first, const EquityMetricInputs &inputs, const EquityRatioResult *ratios=nullptr)
Single per-script-bar equity point.
Definition pineforge.h:326
double open_profit
Mark-to-market open P&L at bar close.
Definition pineforge.h:329
double equity
initial_capital + net_profit + open_profit.
Definition pineforge.h:328
int64_t time_ms
Script-bar OPEN timestamp (Unix ms).
Definition pineforge.h:327
Equity-curve-derived statistics (all-trades only, like TV).
Definition pineforge.h:272
Composite metrics container: trade stats (all / long / short) + equity-curve stats.
Definition pineforge.h:317
pf_trade_stats_t all
Definition pineforge.h:318
Trade-level statistics block — computed once each for all / long / short.
Definition pineforge.h:209
bool same_input(const EquityFoldSnapshot &other) const
bool matches(const EquityFoldSnapshot &first) const
void fold(const pf_equity_point_t &point, int key, bool observation, bool exposed)
void fold(const pf_equity_point_t &point, int key, BarEmit bar, MonthEmit month)
void finish(double &sharpe, double &sortino)
double check(double value, const char *field)
void prepare(const EquityReturnSummary &first, double risk_free, double annualization, bool allowed, const char *sharpe_name, const char *sortino_name)
void fold(const pf_equity_point_t &point, int key)