Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions tests/unit/WebServerTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down
113 changes: 103 additions & 10 deletions visualization/WebServer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
#include "../strategies/analytics/MarketRegimeDetector.h"

#include <algorithm>
#include <charconv>
#include <chrono>
#include <fstream>
#include <limits>
#include <spdlog/spdlog.h>
#include <sstream>

Expand Down Expand Up @@ -82,6 +84,19 @@ void PerformanceCollector::registerStrategy(const std::string& strategyId,
void PerformanceCollector::unregisterStrategy(const std::string& strategyId) {
std::lock_guard<std::mutex> lock(m_mutex);
m_performanceData.erase(strategyId);
m_performanceHistory.erase(strategyId);
}

void PerformanceCollector::recordPerformance(const std::string& strategyId,
const PerformanceData& data) {
std::lock_guard<std::mutex> 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) {
Expand Down Expand Up @@ -114,6 +129,25 @@ PerformanceData PerformanceCollector::getLatestPerformance(
return PerformanceData{};
}

std::vector<PerformanceData> PerformanceCollector::getPerformanceHistory(
const std::string& strategyId, uint64_t startTime, uint64_t endTime) const {
std::lock_guard<std::mutex> lock(m_mutex);
std::vector<PerformanceData> 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<ChartDataPoint>
PerformanceCollector::getChartData(const std::string& strategyId,
const std::string& metric,
Expand All @@ -131,6 +165,13 @@ size_t PerformanceCollector::getRegisteredStrategiesCount() const {
void PerformanceCollector::setMaxHistorySize(size_t maxSize) {
std::lock_guard<std::mutex> 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,
Expand Down Expand Up @@ -968,17 +1009,62 @@ http::response<http::string_body> RestAPIServer::handleGetStrategies() {
http::response<http::string_body>
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<uint64_t>::max();
if ((hasStart && !parseTimestamp(params.at("start"), startTime)) ||
(hasEnd && !parseTimestamp(params.at("end"), endTime)) ||
startTime > endTime) {
http::response<http::string_body> 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);

Expand Down Expand Up @@ -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();
Expand Down
10 changes: 10 additions & 0 deletions visualization/WebServer.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/websocket.hpp>
#include <deque>
#include <memory>
#include <mutex>
#include <nlohmann/json.hpp>
Expand Down Expand Up @@ -91,10 +92,15 @@ class PerformanceCollector {
void registerStrategy(const std::string& strategyId,
std::shared_ptr<void> 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<PerformanceData>
getPerformanceHistory(const std::string& strategyId, uint64_t startTime,
uint64_t endTime) const;
std::vector<ChartDataPoint> getChartData(const std::string& strategyId,
const std::string& metric,
uint64_t timeRange) const;
Expand All @@ -108,6 +114,8 @@ class PerformanceCollector {
std::atomic<bool> m_collecting{false};
std::thread m_collectionThread;
size_t m_maxHistorySize{10000};
std::unordered_map<std::string, std::deque<PerformanceData>>
m_performanceHistory;
std::unordered_map<std::string, MarketData> m_marketData;
};

Expand Down Expand Up @@ -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,
Expand Down
Loading