Files
BitGet_API/Bitget_Server/Application/Services/BitgetTradingService.cs
2026-04-20 20:27:42 +09:00

286 lines
12 KiB
C#

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;
/// <summary>
/// 기존 IndicatorProcess의 Private API 영역(주문/취소/잔고/포지션모드 변경/레버리지 변경/체결상세)을 이식한 서비스.
/// HMAC-SHA256 서명은 BitgetClient.GenerateSignString을 사용한다.
/// 외부 호출 경로는 BitgetClient.SendWebServerAsync → BitgetRateLimiter(20/sec) 순으로 흐른다.
/// </summary>
public class BitgetTradingService
{
private readonly BitgetClient _client;
private readonly GlobalOptions _options;
private readonly ILogger<BitgetTradingService> _logger;
public BitgetTradingService(
BitgetClient client,
GlobalOptions options,
ILogger<BitgetTradingService> 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;
/// <summary>Bitget 서버 타임스탬프 조회.</summary>
public async Task<long> GetTimeStampAsync(CancellationToken ct = default)
{
var result = await _client.SendWebServerAsync(
ExchangeWebReqType.BitgetGetTimeStamp,
BitgetEndpoints.GetTimestamp,
ct: ct).ConfigureAwait(false) as Dictionary<string, object>;
if (result == null) return 0;
var data = JsonConvert.DeserializeObject<Dictionary<string, object>>(
JsonConvert.SerializeObject(result["data"]));
long.TryParse(data["serverTime"].ToString(), out var ts);
return ts;
}
/// <summary>미체결 주문 조회.</summary>
public async Task<Dictionary<string, object>> 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<string, object>;
}
/// <summary>선물 계좌 조회.</summary>
public async Task<List<Dictionary<string, object>>> 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<Dictionary<string, object>>;
}
/// <summary>주문 취소.</summary>
public async Task<Dictionary<string, object>> 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<string, object>;
}
/// <summary>포지션 모드 변경 (one_way / hedge).</summary>
public async Task<Dictionary<string, object>> 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<string, object>;
}
/// <summary>주문 상세 조회.</summary>
public async Task<Dictionary<string, object>> 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<string, object>;
}
/// <summary>
/// 선물 주문 (Buy는 선행적으로 MarginMode/Leverage 변경, Sell은 사전 Cancel).
/// 기존 IndicatorProcess.FutureOrder 로직 그대로 이식.
/// </summary>
public async Task<Dictionary<string, object>> 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<string, object>;
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<string, object>;
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<string, object>;
}
}