diff --git a/src/Managing.Application.Abstractions/Services/IWeb3ProxyService.cs b/src/Managing.Application.Abstractions/Services/IWeb3ProxyService.cs index 9eadc2f1..2a778e9e 100644 --- a/src/Managing.Application.Abstractions/Services/IWeb3ProxyService.cs +++ b/src/Managing.Application.Abstractions/Services/IWeb3ProxyService.cs @@ -1,4 +1,5 @@ using Managing.Domain.Accounts; +using Managing.Domain.Evm; using static Managing.Common.Enums; namespace Managing.Application.Abstractions.Services @@ -19,5 +20,7 @@ namespace Managing.Application.Abstractions.Services Task> GetWalletBalanceAsync(string address, Ticker[] assets, string[] chains); Task GetEstimatedGasFeeUsdAsync(); + + Task GetGasFeeDataAsync(); } } \ No newline at end of file diff --git a/src/Managing.Application/Bots/TradingBotBase.cs b/src/Managing.Application/Bots/TradingBotBase.cs index a9d85fb8..1ae0059e 100644 --- a/src/Managing.Application/Bots/TradingBotBase.cs +++ b/src/Managing.Application/Bots/TradingBotBase.cs @@ -409,7 +409,7 @@ public class TradingBotBase : ITradingBot { internalPosition.Open.SetStatus(TradeStatus.Filled); } - + // Also update the position in the bot's positions dictionary if (positionForSignal.Open != null) { @@ -436,7 +436,7 @@ public class TradingBotBase : ITradingBot { internalPosition.Open.SetStatus(TradeStatus.Filled); } - + // Also update the position in the bot's positions dictionary if (positionForSignal.Open != null) { @@ -515,19 +515,19 @@ public class TradingBotBase : ITradingBot // Calculate net PnL after fees for broker position var brokerNetPnL = brokerPosition.GetNetPnL(); UpdatePositionPnl(positionForSignal.Identifier, brokerNetPnL); - + // Update Open trade status when position is found on broker with 2 orders if (internalPosition.Open != null) { internalPosition.Open.SetStatus(TradeStatus.Filled); } - + // Also update the position in the bot's positions dictionary if (positionForSignal.Open != null) { positionForSignal.Open.SetStatus(TradeStatus.Filled); } - + await SetPositionStatus(signal.Identifier, PositionStatus.Filled); // Notify platform summary about the executed trade @@ -608,17 +608,18 @@ public class TradingBotBase : ITradingBot positionForSignal.StopLoss.SetPrice(executionPrice, 2); positionForSignal.StopLoss.SetDate(lastCandle.Date); positionForSignal.StopLoss.SetStatus(TradeStatus.Filled); - + // Cancel TP trades when SL is hit if (positionForSignal.TakeProfit1 != null) { positionForSignal.TakeProfit1.SetStatus(TradeStatus.Cancelled); } + if (positionForSignal.TakeProfit2 != null) { positionForSignal.TakeProfit2.SetStatus(TradeStatus.Cancelled); } - + await LogInformation( $"🛑 **Stop Loss Hit**\nClosing LONG position\nPrice: `${executionPrice:F2}` (was `${positionForSignal.StopLoss.Price:F2}`)"); await CloseTrade(signal, positionForSignal, positionForSignal.StopLoss, @@ -632,13 +633,13 @@ public class TradingBotBase : ITradingBot positionForSignal.TakeProfit1.SetPrice(executionPrice, 2); positionForSignal.TakeProfit1.SetDate(lastCandle.Date); positionForSignal.TakeProfit1.SetStatus(TradeStatus.Filled); - + // Cancel SL trade when TP is hit if (positionForSignal.StopLoss != null) { positionForSignal.StopLoss.SetStatus(TradeStatus.Cancelled); } - + await LogInformation( $"🎯 **Take Profit 1 Hit**\nClosing LONG position\nPrice: `${executionPrice:F2}` (was `${positionForSignal.TakeProfit1.Price:F2}`)"); await CloseTrade(signal, positionForSignal, positionForSignal.TakeProfit1, @@ -651,13 +652,13 @@ public class TradingBotBase : ITradingBot positionForSignal.TakeProfit2.SetPrice(executionPrice, 2); positionForSignal.TakeProfit2.SetDate(lastCandle.Date); positionForSignal.TakeProfit2.SetStatus(TradeStatus.Filled); - + // Cancel SL trade when TP is hit if (positionForSignal.StopLoss != null) { positionForSignal.StopLoss.SetStatus(TradeStatus.Cancelled); } - + await LogInformation( $"🎯 **Take Profit 2 Hit**\nClosing LONG position\nPrice: `${executionPrice:F2}` (was `${positionForSignal.TakeProfit2.Price:F2}`)"); await CloseTrade(signal, positionForSignal, positionForSignal.TakeProfit2, @@ -673,17 +674,18 @@ public class TradingBotBase : ITradingBot positionForSignal.StopLoss.SetPrice(executionPrice, 2); positionForSignal.StopLoss.SetDate(lastCandle.Date); positionForSignal.StopLoss.SetStatus(TradeStatus.Filled); - + // Cancel TP trades when SL is hit if (positionForSignal.TakeProfit1 != null) { positionForSignal.TakeProfit1.SetStatus(TradeStatus.Cancelled); } + if (positionForSignal.TakeProfit2 != null) { positionForSignal.TakeProfit2.SetStatus(TradeStatus.Cancelled); } - + await LogInformation( $"🛑 **Stop Loss Hit**\nClosing SHORT position\nPrice: `${executionPrice:F2}` (was `${positionForSignal.StopLoss.Price:F2}`)"); await CloseTrade(signal, positionForSignal, positionForSignal.StopLoss, @@ -697,13 +699,13 @@ public class TradingBotBase : ITradingBot positionForSignal.TakeProfit1.SetPrice(executionPrice, 2); positionForSignal.TakeProfit1.SetDate(lastCandle.Date); positionForSignal.TakeProfit1.SetStatus(TradeStatus.Filled); - + // Cancel SL trade when TP is hit if (positionForSignal.StopLoss != null) { positionForSignal.StopLoss.SetStatus(TradeStatus.Cancelled); } - + await LogInformation( $"🎯 **Take Profit 1 Hit**\nClosing SHORT position\nPrice: `${executionPrice:F2}` (was `${positionForSignal.TakeProfit1.Price:F2}`)"); await CloseTrade(signal, positionForSignal, positionForSignal.TakeProfit1, @@ -716,13 +718,13 @@ public class TradingBotBase : ITradingBot positionForSignal.TakeProfit2.SetPrice(executionPrice, 2); positionForSignal.TakeProfit2.SetDate(lastCandle.Date); positionForSignal.TakeProfit2.SetStatus(TradeStatus.Filled); - + // Cancel SL trade when TP is hit if (positionForSignal.StopLoss != null) { positionForSignal.StopLoss.SetStatus(TradeStatus.Cancelled); } - + await LogInformation( $"🎯 **Take Profit 2 Hit**\nClosing SHORT position\nPrice: `${executionPrice:F2}` (was `${positionForSignal.TakeProfit2.Price:F2}`)"); await CloseTrade(signal, positionForSignal, positionForSignal.TakeProfit2, @@ -795,7 +797,7 @@ public class TradingBotBase : ITradingBot { internalPosition.Open.SetStatus(TradeStatus.Filled); } - + // Also update the position in the bot's positions dictionary if (positionForSignal.Open != null) { @@ -1138,7 +1140,8 @@ public class TradingBotBase : ITradingBot } else { - var command = new ClosePositionCommand(position, position.AccountId, lastPrice, isForBacktest: Config.IsForBacktest); + var command = new ClosePositionCommand(position, position.AccountId, lastPrice, + isForBacktest: Config.IsForBacktest); try { Position closedPosition = null; @@ -1264,19 +1267,20 @@ public class TradingBotBase : ITradingBot if (wasStopLossHit) { // Use actual execution price based on direction - closingPrice = position.OriginDirection == TradeDirection.Long - ? minPriceRecent // For LONG, SL hits at the low + closingPrice = position.OriginDirection == TradeDirection.Long + ? minPriceRecent // For LONG, SL hits at the low : maxPriceRecent; // For SHORT, SL hits at the high - + position.StopLoss.SetPrice(closingPrice, 2); position.StopLoss.SetDate(currentCandle.Date); position.StopLoss.SetStatus(TradeStatus.Filled); - + // Cancel TP trades when SL is hit if (position.TakeProfit1 != null) { position.TakeProfit1.SetStatus(TradeStatus.Cancelled); } + if (position.TakeProfit2 != null) { position.TakeProfit2.SetStatus(TradeStatus.Cancelled); @@ -1291,14 +1295,14 @@ public class TradingBotBase : ITradingBot else if (wasTakeProfitHit) { // Use actual execution price based on direction - closingPrice = position.OriginDirection == TradeDirection.Long - ? maxPriceRecent // For LONG, TP hits at the high + closingPrice = position.OriginDirection == TradeDirection.Long + ? maxPriceRecent // For LONG, TP hits at the high : minPriceRecent; // For SHORT, TP hits at the low - + position.TakeProfit1.SetPrice(closingPrice, 2); position.TakeProfit1.SetDate(currentCandle.Date); position.TakeProfit1.SetStatus(TradeStatus.Filled); - + // Cancel SL trade when TP is hit if (position.StopLoss != null) { @@ -1331,7 +1335,7 @@ public class TradingBotBase : ITradingBot position.TakeProfit1.SetPrice(closingPrice, 2); position.TakeProfit1.SetDate(currentCandle.Date); position.TakeProfit1.SetStatus(TradeStatus.Filled); - + // Cancel SL trade when TP is used for manual close if (position.StopLoss != null) { @@ -1343,12 +1347,13 @@ public class TradingBotBase : ITradingBot position.StopLoss.SetPrice(closingPrice, 2); position.StopLoss.SetDate(currentCandle.Date); position.StopLoss.SetStatus(TradeStatus.Filled); - + // Cancel TP trades when SL is used for manual close if (position.TakeProfit1 != null) { position.TakeProfit1.SetStatus(TradeStatus.Cancelled); } + if (position.TakeProfit2 != null) { position.TakeProfit2.SetStatus(TradeStatus.Cancelled); @@ -2163,7 +2168,8 @@ public class TradingBotBase : ITradingBot PositionIdentifier = position.Identifier, Ticker = position.Ticker, Volume = position.Open.Price * position.Open.Quantity * position.Open.Leverage, - Fee = position.Open.Fee + Fee = position.GasFees + position.UiFees, + Direction = position.OriginDirection }; await platformGrain.OnPositionOpenAsync(positionOpenEvent); break; diff --git a/src/Managing.Domain/Evm/GasFeeData.cs b/src/Managing.Domain/Evm/GasFeeData.cs new file mode 100644 index 00000000..3dfc9040 --- /dev/null +++ b/src/Managing.Domain/Evm/GasFeeData.cs @@ -0,0 +1,70 @@ +using System.Text.Json.Serialization; + +namespace Managing.Domain.Evm +{ + /// + /// Gas fee data model + /// + public class GasFeeData + { + /// + /// Estimated gas fee in Wei + /// + [JsonPropertyName("estimatedGasFeeWei")] + public string? EstimatedGasFeeWei { get; set; } + + /// + /// Estimated gas fee in ETH + /// + [JsonPropertyName("estimatedGasFeeEth")] + public string? EstimatedGasFeeEth { get; set; } + + /// + /// Estimated gas fee in USD + /// + [JsonPropertyName("estimatedGasFeeUsd")] + public double? EstimatedGasFeeUsd { get; set; } + + /// + /// ETH balance of the account + /// + [JsonPropertyName("ethBalance")] + public string? EthBalance { get; set; } + + /// + /// Current ETH price in USD + /// + [JsonPropertyName("ethPrice")] + public double? EthPrice { get; set; } + + /// + /// Whether the account has sufficient balance for gas fees + /// + [JsonPropertyName("hasSufficientBalance")] + public bool? HasSufficientBalance { get; set; } + + /// + /// Error message if insufficient balance + /// + [JsonPropertyName("errorMessage")] + public string? ErrorMessage { get; set; } + + /// + /// Maximum allowed gas fee in USD + /// + [JsonPropertyName("maxAllowedUsd")] + public double? MaxAllowedUsd { get; set; } + + /// + /// Gas price in Wei + /// + [JsonPropertyName("gasPrice")] + public string? GasPrice { get; set; } + + /// + /// Gas limit + /// + [JsonPropertyName("gasLimit")] + public string? GasLimit { get; set; } + } +} diff --git a/src/Managing.Infrastructure.Web3/Models/Proxy/Web3ProxyError.cs b/src/Managing.Infrastructure.Web3/Models/Proxy/Web3ProxyError.cs index 7e6b9c39..b474d2aa 100644 --- a/src/Managing.Infrastructure.Web3/Models/Proxy/Web3ProxyError.cs +++ b/src/Managing.Infrastructure.Web3/Models/Proxy/Web3ProxyError.cs @@ -1,5 +1,6 @@ using System.Text.Json.Serialization; using Managing.Domain.Accounts; +using Managing.Domain.Evm; namespace Managing.Infrastructure.Evm.Models.Proxy { @@ -158,21 +159,10 @@ namespace Managing.Infrastructure.Evm.Models.Proxy public class GasFeeResponse : Web3ProxyResponse { /// - /// Estimated gas fee in USD + /// Gas fee data object /// - [JsonPropertyName("estimatedGasFeeUsd")] - public double? EstimatedGasFeeUsd { get; set; } - - /// - /// Current ETH price in USD - /// - [JsonPropertyName("ethPrice")] - public double? EthPrice { get; set; } - - /// - /// Gas price in Gwei - /// - [JsonPropertyName("gasPriceGwei")] - public double? GasPriceGwei { get; set; } + [JsonPropertyName("data")] + public GasFeeData? Data { get; set; } } + } \ No newline at end of file diff --git a/src/Managing.Infrastructure.Web3/Services/Web3ProxyService.cs b/src/Managing.Infrastructure.Web3/Services/Web3ProxyService.cs index 80adce68..d7ec03b3 100644 --- a/src/Managing.Infrastructure.Web3/Services/Web3ProxyService.cs +++ b/src/Managing.Infrastructure.Web3/Services/Web3ProxyService.cs @@ -7,6 +7,7 @@ using System.Web; using Managing.Application.Abstractions.Services; using Managing.Core.Exceptions; using Managing.Domain.Accounts; +using Managing.Domain.Evm; using Managing.Infrastructure.Evm.Models.Proxy; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -408,7 +409,34 @@ namespace Managing.Infrastructure.Evm.Services throw new Web3ProxyException($"Gas fee request failed: {response.Error}"); } - return (decimal)(response.EstimatedGasFeeUsd ?? 0); + if (response.Data is null) + { + throw new Web3ProxyException("Gas fee data is null"); + } + + return (decimal)(response.Data.EstimatedGasFeeUsd ?? 0); + } + + public async Task GetGasFeeDataAsync() + { + var response = await GetGmxServiceAsync("/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) diff --git a/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts b/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts index fe424a85..a5ff11b8 100644 --- a/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts +++ b/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts @@ -82,7 +82,7 @@ function checkMemoryUsage() { * @param estimatedGasFee The estimated gas fee in wei * @returns Object with balance check result and details */ -async function checkGasFeeBalance( +export async function checkGasFeeBalance( sdk: GmxSdk, estimatedGasFee: bigint ): Promise<{ @@ -151,9 +151,8 @@ async function checkGasFeeBalance( * @param params The position increase parameters * @returns Estimated gas fee in wei */ -async function estimatePositionGasFee( +export async function estimatePositionGasFee( sdk: GmxSdk, - params: PositionIncreaseParams ): Promise { try { // Estimate gas for the position opening transaction @@ -440,6 +439,7 @@ declare module 'fastify' { getGmxTrade: typeof getGmxTrade; getGmxPositions: typeof getGmxPositions; swapGmxTokens: typeof swapGmxTokens; + estimatePositionGasFee: typeof estimatePositionGasFee; } } @@ -633,7 +633,7 @@ export const openGmxPositionImpl = async ( // Check gas fees before opening position console.log('⛽ Checking gas fees before opening position...'); - const estimatedGasFee = await estimatePositionGasFee(sdk, params); + const estimatedGasFee = await estimatePositionGasFee(sdk); const gasFeeCheck = await checkGasFeeBalance(sdk, estimatedGasFee); if (!gasFeeCheck.hasSufficientBalance) { @@ -1417,6 +1417,7 @@ export default fp(async (fastify) => { fastify.decorateRequest('claimGmxUiFees', claimGmxUiFees) fastify.decorateRequest('swapGmxTokens', swapGmxTokens) fastify.decorateRequest('checkGmxTokenAllowances', checkGmxTokenAllowances) + fastify.decorateRequest('estimatePositionGasFee', estimatePositionGasFee) // Set up cache refresh without blocking plugin registration setupCacheRefresh(); diff --git a/src/Managing.Web3Proxy/src/routes/api/gmx/index.ts b/src/Managing.Web3Proxy/src/routes/api/gmx/index.ts index 858825aa..6f7613ab 100644 --- a/src/Managing.Web3Proxy/src/routes/api/gmx/index.ts +++ b/src/Managing.Web3Proxy/src/routes/api/gmx/index.ts @@ -2,11 +2,15 @@ import {FastifyPluginAsyncTypebox} from '@fastify/type-provider-typebox' import {Type} from '@sinclair/typebox' import {TradeDirection} from '../../../generated/ManagingApiTypes.js' import { + checkGasFeeBalance, + estimatePositionGasFee, getClaimableFundingFeesImpl, getClaimableUiFeesImpl, getGmxRebateStatsImpl } from '../../../plugins/custom/gmx.js' +const MAX_GAS_FEE_USD = 1.5; // Maximum gas fee in USD + const plugin: FastifyPluginAsyncTypebox = async (fastify) => { // Define route to open a position fastify.post('/open-position', { @@ -194,6 +198,67 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => { ) }) + // Define route to get gas fee estimation for opening a position + fastify.get('/gas-fee', { + schema: { + response: { + 200: Type.Object({ + success: Type.Boolean(), + data: Type.Optional(Type.Object({ + estimatedGasFeeWei: Type.String(), + estimatedGasFeeEth: Type.String(), + estimatedGasFeeUsd: Type.Number(), + ethBalance: Type.String(), + ethPrice: Type.Number(), + hasSufficientBalance: Type.Boolean(), + errorMessage: Type.Optional(Type.String()), + maxAllowedUsd: Type.Number(), + gasPrice: Type.String(), + gasLimit: Type.String() + })), + error: Type.Optional(Type.String()) + }) + } + } + }, async (request, reply) => { + try { + // Use a default address for gas fee estimation + const defaultAccount = "0x0000000000000000000000000000000000000000" + + // Get GMX client for the default account + const sdk = await request.getClientForAddress(defaultAccount) + + // Call the two methods as mentioned + const estimatedGasFee = await estimatePositionGasFee(sdk); + const gasFeeCheck = await checkGasFeeBalance(sdk, estimatedGasFee); + + // Format the response data + const data = { + estimatedGasFeeWei: estimatedGasFee.toString(), + estimatedGasFeeEth: (Number(estimatedGasFee) / 1e18).toFixed(6), + estimatedGasFeeUsd: gasFeeCheck.estimatedGasFeeUsd, + ethBalance: gasFeeCheck.ethBalance, + ethPrice: gasFeeCheck.ethPrice, + hasSufficientBalance: gasFeeCheck.hasSufficientBalance, + errorMessage: gasFeeCheck.errorMessage, + maxAllowedUsd: MAX_GAS_FEE_USD, + gasPrice: (Number(estimatedGasFee) / 500000).toString(), // Approximate gas price + gasLimit: "500000" // Base gas limit used in estimation + } + + return { + success: true, + data + } + } catch (error) { + console.error('Error estimating gas fee:', error) + return { + success: false, + error: `Failed to estimate gas fee: ${error instanceof Error ? error.message : 'Unknown error'}` + } + } + }) + // Define route to get all claimable fees and rebate stats fastify.get('/claimable-summary', { schema: {