The exchange candle APIs return full OHLC but we only kept the close (a line). Now the per-exchange chart draws real candlesticks; the CoinGecko aggregate stays a line (it's close-only). - Adapter keeps OHLC: parseExchangeOHLC() returns open/high/low/close candles (parseExchangeCandles is now a close-only wrapper over it). New data/candle.h holds the dependency-free Candle + bucketOHLC (5-min -> hourly for the 1D view). - Model stores exchange_ohlc_intraday/daily alongside the close series; refreshExchangeChart populates both. market_series::chartCandles() returns the bucketed OHLC for the range, empty unless the per-exchange series is active. - The chart renders wick (low..high) + body (open..close), green up / red down, with a low..high y-range; the line-only bits (fill, hi/lo labels, hover tooltip) are gated off for candles. Falls back to the line for the aggregate / Live view. Verified: OHLC parsers + bucketOHLC + chartCandles unit-tested; a forced-state render shows correct candlesticks; the aggregate still draws a clean line. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
45 lines
1.5 KiB
C++
45 lines
1.5 KiB
C++
#pragma once
|
|
// A single OHLC candle — the shared shape for per-exchange candlestick charts. Kept in its own tiny,
|
|
// dependency-free header so both the parser (data/exchange_candles.h, which pulls in nlohmann/json)
|
|
// and the model (data/wallet_state.h, included widely) can use it without dragging json everywhere.
|
|
#include <ctime>
|
|
#include <vector>
|
|
|
|
namespace dragonx {
|
|
namespace data {
|
|
|
|
struct Candle {
|
|
std::time_t time = 0; // epoch SECONDS (the candle's open/bucket time)
|
|
double open = 0.0;
|
|
double high = 0.0;
|
|
double low = 0.0;
|
|
double close = 0.0;
|
|
};
|
|
|
|
// Aggregate fine candles into fixed time buckets (e.g. 5-min -> hourly for the 1D view): open = first
|
|
// open, high = max high, low = min low, close = last close. Input must be ascending by time.
|
|
inline std::vector<Candle> bucketOHLC(const std::vector<Candle>& src, long windowSec) {
|
|
std::vector<Candle> out;
|
|
if (src.empty() || windowSec <= 0) return out;
|
|
long curBucket = -1;
|
|
Candle cur;
|
|
for (const auto& c : src) {
|
|
const long b = (long)(c.time / windowSec);
|
|
if (b != curBucket) {
|
|
if (curBucket >= 0) out.push_back(cur);
|
|
curBucket = b;
|
|
cur = c;
|
|
cur.time = (std::time_t)(b * windowSec);
|
|
} else {
|
|
if (c.high > cur.high) cur.high = c.high;
|
|
if (c.low < cur.low) cur.low = c.low;
|
|
cur.close = c.close;
|
|
}
|
|
}
|
|
if (curBucket >= 0) out.push_back(cur);
|
|
return out;
|
|
}
|
|
|
|
} // namespace data
|
|
} // namespace dragonx
|