From 58382b41c5a009cc5fdcf299c7c81d55ddc36d89 Mon Sep 17 00:00:00 2001 From: Ethansi2947E <126624443+Ethansi2947E@users.noreply.github.com> Date: Wed, 28 Oct 2026 03:57:47 +0100 Subject: [PATCH] Add full native REAL_TICKS broker tick streaming and replay engine --- core/include/interactive_tester.hpp | 12 +++-- core/src/tester_bindings.cpp | 80 +++++++++++++++++++++++++++-- 2 files changed, 86 insertions(+), 6 deletions(-) diff --git a/core/include/interactive_tester.hpp b/core/include/interactive_tester.hpp index 4598789..b5241ae 100644 --- a/core/include/interactive_tester.hpp +++ b/core/include/interactive_tester.hpp @@ -120,13 +120,16 @@ public: std::vector bars_owned, const SymbolSpec& symbol, const TesterConfig& cfg, - std::vector spread_series_owned = {} + std::vector spread_series_owned = {}, + std::vector> real_ticks_owned = {} ) { bars_storage_ = std::move(bars_owned); spread_storage_ = std::move(spread_series_owned); + real_ticks_storage_ = std::move(real_ticks_owned); const double* ss_ptr = spread_storage_.empty() ? nullptr : spread_storage_.data(); + const auto* rt_ptr = real_ticks_storage_.empty() ? nullptr : &real_ticks_storage_; return init(strategy, bars_storage_.data(), static_cast(bars_storage_.size()), - symbol, cfg, ss_ptr, nullptr); + symbol, cfg, ss_ptr, rt_ptr); } bool init( @@ -197,9 +200,11 @@ public: } int64_t bar_duration_ms = TickGenerator::infer_bar_duration_ms(bars, n); - if (cfg.tick_mode == TickMode::EVERY_TICK || cfg.tick_mode == TickMode::OHLC_M1) { + if (cfg.tick_mode == TickMode::EVERY_TICK || cfg.tick_mode == TickMode::OHLC_M1 || cfg.tick_mode == TickMode::REAL_TICKS) { if (real_ticks && !real_ticks->empty()) { tick_data_ptr_ = real_ticks; + } else if (cfg.tick_mode == TickMode::REAL_TICKS) { + throw std::runtime_error("TickMode::REAL_TICKS was specified, but no real tick data was provided."); } else { synth_ticks_ = TickGenerator::generate_all( bars, spread_series, n, sym_.spread_val, @@ -539,6 +544,7 @@ private: TesterResult result_{}; std::vector bars_storage_; std::vector spread_storage_; + std::vector> real_ticks_storage_; int32_t current_bar_idx_ = 0; int32_t current_tick_in_bar_ = 0; int64_t last_swap_day_ = -1; diff --git a/core/src/tester_bindings.cpp b/core/src/tester_bindings.cpp index 9ab087b..224586c 100644 --- a/core/src/tester_bindings.cpp +++ b/core/src/tester_bindings.cpp @@ -501,11 +501,61 @@ PYBIND11_MODULE(edge_tester, m) { return s->ctx.current_tick; }); + // ---- Helper to group flat tick streams by candle bars ---- + auto group_ticks_by_bars = []( + const MqlRates* bars, + int32_t n, + py::object tick_time_msc_obj, + py::object tick_bid_obj, + py::object tick_ask_obj, + py::object tick_last_obj, + py::object tick_volume_obj, + py::object tick_flags_obj + ) -> std::vector> { + std::vector> grouped(n); + if (tick_time_msc_obj.is_none() || tick_bid_obj.is_none() || tick_ask_obj.is_none() || n <= 0) { + return grouped; + } + + auto t_msc = tick_time_msc_obj.cast>().unchecked<1>(); + auto t_bid = tick_bid_obj.cast>().unchecked<1>(); + auto t_ask = tick_ask_obj.cast>().unchecked<1>(); + + bool has_last = !tick_last_obj.is_none(); + bool has_vol = !tick_volume_obj.is_none(); + bool has_flags = !tick_flags_obj.is_none(); + + size_t n_ticks = t_msc.shape(0); + int32_t current_bar = 0; + + for (size_t k = 0; k < n_ticks; ++k) { + int64_t tick_sec = t_msc(k) / 1000LL; + if (tick_sec < bars[0].time) continue; + + while (current_bar + 1 < n && tick_sec >= bars[current_bar + 1].time) { + current_bar++; + } + if (current_bar >= n) break; + + MqlTick tick{}; + tick.time_msc = t_msc(k); + tick.time = tick_sec; + tick.bid = t_bid(k); + tick.ask = t_ask(k); + tick.last = has_last ? tick_last_obj.cast>().unchecked<1>()(k) : t_bid(k); + tick.volume = has_vol ? tick_volume_obj.cast>().unchecked<1>()(k) : 0; + tick.flags = has_flags ? tick_flags_obj.cast>().unchecked<1>()(k) : 0x06; + + grouped[current_bar].push_back(tick); + } + return grouped; + }; + // ---- Tester ---- py::class_(m, "Tester") .def(py::init<>()) .def("run", - [](const Tester& tester, + [group_ticks_by_bars](const Tester& tester, Strategy* strategy, py::array_t open_a, py::array_t high_a, @@ -516,7 +566,13 @@ PYBIND11_MODULE(edge_tester, m) { py::array_t spread_a, const SymbolSpec& sym, py::object cfg_obj, - py::object spread_series_obj) -> TesterResult + py::object spread_series_obj, + py::object tick_time_msc_obj, + py::object tick_bid_obj, + py::object tick_ask_obj, + py::object tick_last_obj, + py::object tick_volume_obj, + py::object tick_flags_obj) -> TesterResult { TesterConfig cfg{}; if (!cfg_obj.is_none()) @@ -534,7 +590,19 @@ PYBIND11_MODULE(edge_tester, m) { for (int32_t i = 0; i < n; ++i) ss_buf[i] = r(i); ss_ptr = ss_buf.data(); } - return tester.run(strategy, bars.data(), n, sym, cfg, ss_ptr); + + std::vector> real_ticks_grouped; + const std::vector>* real_ticks_ptr = nullptr; + if (!tick_time_msc_obj.is_none()) { + real_ticks_grouped = group_ticks_by_bars( + bars.data(), n, + tick_time_msc_obj, tick_bid_obj, tick_ask_obj, + tick_last_obj, tick_volume_obj, tick_flags_obj + ); + real_ticks_ptr = &real_ticks_grouped; + } + + return tester.run(strategy, bars.data(), n, sym, cfg, ss_ptr, real_ticks_ptr); }, py::arg("strategy"), py::arg("open"), py::arg("high"), py::arg("low"), py::arg("close"), @@ -542,6 +610,12 @@ PYBIND11_MODULE(edge_tester, m) { py::arg("symbol"), py::arg("cfg") = py::none(), py::arg("spread_series") = py::none(), + py::arg("tick_time_msc") = py::none(), + py::arg("tick_bid") = py::none(), + py::arg("tick_ask") = py::none(), + py::arg("tick_last") = py::none(), + py::arg("tick_volume") = py::none(), + py::arg("tick_flags") = py::none(), "Run a full backtest simulation for the given Strategy subclass."); // ---- InteractiveTester (Visual / Step-by-Step Replay Engine) ----