Fetch closed position to get last pnl realized

This commit is contained in:
2025-10-05 23:31:17 +07:00
parent 1b060fb145
commit dac0a9641f
14 changed files with 749 additions and 23 deletions

View File

@@ -961,4 +961,22 @@ public class EvmManager : IEvmManager
{
return await GetCandles(ticker, startDate, timeframe, false);
}
public async Task<List<Position>> GetPositionHistory(
Account account,
Ticker ticker,
DateTime? fromDate = null,
DateTime? toDate = null,
int pageIndex = 0,
int pageSize = 20)
{
var result = await _web3ProxyService.GetGmxPositionHistoryAsync(
account.Key,
pageIndex,
pageSize,
ticker.ToString());
// Map the result to the Position domain object
return result;
}
}

View File

@@ -42,9 +42,21 @@ public class GmxPosition
[JsonProperty("collateral")] public double Collateral { get; set; }
[JsonProperty("markPrice")] public double MarkPrice { get; set; }
[JsonProperty("markPrice")] public double? MarkPrice { get; set; }
[JsonProperty("liquidationPrice")] public double LiquidationPrice { get; set; }
[JsonProperty("liquidationPrice")] public double? LiquidationPrice { get; set; }
[JsonProperty("ProfitAndLoss")]
[JsonPropertyName("ProfitAndLoss")]
public GmxPositionProfitAndLoss ProfitAndLoss { get; set; }
[JsonProperty("UiFees")]
[JsonPropertyName("UiFees")]
public double? UiFees { get; set; }
[JsonProperty("GasFees")]
[JsonPropertyName("GasFees")]
public double? GasFees { get; set; }
[JsonProperty("StopLoss")]
[JsonPropertyName("StopLoss")]
@@ -64,6 +76,26 @@ public class GetGmxPositionsResponse : Web3ProxyBaseResponse
[JsonProperty("positions")] public List<GmxPosition> Positions { get; set; }
}
public class GetGmxPositionHistoryResponse : Web3ProxyResponse
{
[JsonProperty("positions")] public List<GmxPosition> Positions { get; set; }
[JsonProperty("pageIndex")] public int PageIndex { get; set; }
[JsonProperty("pageSize")] public int PageSize { get; set; }
[JsonProperty("count")] public int Count { get; set; }
}
public class GmxPositionProfitAndLoss
{
[JsonProperty("realized")] public double? Realized { get; set; }
[JsonProperty("net")] public double? Net { get; set; }
[JsonProperty("averageOpenPrice")] public double? AverageOpenPrice { get; set; }
}
public class StopLoss
{
[JsonProperty("id")] public string Id { get; set; }

View File

@@ -5,9 +5,11 @@ 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;
@@ -36,10 +38,10 @@ namespace Managing.Infrastructure.Evm.Services
{
_settings = options.Value;
_logger = logger;
_httpClient = new HttpClient();
_httpClient.Timeout = TimeSpan.FromSeconds(_settings.TimeoutSeconds);
_jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
@@ -62,7 +64,7 @@ namespace Managing.Infrastructure.Evm.Services
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);
@@ -90,12 +92,12 @@ namespace Managing.Infrastructure.Evm.Services
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("execution reverted") &&
(lowerError.Contains("allowance") || lowerError.Contains("balance")) ||
lowerError.Contains("gas required exceeds allowance") ||
lowerError.Contains("out of gas") ||
@@ -115,20 +117,20 @@ namespace Managing.Infrastructure.Evm.Services
var lowerError = errorMessage.ToLowerInvariant();
if (lowerError.Contains("erc20: transfer amount exceeds allowance") ||
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") ||
if (lowerError.Contains("gas required exceeds allowance") ||
lowerError.Contains("out of gas") ||
lowerError.Contains("insufficient eth"))
{
return InsufficientFundsType.InsufficientEth;
}
if (lowerError.Contains("insufficient balance") ||
if (lowerError.Contains("insufficient balance") ||
lowerError.Contains("insufficient token"))
{
return InsufficientFundsType.InsufficientBalance;
@@ -142,7 +144,7 @@ namespace Managing.Infrastructure.Evm.Services
try
{
var response = await _retryPolicy.ExecuteAsync(httpCall);
if (!response.IsSuccessStatusCode)
{
await HandleErrorResponse(response);
@@ -164,12 +166,13 @@ namespace Managing.Infrastructure.Evm.Services
}
}
private async Task<T> ExecuteWithRetryAsync<T>(Func<Task<HttpResponseMessage>> httpCall, string operationName, string idempotencyKey)
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);
@@ -185,7 +188,9 @@ namespace Managing.Infrastructure.Evm.Services
}
catch (Exception ex) when (!(ex is Web3ProxyException))
{
_logger.LogError(ex, "Operation {OperationName} failed after all retry attempts (IdempotencyKey: {IdempotencyKey})", operationName, idempotencyKey);
_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}");
}
@@ -202,7 +207,8 @@ namespace Managing.Infrastructure.Evm.Services
var idempotencyKey = Guid.NewGuid().ToString();
return await ExecuteWithRetryAsync<T>(
() => {
() =>
{
var request = new HttpRequestMessage(HttpMethod.Post, url)
{
Content = JsonContent.Create(payload, options: _jsonOptions)
@@ -244,7 +250,8 @@ namespace Managing.Infrastructure.Evm.Services
var idempotencyKey = Guid.NewGuid().ToString();
return await ExecuteWithRetryAsync<T>(
() => {
() =>
{
var request = new HttpRequestMessage(HttpMethod.Post, url)
{
Content = JsonContent.Create(payload, options: _jsonOptions)
@@ -472,14 +479,15 @@ namespace Managing.Infrastructure.Evm.Services
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 InsufficientFundsException(structuredErrorResponse.ErrorDetails.Message,
errorType);
}
throw new Web3ProxyException(structuredErrorResponse.ErrorDetails);
}
}
@@ -496,7 +504,7 @@ namespace Managing.Infrastructure.Evm.Services
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}");
}
@@ -511,14 +519,14 @@ namespace Managing.Infrastructure.Evm.Services
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}");
}
}
@@ -580,5 +588,107 @@ namespace Managing.Infrastructure.Evm.Services
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;
}
}
}