127 lines
4.8 KiB
C#
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();
|
|
}
|
|
}
|