Files
PineSript/Buyer/BitgetRestClient.cs
2026-04-25 17:32:49 +09:00

127 lines
4.8 KiB
C#

using System.Text.Json;
using PineScriptV6;
using Serilog;
namespace Buyer
{
/// <summary>
/// Bitget REST API — 캔들 데이터 조회.
/// GET /api/v2/mix/market/candles
/// </summary>
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");
}
/// <summary>
/// 최근 <paramref name="limit"/>개 봉을 가져옵니다.
/// 반환값은 과거→현재 오름차순으로 정렬됩니다.
/// </summary>
public async Task<List<CandleData>> 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<CandleData>();
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();
}
}