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:
@@ -129,6 +129,41 @@ static std::vector<double> pfResampleHistory(const std::vector<double>& hist, in
|
||||
return out;
|
||||
}
|
||||
|
||||
// Bucket a timestamped (unix-seconds, price) series into fixed-width time windows, averaging the
|
||||
// samples in each window into one point. Input is oldest->newest; output preserves that order.
|
||||
static std::vector<double> pfBucketBySeconds(
|
||||
const std::vector<std::pair<std::time_t, double>>& series, long windowSec)
|
||||
{
|
||||
std::vector<double> out;
|
||||
if (series.empty() || windowSec <= 0) return out;
|
||||
long curBucket = 0; double sum = 0.0; int cnt = 0;
|
||||
for (const auto& sample : series) {
|
||||
long b = (long)(sample.first / windowSec);
|
||||
if (cnt > 0 && b != curBucket) { out.push_back(sum / cnt); sum = 0.0; cnt = 0; }
|
||||
curBucket = b;
|
||||
sum += sample.second; ++cnt;
|
||||
}
|
||||
if (cnt > 0) out.push_back(sum / cnt);
|
||||
return out;
|
||||
}
|
||||
|
||||
// Resolve the price series backing a group's sparkline for the chosen interval:
|
||||
// 0=minute -> the live in-session buffer; 1=hour -> intraday (5-min) data bucketed to hours;
|
||||
// 2=day / 3=week / 4=month -> the ~1yr daily series bucketed accordingly.
|
||||
// Falls back to the in-session buffer when the historical fetch hasn't populated yet.
|
||||
static std::vector<double> pfSparklineSeries(const MarketInfo& m, int interval)
|
||||
{
|
||||
const long kDay = 86400;
|
||||
switch (interval) {
|
||||
case 1: { auto v = pfBucketBySeconds(m.price_chart_intraday, 3600); if (v.size() >= 2) return v; break; }
|
||||
case 2: { auto v = pfBucketBySeconds(m.price_chart_daily, kDay); if (v.size() >= 2) return v; break; }
|
||||
case 3: { auto v = pfBucketBySeconds(m.price_chart_daily, 7 * kDay); if (v.size() >= 2) return v; break; }
|
||||
case 4: { auto v = pfBucketBySeconds(m.price_chart_daily, 30 * kDay); if (v.size() >= 2) return v; break; }
|
||||
default: break; // minute interval, or no historical data yet -> live buffer below
|
||||
}
|
||||
return pfResampleHistory(m.price_history, interval);
|
||||
}
|
||||
|
||||
// A group's value trend tracks the DRGX price, so a group sparkline plots the price history
|
||||
// (normalized) across a rect. Colored by overall direction. Only meaningful for market bases.
|
||||
static void pfDrawSparkline(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
|
||||
@@ -354,7 +389,7 @@ static void RenderPortfolioEditor(App* app)
|
||||
ImVec2(bx + 3.0f, pMax.y - ppad * 0.6f), accent, 2.0f);
|
||||
}
|
||||
if (s_pf_show_sparkline && (s_pf_price_basis == 0 || s_pf_price_basis == 1)) {
|
||||
std::vector<double> h = pfResampleHistory(state.market.price_history, s_pf_spark_interval);
|
||||
std::vector<double> h = pfSparklineSeries(state.market, s_pf_spark_interval);
|
||||
if (h.size() >= 2) {
|
||||
ImU32 spCol = WithAlpha(h.back() >= h.front() ? Success() : Error(), 80);
|
||||
ImVec2 spMin(pMin.x + ppad + (accent ? 6.0f : 0.0f), pMin.y + ph * 0.46f);
|
||||
@@ -1481,7 +1516,7 @@ void RenderMarketTab(App* app)
|
||||
// Sparkline strip along the bottom.
|
||||
float sparkH = 0.0f;
|
||||
if (e.showSparkline && (e.priceBasis == 0 || e.priceBasis == 1)) {
|
||||
std::vector<double> hh = pfResampleHistory(market.price_history, e.sparklineInterval);
|
||||
std::vector<double> hh = pfSparklineSeries(market, e.sparklineInterval);
|
||||
if (hh.size() >= 2) {
|
||||
sparkH = std::max(18.0f * mktDp, (gcMax.y - gcMin.y) * 0.24f);
|
||||
pfDrawSparklineFilled(dl, ImVec2(left, gcMax.y - pad - sparkH), ImVec2(right, gcMax.y - pad),
|
||||
|
||||
Reference in New Issue
Block a user