Files
BitGet_API/Bitget_Server/Infrastructure/Bitget/BitgetClient.cs
2026-04-20 20:27:42 +09:00

278 lines
12 KiB
C#

using System.Collections.Specialized;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using Bitget_Server.Application.State;
using Bitget_Server.Domain;
using Bitget_Server.Domain.Enums;
using Bitget_Server.Domain.Models;
using Bitget_Server.Infrastructure.Utils;
using Newtonsoft.Json;
namespace Bitget_Server.Infrastructure.Bitget;
/// <summary>
/// 기존 WinForms WebConnection.SendWebServer / GenerateSignString 로직을 이식.
/// HttpClient 기반으로 재작성하고, 모든 외부 호출 전에 BitgetRateLimiter를 거친다.
/// </summary>
public class BitgetClient
{
private readonly HttpClient _http;
private readonly BitgetRateLimiter _limiter;
private readonly MarketState _state;
private readonly ILogger<BitgetClient> _logger;
public BitgetClient(
HttpClient http,
BitgetRateLimiter limiter,
MarketState state,
ILogger<BitgetClient> logger)
{
_http = http;
_limiter = limiter;
_state = state;
_logger = logger;
}
/// <summary>
/// 기존 WebConnection.SendWebServer와 동일한 시그니처를 유지한다.
/// 응답을 ExchangeWebReqType에 따라 적절히 역직렬화하여 반환한다.
/// </summary>
public async Task<object> SendWebServerAsync(
ExchangeWebReqType exchangeWebReqType,
string url = "",
MarketCandleDataList marketCandleDataList = null,
MCandleTimeType interval = MCandleTimeType.M1,
string postData = "",
string subUri = "",
NameValueCollection headers = null,
CancellationToken ct = default)
{
var requestUrl = string.IsNullOrEmpty(url) ? GetDefaultUrl(exchangeWebReqType) : url;
if (string.IsNullOrEmpty(requestUrl))
{
_logger.LogWarning("URL 미정 ExchangeWebReqType: {Type}", exchangeWebReqType);
return null;
}
// 1차 호출 → 429 시 1회 재시도
for (int attempt = 0; attempt < 2; attempt++)
{
await _limiter.AcquireAsync(ct).ConfigureAwait(false);
try
{
HttpRequestMessage req;
if (!string.IsNullOrEmpty(postData))
{
req = new HttpRequestMessage(HttpMethod.Post, requestUrl)
{
Content = new StringContent(postData, Encoding.UTF8,
headers != null ? "application/json" : "application/x-www-form-urlencoded")
};
}
else
{
req = new HttpRequestMessage(HttpMethod.Get, requestUrl);
}
if (headers != null)
{
foreach (string key in headers)
req.Headers.TryAddWithoutValidation(key, headers[key]);
if (req.Content != null)
req.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
}
using var resp = await _http.SendAsync(req, ct).ConfigureAwait(false);
var resResult = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
// 429 (TooManyRequests) 는 1회에 한해 500ms 후 재시도
if ((int)resp.StatusCode == 429 && attempt == 0)
{
_logger.LogWarning("Bitget 429 TooManyRequests — 500ms 후 1회 재시도 ({Type})", exchangeWebReqType);
await Task.Delay(500, ct).ConfigureAwait(false);
continue;
}
if (!resp.IsSuccessStatusCode)
{
_logger.LogError("Bitget API 에러 {Status}: {Body}", (int)resp.StatusCode, resResult);
return null;
}
return Parse(exchangeWebReqType, resResult, marketCandleDataList, interval);
}
catch (HttpRequestException webEx)
{
_logger.LogError(webEx, "Bitget HTTP 호출 실패: {Type}", exchangeWebReqType);
return null;
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Bitget 처리 중 예외");
return null;
}
}
return null;
}
private string GetDefaultUrl(ExchangeWebReqType type) => type switch
{
ExchangeWebReqType.UpbitMarket => UpbitEndpoints.MarketInfo,
ExchangeWebReqType.BitgetMarket_Spot => BitgetEndpoints.MarketInfo_Coin,
ExchangeWebReqType.BitgetMarket_Future => BitgetEndpoints.MarketInfo_Future,
ExchangeWebReqType.BitgetCandleStick_Coin => BitgetEndpoints.MarketCandle_Coin,
ExchangeWebReqType.BitgetCandleStick_Future => BitgetEndpoints.MarketCandle_Future,
_ => string.Empty
};
private object Parse(ExchangeWebReqType type, string resResult, MarketCandleDataList marketCandleDataList, MCandleTimeType interval)
{
switch (type)
{
case ExchangeWebReqType.BitgetMarket_Spot:
{
var datalist = JsonConvert.DeserializeObject<Dictionary<string, object>>(resResult);
var marketInfo = JsonConvert.DeserializeObject<List<Dictionary<string, object>>>(datalist["data"]?.ToString());
return marketInfo.FindAll(x => x["coin"].ToString().Contains("USDT"));
}
case ExchangeWebReqType.BitgetMarket_Future:
{
var datalist = JsonConvert.DeserializeObject<Dictionary<string, object>>(resResult);
var marketInfos = JsonConvert.DeserializeObject<List<Dictionary<string, object>>>(datalist["data"]?.ToString());
var newList = new List<MarketInfo>();
foreach (var item in marketInfos)
{
var marketInfo = new MarketInfo
{
MarketName = item["symbol"].ToString(),
ExchangePlace = Enum.GetName(typeof(ExchangePlace), _state.selectedChangePlace),
lastPr = Convert.ToDouble(item["lastPr"]),
change24h = Convert.ToDouble(item["change24h"]),
baseVolume = Convert.ToDouble(item["baseVolume"]),
quoteVolume = Convert.ToDouble(item["quoteVolume"]),
high24h = Convert.ToDouble(item["high24h"]),
low24h = Convert.ToDouble(item["low24h"]),
open24h = Convert.ToDouble(item["open24h"])
};
newList.Add(marketInfo);
}
lock (_state.SyncRoot) { _state.marketInfoList = newList; }
return newList;
}
case ExchangeWebReqType.BitgetCandleStick_Future:
{
if (marketCandleDataList == null) return null;
var datalist = JsonConvert.DeserializeObject<Dictionary<string, object>>(resResult);
var datas = JsonConvert.DeserializeObject<List<List<object>>>(datalist["data"]?.ToString());
var candleDataList = marketCandleDataList.candleDataList[interval];
lock (_state.SyncRoot)
{
foreach (var item in datas)
{
var candleData = new CandleData
{
Argument = Convertor.GetUnixtimeStampToDateTime(long.Parse(item[0].ToString())),
Open = Convert.ToDouble(item[1]),
High = Convert.ToDouble(item[2]),
Low = Convert.ToDouble(item[3]),
Close = Convert.ToDouble(item[4]),
Vol_C = Convert.ToDouble(item[5]),
Vol_USD = Convert.ToDouble(item[6])
};
if (item == datas.Last())
{
var span = GetIntervalSpan(interval);
var same = candleDataList.Find(i =>
candleData.Argument <= i.Argument &&
i.Argument < (candleData.Argument + span) &&
i.WebSocketUpdate);
if (same != null)
{
same.Open = candleData.Open;
same.Low = Math.Min(same.Low, candleData.Low);
same.High = Math.Max(same.High, candleData.High);
same.WebServiceUpdate = true;
}
else
{
candleDataList.Add(candleData);
candleData.WebServiceUpdate = true;
}
}
else
{
candleDataList.Add(candleData);
candleData.WebServiceUpdate = true;
}
}
}
return null;
}
case ExchangeWebReqType.BitgetAccountList_Future:
{
var datalist = JsonConvert.DeserializeObject<Dictionary<string, object>>(resResult);
return JsonConvert.DeserializeObject<List<Dictionary<string, object>>>(datalist["data"]?.ToString());
}
case ExchangeWebReqType.BitgetOrder_Future:
case ExchangeWebReqType.BitgetCancelOrder_Future:
case ExchangeWebReqType.BitgetChangeLeverage_Future:
case ExchangeWebReqType.BitgetChangeMarginMode_Future:
case ExchangeWebReqType.BitgetChangePositionMode_Future:
case ExchangeWebReqType.BitgetOrderDetail_Future:
case ExchangeWebReqType.BitgetGetTimeStamp:
case ExchangeWebReqType.BitgetPendingOrder_Future:
return JsonConvert.DeserializeObject<Dictionary<string, object>>(resResult);
default:
return null;
}
}
private static TimeSpan GetIntervalSpan(MCandleTimeType interval) => interval switch
{
MCandleTimeType.M1 => TimeSpan.FromMinutes(1),
MCandleTimeType.M3 => TimeSpan.FromMinutes(3),
MCandleTimeType.M5 => TimeSpan.FromMinutes(5),
MCandleTimeType.M15 => TimeSpan.FromMinutes(15),
MCandleTimeType.M30 => TimeSpan.FromMinutes(30),
MCandleTimeType.H1 => TimeSpan.FromHours(1),
MCandleTimeType.H2 => TimeSpan.FromHours(2),
MCandleTimeType.H3 => TimeSpan.FromHours(3),
MCandleTimeType.H4 => TimeSpan.FromHours(4),
MCandleTimeType.H6 => TimeSpan.FromHours(6),
MCandleTimeType.H12 => TimeSpan.FromHours(12),
_ => TimeSpan.Zero
};
/// <summary>
/// 기존 IndicatorProcess.GenerateSignString을 그대로 이식.
/// </summary>
public static string GenerateSignString(string timestamp, string method, string requestPath, string queryString, string body, string secretKey)
{
method = method.ToUpper();
body = body == null ? "" : body;
queryString = string.IsNullOrWhiteSpace(queryString) ? "" : "?" + queryString;
string preHash = timestamp + method + requestPath + queryString + body;
var encSecretKey_byteData = Encoding.UTF8.GetBytes(secretKey);
var encPreHash_byteData = Encoding.UTF8.GetBytes(preHash);
return Convert.ToBase64String(new HMACSHA256(encSecretKey_byteData).ComputeHash(encPreHash_byteData));
}
}