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); } // ─── Paires/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, // 가상자산의 영문명 new UpbitMarketEvent( // 종목 경보 정보 m.MarketEvent?.Warning ?? false, // 유의 종목 여부 new UpbitMarketCaution( // 주의 종목 여부 m.MarketEvent?.Caution?.PriceFluctuations ?? false, // 가격 급등락 경보 m.MarketEvent?.Caution?.TradingVolumeSoaring ?? false, // 거래량 급증 경보 m.MarketEvent?.Caution?.DepositAmountSoaring ?? false, // 입금량 급증 경보 m.MarketEvent?.Caution?.GlobalPriceDifferences ?? false, // 국내외 가격 차이 경보 m.MarketEvent?.Caution?.ConcentrationOfSmallAccounts ?? false // 소수 계정 집중 거래 경보 ) ) ))]; } // ─── 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.TradeDate, // 체근 체결 일자(UTC) t.TradeTime, // 체근 체결 시작 (UTC) t.TradeDateKst, // 체근 체결 일자(KST) t.TradeTimeKst, // 체근 체결 시각(KST) t.TradeTimestamp, // 체결 시각의 밀리초단위 타임스탬프 t.OpeningPrice, // 해당 페어의 첫 거래 가격 t.HighPrice, // 해당 페어의 최고 거래 가격 t.LowPrice, // 해덩 페어의 최저 거래 가격 t.TradePrice, // 해당 페어의 현재 가격 t.PrevClosingPrice, // 해당 페어의 전일 종가 t.Change, // 가격 변동 상태(EVEN: 보합, RISE: 항승, FALL: 하락) t.ChangePrice, // 전일 종가 대비 가격 변화 t.ChangeRate, // 전일 종가 대비 가격 변화율 t.SignedChangePrice, // 전일 종가 대비 가격 변화 (부호 있는)(RISE: +, FALL: -) t.SignedChangeRate, // 전일 종가 대비 가격 변화율 t.TradeVolume, // 최근 거래 수량 t.AccTradePrice, // 누적 거래 금액(UTC) t.AccTradePrice24h, // 24시간 누적 거래 금액 t.AccTradeVolume, // 누적 거래량(UTC) t.AccTradeVolume24h, // 24시간 누적 거래량 t.Highest52WeekPrice, // 52주 신고가 t.Highest52WeekDate, // 52주 신고가 달성일 t.Lowest52WeekPrice, // 52주 신저가 t.Lowest52WeekDate, // 52주 신저가 달성일 t.Timestamp // 현재가 정보가 반영된 시각의 시간(mm) ))]; } // ─── 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.TradeDateUtc, // 체결 일자(UTC) t.TradeTimeUtc, // 체결 시각(UTC) t.Timestamp, // 체결 시간(mm) t.TradePrice, // 최근 체결 가격 t.TradeVolume, // 최근 체결량 t.PrevClosingPrice, // 전일 종가 t.ChangePrice, // 전일 종가 대비 가격 변화 t.AskBid, // 매수/매도 구분(BID: 매수, ASK: 매도) t.SequentialId // 체결 번호(Unique) ))]; } // ─── 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( // 호가 정보가 담긴 목록 (총 30호) u.AskPrice, // 매도 호가 u.BidPrice, // 매수 호가 u.AskSize, // 매도 잔량 u.BidSize // 매수 잔량 ))], o.Timestamp, // 조회 요청 시간(ms) o.Level // 호가가 적용된 가격 단위 ))]; } // ─── 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, // 캔들 기준 시각 (UTC) c.CandleDateTimeKst, // 캔들 기준 시각 (KST) c.OpeningPrice, // 시가 c.HighPrice, // 고가 c.LowPrice, // 저가 c.TradePrice, // 종가 (현재가) c.Timestamp, // 마지막 틱이 저장된 시각 (ms) c.CandleAccTradePrice, // 누적 거래 금액 c.CandleAccTradeVolume, // 누적 거래량 c.Unit, // 캔들 집계 시간 단위 (분) c.PrevClosingPrice, // 전일 종가 (UTC 0시 기준) c.ChangePrice, // 전일 종가 대비 가격 변화 c.ChangeRate, // 전일 종가 대비 가격 변화율 c.ConvertedTradePrice, // 종가 환산 화폐 단위로 환산된 가격 c.FirstDayOfPeriod // 캔들 집계 시작일자 ))]; } // ─── Internal DTOs ──────────────────────────────────────── private sealed class UpbitCandleDto { public string Market { get; set; } = default!; public string CandleDateTimeUtc { get; set; } = default!; public string CandleDateTimeKst { get; set; } = default!; 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 int? Unit { get; set; } public decimal? PrevClosingPrice { get; set; } public decimal? ChangePrice { get; set; } public decimal? ChangeRate { get; set; } public decimal? ConvertedTradePrice { 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 UpbitMarketEventDto? MarketEvent { get; set; } } private sealed class UpbitMarketEventDto { public bool Warning { get; set; } public UpbitMarketCautionDto? Caution { get; set; } } private sealed class UpbitMarketCautionDto { public bool PriceFluctuations { get; set; } public bool TradingVolumeSoaring { get; set; } public bool DepositAmountSoaring { get; set; } public bool GlobalPriceDifferences { get; set; } public bool ConcentrationOfSmallAccounts { get; set; } } private sealed class UpbitTickerDetailDto { public string Market { get; set; } = default!; public string TradeDate { get; set; } = default!; public string TradeTime { get; set; } = default!; public string TradeDateKst { get; set; } = default!; public string TradeTimeKst { get; set; } = default!; public long TradeTimestamp { get; set; } public decimal OpeningPrice { get; set; } public decimal HighPrice { get; set; } public decimal LowPrice { get; set; } public decimal TradePrice { get; set; } public decimal PrevClosingPrice { get; set; } public string Change { get; set; } = default!; public decimal ChangePrice { get; set; } public decimal ChangeRate { get; set; } public decimal SignedChangePrice { get; set; } public decimal SignedChangeRate { get; set; } public decimal TradeVolume { 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 TradeDateUtc { get; set; } = default!; public string TradeTimeUtc { get; set; } = default!; public long Timestamp { get; set; } public decimal TradePrice { get; set; } public decimal TradeVolume { get; set; } public decimal PrevClosingPrice { get; set; } public decimal ChangePrice { 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; } public decimal Level { 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; } } }