#include "raid/data/bar_series.hpp" #include "raid/core/error.hpp" #include #include #include namespace raid { nlohmann::json QualityReport::to_json() const { return {{"symbol", symbol}, {"n_bars", n_bars}, {"n_sessions", n_sessions}, {"ohlc_inconsistent", ohlc_inconsistent}, {"nonpositive_price", nonpositive_price}, {"negative_volume", negative_volume}, {"zero_volume", zero_volume}, {"duplicates", duplicates}, {"off_interval", off_interval}, {"outside_session", outside_session}, {"non_trading_day", non_trading_day}, {"unsorted", unsorted}, {"missing_regular_bars", missing_regular_bars}, {"price_jumps", price_jumps}, {"coverage", coverage}, {"per_session_missing", per_session_missing}, {"notes", notes}, {"has_errors", has_errors()}}; } BarSeries::NormalizeStats BarSeries::normalize() { const usize n = size(); usize reordered = 0; bool dup = false; for (usize i = 1; i < n; ++i) { if (t_open[i] < t_open[i - 1]) ++reordered; else if (t_open[i] == t_open[i - 1]) dup = true; } if (reordered == 0 && !dup) { index_sessions(); return {0, 0}; } // fast path: no copy std::vector idx(n); std::iota(idx.begin(), idx.end(), 0); if (reordered) std::stable_sort(idx.begin(), idx.end(), [&](usize a, usize b) { return t_open[a] < t_open[b]; }); BarSeries out; out.symbol = symbol; out.instrument_id = instrument_id; out.interval = interval; out.adjustment = adjustment; out.reserve(n); usize dups = 0; for (usize k = 0; k < n; ++k) { const usize i = idx[k]; if (!out.empty() && out.t_open.back() == t_open[i]) { ++dups; continue; } out.t_open.push_back(t_open[i]); out.session.push_back(session[i]); out.open.push_back(open[i]); out.high.push_back(high[i]); out.low.push_back(low[i]); out.close.push_back(close[i]); out.volume.push_back(volume[i]); out.flags.push_back(flags[i]); } *this = std::move(out); index_sessions(); return {dups, reordered}; } void BarSeries::index_sessions() { session_offsets.clear(); session_dates.clear(); for (usize i = 0; i < size(); ++i) { if (i == 0 || session[i] != session[i - 1]) { session_offsets.push_back(static_cast(i)); session_dates.push_back(session[i]); } } session_offsets.push_back(static_cast(size())); if (size() == 0) session_offsets = {0}; } QualityReport BarSeries::validate(const MarketCalendar& cal, SessionPolicy policy, f64 jump_threshold) const { QualityReport r; r.symbol = symbol; r.n_bars = size(); r.n_sessions = n_sessions(); const i64 step = to_duration(interval).us; for (usize i = 0; i < size(); ++i) { if (high[i] < low[i] || high[i] < std::max(open[i], close[i]) || low[i] > std::min(open[i], close[i])) ++r.ohlc_inconsistent; if (!(open[i] > 0 && high[i] > 0 && low[i] > 0 && close[i] > 0)) ++r.nonpositive_price; if (volume[i] < 0) ++r.negative_volume; if (volume[i] == 0) ++r.zero_volume; if (i > 0 && t_open[i] <= t_open[i - 1]) ++r.unsorted; if (interval != Interval::d1 && (t_open[i].us % step) != 0) ++r.off_interval; } usize expected_total = 0, present_total = 0; for (usize k = 0; k < n_sessions(); ++k) { const Date d = session_dates[k]; if (!cal.is_trading_day(d)) { r.non_trading_day += static_cast(session_offsets[k + 1] - session_offsets[k]); continue; } const auto sb = cal.session(d); usize present = 0; f64 prev_close = -1; for (i64 i = session_offsets[k]; i < session_offsets[k + 1]; ++i) { const auto t = t_open[static_cast(i)]; const bool regular = t >= sb.open_utc && t <= sb.close_utc; const bool extended = t >= sb.ext_open_utc && t < sb.ext_close_utc; if (policy == SessionPolicy::regular ? !regular : !extended) ++r.outside_session; if (regular) ++present; if (prev_close > 0 && std::abs(close[static_cast(i)] / prev_close - 1.0) > jump_threshold) ++r.price_jumps; prev_close = close[static_cast(i)]; } const usize expected = cal.expected_regular_bars(d, interval); expected_total += expected; present_total += std::min(present, expected); if (present < expected) { r.missing_regular_bars += expected - present; r.per_session_missing[d.iso()] = expected - present; } } r.coverage = expected_total ? static_cast(present_total) / static_cast(expected_total) : 1.0; return r; } BarSeries BarSeries::filter_session(const MarketCalendar& cal, SessionPolicy policy) const { BarSeries out; out.symbol = symbol; out.instrument_id = instrument_id; out.interval = interval; out.adjustment = adjustment; out.reserve(size()); for (usize k = 0; k < n_sessions(); ++k) { const Date d = session_dates[k]; if (!cal.is_trading_day(d)) continue; const auto sb = cal.session(d); for (i64 i = session_offsets[k]; i < session_offsets[k + 1]; ++i) { const usize j = static_cast(i); const auto t = t_open[j]; const bool keep = policy == SessionPolicy::regular ? (t >= sb.open_utc && t <= sb.close_utc) : (t >= sb.ext_open_utc && t < sb.ext_close_utc); if (keep) { out.t_open.push_back(t); out.session.push_back(session[j]); out.open.push_back(open[j]); out.high.push_back(high[j]); out.low.push_back(low[j]); out.close.push_back(close[j]); out.volume.push_back(volume[j]); out.flags.push_back(flags[j]); } } } out.index_sessions(); return out; } } // namespace raid