refactor(market): extract pure price-series math to data/market_series.h + tests
Move the no-I/O, no-ImGui series helpers (resampleHistory, bucketBySeconds, sparklineSeries, chartSeries) out of market_tab.cpp into an inline header under data/ (mirroring data/portfolio.h) and cover them with a testMarketSeries() group in test_phase4.cpp (resample block averaging + partial blocks, window bucketing + guards, sparkline fallback, chart time-window filtering + live timestamp synthesis). market_tab now calls data::sparklineSeries/chartSeries. No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -40,6 +40,7 @@
|
||||
#include "config/settings.h"
|
||||
#include "data/wallet_state.h"
|
||||
#include "data/portfolio.h"
|
||||
#include "data/market_series.h"
|
||||
#include "fake_lite_backend.h"
|
||||
|
||||
#include <chrono>
|
||||
@@ -2735,6 +2736,74 @@ void testPortfolioHelpers()
|
||||
EXPECT_EQ(group[0], std::string("R-t2"));
|
||||
}
|
||||
|
||||
// Market price-series math (data/market_series.h) — minute-resampling + time-window bucketing
|
||||
// that feed the price chart and the portfolio-group sparklines.
|
||||
void testMarketSeries()
|
||||
{
|
||||
using dragonx::MarketInfo;
|
||||
using dragonx::data::resampleHistory;
|
||||
using dragonx::data::bucketBySeconds;
|
||||
using dragonx::data::sparklineSeries;
|
||||
using dragonx::data::chartSeries;
|
||||
|
||||
// resampleHistory: interval 0 (minute) returns the input untouched.
|
||||
std::vector<double> minute = {1.0, 2.0, 3.0, 4.0};
|
||||
EXPECT_EQ(resampleHistory(minute, 0).size(), static_cast<size_t>(4));
|
||||
EXPECT_NEAR(resampleHistory(minute, 0)[3], 4.0, 1e-9);
|
||||
// interval 1 (hour = 60 minute-samples): 120 samples -> 2 averaged points.
|
||||
std::vector<double> longHist(120, 0.0);
|
||||
for (int i = 0; i < 120; i++) longHist[i] = (i < 60) ? 10.0 : 20.0;
|
||||
auto hourly = resampleHistory(longHist, 1);
|
||||
EXPECT_EQ(hourly.size(), static_cast<size_t>(2));
|
||||
EXPECT_NEAR(hourly[0], 10.0, 1e-9);
|
||||
EXPECT_NEAR(hourly[1], 20.0, 1e-9);
|
||||
// A trailing partial block still averages (61 samples -> 2 points).
|
||||
EXPECT_EQ(resampleHistory(std::vector<double>(61, 5.0), 1).size(), static_cast<size_t>(2));
|
||||
// Empty input -> empty output.
|
||||
EXPECT_TRUE(resampleHistory({}, 2).empty());
|
||||
|
||||
// bucketBySeconds: guards + averaging within fixed windows.
|
||||
EXPECT_TRUE(bucketBySeconds({}, 3600).empty());
|
||||
EXPECT_TRUE(bucketBySeconds({{0, 1.0}}, 0).empty());
|
||||
std::vector<std::pair<std::time_t, double>> s = {
|
||||
{0, 10.0}, {1000, 20.0}, // bucket 0 (< 3600s) -> avg 15
|
||||
{3600, 40.0}, {7000, 60.0}, // bucket 1 -> avg 50
|
||||
};
|
||||
auto b = bucketBySeconds(s, 3600);
|
||||
EXPECT_EQ(b.size(), static_cast<size_t>(2));
|
||||
EXPECT_NEAR(b[0], 15.0, 1e-9);
|
||||
EXPECT_NEAR(b[1], 50.0, 1e-9);
|
||||
|
||||
// sparklineSeries: hour interval buckets the intraday series...
|
||||
MarketInfo m;
|
||||
m.price_history = {1.0, 2.0, 3.0};
|
||||
m.price_chart_intraday = { {0, 10.0}, {100, 12.0}, {3600, 20.0}, {3700, 22.0} }; // 2 hourly buckets
|
||||
EXPECT_EQ(sparklineSeries(m, 1).size(), static_cast<size_t>(2));
|
||||
// ...interval 0 (minute) always uses the live buffer...
|
||||
EXPECT_EQ(sparklineSeries(m, 0).size(), static_cast<size_t>(3));
|
||||
// ...and with no historical series, an interval falls back to the resampled live buffer.
|
||||
MarketInfo bare; bare.price_history = std::vector<double>(120, 7.0);
|
||||
EXPECT_EQ(sparklineSeries(bare, 1).size(), static_cast<size_t>(2));
|
||||
|
||||
// chartSeries: the 1H window keeps only samples within the last hour of `now`.
|
||||
std::time_t now = 100000;
|
||||
MarketInfo cm;
|
||||
cm.price_chart_intraday = {
|
||||
{now - 7200, 1.0}, // 2h ago -> excluded from 1H
|
||||
{now - 1800, 2.0}, // 30m ago -> included
|
||||
{now - 60, 3.0}, // 1m ago -> included
|
||||
};
|
||||
auto oneH = chartSeries(cm, 1, now);
|
||||
EXPECT_EQ(oneH.size(), static_cast<size_t>(2));
|
||||
EXPECT_NEAR(oneH[0].second, 2.0, 1e-9);
|
||||
// Live (interval 0) synthesizes timestamps from the minute buffer, newest at `now`.
|
||||
MarketInfo lm; lm.price_history = {1.0, 2.0, 3.0};
|
||||
auto live = chartSeries(lm, 0, now);
|
||||
EXPECT_EQ(live.size(), static_cast<size_t>(3));
|
||||
EXPECT_EQ(static_cast<long>(live.back().first), static_cast<long>(now));
|
||||
EXPECT_NEAR(live.back().second, 3.0, 1e-9);
|
||||
}
|
||||
|
||||
// Regression: the Market-tab portfolio (and other consumers) read the combined
|
||||
// WalletState::addresses view, which the full-node refresh path builds from the
|
||||
// authoritative z/t lists via rebuildAddressList(). Before the fix that rebuild
|
||||
@@ -5507,6 +5576,7 @@ int main()
|
||||
testConsoleTextLayout();
|
||||
testConsoleModel();
|
||||
testPortfolioHelpers();
|
||||
testMarketSeries();
|
||||
testWalletStateAddressListRebuild();
|
||||
testConsoleFoldSpans();
|
||||
testConsoleScrollController();
|
||||
|
||||
Reference in New Issue
Block a user