Skip to content

Commit

Permalink
Upbit Public - migrate to glaze json
Browse files Browse the repository at this point in the history
  • Loading branch information
sjanel committed Dec 21, 2024
1 parent 1408e16 commit 21b44e9
Show file tree
Hide file tree
Showing 2 changed files with 160 additions and 61 deletions.
95 changes: 94 additions & 1 deletion src/api/exchanges/src/upbit-schema.hpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,96 @@
#pragma once

namespace cct::schema::upbit {}
#include <cstdint>
#include <glaze/glaze.hpp>

#include "cct_string.hpp"
#include "cct_vector.hpp"

namespace cct::schema::upbit {

template <class T>
using has_error_t = decltype(std::declval<T>().error);

template <class T>
using has_name_t = decltype(std::declval<T>().name);

template <class T>
using has_message_t = decltype(std::declval<T>().message);

// PUBLIC

// https://docs.upbit.com/reference/ticker%ED%98%84%EC%9E%AC%EA%B0%80-%EC%A0%95%EB%B3%B4

struct V1Ticker {
int64_t timestamp;
};

using V1Tickers = vector<V1Ticker>;

// https://docs.upbit.com/reference/%EB%A7%88%EC%BC%93-%EC%BD%94%EB%93%9C-%EC%A1%B0%ED%9A%8C

struct V1Market {
using trivially_relocatable = is_trivially_relocatable<string>::type;

auto operator<=>(const V1Market&) const = default;

string market;
string market_warning;
};

using V1MarketAll = vector<V1Market>;

// https://docs.upbit.com/reference/%ED%98%B8%EA%B0%80-%EC%A0%95%EB%B3%B4-%EC%A1%B0%ED%9A%8C

struct V1OrderBook {
using trivially_relocatable = is_trivially_relocatable<string>::type;

auto operator<=>(const V1OrderBook&) const = default;

struct Unit {
auto operator<=>(const Unit&) const = default;

double ask_price;
double bid_price;
double ask_size;
double bid_size;
};

string market;
vector<Unit> orderbook_units;
};

using V1Orderbooks = vector<V1OrderBook>;

// https://docs.upbit.com/reference/%EC%9D%BCday-%EC%BA%94%EB%93%A4-1

struct V1CandleDay {
auto operator<=>(const V1CandleDay&) const = default;

double candle_acc_trade_volume;
};

using V1CandlesDay = vector<V1CandleDay>;

// https://docs.upbit.com/reference/%EC%B5%9C%EA%B7%BC-%EC%B2%B4%EA%B2%B0-%EB%82%B4%EC%97%AD

struct V1TradesTick {
auto operator<=>(const V1TradesTick&) const = default;

enum class AskBid : int8_t { ASK, BID };

double trade_volume;
double trade_price;
int64_t timestamp;
AskBid ask_bid;
};

using V1TradesTicks = vector<V1TradesTick>;

} // namespace cct::schema::upbit

template <>
struct glz::meta<::cct::schema::upbit::V1TradesTick::AskBid> {
using enum ::cct::schema::upbit::V1TradesTick::AskBid;
static constexpr auto value = enumerate(ASK, BID);
};
126 changes: 66 additions & 60 deletions src/api/exchanges/src/upbitpublicapi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
#include "apiquerytypeenum.hpp"
#include "cachedresult.hpp"
#include "cct_const.hpp"
#include "cct_json-container.hpp"
#include "cct_log.hpp"
#include "cct_string.hpp"
#include "coincenterinfo.hpp"
Expand All @@ -39,23 +38,28 @@
#include "request-retry.hpp"
#include "timedef.hpp"
#include "tradeside.hpp"
#include "upbit-schema.hpp"
#include "withdraw-fees-file-schema.hpp"

namespace cct::api {
namespace {

json::container PublicQuery(CurlHandle& curlHandle, std::string_view endpoint,
CurlPostData&& postData = CurlPostData()) {
template <class T>
T PublicQuery(CurlHandle& curlHandle, std::string_view endpoint, CurlPostData&& postData = CurlPostData()) {
RequestRetry requestRetry(curlHandle, CurlOptions(HttpRequestType::kGet, std::move(postData)));

return requestRetry.queryJson(endpoint, [](const json::container& jsonResponse) {
const auto foundErrorIt = jsonResponse.find("error");
if (foundErrorIt != jsonResponse.end()) {
const auto statusCodeIt = jsonResponse.find("name");
const long statusCode = statusCodeIt == jsonResponse.end() ? -1 : statusCodeIt->get<long>();
const auto msgIt = jsonResponse.find("message");
const std::string_view msg = msgIt == jsonResponse.end() ? "Unknown" : msgIt->get<std::string_view>();
log::warn("Upbit error ({}, '{}'), full: '{}'", statusCode, msg, jsonResponse.dump());
return requestRetry.query<T>(endpoint, [](const T& response) {
if constexpr (amc::is_detected<schema::upbit::has_error_t, T>::value) {
long statusCode = -1;
if constexpr (amc::is_detected<schema::upbit::has_name_t, T>::value) {
statusCode = response.name;
}
std::string_view msg;
if constexpr (amc::is_detected<schema::upbit::has_message_t, T>::value) {
msg = response.message;
}

log::warn("Upbit error ({}, '{}')", statusCode, msg);
return RequestRetry::Status::kResponseError;
}
return RequestRetry::Status::kResponseOK;
Expand Down Expand Up @@ -94,20 +98,8 @@ UpbitPublic::UpbitPublic(const CoincenterInfo& config, FiatConverter& fiatConver
_curlHandle) {}

bool UpbitPublic::healthCheck() {
static constexpr auto kAllowExceptions = false;
json::container result = json::container::parse(
_curlHandle.query("/v1/ticker", CurlOptions(HttpRequestType::kGet, {{"markets", "KRW-BTC"}})), nullptr,
kAllowExceptions);
if (result.is_discarded()) {
log::error("{} health check response badly formatted", name());
return false;
}
auto errorIt = result.find("error");
if (errorIt != result.end()) {
log::error("Error in {} status: {}", name(), errorIt->dump());
return false;
}
return !result.empty() && result.is_array() && result.front().find("timestamp") != result.front().end();
auto result = PublicQuery<schema::upbit::V1Tickers>(_curlHandle, "/v1/ticker", {{"markets", "KRW-BTC"}});
return !result.empty() && result.front().timestamp != 0;
}

std::optional<MonetaryAmount> UpbitPublic::queryWithdrawalFee(CurrencyCode currencyCode) {
Expand Down Expand Up @@ -142,24 +134,36 @@ bool UpbitPublic::CheckCurrencyCode(CurrencyCode standardCode, const CurrencyCod
}

MarketSet UpbitPublic::MarketsFunc::operator()() {
json::container result = PublicQuery(_curlHandle, "/v1/market/all", {{"isDetails", "true"}});
auto result = PublicQuery<schema::upbit::V1MarketAll>(_curlHandle, "/v1/market/all", {{"isDetails", "true"}});
const CurrencyCodeSet& excludedCurrencies = _assetConfig.allExclude;
MarketSet ret;
ret.reserve(static_cast<MarketSet::size_type>(result.size()));
for (const json::container& marketDetails : result) {
std::string_view marketStr = marketDetails["market"].get<std::string_view>();
auto marketWarningIt = marketDetails.find("market_warning");
if (marketWarningIt != marketDetails.end() && marketWarningIt->get<std::string_view>() != "NONE") {
log::error("Discard Upbit market {} as it has no warning", marketStr, marketWarningIt->get<std::string_view>());
for (const auto& marketDetails : result) {
std::string_view marketStr = marketDetails.market;
if (!marketDetails.market_warning.empty() && marketDetails.market_warning != "NONE") {
log::error("Discard Upbit market {} as it has a warning", marketStr, marketDetails.market_warning);
continue;
}
// Upbit markets are inverted
Market market(marketStr, '-');
market = market.reverse();
if (!CheckCurrencyCode(market.base(), excludedCurrencies) ||
!CheckCurrencyCode(market.quote(), excludedCurrencies)) {
auto dashPos = marketStr.find('-');
if (dashPos == std::string_view::npos) {
log::error("Unable to parse Upbit market {}", marketStr);
continue;
}
std::string_view quote = marketStr.substr(0, dashPos);
if (quote.size() > CurrencyCode::kMaxLen) {
log::error("Discard Upbit market {} as quote currency is too long", marketStr);
continue;
}
std::string_view base = marketStr.substr(dashPos + 1);
if (base.size() > CurrencyCode::kMaxLen) {
log::error("Discard Upbit market {} as base currency is too long", marketStr);
continue;
}
if (!CheckCurrencyCode(base, excludedCurrencies) || !CheckCurrencyCode(quote, excludedCurrencies)) {
continue;
}
Market market(base, quote);
log::debug("Retrieved Upbit market {}", market);
ret.emplace(std::move(market));
}
Expand Down Expand Up @@ -190,14 +194,14 @@ MonetaryAmountByCurrencySet UpbitPublic::WithdrawalFeesFunc::operator()() const
namespace {

template <class OutputType>
OutputType ParseOrderBooks(const json::container& result, int depth) {
OutputType ParseOrderBooks(const schema::upbit::V1Orderbooks& result, int depth) {
OutputType ret;
const auto time = Clock::now();

MarketOrderBookLines orderBookLines;

for (const json::container& marketDetails : result) {
std::string_view marketStr = marketDetails["market"].get<std::string_view>();
for (const auto& marketDetails : result) {
std::string_view marketStr = marketDetails.market;
std::size_t dashPos = marketStr.find('-');
if (dashPos == std::string_view::npos) {
log::error("Unable to parse order book json for market {}", marketStr);
Expand All @@ -209,17 +213,17 @@ OutputType ParseOrderBooks(const json::container& result, int depth) {
CurrencyCode base(marketStr.substr(dashPos + 1));
Market market(base, quote);

const auto& orderBookLinesJson = marketDetails["orderbook_units"];
const auto& orderBookLinesJson = marketDetails.orderbook_units;

orderBookLines.clear();
orderBookLines.reserve(orderBookLinesJson.size() * 2U);

for (const json::container& orderbookDetails : orderBookLinesJson | std::ranges::views::take(depth)) {
for (const auto& orderbookDetails : orderBookLinesJson | std::ranges::views::take(depth)) {
// Amounts are not strings, but doubles
MonetaryAmount askPri(orderbookDetails["ask_price"].get<double>(), quote);
MonetaryAmount bidPri(orderbookDetails["bid_price"].get<double>(), quote);
MonetaryAmount askVol(orderbookDetails["ask_size"].get<double>(), base);
MonetaryAmount bidVol(orderbookDetails["bid_size"].get<double>(), base);
MonetaryAmount askPri(orderbookDetails.ask_price, quote);
MonetaryAmount bidPri(orderbookDetails.bid_price, quote);
MonetaryAmount askVol(orderbookDetails.ask_size, base);
MonetaryAmount bidVol(orderbookDetails.bid_size, base);

orderBookLines.pushAsk(askVol, askPri);
orderBookLines.pushBid(bidVol, bidPri);
Expand Down Expand Up @@ -253,34 +257,36 @@ MarketOrderBookMap UpbitPublic::AllOrderBooksFunc::operator()(int depth) {
}
marketsStr.append(ReverseMarketStr(mk));
}
return ParseOrderBooks<MarketOrderBookMap>(PublicQuery(_curlHandle, "/v1/orderbook", {{"markets", marketsStr}}),
depth);
return ParseOrderBooks<MarketOrderBookMap>(
PublicQuery<schema::upbit::V1Orderbooks>(_curlHandle, "/v1/orderbook", {{"markets", marketsStr}}), depth);
}

MarketOrderBook UpbitPublic::OrderBookFunc::operator()(Market mk, int depth) {
return ParseOrderBooks<MarketOrderBook>(
PublicQuery(_curlHandle, "/v1/orderbook", {{"markets", ReverseMarketStr(mk)}}), depth);
PublicQuery<schema::upbit::V1Orderbooks>(_curlHandle, "/v1/orderbook", {{"markets", ReverseMarketStr(mk)}}),
depth);
}

MonetaryAmount UpbitPublic::TradedVolumeFunc::operator()(Market mk) {
json::container result =
PublicQuery(_curlHandle, "/v1/candles/days", {{"count", 1}, {"market", ReverseMarketStr(mk)}});
double last24hVol = result.empty() ? 0 : result.front()["candle_acc_trade_volume"].get<double>();
auto result = PublicQuery<schema::upbit::V1CandlesDay>(_curlHandle, "/v1/candles/days",
{{"count", 1}, {"market", ReverseMarketStr(mk)}});
double last24hVol = result.empty() ? 0 : result.front().candle_acc_trade_volume;
return MonetaryAmount(last24hVol, mk.base());
}

PublicTradeVector UpbitPublic::queryLastTrades(Market mk, int nbTrades) {
json::container result =
PublicQuery(_curlHandle, "/v1/trades/ticks", {{"count", nbTrades}, {"market", ReverseMarketStr(mk)}});
auto result = PublicQuery<schema::upbit::V1TradesTicks>(_curlHandle, "/v1/trades/ticks",
{{"count", nbTrades}, {"market", ReverseMarketStr(mk)}});

PublicTradeVector ret;
ret.reserve(static_cast<PublicTradeVector::size_type>(result.size()));

for (const json::container& detail : result) {
MonetaryAmount amount(detail["trade_volume"].get<double>(), mk.base());
MonetaryAmount price(detail["trade_price"].get<double>(), mk.quote());
int64_t millisecondsSinceEpoch = detail["timestamp"].get<int64_t>();
TradeSide tradeSide = detail["ask_bid"].get<std::string_view>() == "BID" ? TradeSide::kBuy : TradeSide::kSell;
for (const auto& detail : result) {
MonetaryAmount amount(detail.trade_volume, mk.base());
MonetaryAmount price(detail.trade_price, mk.quote());
int64_t millisecondsSinceEpoch = detail.timestamp;
TradeSide tradeSide =
detail.ask_bid == schema::upbit::V1TradesTick::AskBid::BID ? TradeSide::kBuy : TradeSide::kSell;

ret.emplace_back(tradeSide, amount, price, TimePoint(milliseconds(millisecondsSinceEpoch)));
}
Expand All @@ -289,9 +295,9 @@ PublicTradeVector UpbitPublic::queryLastTrades(Market mk, int nbTrades) {
}

MonetaryAmount UpbitPublic::TickerFunc::operator()(Market mk) {
json::container result =
PublicQuery(_curlHandle, "/v1/trades/ticks", {{"count", 1}, {"market", ReverseMarketStr(mk)}});
double lastPrice = result.empty() ? 0 : result.front()["trade_price"].get<double>();
auto result = PublicQuery<schema::upbit::V1TradesTicks>(_curlHandle, "/v1/trades/ticks",
{{"count", 1}, {"market", ReverseMarketStr(mk)}});
double lastPrice = result.empty() ? 0 : result.front().trade_price;
return MonetaryAmount(lastPrice, mk.quote());
}

Expand Down

0 comments on commit 21b44e9

Please sign in to comment.