단순 이관
This commit is contained in:
277
Bitget_Server/Infrastructure/Bitget/BitgetClient.cs
Normal file
277
Bitget_Server/Infrastructure/Bitget/BitgetClient.cs
Normal file
@@ -0,0 +1,277 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user