286 lines
12 KiB
C#
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>;
|
|
}
|
|
}
|