// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #pragma once #include #include #include #include "wallet_state.h" // MarketInfo // Pure (no-I/O, no-ImGui) price-series math backing the market chart and the portfolio-group // sparklines. Kept header-only + inline so it can be unit-tested without the GUI (see // tests/test_phase4.cpp), mirroring data/portfolio.h. namespace dragonx { namespace data { // Resample a ~1-sample/minute price history to the given interval by averaging each block of K // minute-samples into one point. interval: 0=min 1=hour 2=day 3=week 4=month. inline std::vector resampleHistory(const std::vector& hist, int interval) { static const int kMins[5] = {1, 60, 1440, 10080, 43200}; int k = kMins[(interval >= 0 && interval < 5) ? interval : 0]; if (k <= 1) return hist; std::vector out; for (size_t i = 0; i < hist.size(); i += (size_t)k) { double sum = 0.0; size_t cnt = 0; for (size_t j = i; j < hist.size() && j < i + (size_t)k; ++j) { sum += hist[j]; ++cnt; } if (cnt) out.push_back(sum / (double)cnt); } 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. inline std::vector bucketBySeconds( const std::vector>& series, long windowSec) { std::vector 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. inline std::vector sparklineSeries(const MarketInfo& m, int interval) { const long kDay = 86400; switch (interval) { case 1: { auto v = bucketBySeconds(m.price_chart_intraday, 3600); if (v.size() >= 2) return v; break; } case 2: { auto v = bucketBySeconds(m.price_chart_daily, kDay); if (v.size() >= 2) return v; break; } case 3: { auto v = bucketBySeconds(m.price_chart_daily, 7 * kDay); if (v.size() >= 2) return v; break; } case 4: { auto v = bucketBySeconds(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 resampleHistory(m.price_history, interval); } // Timestamped price series backing the main chart for the selected RANGE (Live/1H/1D/1W/1M), // ending at `now`. The interval buttons select a time window ending at `now`: 1H = last hour, // 1D = last 24h, 1W = last 7 days, 1M = last 30 days. 1H/1D come from the 5-minute intraday // series; 1W/1M from the daily series. Live (or an un-fetched/empty range) uses the minute buffer. inline std::vector> chartSeries(const MarketInfo& m, int interval, std::time_t now) { const long kDay = 86400; auto lastWindow = [now](const std::vector>& src, long rangeSec) { std::vector> out; std::time_t cutoff = now - (std::time_t)rangeSec; for (const auto& s : src) if (s.first >= cutoff) out.push_back(s); return out; }; // Draw the SELECTED exchange's own candles when active (data/exchange_candles.h), else the CoinGecko // cross-exchange aggregate. Only the main chart switches source; portfolio sparklines stay aggregate. const auto& intraday = m.exchange_chart_active ? m.exchange_chart_intraday : m.price_chart_intraday; const auto& daily = m.exchange_chart_active ? m.exchange_chart_daily : m.price_chart_daily; switch (interval) { case 1: { auto v = lastWindow(intraday, 3600); if (v.size() >= 2) return v; break; } // 1H case 2: { auto v = lastWindow(intraday, kDay); if (v.size() >= 2) return v; break; } // 1D case 3: { auto v = lastWindow(daily, 7 * kDay); if (v.size() >= 2) return v; break; } // 1W case 4: { auto v = lastWindow(daily, 30 * kDay); if (v.size() >= 2) return v; break; } // 1M default: break; } std::vector> out; const auto& h = m.price_history; for (size_t i = 0; i < h.size(); i++) out.push_back({ now - (std::time_t)((h.size() - 1 - i) * 60), h[i] }); return out; } // OHLC candles for the main chart at the selected RANGE — only when the per-exchange series is active // (the CoinGecko aggregate is close-only, so this returns empty and the chart draws a line). The 1D // view buckets the 5-minute intraday to hourly so it isn't ~288 hair-thin candles; other ranges use // the raw candles. Empty for the Live range (uses the in-session line). inline std::vector chartCandles(const MarketInfo& m, int interval, std::time_t now) { if (!m.exchange_chart_active) return {}; const long kDay = 86400; auto window = [now](const std::vector& src, long rangeSec) { std::vector out; const std::time_t cutoff = now - (std::time_t)rangeSec; for (const auto& c : src) if (c.time >= cutoff) out.push_back(c); return out; }; switch (interval) { case 1: return window(m.exchange_ohlc_intraday, 3600); // 1H: 5-min candles (~12) case 2: return bucketOHLC(window(m.exchange_ohlc_intraday, kDay), 3600); // 1D: hourly candles (~24) case 3: return window(m.exchange_ohlc_daily, 7 * kDay); // 1W: daily (~7) case 4: return window(m.exchange_ohlc_daily, 30 * kDay); // 1M: daily (~30) default: return {}; // Live -> line } } } // namespace data } // namespace dragonx