1#include "raid/data/returns.hpp"2#include "raid/data/resample.hpp"3#include "raid/core/error.hpp"4#include <cmath>5#include <limits>67namespace raid::data {89std::vector<f64> returns(std::span<const f64> p, ReturnKind kind) {10 std::vector<f64> r; if (p.size() < 2) return r; r.resize(p.size() - 1);11 for (usize i = 1; i < p.size(); ++i) {12 RAID_REQUIRE(p[i] > 0 && p[i - 1] > 0, data_quality, "returns: non-positive price");13 r[i - 1] = kind == ReturnKind::log ? std::log(p[i] / p[i - 1]) : p[i] / p[i - 1] - 1.0;14 }15 return r;16}1718IntradayReturns intraday_returns(const BarSeries& b, ReturnKind kind, bool use_open_first) {19 IntradayReturns out; const usize S = b.n_sessions();20 out.session_offsets.push_back(0); out.session_dates = b.session_dates;21 const f64 nan = std::numeric_limits<f64>::quiet_NaN();22 f64 prev_close = nan;23 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; };24 for (usize k = 0; k < S; ++k) {25 const usize b0 = static_cast<usize>(b.session_offsets[k]), b1 = static_cast<usize>(b.session_offsets[k + 1]);26 if (use_open_first) out.r.push_back(ret(b.open[b0], b.close[b0]));27 for (usize i = b0 + 1; i < b1; ++i) out.r.push_back(ret(b.close[i - 1], b.close[i]));28 out.session_offsets.push_back(static_cast<i64>(out.r.size()));29 out.overnight.push_back(std::isnan(prev_close) ? nan : ret(prev_close, b.open[b0]));30 out.open_to_close.push_back(ret(b.open[b0], b.close[b1 - 1]));31 out.close_to_close.push_back(std::isnan(prev_close) ? nan : ret(prev_close, b.close[b1 - 1]));32 prev_close = b.close[b1 - 1];33 }34 return out;35}3637BarSeries aggregate(const BarSeries& bars, Interval target, const MarketCalendar& cal, SessionPolicy policy) {38 RAID_REQUIRE(to_duration(target).us > to_duration(bars.interval).us, invalid_argument, "aggregate: target must be coarser than source");39 BarSeries out; out.symbol = bars.symbol; out.instrument_id = bars.instrument_id; out.interval = target; out.adjustment = bars.adjustment;40 const i64 step = to_duration(target).us;41 for (usize k = 0; k < bars.n_sessions(); ++k) {42 const Date d = bars.session_dates[k];43 if (!cal.is_trading_day(d)) continue;44 const auto sb = cal.session(d);45 const Timestamp anchor = policy == SessionPolicy::regular ? sb.open_utc : sb.ext_open_utc;46 const Timestamp end = policy == SessionPolicy::regular ? sb.close_utc : sb.ext_close_utc;47 i64 cur_bin = -1; f64 o = 0, h = 0, l = 0, c = 0, v = 0; u32 fl = 0; Timestamp bin_t;48 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); } };49 for (i64 i = bars.session_offsets[k]; i < bars.session_offsets[k + 1]; ++i) {50 const usize j = static_cast<usize>(i); const Timestamp t = bars.t_open[j];51 if (t < anchor) continue;52 if (policy == SessionPolicy::regular ? t > end : t >= end) continue;53 i64 bin = (t - anchor).us / step;54 if (target == Interval::d1) bin = 0;55 // The 16:00 closing print (t == close) forms its own bin, as in FinData's derived 5m/30m/1h datasets56 // (verified live 2026-11-02: 79 regular 5m bars per normal session, 43 on early closes).57 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]; }58 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]; }59 }60 flush();61 }62 out.index_sessions();63 return out;64}6566SampledPrices subsample(const BarSeries& bars, usize step, usize offset) {67 RAID_REQUIRE(step >= 1 && offset < step, invalid_argument, "subsample: need step >= 1 and offset < step");68 SampledPrices s; s.session_offsets.push_back(0); s.session_dates = bars.session_dates;69 for (usize k = 0; k < bars.n_sessions(); ++k) {70 const usize b0 = static_cast<usize>(bars.session_offsets[k]), b1 = static_cast<usize>(bars.session_offsets[k + 1]);71 for (usize i = b0 + offset; i < b1; i += step) s.p.push_back(bars.close[i]);72 // always include the last price of the session so the grid ends at the close73 if (b1 > b0 && ((b1 - 1 - b0) % step) != offset % step && (b1 - 1) >= b0 + offset) s.p.push_back(bars.close[b1 - 1]);74 s.session_offsets.push_back(static_cast<i64>(s.p.size()));75 }76 return s;77}7879} // namespace raid::data80