#pragma once // Per-exchange OHLC candle adapters. CoinGecko tells us WHICH exchanges list DRGX (via the ticker // `market.identifier`, e.g. "ourbit" / "nonkyc_io") but NOT their API — this is the hand-maintained // mapping from that identifier to each venue's public candle endpoint, so the market chart can show the // SELECTED exchange's real price history instead of CoinGecko's cross-exchange aggregate. // // Header-only + pure (no I/O — the actual HTTP fetch happens in app_network.cpp via util::httpGetString) // so the URL builder + parsers are unit-tested against real captured responses. An unmapped exchange // returns an empty URL, so the caller falls back to the CoinGecko aggregate and nothing regresses. #include #include #include #include #include #include #include "candle.h" namespace dragonx { namespace data { using PricePoint = std::pair; // (epoch SECONDS, close price) enum class CandleRange { Intraday, // ~2 days of 5-minute candles — backs the Live / 1H / 1D views Daily, // ~1 year of daily candles — backs the 1W / 1M views }; // True when we have a candle adapter for this CoinGecko exchange identifier. inline bool hasExchangeCandleAdapter(const std::string& identifier) { return identifier == "ourbit" || identifier == "nonkyc_io"; } // Build the venue's candle URL for a pair + range. Returns "" when the exchange isn't mapped. // `now` is epoch seconds, passed in (no hidden clock reads) so URLs are deterministic in tests. inline std::string buildExchangeCandleUrl(const std::string& identifier, const std::string& base, const std::string& quote, CandleRange range, std::time_t now) { if (identifier == "ourbit") { // Ourbit = MEXC-style /api/v3/klines: symbol=BASEQUOTE, interval=5m|1d, limit. const char* iv = (range == CandleRange::Intraday) ? "5m" : "1d"; const int limit = (range == CandleRange::Intraday) ? 576 : 365; return "https://api.ourbit.com/api/v3/klines?symbol=" + base + quote + "&interval=" + iv + "&limit=" + std::to_string(limit); } if (identifier == "nonkyc_io") { // NonKYC = TradingView-UDF candles: symbol=BASE_QUOTE, resolution in minutes, from/to seconds. const char* res = (range == CandleRange::Intraday) ? "5" : "1440"; const std::time_t span = (range == CandleRange::Intraday) ? (std::time_t)2 * 24 * 3600 : (std::time_t)365 * 24 * 3600; return "https://api.nonkyc.io/api/v2/market/candles?symbol=" + base + "_" + quote + "&resolution=" + res + "&from=" + std::to_string(now - span) + "&to=" + std::to_string(now); } return ""; } namespace detail { // Read a JSON value that may be a number or a numeric string (Ourbit sends prices as strings). inline double jnum(const nlohmann::json& v) { if (v.is_number()) return v.get(); if (v.is_string()) { try { return std::stod(v.get()); } catch (...) { return 0.0; } } return 0.0; } } // namespace detail // Parse the venue's candle response into ascending OHLC candles. Empty on failure. inline std::vector parseExchangeOHLC(const std::string& identifier, const std::string& body) { std::vector out; if (body.empty()) return out; try { const nlohmann::json j = nlohmann::json::parse(body); if (identifier == "ourbit") { // [[openTime_ms, open, high, low, close, volume, closeTime, quoteVol], ...] if (!j.is_array()) return out; out.reserve(j.size()); for (const auto& k : j) { if (!k.is_array() || k.size() < 5) continue; 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":..}, ...]} if (!j.contains("bars") || !j["bars"].is_array()) return out; out.reserve(j["bars"].size()); for (const auto& b : j["bars"]) { if (!b.is_object() || !b.contains("time") || !b.contains("close")) continue; 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 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 parseExchangeCandles(const std::string& identifier, const std::string& body) { std::vector out; for (const auto& c : parseExchangeOHLC(identifier, body)) out.emplace_back(c.time, c.close); return out; } } // namespace data } // namespace dragonx