1#include "raid/data/bar_series.hpp"2#include "raid/core/error.hpp"3#include <algorithm>4#include <numeric>5#include <cmath>67namespace raid {89nlohmann::json QualityReport::to_json() const {10 return {{"symbol", symbol}, {"n_bars", n_bars}, {"n_sessions", n_sessions}, {"ohlc_inconsistent", ohlc_inconsistent},11 {"nonpositive_price", nonpositive_price}, {"negative_volume", negative_volume}, {"zero_volume", zero_volume},12 {"duplicates", duplicates}, {"off_interval", off_interval}, {"outside_session", outside_session},13 {"non_trading_day", non_trading_day}, {"unsorted", unsorted}, {"missing_regular_bars", missing_regular_bars},14 {"price_jumps", price_jumps}, {"coverage", coverage}, {"per_session_missing", per_session_missing}, {"notes", notes}, {"has_errors", has_errors()}};15}1617BarSeries::NormalizeStats BarSeries::normalize() {18 const usize n = size();19 usize reordered = 0; bool dup = false;20 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; }21 if (reordered == 0 && !dup) { index_sessions(); return {0, 0}; } // fast path: no copy22 std::vector<usize> idx(n); std::iota(idx.begin(), idx.end(), 0);23 if (reordered) std::stable_sort(idx.begin(), idx.end(), [&](usize a, usize b) { return t_open[a] < t_open[b]; });24 BarSeries out; out.symbol = symbol; out.instrument_id = instrument_id; out.interval = interval; out.adjustment = adjustment; out.reserve(n);25 usize dups = 0;26 for (usize k = 0; k < n; ++k) {27 const usize i = idx[k];28 if (!out.empty() && out.t_open.back() == t_open[i]) { ++dups; continue; }29 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]);30 out.low.push_back(low[i]); out.close.push_back(close[i]); out.volume.push_back(volume[i]); out.flags.push_back(flags[i]);31 }32 *this = std::move(out);33 index_sessions();34 return {dups, reordered};35}3637void BarSeries::index_sessions() {38 session_offsets.clear(); session_dates.clear();39 for (usize i = 0; i < size(); ++i) {40 if (i == 0 || session[i] != session[i - 1]) { session_offsets.push_back(static_cast<i64>(i)); session_dates.push_back(session[i]); }41 }42 session_offsets.push_back(static_cast<i64>(size()));43 if (size() == 0) session_offsets = {0};44}4546QualityReport BarSeries::validate(const MarketCalendar& cal, SessionPolicy policy, f64 jump_threshold) const {47 QualityReport r; r.symbol = symbol; r.n_bars = size(); r.n_sessions = n_sessions();48 const i64 step = to_duration(interval).us;49 for (usize i = 0; i < size(); ++i) {50 if (high[i] < low[i] || high[i] < std::max(open[i], close[i]) || low[i] > std::min(open[i], close[i])) ++r.ohlc_inconsistent;51 if (!(open[i] > 0 && high[i] > 0 && low[i] > 0 && close[i] > 0)) ++r.nonpositive_price;52 if (volume[i] < 0) ++r.negative_volume;53 if (volume[i] == 0) ++r.zero_volume;54 if (i > 0 && t_open[i] <= t_open[i - 1]) ++r.unsorted;55 if (interval != Interval::d1 && (t_open[i].us % step) != 0) ++r.off_interval;56 }57 usize expected_total = 0, present_total = 0;58 for (usize k = 0; k < n_sessions(); ++k) {59 const Date d = session_dates[k];60 if (!cal.is_trading_day(d)) { r.non_trading_day += static_cast<usize>(session_offsets[k + 1] - session_offsets[k]); continue; }61 const auto sb = cal.session(d);62 usize present = 0;63 f64 prev_close = -1;64 for (i64 i = session_offsets[k]; i < session_offsets[k + 1]; ++i) {65 const auto t = t_open[static_cast<usize>(i)];66 const bool regular = t >= sb.open_utc && t <= sb.close_utc;67 const bool extended = t >= sb.ext_open_utc && t < sb.ext_close_utc;68 if (policy == SessionPolicy::regular ? !regular : !extended) ++r.outside_session;69 if (regular) ++present;70 if (prev_close > 0 && std::abs(close[static_cast<usize>(i)] / prev_close - 1.0) > jump_threshold) ++r.price_jumps;71 prev_close = close[static_cast<usize>(i)];72 }73 const usize expected = cal.expected_regular_bars(d, interval);74 expected_total += expected; present_total += std::min(present, expected);75 if (present < expected) { r.missing_regular_bars += expected - present; r.per_session_missing[d.iso()] = expected - present; }76 }77 r.coverage = expected_total ? static_cast<f64>(present_total) / static_cast<f64>(expected_total) : 1.0;78 return r;79}8081BarSeries BarSeries::filter_session(const MarketCalendar& cal, SessionPolicy policy) const {82 BarSeries out; out.symbol = symbol; out.instrument_id = instrument_id; out.interval = interval; out.adjustment = adjustment; out.reserve(size());83 for (usize k = 0; k < n_sessions(); ++k) {84 const Date d = session_dates[k];85 if (!cal.is_trading_day(d)) continue;86 const auto sb = cal.session(d);87 for (i64 i = session_offsets[k]; i < session_offsets[k + 1]; ++i) {88 const usize j = static_cast<usize>(i); const auto t = t_open[j];89 const bool keep = policy == SessionPolicy::regular ? (t >= sb.open_utc && t <= sb.close_utc) : (t >= sb.ext_open_utc && t < sb.ext_close_utc);90 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]); }91 }92 }93 out.index_sessions();94 return out;95}9697} // namespace raid98