UpbitRestClient.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. using Application.Abstractions.Crypto;
  2. using System.Net.Http.Json;
  3. using System.Text.Json;
  4. using System.Text.Json.Serialization;
  5. namespace Infrastructure.Crypto;
  6. public sealed class UpbitRestClient(HttpClient http) : IUpbitClient
  7. {
  8. private static readonly JsonSerializerOptions _jsonOptions = new()
  9. {
  10. PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
  11. NumberHandling = JsonNumberHandling.AllowReadingFromString
  12. };
  13. // ─── Candle ───────────────────────────────────────────────
  14. // 초 단위
  15. public async Task<IReadOnlyList<UpbitCandle>> GetSecondCandlesAsync(string market, int count, CancellationToken ct = default)
  16. {
  17. return await FetchCandlesAsync($"candles/seconds?market={market}&count={count}", ct);
  18. }
  19. // 분 단위
  20. public async Task<IReadOnlyList<UpbitCandle>> GetMinuteCandlesAsync(string market, int unit, int count, CancellationToken ct = default)
  21. {
  22. return await FetchCandlesAsync($"candles/minutes/{unit}?market={market}&count={count}", ct);
  23. }
  24. // 일 단위
  25. public async Task<IReadOnlyList<UpbitCandle>> GetDayCandlesAsync(string market, int count, CancellationToken ct = default)
  26. {
  27. return await FetchCandlesAsync($"candles/days?market={market}&count={count}", ct);
  28. }
  29. // 주 단위
  30. public async Task<IReadOnlyList<UpbitCandle>> GetWeekCandlesAsync(string market, int count, CancellationToken ct = default)
  31. {
  32. return await FetchCandlesAsync($"candles/weeks?market={market}&count={count}", ct);
  33. }
  34. // 월 단위
  35. public async Task<IReadOnlyList<UpbitCandle>> GetMonthCandlesAsync(string market, int count, CancellationToken ct = default)
  36. {
  37. return await FetchCandlesAsync($"candles/months?market={market}&count={count}", ct);
  38. }
  39. // 연 단위
  40. public async Task<IReadOnlyList<UpbitCandle>> GetYearCandlesAsync(string market, int count, CancellationToken ct = default)
  41. {
  42. return await FetchCandlesAsync($"candles/years?market={market}&count={count}", ct);
  43. }
  44. // ─── Paires/Market ───────────────────────────────────────────────
  45. public async Task<IReadOnlyList<UpbitMarket>> GetMarketsAsync(CancellationToken ct = default)
  46. {
  47. var items = await http.GetFromJsonAsync<List<UpbitMarketDto>>("market/all?is_details=true", _jsonOptions, ct) ?? [];
  48. return [..items.Select(m => new UpbitMarket(
  49. m.Market, // 페어(거래쌍)의 코드
  50. m.KoreanName, // 가상자산의 한글명
  51. m.EnglishName, // 가상자산의 영문명
  52. new UpbitMarketEvent( // 종목 경보 정보
  53. m.MarketEvent?.Warning ?? false, // 유의 종목 여부
  54. new UpbitMarketCaution( // 주의 종목 여부
  55. m.MarketEvent?.Caution?.PriceFluctuations ?? false, // 가격 급등락 경보
  56. m.MarketEvent?.Caution?.TradingVolumeSoaring ?? false, // 거래량 급증 경보
  57. m.MarketEvent?.Caution?.DepositAmountSoaring ?? false, // 입금량 급증 경보
  58. m.MarketEvent?.Caution?.GlobalPriceDifferences ?? false, // 국내외 가격 차이 경보
  59. m.MarketEvent?.Caution?.ConcentrationOfSmallAccounts ?? false // 소수 계정 집중 거래 경보
  60. )
  61. )
  62. ))];
  63. }
  64. // ─── Ticker ───────────────────────────────────────────────
  65. // 현재가
  66. public async Task<IReadOnlyList<UpbitTickerDetail>> GetTickersAsync(string[] markets, CancellationToken ct = default)
  67. {
  68. var query = string.Join(",", markets);
  69. var items = await http.GetFromJsonAsync<List<UpbitTickerDetailDto>>($"ticker?markets={query}", _jsonOptions, ct) ?? [];
  70. return [..items.Select(t => new UpbitTickerDetail(
  71. t.Market, // 페어(거래쌍) 코드
  72. t.TradeDate, // 체근 체결 일자(UTC)
  73. t.TradeTime, // 체근 체결 시작 (UTC)
  74. t.TradeDateKst, // 체근 체결 일자(KST)
  75. t.TradeTimeKst, // 체근 체결 시각(KST)
  76. t.TradeTimestamp, // 체결 시각의 밀리초단위 타임스탬프
  77. t.OpeningPrice, // 해당 페어의 첫 거래 가격
  78. t.HighPrice, // 해당 페어의 최고 거래 가격
  79. t.LowPrice, // 해덩 페어의 최저 거래 가격
  80. t.TradePrice, // 해당 페어의 현재 가격
  81. t.PrevClosingPrice, // 해당 페어의 전일 종가
  82. t.Change, // 가격 변동 상태(EVEN: 보합, RISE: 항승, FALL: 하락)
  83. t.ChangePrice, // 전일 종가 대비 가격 변화
  84. t.ChangeRate, // 전일 종가 대비 가격 변화율
  85. t.SignedChangePrice, // 전일 종가 대비 가격 변화 (부호 있는)(RISE: +, FALL: -)
  86. t.SignedChangeRate, // 전일 종가 대비 가격 변화율
  87. t.TradeVolume, // 최근 거래 수량
  88. t.AccTradePrice, // 누적 거래 금액(UTC)
  89. t.AccTradePrice24h, // 24시간 누적 거래 금액
  90. t.AccTradeVolume, // 누적 거래량(UTC)
  91. t.AccTradeVolume24h, // 24시간 누적 거래량
  92. t.Highest52WeekPrice, // 52주 신고가
  93. t.Highest52WeekDate, // 52주 신고가 달성일
  94. t.Lowest52WeekPrice, // 52주 신저가
  95. t.Lowest52WeekDate, // 52주 신저가 달성일
  96. t.Timestamp // 현재가 정보가 반영된 시각의 시간(mm)
  97. ))];
  98. }
  99. // ─── Trade ────────────────────────────────────────────────
  100. public async Task<IReadOnlyList<UpbitTrade>> GetTradesAsync(string market, int count, CancellationToken ct = default)
  101. {
  102. var items = await http.GetFromJsonAsync<List<UpbitTradeDto>>($"trades/ticks?market={market}&count={count}", _jsonOptions, ct) ?? [];
  103. return [..items.Select(t => new UpbitTrade(
  104. t.Market, // 페어(거래쌍)의 코드
  105. t.TradeDateUtc, // 체결 일자(UTC)
  106. t.TradeTimeUtc, // 체결 시각(UTC)
  107. t.Timestamp, // 체결 시간(mm)
  108. t.TradePrice, // 최근 체결 가격
  109. t.TradeVolume, // 최근 체결량
  110. t.PrevClosingPrice, // 전일 종가
  111. t.ChangePrice, // 전일 종가 대비 가격 변화
  112. t.AskBid, // 매수/매도 구분(BID: 매수, ASK: 매도)
  113. t.SequentialId // 체결 번호(Unique)
  114. ))];
  115. }
  116. // ─── Orderbook ────────────────────────────────────────────
  117. public async Task<IReadOnlyList<UpbitOrderbook>> GetOrderbookAsync(string[] markets, CancellationToken ct = default)
  118. {
  119. var query = string.Join(",", markets);
  120. var items = await http.GetFromJsonAsync<List<UpbitOrderbookDto>>($"orderbook?markets={query}", _jsonOptions, ct) ?? [];
  121. return [..items.Select(o => new UpbitOrderbook(
  122. o.Market, // 페어(거래쌍)의 코드
  123. o.TotalAskSize, // 현재 호가의 전체 매도 잔량 합계
  124. o.TotalBidSize, // 현재 호가의 전체 매수 잔량 합계
  125. [..o.OrderbookUnits.Select(u => new UpbitOrderbookUnit( // 호가 정보가 담긴 목록 (총 30호)
  126. u.AskPrice, // 매도 호가
  127. u.BidPrice, // 매수 호가
  128. u.AskSize, // 매도 잔량
  129. u.BidSize // 매수 잔량
  130. ))],
  131. o.Timestamp, // 조회 요청 시간(ms)
  132. o.Level // 호가가 적용된 가격 단위
  133. ))];
  134. }
  135. // ─── Private helpers ──────────────────────────────────────
  136. // 캔들 정보 조회
  137. private async Task<IReadOnlyList<UpbitCandle>> FetchCandlesAsync(string url, CancellationToken ct)
  138. {
  139. var items = await http.GetFromJsonAsync<List<UpbitCandleDto>>(url, _jsonOptions, ct) ?? [];
  140. return [..items.Select(c => new UpbitCandle(
  141. c.Market, // 페어(거래쌍)의 코드
  142. c.CandleDateTimeUtc, // 캔들 기준 시각 (UTC)
  143. c.CandleDateTimeKst, // 캔들 기준 시각 (KST)
  144. c.OpeningPrice, // 시가
  145. c.HighPrice, // 고가
  146. c.LowPrice, // 저가
  147. c.TradePrice, // 종가 (현재가)
  148. c.Timestamp, // 마지막 틱이 저장된 시각 (ms)
  149. c.CandleAccTradePrice, // 누적 거래 금액
  150. c.CandleAccTradeVolume, // 누적 거래량
  151. c.Unit, // 캔들 집계 시간 단위 (분)
  152. c.PrevClosingPrice, // 전일 종가 (UTC 0시 기준)
  153. c.ChangePrice, // 전일 종가 대비 가격 변화
  154. c.ChangeRate, // 전일 종가 대비 가격 변화율
  155. c.ConvertedTradePrice, // 종가 환산 화폐 단위로 환산된 가격
  156. c.FirstDayOfPeriod // 캔들 집계 시작일자
  157. ))];
  158. }
  159. // ─── Internal DTOs ────────────────────────────────────────
  160. private sealed class UpbitCandleDto
  161. {
  162. public string Market { get; set; } = default!;
  163. public string CandleDateTimeUtc { get; set; } = default!;
  164. public string CandleDateTimeKst { get; set; } = default!;
  165. public decimal OpeningPrice { get; set; }
  166. public decimal HighPrice { get; set; }
  167. public decimal LowPrice { get; set; }
  168. public decimal TradePrice { get; set; }
  169. public long Timestamp { get; set; }
  170. public decimal CandleAccTradePrice { get; set; }
  171. public decimal CandleAccTradeVolume { get; set; }
  172. public int? Unit { get; set; }
  173. public decimal? PrevClosingPrice { get; set; }
  174. public decimal? ChangePrice { get; set; }
  175. public decimal? ChangeRate { get; set; }
  176. public decimal? ConvertedTradePrice { get; set; }
  177. public string? FirstDayOfPeriod { get; set; }
  178. }
  179. private sealed class UpbitMarketDto
  180. {
  181. public string Market { get; set; } = default!;
  182. public string KoreanName { get; set; } = default!;
  183. public string EnglishName { get; set; } = default!;
  184. public UpbitMarketEventDto? MarketEvent { get; set; }
  185. }
  186. private sealed class UpbitMarketEventDto
  187. {
  188. public bool Warning { get; set; }
  189. public UpbitMarketCautionDto? Caution { get; set; }
  190. }
  191. private sealed class UpbitMarketCautionDto
  192. {
  193. public bool PriceFluctuations { get; set; }
  194. public bool TradingVolumeSoaring { get; set; }
  195. public bool DepositAmountSoaring { get; set; }
  196. public bool GlobalPriceDifferences { get; set; }
  197. public bool ConcentrationOfSmallAccounts { get; set; }
  198. }
  199. private sealed class UpbitTickerDetailDto
  200. {
  201. public string Market { get; set; } = default!;
  202. public string TradeDate { get; set; } = default!;
  203. public string TradeTime { get; set; } = default!;
  204. public string TradeDateKst { get; set; } = default!;
  205. public string TradeTimeKst { get; set; } = default!;
  206. public long TradeTimestamp { get; set; }
  207. public decimal OpeningPrice { get; set; }
  208. public decimal HighPrice { get; set; }
  209. public decimal LowPrice { get; set; }
  210. public decimal TradePrice { get; set; }
  211. public decimal PrevClosingPrice { get; set; }
  212. public string Change { get; set; } = default!;
  213. public decimal ChangePrice { get; set; }
  214. public decimal ChangeRate { get; set; }
  215. public decimal SignedChangePrice { get; set; }
  216. public decimal SignedChangeRate { get; set; }
  217. public decimal TradeVolume { get; set; }
  218. public decimal AccTradePrice { get; set; }
  219. public decimal AccTradePrice24h { get; set; }
  220. public decimal AccTradeVolume { get; set; }
  221. public decimal AccTradeVolume24h { get; set; }
  222. public decimal Highest52WeekPrice { get; set; }
  223. public string Highest52WeekDate { get; set; } = default!;
  224. public decimal Lowest52WeekPrice { get; set; }
  225. public string Lowest52WeekDate { get; set; } = default!;
  226. public long Timestamp { get; set; }
  227. }
  228. private sealed class UpbitTradeDto
  229. {
  230. public string Market { get; set; } = default!;
  231. public string TradeDateUtc { get; set; } = default!;
  232. public string TradeTimeUtc { get; set; } = default!;
  233. public long Timestamp { get; set; }
  234. public decimal TradePrice { get; set; }
  235. public decimal TradeVolume { get; set; }
  236. public decimal PrevClosingPrice { get; set; }
  237. public decimal ChangePrice { get; set; }
  238. public string AskBid { get; set; } = default!;
  239. public long SequentialId { get; set; }
  240. }
  241. private sealed class UpbitOrderbookDto
  242. {
  243. public string Market { get; set; } = default!;
  244. public decimal TotalAskSize { get; set; }
  245. public decimal TotalBidSize { get; set; }
  246. public List<UpbitOrderbookUnitDto> OrderbookUnits { get; set; } = [];
  247. public long Timestamp { get; set; }
  248. public decimal Level { get; set; }
  249. }
  250. private sealed class UpbitOrderbookUnitDto
  251. {
  252. public decimal AskPrice { get; set; }
  253. public decimal BidPrice { get; set; }
  254. public decimal AskSize { get; set; }
  255. public decimal BidSize { get; set; }
  256. }
  257. }