feat(market): candlestick chart for per-exchange OHLC
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>
This commit is contained in:
+28
-11
@@ -15,6 +15,8 @@
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include "candle.h"
|
||||
|
||||
namespace dragonx {
|
||||
namespace data {
|
||||
|
||||
@@ -61,9 +63,9 @@ inline double jnum(const nlohmann::json& v) {
|
||||
}
|
||||
} // namespace detail
|
||||
|
||||
// Parse the venue's candle response into ascending (epoch-seconds, close) points. Empty on failure.
|
||||
inline std::vector<PricePoint> parseExchangeCandles(const std::string& identifier, const std::string& body) {
|
||||
std::vector<PricePoint> out;
|
||||
// Parse the venue's candle response into ascending OHLC candles. Empty on failure.
|
||||
inline std::vector<Candle> parseExchangeOHLC(const std::string& identifier, const std::string& body) {
|
||||
std::vector<Candle> out;
|
||||
if (body.empty()) return out;
|
||||
try {
|
||||
const nlohmann::json j = nlohmann::json::parse(body);
|
||||
@@ -73,9 +75,13 @@ inline std::vector<PricePoint> parseExchangeCandles(const std::string& identifie
|
||||
out.reserve(j.size());
|
||||
for (const auto& k : j) {
|
||||
if (!k.is_array() || k.size() < 5) continue;
|
||||
const std::time_t t = (std::time_t)(detail::jnum(k[0]) / 1000.0);
|
||||
const double close = detail::jnum(k[4]);
|
||||
if (t > 0 && close > 0) out.emplace_back(t, close);
|
||||
Candle c;
|
||||
c.time = (std::time_t)(detail::jnum(k[0]) / 1000.0);
|
||||
c.open = detail::jnum(k[1]);
|
||||
c.high = detail::jnum(k[2]);
|
||||
c.low = detail::jnum(k[3]);
|
||||
c.close = detail::jnum(k[4]);
|
||||
if (c.time > 0 && c.close > 0) out.push_back(c);
|
||||
}
|
||||
} else if (identifier == "nonkyc_io") {
|
||||
// {"bars":[{"time":ms,"open":..,"high":..,"low":..,"close":..,"volume":..}, ...]}
|
||||
@@ -83,16 +89,27 @@ inline std::vector<PricePoint> parseExchangeCandles(const std::string& identifie
|
||||
out.reserve(j["bars"].size());
|
||||
for (const auto& b : j["bars"]) {
|
||||
if (!b.is_object() || !b.contains("time") || !b.contains("close")) continue;
|
||||
const std::time_t t = (std::time_t)(detail::jnum(b["time"]) / 1000.0);
|
||||
const double close = detail::jnum(b["close"]);
|
||||
if (t > 0 && close > 0) out.emplace_back(t, close);
|
||||
Candle c;
|
||||
c.time = (std::time_t)(detail::jnum(b["time"]) / 1000.0);
|
||||
c.close = detail::jnum(b["close"]);
|
||||
c.open = b.contains("open") ? detail::jnum(b["open"]) : c.close;
|
||||
c.high = b.contains("high") ? detail::jnum(b["high"]) : c.close;
|
||||
c.low = b.contains("low") ? detail::jnum(b["low"]) : c.close;
|
||||
if (c.time > 0 && c.close > 0) out.push_back(c);
|
||||
}
|
||||
}
|
||||
} catch (...) {
|
||||
return {};
|
||||
}
|
||||
std::sort(out.begin(), out.end(),
|
||||
[](const PricePoint& a, const PricePoint& b) { return a.first < b.first; });
|
||||
std::sort(out.begin(), out.end(), [](const Candle& a, const Candle& b) { return a.time < b.time; });
|
||||
return out;
|
||||
}
|
||||
|
||||
// Close-only convenience over parseExchangeOHLC (backs the line chart + change-% fallback). bucketOHLC
|
||||
// for candlestick resampling lives in candle.h (dependency-free, reused by market_series.h).
|
||||
inline std::vector<PricePoint> parseExchangeCandles(const std::string& identifier, const std::string& body) {
|
||||
std::vector<PricePoint> out;
|
||||
for (const auto& c : parseExchangeOHLC(identifier, body)) out.emplace_back(c.time, c.close);
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user