Fetch closed position to get last pnl realized
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user