using System.Text.Json; using PineScriptV6; using Serilog; namespace Buyer { /// /// Bitget REST API — 캔들 데이터 조회. /// GET /api/v2/mix/market/candles /// public class BitgetRestClient : IDisposable { private readonly HttpClient _http; private readonly BitgetConfig _cfg; public BitgetRestClient(BitgetConfig cfg) { _cfg = cfg; _http = new HttpClient { BaseAddress = new Uri(cfg.RestBaseUrl), Timeout = TimeSpan.FromSeconds(cfg.RestTimeoutSec) }; _http.DefaultRequestHeaders.Add("User-Agent", "Buyer/1.0"); } /// /// 최근 개 봉을 가져옵니다. /// 반환값은 과거→현재 오름차순으로 정렬됩니다. /// public async Task> GetCandlesAsync(int limit = 500) { int safeLimit = Math.Min(limit, 1000); // 기존 동작 확인된 파라미터 순서: symbol, granularity, limit, productType string url = "/api/v2/mix/market/candles" + $"?symbol={_cfg.Symbol}" + $"&granularity={_cfg.RestGranularity}" + $"&limit={safeLimit}" + $"&productType={_cfg.ProductType}"; Log.Debug("[REST] 요청: {Url}", url); // GetStringAsync는 4xx/5xx 시 본문 없이 예외 → GetAsync로 변경하여 본문 확인 HttpResponseMessage response; string body; try { response = await _http.GetAsync(url); body = await response.Content.ReadAsStringAsync(); } catch (Exception ex) { Log.Error(ex, "[REST] HTTP 요청 자체 실패 (네트워크 오류)"); throw; } // HTTP 오류 시 응답 본문을 로그에 찍고 예외 if (!response.IsSuccessStatusCode) { Log.Error("[REST] HTTP {Status} — 응답 본문: {Body}", (int)response.StatusCode, body); throw new HttpRequestException( $"HTTP {(int)response.StatusCode}: {body}"); } // JSON 파싱 using var doc = JsonDocument.Parse(body); var root = doc.RootElement; string code = root.TryGetProperty("code", out var codeEl) ? codeEl.GetString() ?? "" : ""; if (code != "00000") { string msg = root.TryGetProperty("msg", out var msgEl) ? msgEl.GetString() ?? "" : ""; Log.Error("[REST] API 오류 code={Code} msg={Msg} body={Body}", code, msg, body); throw new Exception($"Bitget API 오류 code={code} msg={msg}"); } var candles = new List(); foreach (var item in root.GetProperty("data").EnumerateArray()) { var arr = item.EnumerateArray().ToList(); if (arr.Count < 6) continue; long ts = long.Parse(arr[0].GetString()!); decimal o = decimal.Parse(arr[1].GetString()!, System.Globalization.CultureInfo.InvariantCulture); decimal h = decimal.Parse(arr[2].GetString()!, System.Globalization.CultureInfo.InvariantCulture); decimal l = decimal.Parse(arr[3].GetString()!, System.Globalization.CultureInfo.InvariantCulture); decimal c = decimal.Parse(arr[4].GetString()!, System.Globalization.CultureInfo.InvariantCulture); decimal vol = decimal.Parse(arr[5].GetString()!, System.Globalization.CultureInfo.InvariantCulture); candles.Add(new CandleData { Time = DateTimeOffset.FromUnixTimeMilliseconds(ts), Open = o, High = h, Low = l, Close = c, Volume = vol }); } // Bitget은 최신→과거 순으로 반환 → 오름차순 정렬 candles.Sort((a, b) => a.Time.CompareTo(b.Time)); Log.Debug("[REST] 수신 {Count}봉 {From} ~ {To}", candles.Count, candles.FirstOrDefault()?.Time.ToLocalTime().ToString("yyyy-MM-dd HH:mm") ?? "-", candles.LastOrDefault() ?.Time.ToLocalTime().ToString("yyyy-MM-dd HH:mm") ?? "-"); return candles; } public void Dispose() => _http.Dispose(); } }