using System.Collections.Specialized; using Bitget_Server.Application.Options; using Bitget_Server.Domain; using Bitget_Server.Domain.Enums; using Bitget_Server.Infrastructure.Bitget; using Bitget_Server.Infrastructure.Utils; using Newtonsoft.Json; namespace Bitget_Server.Application.Services; /// /// 기존 IndicatorProcess의 Private API 영역(주문/취소/잔고/포지션모드 변경/레버리지 변경/체결상세)을 이식한 서비스. /// HMAC-SHA256 서명은 BitgetClient.GenerateSignString을 사용한다. /// 외부 호출 경로는 BitgetClient.SendWebServerAsync → BitgetRateLimiter(20/sec) 순으로 흐른다. /// public class BitgetTradingService { private readonly BitgetClient _client; private readonly GlobalOptions _options; private readonly ILogger _logger; public BitgetTradingService( BitgetClient client, GlobalOptions options, ILogger logger) { _client = client; _options = options; _logger = logger; } private string AccessKey => _options.SelectAPI?.AccessKey ?? string.Empty; private string SecretKey => _options.SelectAPI?.SecretKey ?? string.Empty; private string Passphrase => _options.SelectAPI?.PassphraseKey ?? string.Empty; /// Bitget 서버 타임스탬프 조회. public async Task GetTimeStampAsync(CancellationToken ct = default) { var result = await _client.SendWebServerAsync( ExchangeWebReqType.BitgetGetTimeStamp, BitgetEndpoints.GetTimestamp, ct: ct).ConfigureAwait(false) as Dictionary; if (result == null) return 0; var data = JsonConvert.DeserializeObject>( JsonConvert.SerializeObject(result["data"])); long.TryParse(data["serverTime"].ToString(), out var ts); return ts; } /// 미체결 주문 조회. public async Task> GetPendingOrdersAsync(CancellationToken ct = default) { var timestamp = (await GetTimeStampAsync(ct).ConfigureAwait(false)).ToString(); const string reqPath = "/api/v2/mix/order/orders-pending"; var nvc = new NameValueCollection { { "ACCESS-KEY", AccessKey }, { "ACCESS-SIGN", BitgetClient.GenerateSignString(timestamp, "GET", reqPath, "productType=USDT-FUTURES", null, SecretKey) }, { "ACCESS-PASSPHRASE", Passphrase }, { "ACCESS-TIMESTAMP", timestamp } }; return await _client.SendWebServerAsync( ExchangeWebReqType.BitgetPendingOrder_Future, BitgetEndpoints.PendingOrders_Future, headers: nvc, ct: ct).ConfigureAwait(false) as Dictionary; } /// 선물 계좌 조회. public async Task>> GetFutureAccountListAsync(CancellationToken ct = default) { var timestamp = (await GetTimeStampAsync(ct).ConfigureAwait(false)).ToString(); const string reqPath = "/api/v2/mix/account/accounts"; var nvc = new NameValueCollection { { "ACCESS-KEY", AccessKey }, { "ACCESS-SIGN", BitgetClient.GenerateSignString(timestamp, "GET", reqPath, "productType=USDT-FUTURES", null, SecretKey) }, { "ACCESS-PASSPHRASE", Passphrase }, { "ACCESS-TIMESTAMP", timestamp } }; return await _client.SendWebServerAsync( ExchangeWebReqType.BitgetAccountList_Future, BitgetEndpoints.AccountList_Future, headers: nvc, ct: ct).ConfigureAwait(false) as List>; } /// 주문 취소. public async Task> CancelFutureOrderAsync(string symbol, string orderId, CancellationToken ct = default) { var timestamp = (await GetTimeStampAsync(ct).ConfigureAwait(false)).ToString(); const string reqPath = "/api/v2/mix/order/cancel-order"; var postStr = "{\"symbol\" : \"" + symbol + "\", \"orderId\" : \"" + orderId + "\", \"productType\" : \"usdt-futures\" , \"marginCoin\" : \"USDT\"}"; var nvc = new NameValueCollection { { "ACCESS-KEY", AccessKey }, { "ACCESS-SIGN", BitgetClient.GenerateSignString(timestamp, "POST", reqPath, "", postStr, SecretKey) }, { "ACCESS-PASSPHRASE", Passphrase }, { "ACCESS-TIMESTAMP", timestamp } }; return await _client.SendWebServerAsync( ExchangeWebReqType.BitgetCancelOrder_Future, BitgetEndpoints.CancelOrder_Future, postData: postStr, headers: nvc, ct: ct).ConfigureAwait(false) as Dictionary; } /// 포지션 모드 변경 (one_way / hedge). public async Task> ChangeFuturePositionModeAsync(FuturePositionMode mode, CancellationToken ct = default) { var timestamp = (await GetTimeStampAsync(ct).ConfigureAwait(false)).ToString(); const string reqPath = "/api/v2/mix/account/set-position-mode"; var postStr = "{\"productType\" : \"USDT-FUTURES\", \"posMode\" : \"" + Convertor.GetEnumDescription(mode) + "\"}"; var nvc = new NameValueCollection { { "ACCESS-KEY", AccessKey }, { "ACCESS-SIGN", BitgetClient.GenerateSignString(timestamp, "POST", reqPath, "", postStr, SecretKey) }, { "ACCESS-PASSPHRASE", Passphrase }, { "ACCESS-TIMESTAMP", timestamp } }; return await _client.SendWebServerAsync( ExchangeWebReqType.BitgetChangePositionMode_Future, BitgetEndpoints.ChangePositionMode_Future, postData: postStr, headers: nvc, ct: ct).ConfigureAwait(false) as Dictionary; } /// 주문 상세 조회. public async Task> GetFutureOrderDetailAsync(string symbol, string orderId, CancellationToken ct = default) { var timestamp = (await GetTimeStampAsync(ct).ConfigureAwait(false)).ToString(); const string reqPath = "/api/v2/mix/order/detail"; var queryString = $"symbol={symbol}&orderId={orderId}&productType=USDT-FUTURES"; var nvc = new NameValueCollection { { "ACCESS-KEY", AccessKey }, { "ACCESS-SIGN", BitgetClient.GenerateSignString(timestamp, "GET", reqPath, queryString, null, SecretKey) }, { "ACCESS-PASSPHRASE", Passphrase }, { "ACCESS-TIMESTAMP", timestamp } }; var url = BitgetEndpoints.OrderDetail_Future + "?" + queryString; return await _client.SendWebServerAsync( ExchangeWebReqType.BitgetOrderDetail_Future, url, headers: nvc, ct: ct).ConfigureAwait(false) as Dictionary; } /// /// 선물 주문 (Buy는 선행적으로 MarginMode/Leverage 변경, Sell은 사전 Cancel). /// 기존 IndicatorProcess.FutureOrder 로직 그대로 이식. /// public async Task> FutureOrderAsync( string symbol, FutureMarginMode marginMode, double size, double price, FutureSide futureSide, OrderType orderType, double leverage, string id = "", CancellationToken ct = default) { var side = string.Empty; var tradeSide = string.Empty; switch (futureSide) { case FutureSide.BuyShort: id = Guid.NewGuid().ToString(); side = "sell"; tradeSide = "open"; break; case FutureSide.BuyLong: id = Guid.NewGuid().ToString(); side = "buy"; tradeSide = "Open"; break; case FutureSide.SellShort: side = "sell"; tradeSide = "close"; break; case FutureSide.SellLong: side = "buy"; tradeSide = "close"; break; } if (orderType == OrderType.market) price = 0; var timestamp = (await GetTimeStampAsync(ct).ConfigureAwait(false)).ToString(); // Buy 주문: Margin Mode → Leverage → Order 체인 if (futureSide == FutureSide.BuyShort || futureSide == FutureSide.BuyLong) { var changeMarginModeStr = "{\"symbol\": \"" + symbol + "\", \"productType\": \"usdt-futures\", \"marginCoin\": \"USDT\", \"marginMode\": \"" + Convertor.GetEnumDescription(marginMode) + "\"}"; var nvcMargin = new NameValueCollection { { "ACCESS-KEY", AccessKey }, { "ACCESS-SIGN", BitgetClient.GenerateSignString(timestamp, "POST", "/api/v2/mix/account/set-margin-mode", "", changeMarginModeStr, SecretKey) }, { "ACCESS-PASSPHRASE", Passphrase }, { "ACCESS-TIMESTAMP", timestamp } }; var marginResult = await _client.SendWebServerAsync( ExchangeWebReqType.BitgetChangeMarginMode_Future, BitgetEndpoints.ChangeMarginMode_Future, postData: changeMarginModeStr, headers: nvcMargin, ct: ct).ConfigureAwait(false) as Dictionary; if (marginResult == null || marginResult["msg"]?.ToString() != "success") { _logger.LogWarning("MarginMode 변경 실패: {Symbol}", symbol); return null; } var leverageStr = "{\"symbol\": \"" + symbol + "\", \"productType\": \"USDT-FUTURES\", \"marginCoin\": \"USDT\", \"leverage\": \"" + leverage.ToString() + "\", \"holdSide\": \"" + (futureSide == FutureSide.BuyShort ? "short" : "long") + "\"}"; timestamp = (await GetTimeStampAsync(ct).ConfigureAwait(false)).ToString(); var nvcLeverage = new NameValueCollection { { "ACCESS-KEY", AccessKey }, { "ACCESS-SIGN", BitgetClient.GenerateSignString(timestamp, "POST", "/api/v2/mix/account/set-leverage", "", leverageStr, SecretKey) }, { "ACCESS-PASSPHRASE", Passphrase }, { "ACCESS-TIMESTAMP", timestamp } }; var leverageResult = await _client.SendWebServerAsync( ExchangeWebReqType.BitgetChangeLeverage_Future, BitgetEndpoints.ChangeLeverage_Future, postData: leverageStr, headers: nvcLeverage, ct: ct).ConfigureAwait(false) as Dictionary; if (leverageResult == null || leverageResult["msg"]?.ToString() != "success") { _logger.LogWarning("Leverage 변경 실패: {Symbol}", symbol); return null; } } else { // Sell 주문: 미체결 주문 취소 후 진행 var cancelResult = await CancelFutureOrderAsync(symbol, id, ct).ConfigureAwait(false); if (cancelResult == null || cancelResult["msg"]?.ToString() != "success") _logger.LogError("Sell Cancel 실패: {Symbol} / {OrderId}", symbol, id); } // 실제 주문 전송 const string reqPath = "/api/v2/mix/order/place-order"; var postStr = "{\"symbol\": \"" + symbol + "\", \"productType\": \"usdt-futures\", \"marginMode\": \"" + Convertor.GetEnumDescription(marginMode) + "\", \"marginCoin\": \"USDT\", \"size\": \"" + size.ToString() + "\", \"price\": \"" + price.ToString() + "\", \"side\": \"" + side + "\", \"tradeSide\": \"" + tradeSide + "\", \"orderType\": \"" + Convertor.GetEnumDescription(orderType) + "\", \"force\": \"gtc\", \"clientOid\": \"" + id + "\" }"; timestamp = (await GetTimeStampAsync(ct).ConfigureAwait(false)).ToString(); var nvc = new NameValueCollection { { "ACCESS-KEY", AccessKey }, { "ACCESS-SIGN", BitgetClient.GenerateSignString(timestamp, "POST", reqPath, "", postStr, SecretKey) }, { "ACCESS-PASSPHRASE", Passphrase }, { "ACCESS-TIMESTAMP", timestamp } }; return await _client.SendWebServerAsync( ExchangeWebReqType.BitgetOrder_Future, BitgetEndpoints.BuySellOrder_Future, postData: postStr, headers: nvc, ct: ct).ConfigureAwait(false) as Dictionary; } }