694 lines
27 KiB
C#
694 lines
27 KiB
C#
using System.Collections;
|
|
using System.Net;
|
|
using System.Net.Http.Json;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Web;
|
|
using Managing.Application.Abstractions.Services;
|
|
using Managing.Core;
|
|
using Managing.Core.Exceptions;
|
|
using Managing.Domain.Accounts;
|
|
using Managing.Domain.Evm;
|
|
using Managing.Domain.Trades;
|
|
using Managing.Infrastructure.Evm.Models.Proxy;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using Polly;
|
|
using static Managing.Common.Enums;
|
|
|
|
namespace Managing.Infrastructure.Evm.Services
|
|
{
|
|
public class Web3ProxySettings
|
|
{
|
|
public string BaseUrl { get; set; } = "http://localhost:3000";
|
|
public int MaxRetryAttempts { get; set; } = 3;
|
|
public int RetryDelayMs { get; set; } = 1000;
|
|
public int TimeoutSeconds { get; set; } = 30;
|
|
}
|
|
|
|
public class Web3ProxyService : IWeb3ProxyService
|
|
{
|
|
private readonly HttpClient _httpClient;
|
|
private readonly Web3ProxySettings _settings;
|
|
private readonly JsonSerializerOptions _jsonOptions;
|
|
private readonly IAsyncPolicy<HttpResponseMessage> _retryPolicy;
|
|
private readonly ILogger<Web3ProxyService> _logger;
|
|
|
|
public Web3ProxyService(IOptions<Web3ProxySettings> options, ILogger<Web3ProxyService> logger)
|
|
{
|
|
_settings = options.Value;
|
|
_logger = logger;
|
|
|
|
_httpClient = new HttpClient();
|
|
_httpClient.Timeout = TimeSpan.FromSeconds(_settings.TimeoutSeconds);
|
|
|
|
_jsonOptions = new JsonSerializerOptions
|
|
{
|
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
|
};
|
|
|
|
// Configure retry policy
|
|
_retryPolicy = Policy
|
|
.Handle<HttpRequestException>()
|
|
.Or<TaskCanceledException>()
|
|
.Or<TimeoutException>()
|
|
.OrResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode && IsRetryableStatusCode(r.StatusCode))
|
|
.WaitAndRetryAsync(
|
|
retryCount: _settings.MaxRetryAttempts,
|
|
sleepDurationProvider: retryAttempt => TimeSpan.FromMilliseconds(
|
|
_settings.RetryDelayMs * Math.Pow(2, retryAttempt - 1) + // Exponential backoff
|
|
new Random().Next(0, _settings.RetryDelayMs / 4) // Add jitter
|
|
),
|
|
onRetry: (outcome, timespan, retryCount, context) =>
|
|
{
|
|
var exception = outcome.Exception;
|
|
var response = outcome.Result;
|
|
var errorMessage = exception?.Message ?? $"HTTP {response?.StatusCode}";
|
|
|
|
_logger.LogWarning(
|
|
"Web3Proxy request failed (attempt {RetryCount}/{MaxRetries}): {Error}. Retrying in {Delay}ms",
|
|
retryCount, _settings.MaxRetryAttempts + 1, errorMessage, timespan.TotalMilliseconds);
|
|
});
|
|
}
|
|
|
|
private static bool IsRetryableStatusCode(HttpStatusCode statusCode)
|
|
{
|
|
return statusCode == HttpStatusCode.RequestTimeout ||
|
|
statusCode == HttpStatusCode.TooManyRequests ||
|
|
statusCode == HttpStatusCode.InternalServerError ||
|
|
statusCode == HttpStatusCode.BadGateway ||
|
|
statusCode == HttpStatusCode.ServiceUnavailable ||
|
|
statusCode == HttpStatusCode.GatewayTimeout;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if an error message indicates insufficient funds or allowance that should not be retried
|
|
/// </summary>
|
|
/// <param name="errorMessage">The error message to check</param>
|
|
/// <returns>True if this is a non-retryable insufficient funds error</returns>
|
|
private static bool IsInsufficientFundsError(string errorMessage)
|
|
{
|
|
if (string.IsNullOrEmpty(errorMessage))
|
|
return false;
|
|
|
|
var lowerError = errorMessage.ToLowerInvariant();
|
|
|
|
// Check for common insufficient funds/allowance error patterns
|
|
return lowerError.Contains("erc20: transfer amount exceeds allowance") ||
|
|
lowerError.Contains("insufficient funds") ||
|
|
lowerError.Contains("insufficient balance") ||
|
|
lowerError.Contains("execution reverted") &&
|
|
(lowerError.Contains("allowance") || lowerError.Contains("balance")) ||
|
|
lowerError.Contains("gas required exceeds allowance") ||
|
|
lowerError.Contains("out of gas") ||
|
|
lowerError.Contains("insufficient eth") ||
|
|
lowerError.Contains("insufficient token");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines the type of insufficient funds error based on the error message
|
|
/// </summary>
|
|
/// <param name="errorMessage">The error message to analyze</param>
|
|
/// <returns>The type of insufficient funds error</returns>
|
|
private static InsufficientFundsType GetInsufficientFundsType(string errorMessage)
|
|
{
|
|
if (string.IsNullOrEmpty(errorMessage))
|
|
return InsufficientFundsType.General;
|
|
|
|
var lowerError = errorMessage.ToLowerInvariant();
|
|
|
|
if (lowerError.Contains("erc20: transfer amount exceeds allowance") ||
|
|
lowerError.Contains("allowance"))
|
|
{
|
|
return InsufficientFundsType.InsufficientAllowance;
|
|
}
|
|
|
|
if (lowerError.Contains("gas required exceeds allowance") ||
|
|
lowerError.Contains("out of gas") ||
|
|
lowerError.Contains("insufficient eth"))
|
|
{
|
|
return InsufficientFundsType.InsufficientEth;
|
|
}
|
|
|
|
if (lowerError.Contains("insufficient balance") ||
|
|
lowerError.Contains("insufficient token"))
|
|
{
|
|
return InsufficientFundsType.InsufficientBalance;
|
|
}
|
|
|
|
return InsufficientFundsType.General;
|
|
}
|
|
|
|
private async Task<T> ExecuteWithRetryAsync<T>(Func<Task<HttpResponseMessage>> httpCall, string operationName)
|
|
{
|
|
try
|
|
{
|
|
var response = await _retryPolicy.ExecuteAsync(httpCall);
|
|
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
await HandleErrorResponse(response);
|
|
}
|
|
|
|
var result = await response.Content.ReadFromJsonAsync<T>(_jsonOptions);
|
|
return result ?? throw new Web3ProxyException($"Failed to deserialize response for {operationName}");
|
|
}
|
|
catch (InsufficientFundsException)
|
|
{
|
|
// Re-throw insufficient funds exceptions immediately without retrying
|
|
throw;
|
|
}
|
|
catch (Exception ex) when (!(ex is Web3ProxyException))
|
|
{
|
|
_logger.LogError(ex, "Operation {OperationName} failed after all retry attempts", operationName);
|
|
SentrySdk.CaptureException(ex);
|
|
throw new Web3ProxyException($"Failed to execute {operationName}: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private async Task<T> ExecuteWithRetryAsync<T>(Func<Task<HttpResponseMessage>> httpCall, string operationName,
|
|
string idempotencyKey)
|
|
{
|
|
try
|
|
{
|
|
var response = await _retryPolicy.ExecuteAsync(httpCall);
|
|
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
await HandleErrorResponse(response);
|
|
}
|
|
|
|
var result = await response.Content.ReadFromJsonAsync<T>(_jsonOptions);
|
|
return result ?? throw new Web3ProxyException($"Failed to deserialize response for {operationName}");
|
|
}
|
|
catch (InsufficientFundsException)
|
|
{
|
|
// Re-throw insufficient funds exceptions immediately without retrying
|
|
throw;
|
|
}
|
|
catch (Exception ex) when (!(ex is Web3ProxyException))
|
|
{
|
|
_logger.LogError(ex,
|
|
"Operation {OperationName} failed after all retry attempts (IdempotencyKey: {IdempotencyKey})",
|
|
operationName, idempotencyKey);
|
|
SentrySdk.CaptureException(ex);
|
|
throw new Web3ProxyException($"Failed to execute {operationName}: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
public async Task<T> CallPrivyServiceAsync<T>(string endpoint, object payload)
|
|
{
|
|
if (!endpoint.StartsWith("/"))
|
|
{
|
|
endpoint = $"/{endpoint}";
|
|
}
|
|
|
|
var url = $"{_settings.BaseUrl}/api/privy{endpoint}";
|
|
var idempotencyKey = Guid.NewGuid().ToString();
|
|
|
|
return await ExecuteWithRetryAsync<T>(
|
|
() =>
|
|
{
|
|
var request = new HttpRequestMessage(HttpMethod.Post, url)
|
|
{
|
|
Content = JsonContent.Create(payload, options: _jsonOptions)
|
|
};
|
|
request.Headers.Add("Idempotency-Key", idempotencyKey);
|
|
return _httpClient.SendAsync(request);
|
|
},
|
|
$"CallPrivyServiceAsync({endpoint})",
|
|
idempotencyKey);
|
|
}
|
|
|
|
public async Task<T> GetPrivyServiceAsync<T>(string endpoint, object? payload = null)
|
|
{
|
|
if (!endpoint.StartsWith("/"))
|
|
{
|
|
endpoint = $"/{endpoint}";
|
|
}
|
|
|
|
var url = $"{_settings.BaseUrl}/api/privy{endpoint}";
|
|
|
|
if (payload != null)
|
|
{
|
|
url += BuildQueryString(payload);
|
|
}
|
|
|
|
return await ExecuteWithRetryAsync<T>(
|
|
() => _httpClient.GetAsync(url),
|
|
$"GetPrivyServiceAsync({endpoint})");
|
|
}
|
|
|
|
public async Task<T> CallGmxServiceAsync<T>(string endpoint, object payload)
|
|
{
|
|
if (!endpoint.StartsWith("/"))
|
|
{
|
|
endpoint = $"/{endpoint}";
|
|
}
|
|
|
|
var url = $"{_settings.BaseUrl}/api/gmx{endpoint}";
|
|
var idempotencyKey = Guid.NewGuid().ToString();
|
|
|
|
return await ExecuteWithRetryAsync<T>(
|
|
() =>
|
|
{
|
|
var request = new HttpRequestMessage(HttpMethod.Post, url)
|
|
{
|
|
Content = JsonContent.Create(payload, options: _jsonOptions)
|
|
};
|
|
request.Headers.Add("Idempotency-Key", idempotencyKey);
|
|
return _httpClient.SendAsync(request);
|
|
},
|
|
$"CallGmxServiceAsync({endpoint})",
|
|
idempotencyKey);
|
|
}
|
|
|
|
public async Task<T> GetGmxServiceAsync<T>(string endpoint, object? payload = null)
|
|
{
|
|
if (!endpoint.StartsWith("/"))
|
|
{
|
|
endpoint = $"/{endpoint}";
|
|
}
|
|
|
|
var url = $"{_settings.BaseUrl}/api/gmx{endpoint}";
|
|
|
|
if (payload != null)
|
|
{
|
|
url += BuildQueryString(payload);
|
|
}
|
|
|
|
return await ExecuteWithRetryAsync<T>(
|
|
() => _httpClient.GetAsync(url),
|
|
$"GetGmxServiceAsync({endpoint})");
|
|
}
|
|
|
|
public async Task<GmxClaimableSummary> GetGmxClaimableSummaryAsync(string account)
|
|
{
|
|
var payload = new { account };
|
|
var response = await GetGmxServiceAsync<ClaimingFeesResponse>("/claimable-summary", payload);
|
|
|
|
|
|
if (response.Data == null)
|
|
{
|
|
throw new Web3ProxyException("GMX claimable summary data is null");
|
|
}
|
|
|
|
// Map from Web3Proxy response model to domain model
|
|
return new GmxClaimableSummary
|
|
{
|
|
ClaimableFundingFees = new FundingFeesData
|
|
{
|
|
TotalUsdc = response.Data.ClaimableFundingFees.TotalUsdc
|
|
},
|
|
ClaimableUiFees = new UiFeesData
|
|
{
|
|
TotalUsdc = response.Data.ClaimableUiFees.TotalUsdc
|
|
},
|
|
RebateStats = new RebateStatsData
|
|
{
|
|
TotalRebateUsdc = response.Data.RebateStats.TotalRebateUsdc,
|
|
DiscountUsdc = response.Data.RebateStats.DiscountUsdc,
|
|
RebateFactor = response.Data.RebateStats.RebateFactor,
|
|
DiscountFactor = response.Data.RebateStats.DiscountFactor
|
|
}
|
|
};
|
|
}
|
|
|
|
public async Task<SwapInfos> SwapGmxTokensAsync(string account, Ticker fromTicker, Ticker toTicker,
|
|
double amount, string orderType = "market", double? triggerRatio = null, double allowedSlippage = 0.5)
|
|
{
|
|
var payload = new
|
|
{
|
|
account,
|
|
fromTicker = fromTicker.ToString(),
|
|
toTicker = toTicker.ToString(),
|
|
amount,
|
|
orderType,
|
|
triggerRatio,
|
|
allowedSlippage
|
|
};
|
|
|
|
var response = await CallGmxServiceAsync<GmxSwapResponse>("/swap-tokens", payload);
|
|
|
|
if (response == null)
|
|
{
|
|
throw new Web3ProxyException("GMX swap response is null");
|
|
}
|
|
|
|
// Map from infrastructure model to domain model
|
|
return new SwapInfos
|
|
{
|
|
Success = response.Success,
|
|
Hash = response.Hash,
|
|
Message = response.Message,
|
|
Error = null, // GmxSwapResponse doesn't have Error property
|
|
ErrorType = response.ErrorType,
|
|
Suggestion = response.Suggestion
|
|
};
|
|
}
|
|
|
|
public async Task<SwapInfos> SendTokenAsync(string senderAddress, string recipientAddress, Ticker ticker,
|
|
decimal amount, int? chainId = null)
|
|
{
|
|
var payload = new
|
|
{
|
|
senderAddress,
|
|
recipientAddress,
|
|
ticker = ticker.ToString(),
|
|
amount = amount.ToString(), // Convert decimal to string for bigint compatibility
|
|
chainId
|
|
};
|
|
|
|
var response = await CallPrivyServiceAsync<Web3ProxyTokenSendResponse>("/send-token", payload);
|
|
|
|
if (response == null)
|
|
{
|
|
throw new Web3ProxyException("Token send response is null");
|
|
}
|
|
|
|
// Map from infrastructure model to domain model
|
|
return new SwapInfos
|
|
{
|
|
Success = response.Success,
|
|
Hash = response.Hash,
|
|
Message = null, // Web3ProxyTokenSendResponse doesn't have Message property
|
|
Error = response.Error,
|
|
ErrorType = null, // Web3ProxyTokenSendResponse doesn't have ErrorType property
|
|
Suggestion = null // Web3ProxyTokenSendResponse doesn't have Suggestion property
|
|
};
|
|
}
|
|
|
|
public async Task<List<Balance>> GetWalletBalanceAsync(string address, Ticker[] assets, string[] chains)
|
|
{
|
|
var payload = new
|
|
{
|
|
address,
|
|
asset = assets,
|
|
chain = chains
|
|
};
|
|
|
|
var response = await GetPrivyServiceAsync<Web3ProxyBalanceResponse>("/wallet-balance", payload);
|
|
|
|
if (response == null)
|
|
{
|
|
throw new Web3ProxyException("Wallet balance response is null");
|
|
}
|
|
|
|
if (!response.Success)
|
|
{
|
|
throw new Web3ProxyException($"Wallet balance request failed: {response.Error}");
|
|
}
|
|
|
|
return response.Balances ?? new List<Balance>();
|
|
}
|
|
|
|
public async Task<decimal> GetEstimatedGasFeeUsdAsync()
|
|
{
|
|
var response = await GetGmxServiceAsync<GasFeeResponse>("/gas-fee", null);
|
|
|
|
if (response == null)
|
|
{
|
|
throw new Web3ProxyException("Gas fee response is null");
|
|
}
|
|
|
|
if (!response.Success)
|
|
{
|
|
throw new Web3ProxyException($"Gas fee request failed: {response.Error}");
|
|
}
|
|
|
|
if (response.Data is null)
|
|
{
|
|
throw new Web3ProxyException("Gas fee data is null");
|
|
}
|
|
|
|
return (decimal)(response.Data.EstimatedGasFeeUsd ?? 0);
|
|
}
|
|
|
|
public async Task<GasFeeData> GetGasFeeDataAsync()
|
|
{
|
|
var response = await GetGmxServiceAsync<GasFeeResponse>("/gas-fee", null);
|
|
|
|
if (response == null)
|
|
{
|
|
throw new Web3ProxyException("Gas fee response is null");
|
|
}
|
|
|
|
if (!response.Success)
|
|
{
|
|
throw new Web3ProxyException($"Gas fee request failed: {response.Error}");
|
|
}
|
|
|
|
if (response.Data is null)
|
|
{
|
|
throw new Web3ProxyException("Gas fee data is null");
|
|
}
|
|
|
|
return response.Data;
|
|
}
|
|
|
|
private async Task HandleErrorResponse(HttpResponseMessage response)
|
|
{
|
|
var statusCode = (int)response.StatusCode;
|
|
|
|
try
|
|
{
|
|
// Try to parse as the Web3Proxy error format (success: false, error: string)
|
|
var content = await response.Content.ReadAsStringAsync();
|
|
var errorResponse = await response.Content.ReadFromJsonAsync<Web3ProxyResponse>(_jsonOptions);
|
|
|
|
if (errorResponse != null && !errorResponse.Success && !string.IsNullOrEmpty(errorResponse.Error))
|
|
{
|
|
// Check if this is an insufficient funds error that should not be retried
|
|
if (IsInsufficientFundsError(errorResponse.Error))
|
|
{
|
|
var errorType = GetInsufficientFundsType(errorResponse.Error);
|
|
throw new InsufficientFundsException(errorResponse.Error, errorType);
|
|
}
|
|
|
|
// Handle the standard Web3Proxy error format
|
|
throw new Web3ProxyException(errorResponse.Error);
|
|
}
|
|
|
|
// Fallback for other error formats
|
|
try
|
|
{
|
|
// Try to parse as structured error if it doesn't match the simple format
|
|
var structuredErrorResponse =
|
|
await response.Content.ReadFromJsonAsync<Web3ProxyErrorResponse>(_jsonOptions);
|
|
|
|
if (structuredErrorResponse?.ErrorDetails != null)
|
|
{
|
|
structuredErrorResponse.ErrorDetails.StatusCode = statusCode;
|
|
|
|
// Check if this is an insufficient funds error that should not be retried
|
|
if (IsInsufficientFundsError(structuredErrorResponse.ErrorDetails.Message))
|
|
{
|
|
var errorType = GetInsufficientFundsType(structuredErrorResponse.ErrorDetails.Message);
|
|
throw new InsufficientFundsException(structuredErrorResponse.ErrorDetails.Message,
|
|
errorType);
|
|
}
|
|
|
|
throw new Web3ProxyException(structuredErrorResponse.ErrorDetails);
|
|
}
|
|
}
|
|
catch (Exception ex) when (ex is InsufficientFundsException)
|
|
{
|
|
// Re-throw insufficient funds exceptions as-is
|
|
throw;
|
|
}
|
|
catch
|
|
{
|
|
// Check if the raw content contains insufficient funds errors
|
|
if (IsInsufficientFundsError(content))
|
|
{
|
|
var errorType = GetInsufficientFundsType(content);
|
|
throw new InsufficientFundsException(content, errorType);
|
|
}
|
|
|
|
// If we couldn't parse as structured error, use the simple error or fallback
|
|
throw new Web3ProxyException($"HTTP error {statusCode}: {content}");
|
|
}
|
|
}
|
|
catch (Exception ex) when (ex is InsufficientFundsException)
|
|
{
|
|
// Re-throw insufficient funds exceptions as-is
|
|
throw;
|
|
}
|
|
catch (Exception ex) when (!(ex is Web3ProxyException))
|
|
{
|
|
SentrySdk.CaptureException(ex);
|
|
// If we couldn't parse the error as JSON or another issue occurred
|
|
var content = await response.Content.ReadAsStringAsync();
|
|
|
|
// Check if the raw content contains insufficient funds errors
|
|
if (IsInsufficientFundsError(content))
|
|
{
|
|
var errorType = GetInsufficientFundsType(content);
|
|
throw new InsufficientFundsException(content, errorType);
|
|
}
|
|
|
|
throw new Web3ProxyException($"HTTP error {statusCode}: {content}");
|
|
}
|
|
}
|
|
|
|
private string BuildQueryString(object payload)
|
|
{
|
|
var properties = payload.GetType().GetProperties();
|
|
if (properties.Length == 0)
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
var queryString = new StringBuilder("?");
|
|
bool isFirst = true;
|
|
|
|
foreach (var prop in properties)
|
|
{
|
|
var value = prop.GetValue(payload);
|
|
if (value != null)
|
|
{
|
|
var paramName = prop.Name;
|
|
// Apply camelCase to match JSON property naming
|
|
paramName = char.ToLowerInvariant(paramName[0]) + paramName.Substring(1);
|
|
|
|
// Check if the value is an array or enumerable (but not string)
|
|
if (value is IEnumerable enumerable && !(value is string))
|
|
{
|
|
// Handle arrays by creating multiple query parameters with the same name
|
|
foreach (var item in enumerable)
|
|
{
|
|
if (!isFirst)
|
|
{
|
|
queryString.Append("&");
|
|
}
|
|
|
|
queryString.Append(HttpUtility.UrlEncode(paramName));
|
|
queryString.Append("=");
|
|
queryString.Append(HttpUtility.UrlEncode(item?.ToString() ?? ""));
|
|
|
|
isFirst = false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Handle single values
|
|
if (!isFirst)
|
|
{
|
|
queryString.Append("&");
|
|
}
|
|
|
|
queryString.Append(HttpUtility.UrlEncode(paramName));
|
|
queryString.Append("=");
|
|
queryString.Append(HttpUtility.UrlEncode(value.ToString()));
|
|
|
|
isFirst = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return queryString.ToString();
|
|
}
|
|
|
|
public async Task<List<Position>> GetGmxPositionHistoryAsync(string account, int pageIndex = 0,
|
|
int pageSize = 20, string? ticker = null)
|
|
{
|
|
var payload = new
|
|
{
|
|
account,
|
|
pageIndex,
|
|
pageSize,
|
|
ticker
|
|
};
|
|
|
|
var response = await GetGmxServiceAsync<GetGmxPositionHistoryResponse>("/position-history", payload);
|
|
|
|
if (response == null)
|
|
{
|
|
throw new Web3ProxyException("Position history response is null");
|
|
}
|
|
|
|
if (!response.Success)
|
|
{
|
|
throw new Web3ProxyException($"Position history request failed: {response.Error}");
|
|
}
|
|
|
|
// Map to domain Positions manually
|
|
var positions = new List<Position>();
|
|
if (response.Positions != null)
|
|
{
|
|
foreach (var g in response.Positions)
|
|
{
|
|
try
|
|
{
|
|
var tickerEnum = MiscExtensions.ParseEnum<Ticker>(g.Ticker);
|
|
var directionEnum = MiscExtensions.ParseEnum<TradeDirection>(g.Direction);
|
|
|
|
var position = new Position(
|
|
Guid.NewGuid(),
|
|
0, // AccountId unknown at this layer
|
|
directionEnum,
|
|
tickerEnum,
|
|
null, // MoneyManagement not available here
|
|
PositionInitiator.Bot,
|
|
g.Date,
|
|
null // User not available here
|
|
);
|
|
|
|
position.Status = MiscExtensions.ParseEnum<PositionStatus>(g.Status);
|
|
|
|
// Open trade represents the closing event from GMX history
|
|
if (g.Open != null)
|
|
{
|
|
position.Open = new Trade(
|
|
g.Open.Date,
|
|
MiscExtensions.ParseEnum<TradeDirection>(g.Open.Direction),
|
|
MiscExtensions.ParseEnum<TradeStatus>(g.Open.Status),
|
|
TradeType.Market,
|
|
tickerEnum,
|
|
(decimal)g.Open.Quantity,
|
|
(decimal)g.Open.Price,
|
|
(decimal)g.Open.Leverage,
|
|
g.Open.ExchangeOrderId,
|
|
string.Empty
|
|
);
|
|
}
|
|
else
|
|
{
|
|
position.Open = new Trade(
|
|
g.Date,
|
|
directionEnum,
|
|
TradeStatus.Filled,
|
|
TradeType.Market,
|
|
tickerEnum,
|
|
(decimal)g.Quantity,
|
|
(decimal)g.Price,
|
|
(decimal)g.Leverage,
|
|
g.ExchangeOrderId,
|
|
"GMX Position Close"
|
|
);
|
|
}
|
|
|
|
position.ProfitAndLoss = new ProfitAndLoss
|
|
{
|
|
Realized = g.ProfitAndLoss?.Realized != null
|
|
? (decimal)g.ProfitAndLoss.Realized
|
|
: (decimal)g.Pnl,
|
|
Net = g.ProfitAndLoss?.Net != null ? (decimal)g.ProfitAndLoss.Net : (decimal)g.Pnl
|
|
};
|
|
|
|
position.UiFees = g.UiFees.HasValue ? (decimal)g.UiFees.Value : 0;
|
|
position.GasFees = g.GasFees.HasValue ? (decimal)g.GasFees.Value : 0;
|
|
|
|
positions.Add(position);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Failed to map GMX position history entry");
|
|
}
|
|
}
|
|
}
|
|
|
|
return positions;
|
|
}
|
|
}
|
|
} |