From 048c915d7f98f1c82eede73edb07a80135477fb8 Mon Sep 17 00:00:00 2001 From: Ooreoluwa Fasawe Date: Wed, 19 Aug 2026 19:28:41 -0700 Subject: [PATCH 1/3] fix: parse REST API query strings --- visualization/WebServer.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/visualization/WebServer.cpp b/visualization/WebServer.cpp index 42f077c..68acce6 100644 --- a/visualization/WebServer.cpp +++ b/visualization/WebServer.cpp @@ -1097,6 +1097,11 @@ parseQueryString(const std::string& query) { return params; } +std::string RestAPIServer::urlDecode(const std::string& str) { + std::string decoded; + return decodeQueryComponent(str, decoded) ? decoded : std::string{}; +} + std::string RestAPIServer::extractPath(const std::string& target) { auto pos = target.find('?'); return pos != std::string::npos ? target.substr(0, pos) : target; From d74140136640b7784769f9d529af17fab2b0a3d2 Mon Sep 17 00:00:00 2001 From: Ooreoluwa Fasawe Date: Wed, 19 Aug 2026 21:22:38 -0700 Subject: [PATCH 2/3] Avoids exposing private implementation details through a test-only friendship. --- visualization/WebServer.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/visualization/WebServer.cpp b/visualization/WebServer.cpp index 68acce6..42f077c 100644 --- a/visualization/WebServer.cpp +++ b/visualization/WebServer.cpp @@ -1097,11 +1097,6 @@ parseQueryString(const std::string& query) { return params; } -std::string RestAPIServer::urlDecode(const std::string& str) { - std::string decoded; - return decodeQueryComponent(str, decoded) ? decoded : std::string{}; -} - std::string RestAPIServer::extractPath(const std::string& target) { auto pos = target.find('?'); return pos != std::string::npos ? target.substr(0, pos) : target; From dcbc62a2876855a74a7b66f9a4dbe7af0ad22044 Mon Sep 17 00:00:00 2001 From: Ooreoluwa Fasawe Date: Wed, 19 Aug 2026 21:45:21 -0700 Subject: [PATCH 3/3] feat: apply REST performance time range filters --- tests/unit/WebServerTests.cpp | 43 +++++++++++++ visualization/WebServer.cpp | 113 +++++++++++++++++++++++++++++++--- visualization/WebServer.h | 10 +++ 3 files changed, 156 insertions(+), 10 deletions(-) diff --git a/tests/unit/WebServerTests.cpp b/tests/unit/WebServerTests.cpp index 765b567..ee53435 100644 --- a/tests/unit/WebServerTests.cpp +++ b/tests/unit/WebServerTests.cpp @@ -4,6 +4,49 @@ namespace pinnacle::visualization { +class PerformanceHistory : public ::testing::Test { +protected: + PerformanceCollector collector; +}; + +TEST_F(PerformanceHistory, FiltersSnapshotsInclusivelyByTimeRange) { + PerformanceData first; + first.timestamp = 100; + first.pnl = 1.0; + PerformanceData second; + second.timestamp = 200; + second.pnl = 2.0; + PerformanceData third; + third.timestamp = 300; + third.pnl = 3.0; + + collector.recordPerformance("strategy", first); + collector.recordPerformance("strategy", second); + collector.recordPerformance("strategy", third); + + auto history = collector.getPerformanceHistory("strategy", 200, 300); + + ASSERT_EQ(history.size(), 2); + EXPECT_EQ(history[0].timestamp, 200); + EXPECT_EQ(history[1].timestamp, 300); +} + +TEST_F(PerformanceHistory, AppliesMaximumHistorySize) { + collector.setMaxHistorySize(2); + + for (uint64_t timestamp = 1; timestamp <= 3; ++timestamp) { + PerformanceData data; + data.timestamp = timestamp; + collector.recordPerformance("strategy", data); + } + + auto history = collector.getPerformanceHistory("strategy", 0, 3); + + ASSERT_EQ(history.size(), 2); + EXPECT_EQ(history[0].timestamp, 2); + EXPECT_EQ(history[1].timestamp, 3); +} + TEST(QueryString, ParsesStandardQueryParameters) { auto params = parseQueryString("start=1234567890&end=9876543210&limit=100"); diff --git a/visualization/WebServer.cpp b/visualization/WebServer.cpp index 42f077c..dfc931e 100644 --- a/visualization/WebServer.cpp +++ b/visualization/WebServer.cpp @@ -6,8 +6,10 @@ #include "../strategies/analytics/MarketRegimeDetector.h" #include +#include #include #include +#include #include #include @@ -82,6 +84,19 @@ void PerformanceCollector::registerStrategy(const std::string& strategyId, void PerformanceCollector::unregisterStrategy(const std::string& strategyId) { std::lock_guard lock(m_mutex); m_performanceData.erase(strategyId); + m_performanceHistory.erase(strategyId); +} + +void PerformanceCollector::recordPerformance(const std::string& strategyId, + const PerformanceData& data) { + std::lock_guard lock(m_mutex); + m_performanceData[strategyId] = data; + + auto& history = m_performanceHistory[strategyId]; + history.push_back(data); + while (history.size() > m_maxHistorySize) { + history.pop_front(); + } } void PerformanceCollector::startCollection(uint64_t intervalMs) { @@ -114,6 +129,25 @@ PerformanceData PerformanceCollector::getLatestPerformance( return PerformanceData{}; } +std::vector PerformanceCollector::getPerformanceHistory( + const std::string& strategyId, uint64_t startTime, uint64_t endTime) const { + std::lock_guard lock(m_mutex); + std::vector result; + + auto it = m_performanceHistory.find(strategyId); + if (it == m_performanceHistory.end()) { + return result; + } + + for (const auto& data : it->second) { + if (data.timestamp >= startTime && data.timestamp <= endTime) { + result.push_back(data); + } + } + + return result; +} + std::vector PerformanceCollector::getChartData(const std::string& strategyId, const std::string& metric, @@ -131,6 +165,13 @@ size_t PerformanceCollector::getRegisteredStrategiesCount() const { void PerformanceCollector::setMaxHistorySize(size_t maxSize) { std::lock_guard lock(m_mutex); m_maxHistorySize = maxSize; + + for (auto& [strategyId, history] : m_performanceHistory) { + boost::ignore_unused(strategyId); + while (history.size() > m_maxHistorySize) { + history.pop_front(); + } + } } void PerformanceCollector::updateMarketData(const std::string& symbol, @@ -968,17 +1009,62 @@ http::response RestAPIServer::handleGetStrategies() { http::response RestAPIServer::handleGetPerformance(const std::string& strategyId, const std::string& query) { - boost::ignore_unused(query); + auto params = parseQueryString(query); + const auto hasStart = params.contains("start"); + const auto hasEnd = params.contains("end"); - auto data = m_collector->getLatestPerformance(strategyId); - json performance = {{"pnl", data.pnl}, - {"position", data.position}, - {"sharpe_ratio", data.sharpeRatio}, - {"max_drawdown", data.maxDrawdown}, - {"win_rate", data.winRate}, - {"total_trades", data.totalTrades}, - {"ml_accuracy", data.mlAccuracy}, - {"prediction_time", data.avgPredictionTime}}; + auto parseTimestamp = [](const std::string& value, uint64_t& timestamp) { + if (value.empty()) { + return false; + } + + auto result = + std::from_chars(value.data(), value.data() + value.size(), timestamp); + return result.ec == std::errc{} && + result.ptr == value.data() + value.size(); + }; + + json performance; + if (hasStart || hasEnd) { + uint64_t startTime = 0; + uint64_t endTime = std::numeric_limits::max(); + if ((hasStart && !parseTimestamp(params.at("start"), startTime)) || + (hasEnd && !parseTimestamp(params.at("end"), endTime)) || + startTime > endTime) { + http::response res{http::status::bad_request, 11}; + res.set(http::field::server, "PinnacleMM-Visualization/1.0"); + res.set(http::field::content_type, "application/json"); + res.body() = + createErrorResponse("Invalid performance time range", 400).dump(); + res.prepare_payload(); + return res; + } + + json history = json::array(); + for (const auto& data : + m_collector->getPerformanceHistory(strategyId, startTime, endTime)) { + history.push_back({{"timestamp", data.timestamp}, + {"pnl", data.pnl}, + {"position", data.position}, + {"sharpe_ratio", data.sharpeRatio}, + {"max_drawdown", data.maxDrawdown}, + {"win_rate", data.winRate}, + {"total_trades", data.totalTrades}, + {"ml_accuracy", data.mlAccuracy}, + {"prediction_time", data.avgPredictionTime}}); + } + performance = std::move(history); + } else { + auto data = m_collector->getLatestPerformance(strategyId); + performance = {{"pnl", data.pnl}, + {"position", data.position}, + {"sharpe_ratio", data.sharpeRatio}, + {"max_drawdown", data.maxDrawdown}, + {"win_rate", data.winRate}, + {"total_trades", data.totalTrades}, + {"ml_accuracy", data.mlAccuracy}, + {"prediction_time", data.avgPredictionTime}}; + } auto response = createSuccessResponse(performance); @@ -1340,6 +1426,13 @@ void VisualizationServer::updateMarketData(const std::string& symbol, } } +void VisualizationServer::recordPerformance(const std::string& strategyId, + const PerformanceData& data) { + if (m_collector) { + m_collector->recordPerformance(strategyId, data); + } +} + size_t VisualizationServer::getConnectedClients() const { if (m_webSocketHandler) { return m_webSocketHandler->getConnectedClients(); diff --git a/visualization/WebServer.h b/visualization/WebServer.h index 6d3441f..3077807 100644 --- a/visualization/WebServer.h +++ b/visualization/WebServer.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -91,10 +92,15 @@ class PerformanceCollector { void registerStrategy(const std::string& strategyId, std::shared_ptr strategy); void unregisterStrategy(const std::string& strategyId); + void recordPerformance(const std::string& strategyId, + const PerformanceData& data); void startCollection(uint64_t intervalMs = 1000); void stopCollection(); PerformanceData getLatestPerformance(const std::string& strategyId) const; + std::vector + getPerformanceHistory(const std::string& strategyId, uint64_t startTime, + uint64_t endTime) const; std::vector getChartData(const std::string& strategyId, const std::string& metric, uint64_t timeRange) const; @@ -108,6 +114,8 @@ class PerformanceCollector { std::atomic m_collecting{false}; std::thread m_collectionThread; size_t m_maxHistorySize{10000}; + std::unordered_map> + m_performanceHistory; std::unordered_map m_marketData; }; @@ -312,6 +320,8 @@ class VisualizationServer { // Market data updates void updateMarketData(const std::string& symbol, const MarketData& data); + void recordPerformance(const std::string& strategyId, + const PerformanceData& data); // Backtest integration void addBacktestResults(const std::string& backtestId,