using Application.Abstractions.Crypto; using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Serialization; namespace Infrastructure.Crypto; public sealed class UpbitRestClient(HttpClient http) : IUpbitClient { private static readonly JsonSerializerOptions _jsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, NumberHandling = JsonNumberHandling.AllowReadingFromString }; // ─── Candle ─────────────────────────────────────────────── public async Task> GetSecondCandlesAsync(string market, int count, CancellationToken ct = default) { return await FetchCandlesAsync($"candles/seconds?market={market}&count={count}", ct); } public async Task> GetMinuteCandlesAsync(string market, int unit, int count, CancellationToken ct = default) { return await FetchCandlesAsync($"candles/minutes/{unit}?market={market}&count={count}", ct); } public async Task> GetDayCandlesAsync(string market, int count, CancellationToken ct = default) { return await FetchCandlesAsync($"candles/days?market={market}&count={count}", ct); } public async Task> GetWeekCandlesAsync(string market, int count, CancellationToken ct = default) { return await FetchCandlesAsync($"candles/weeks?market={market}&count={count}", ct); } public async Task> GetMonthCandlesAsync(string market, int count, CancellationToken ct = default) { return await FetchCandlesAsync($"candles/months?market={market}&count={count}", ct); } public async Task> GetYearCandlesAsync(string market, int count, CancellationToken ct = default) { return await FetchCandlesAsync($"candles/years?market={market}&count={count}", ct); } // ─── Market ─────────────────────────────────────────────── public async Task> GetMarketsAsync(CancellationToken ct = default) { var items = await http.GetFromJsonAsync>("market/all?is_details=true", _jsonOptions, ct) ?? []; return [..items.Select(m => new UpbitMarket( m.Market, m.KoreanName, m.EnglishName, m.MarketWarning ))]; } // ─── Ticker ─────────────────────────────────────────────── public async Task> GetTickersAsync(string[] markets, CancellationToken ct = default) { var query = string.Join(",", markets); var items = await http.GetFromJsonAsync>($"ticker?markets={query}", _jsonOptions, ct) ?? []; return [..items.Select(t => new UpbitTickerDetail( t.Market, t.TradePrice, t.Change, t.SignedChangePrice, t.SignedChangeRate, t.OpeningPrice, t.HighPrice, t.LowPrice, t.PrevClosingPrice, t.AccTradePrice, t.AccTradePrice24h, t.AccTradeVolume, t.AccTradeVolume24h, t.Highest52WeekPrice, t.Highest52WeekDate, t.Lowest52WeekPrice, t.Lowest52WeekDate, t.Timestamp ))]; } // ─── Trade ──────────────────────────────────────────────── public async Task> GetTradesAsync(string market, int count, CancellationToken ct = default) { var items = await http.GetFromJsonAsync>($"trades/ticks?market={market}&count={count}", _jsonOptions, ct) ?? []; return [..items.Select(t => new UpbitTrade( t.Market, t.TradeDate, t.TradeTime, t.Timestamp, t.TradePrice, t.TradeVolume, t.PrevClosingPrice, t.AskBid, t.SequentialId ))]; } // ─── Orderbook ──────────────────────────────────────────── public async Task> GetOrderbookAsync(string[] markets, CancellationToken ct = default) { var query = string.Join(",", markets); var items = await http.GetFromJsonAsync>($"orderbook?markets={query}", _jsonOptions, ct) ?? []; return [..items.Select(o => new UpbitOrderbook( o.Market, o.TotalAskSize, o.TotalBidSize, [..o.OrderbookUnits.Select(u => new UpbitOrderbookUnit( u.AskPrice, u.BidPrice, u.AskSize, u.BidSize ))], o.Timestamp ))]; } // ─── Private helpers ────────────────────────────────────── private async Task> FetchCandlesAsync(string url, CancellationToken ct) { var items = await http.GetFromJsonAsync>(url, _jsonOptions, ct) ?? []; return [..items.Select(c => new UpbitCandle( c.Market, c.CandleDateTimeUtc, c.OpeningPrice, c.HighPrice, c.LowPrice, c.TradePrice, c.Timestamp, c.CandleAccTradePrice, c.CandleAccTradeVolume, c.FirstDayOfPeriod ))]; } // ─── Internal DTOs ──────────────────────────────────────── private sealed class UpbitCandleDto { public string Market { get; set; } = default!; public DateTime CandleDateTimeUtc { get; set; } public decimal OpeningPrice { get; set; } public decimal HighPrice { get; set; } public decimal LowPrice { get; set; } public decimal TradePrice { get; set; } public long Timestamp { get; set; } public decimal CandleAccTradePrice { get; set; } public decimal CandleAccTradeVolume { get; set; } public string? FirstDayOfPeriod { get; set; } } private sealed class UpbitMarketDto { public string Market { get; set; } = default!; public string KoreanName { get; set; } = default!; public string EnglishName { get; set; } = default!; public string? MarketWarning { get; set; } } private sealed class UpbitTickerDetailDto { public string Market { get; set; } = default!; public decimal TradePrice { get; set; } public string Change { get; set; } = default!; public decimal SignedChangePrice { get; set; } public decimal SignedChangeRate { get; set; } public decimal OpeningPrice { get; set; } public decimal HighPrice { get; set; } public decimal LowPrice { get; set; } public decimal PrevClosingPrice { get; set; } public decimal AccTradePrice { get; set; } public decimal AccTradePrice24h { get; set; } public decimal AccTradeVolume { get; set; } public decimal AccTradeVolume24h { get; set; } public decimal Highest52WeekPrice { get; set; } public string Highest52WeekDate { get; set; } = default!; public decimal Lowest52WeekPrice { get; set; } public string Lowest52WeekDate { get; set; } = default!; public long Timestamp { get; set; } } private sealed class UpbitTradeDto { public string Market { get; set; } = default!; public string TradeDate { get; set; } = default!; public string TradeTime { get; set; } = default!; public long Timestamp { get; set; } public decimal TradePrice { get; set; } public decimal TradeVolume { get; set; } public decimal PrevClosingPrice { get; set; } public string AskBid { get; set; } = default!; public long SequentialId { get; set; } } private sealed class UpbitOrderbookDto { public string Market { get; set; } = default!; public decimal TotalAskSize { get; set; } public decimal TotalBidSize { get; set; } public List OrderbookUnits { get; set; } = []; public long Timestamp { get; set; } } private sealed class UpbitOrderbookUnitDto { public decimal AskPrice { get; set; } public decimal BidPrice { get; set; } public decimal AskSize { get; set; } public decimal BidSize { get; set; } } }