feat(market): real historical sparklines via CoinGecko market_chart
Portfolio-group sparklines previously resampled the ~24-minute in-session price
buffer, so the hour/day/week/month intervals could never fill. Fetch real
historical USD series from CoinGecko /market_chart and back each interval with
appropriately-grained data.
- MarketInfo gains two timestamped series: price_chart_intraday (days=1, 5-min)
and price_chart_daily (days=365, daily), plus a fetch timestamp.
- App::refreshMarketChart() fetches both on the RPC worker via the TLS-verifying
util::httpGetString helper, self-throttled to ~30 min (historical data moves
slowly). Gated by getFetchPrices(); triggered on connect and each price tick.
- Pure NetworkRefreshService::parseCoinGeckoMarketChart() parses {"prices":
[[ms,price],...]} into (unix-seconds, price); malformed rows skipped. Unit-tested.
- market_tab pfSparklineSeries() maps interval -> series+bucket: minute = live
buffer; hour = intraday bucketed to 1h; day/week/month = daily bucketed to
1d/7d/30d. Falls back to the in-session buffer until the fetch populates.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1896,6 +1896,26 @@ void testNetworkRefreshResultModels()
|
||||
EXPECT_FALSE(priceHttpParseResult.price.has_value());
|
||||
EXPECT_EQ(priceHttpParseResult.errorMessage, std::string("Price fetch returned an unrecognized response"));
|
||||
|
||||
// CoinGecko market_chart parser: [ms, price] rows -> (seconds, price), oldest->newest.
|
||||
auto chart = Refresh::parseCoinGeckoMarketChart(
|
||||
R"({"prices":[[1700000000000,1.5],[1700003600000,1.6],[1700007200000,1.55]],)"
|
||||
R"("market_caps":[[1700000000000,100]],"total_volumes":[[1700000000000,10]]})");
|
||||
EXPECT_EQ(chart.size(), static_cast<size_t>(3));
|
||||
if (chart.size() == 3) {
|
||||
EXPECT_EQ(chart[0].first, static_cast<std::time_t>(1700000000));
|
||||
EXPECT_NEAR(chart[0].second, 1.5, 0.00000001);
|
||||
EXPECT_EQ(chart[2].first, static_cast<std::time_t>(1700007200));
|
||||
EXPECT_NEAR(chart[2].second, 1.55, 0.00000001);
|
||||
}
|
||||
// Malformed rows are skipped; a good row still comes through.
|
||||
auto chartSkip = Refresh::parseCoinGeckoMarketChart(
|
||||
R"({"prices":[[1700000000000],["x",1.0],[1700003600000,2.0]]})");
|
||||
EXPECT_EQ(chartSkip.size(), static_cast<size_t>(1));
|
||||
if (chartSkip.size() == 1) EXPECT_NEAR(chartSkip[0].second, 2.0, 0.00000001);
|
||||
// Missing/!array "prices" and invalid JSON both yield an empty series.
|
||||
EXPECT_EQ(Refresh::parseCoinGeckoMarketChart(R"({"nope":[]})").size(), static_cast<size_t>(0));
|
||||
EXPECT_EQ(Refresh::parseCoinGeckoMarketChart("not json").size(), static_cast<size_t>(0));
|
||||
|
||||
Refresh::AddressRefreshResult addresses;
|
||||
dragonx::AddressInfo zAddr;
|
||||
zAddr.address = "zs-refresh";
|
||||
|
||||
Reference in New Issue
Block a user