#include "raid/data/returns.hpp" #include "raid/data/resample.hpp" #include "raid/core/error.hpp" #include #include namespace raid::data { std::vector returns(std::span p, ReturnKind kind) { std::vector r; if (p.size() < 2) return r; r.resize(p.size() - 1); for (usize i = 1; i < p.size(); ++i) { RAID_REQUIRE(p[i] > 0 && p[i - 1] > 0, data_quality, "returns: non-positive price"); r[i - 1] = kind == ReturnKind::log ? std::log(p[i] / p[i - 1]) : p[i] / p[i - 1] - 1.0; } return r; } IntradayReturns intraday_returns(const BarSeries& b, ReturnKind kind, bool use_open_first) { IntradayReturns out; const usize S = b.n_sessions(); out.session_offsets.push_back(0); out.session_dates = b.session_dates; const f64 nan = std::numeric_limits::quiet_NaN(); f64 prev_close = nan; auto ret = [&](f64 a, f64 c) { RAID_REQUIRE(a > 0 && c > 0, data_quality, "returns: non-positive price"); return kind == ReturnKind::log ? std::log(c / a) : c / a - 1.0; }; for (usize k = 0; k < S; ++k) { const usize b0 = static_cast(b.session_offsets[k]), b1 = static_cast(b.session_offsets[k + 1]); if (use_open_first) out.r.push_back(ret(b.open[b0], b.close[b0])); for (usize i = b0 + 1; i < b1; ++i) out.r.push_back(ret(b.close[i - 1], b.close[i])); out.session_offsets.push_back(static_cast(out.r.size())); out.overnight.push_back(std::isnan(prev_close) ? nan : ret(prev_close, b.open[b0])); out.open_to_close.push_back(ret(b.open[b0], b.close[b1 - 1])); out.close_to_close.push_back(std::isnan(prev_close) ? nan : ret(prev_close, b.close[b1 - 1])); prev_close = b.close[b1 - 1]; } return out; } BarSeries aggregate(const BarSeries& bars, Interval target, const MarketCalendar& cal, SessionPolicy policy) { RAID_REQUIRE(to_duration(target).us > to_duration(bars.interval).us, invalid_argument, "aggregate: target must be coarser than source"); BarSeries out; out.symbol = bars.symbol; out.instrument_id = bars.instrument_id; out.interval = target; out.adjustment = bars.adjustment; const i64 step = to_duration(target).us; for (usize k = 0; k < bars.n_sessions(); ++k) { const Date d = bars.session_dates[k]; if (!cal.is_trading_day(d)) continue; const auto sb = cal.session(d); const Timestamp anchor = policy == SessionPolicy::regular ? sb.open_utc : sb.ext_open_utc; const Timestamp end = policy == SessionPolicy::regular ? sb.close_utc : sb.ext_close_utc; i64 cur_bin = -1; f64 o = 0, h = 0, l = 0, c = 0, v = 0; u32 fl = 0; Timestamp bin_t; auto flush = [&]() { if (cur_bin >= 0) { out.t_open.push_back(bin_t); out.session.push_back(d); out.open.push_back(o); out.high.push_back(h); out.low.push_back(l); out.close.push_back(c); out.volume.push_back(v); out.flags.push_back(fl); } }; for (i64 i = bars.session_offsets[k]; i < bars.session_offsets[k + 1]; ++i) { const usize j = static_cast(i); const Timestamp t = bars.t_open[j]; if (t < anchor) continue; if (policy == SessionPolicy::regular ? t > end : t >= end) continue; i64 bin = (t - anchor).us / step; if (target == Interval::d1) bin = 0; // The 16:00 closing print (t == close) forms its own bin, as in FinData's derived 5m/30m/1h datasets // (verified live 2026-11-02: 79 regular 5m bars per normal session, 43 on early closes). if (bin != cur_bin) { flush(); cur_bin = bin; bin_t = anchor + Duration(bin * step); o = bars.open[j]; h = bars.high[j]; l = bars.low[j]; c = bars.close[j]; v = bars.volume[j]; fl = bars.flags[j]; } else { h = std::max(h, bars.high[j]); l = std::min(l, bars.low[j]); c = bars.close[j]; v += bars.volume[j]; fl |= bars.flags[j]; } } flush(); } out.index_sessions(); return out; } SampledPrices subsample(const BarSeries& bars, usize step, usize offset) { RAID_REQUIRE(step >= 1 && offset < step, invalid_argument, "subsample: need step >= 1 and offset < step"); SampledPrices s; s.session_offsets.push_back(0); s.session_dates = bars.session_dates; for (usize k = 0; k < bars.n_sessions(); ++k) { const usize b0 = static_cast(bars.session_offsets[k]), b1 = static_cast(bars.session_offsets[k + 1]); for (usize i = b0 + offset; i < b1; i += step) s.p.push_back(bars.close[i]); // always include the last price of the session so the grid ends at the close if (b1 > b0 && ((b1 - 1 - b0) % step) != offset % step && (b1 - 1) >= b0 + offset) s.p.push_back(bars.close[b1 - 1]); s.session_offsets.push_back(static_cast(s.p.size())); } return s; } } // namespace raid::data