diff --git a/src/Managing.Api/HealthChecks/Web3ProxyHealthCheck.cs b/src/Managing.Api/HealthChecks/Web3ProxyHealthCheck.cs index ed7cbdf..7aec7dd 100644 --- a/src/Managing.Api/HealthChecks/Web3ProxyHealthCheck.cs +++ b/src/Managing.Api/HealthChecks/Web3ProxyHealthCheck.cs @@ -109,6 +109,11 @@ namespace Managing.Api.HealthChecks gmxData["responseTimeMs"] = responseTimeElement.GetInt32(); } + if (gmxDataElement.TryGetProperty("uiFeeFactor", out var uiFeeFactorElement)) + { + gmxData["uiFeeFactor"] = uiFeeFactorElement.GetString(); + } + if (gmxDataElement.TryGetProperty("sampleMarkets", out var sampleMarketsElement)) { var sampleMarkets = new List>(); diff --git a/src/Managing.Application/Bots/TradingBot.cs b/src/Managing.Application/Bots/TradingBot.cs index d995036..2115923 100644 --- a/src/Managing.Application/Bots/TradingBot.cs +++ b/src/Managing.Application/Bots/TradingBot.cs @@ -347,6 +347,11 @@ public class TradingBot : Bot, ITradingBot if (!IsForBacktest) { position = positionsExchange.FirstOrDefault(p => p.Ticker == Ticker); + + if (position != null) + { + UpdatePositionPnl(positionForSignal.Identifier, position.ProfitAndLoss.Realized); + } } if (position.Status == PositionStatus.New) @@ -411,8 +416,6 @@ public class TradingBot : Bot, ITradingBot { Logger.LogInformation( $"Position {signal.Identifier} don't need to be update. Position still opened"); - - await SetPositionStatus(signal.Identifier, PositionStatus.Filled); } } @@ -447,10 +450,6 @@ public class TradingBot : Bot, ITradingBot { Logger.LogInformation( $"Position {signal.Identifier} don't need to be update. Position still opened"); - - position.Status = PositionStatus.Filled; - - await SetPositionStatus(signal.Identifier, PositionStatus.Filled); } } } @@ -468,8 +467,8 @@ public class TradingBot : Bot, ITradingBot } catch (Exception ex) { - Logger.LogError(ex, ex.Message); - //SentrySdk.CaptureException(ex); + await LogWarning($"Cannot update position {positionForSignal.Identifier}: {ex.Message}"); + SentrySdk.CaptureException(ex); return; } } @@ -727,6 +726,14 @@ public class TradingBot : Bot, ITradingBot positionStatus == PositionStatus.Filled ? SignalStatus.PositionOpen : SignalStatus.Expired); } + private void UpdatePositionPnl(string identifier, decimal realized) + { + Positions.First(p => p.Identifier == identifier).ProfitAndLoss = new ProfitAndLoss() + { + Realized = realized + }; + } + private void SetSignalStatus(string signalIdentifier, SignalStatus signalStatus) { if (Signals.Any(s => s.Identifier == signalIdentifier)) diff --git a/src/Managing.Infrastructure.Web3/Services/Gmx/GmxV2Mappers.cs b/src/Managing.Infrastructure.Web3/Services/Gmx/GmxV2Mappers.cs index c95df09..0c42391 100644 --- a/src/Managing.Infrastructure.Web3/Services/Gmx/GmxV2Mappers.cs +++ b/src/Managing.Infrastructure.Web3/Services/Gmx/GmxV2Mappers.cs @@ -3,19 +3,19 @@ using Managing.ABI.GmxV2.SyntheticsReader.ContractDefinition; using Managing.Common; using Managing.Core; using Managing.Domain.Candles; +using Managing.Domain.MoneyManagements; using Managing.Domain.Trades; +using Managing.Domain.Users; using Managing.Infrastructure.Evm.Models.Gmx.v2; using Managing.Infrastructure.Evm.Models.Proxy; using Nethereum.Web3; -using Managing.Domain.MoneyManagements; -using Managing.Domain.Users; using static Managing.Common.Enums; namespace Managing.Infrastructure.Evm.Services.Gmx; internal static class GmxV2Mappers { - public static Trade Map(GmxV2Position position, Enums.Ticker ticker) + public static Trade Map(GmxV2Position position, Ticker ticker) { var entryPrice = GmxV2Helpers.GetEntryPrice(position.SizeInUsd, position.SizeInTokens, position.TokenData.Decimals); @@ -24,9 +24,9 @@ internal static class GmxV2Mappers var collateralLeverage = CalculateCollateralAndLeverage(position.SizeInUsd, position.CollateralAmount); var trade = new Trade( DateHelpers.GetFromUnixTimestamp((int)position.IncreasedAtTime), - position.IsLong ? Enums.TradeDirection.Long : Enums.TradeDirection.Short, - Enums.TradeStatus.Filled, - Enums.TradeType.Limit, + position.IsLong ? TradeDirection.Long : TradeDirection.Short, + TradeStatus.Filled, + TradeType.Limit, ticker, collateralLeverage.collateral / parsedEntryPrice, parsedEntryPrice, @@ -56,8 +56,8 @@ internal static class GmxV2Mappers var trade = new Trade( order.Date, - order.IsLong ? Enums.TradeDirection.Long : Enums.TradeDirection.Short, - Enums.TradeStatus.PendingOpen, + order.IsLong ? TradeDirection.Long : TradeDirection.Short, + TradeStatus.PendingOpen, GmxV2Helpers.GetTradeType(order.OrderType), ticker, Convert.ToDecimal(quantity), @@ -111,7 +111,7 @@ internal static class GmxV2Mappers }).ToList(); } - public static Candle Map(List marketPrices, Enums.Ticker ticker, Enums.Timeframe timeframe, int timeBetween) + public static Candle Map(List marketPrices, Ticker ticker, Timeframe timeframe, int timeBetween) { return new Candle() { @@ -121,20 +121,20 @@ internal static class GmxV2Mappers High = Convert.ToDecimal(marketPrices[2]), Low = Convert.ToDecimal(marketPrices[3]), Close = Convert.ToDecimal(marketPrices[4]), - Exchange = Enums.TradingExchanges.Evm, + Exchange = TradingExchanges.Evm, Ticker = ticker.ToString(), Timeframe = timeframe }; } - public static List Map(GmxV2TokenList tokenList) + public static List Map(GmxV2TokenList tokenList) { - var tokens = new List(); + var tokens = new List(); foreach (var t in tokenList.Tokens) { try { - var ticker = MiscExtensions.ParseEnum(t.Symbol); + var ticker = MiscExtensions.ParseEnum(t.Symbol); tokens.Add(ticker); } catch (Exception e) @@ -187,7 +187,7 @@ internal static class GmxV2Mappers position.ProfitAndLoss = new ProfitAndLoss() { - Net = (decimal)gmxPosition.Pnl + Realized = (decimal)gmxPosition.Pnl }; position.Status = MiscExtensions.ParseEnum(gmxPosition.Status); diff --git a/src/Managing.Web3Proxy/src/routes/home.ts b/src/Managing.Web3Proxy/src/routes/home.ts index 3ec08f8..d2f565b 100644 --- a/src/Managing.Web3Proxy/src/routes/home.ts +++ b/src/Managing.Web3Proxy/src/routes/home.ts @@ -131,11 +131,17 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => { const marketsInfo = await sdk.markets.getMarketsInfo(); const responseTime = Date.now() - startTime; + // Get the uiFeeFactor + const uiFeeFactor = await sdk.utils.getUiFeeFactor(); + if (!marketsInfo.marketsInfoData || Object.keys(marketsInfo.marketsInfoData).length === 0) { return { status: 'degraded', message: 'GMX SDK returned empty markets info data', - data: { responseTimeMs: responseTime } + data: { + responseTimeMs: responseTime, + uiFeeFactor: uiFeeFactor.toString() + } }; } @@ -168,7 +174,8 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => { message: 'ETH market not found in GMX markets data', data: { availableMarkets: marketInfoDetails, - responseTimeMs: responseTime + responseTimeMs: responseTime, + uiFeeFactor: uiFeeFactor.toString() } }; } @@ -179,7 +186,8 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => { data: { marketCount: Object.keys(marketsInfo.marketsInfoData).length, responseTimeMs: responseTime, - sampleMarkets: marketInfoDetails.slice(0, 3) // Just include first 3 markets for brevity + sampleMarkets: marketInfoDetails.slice(0, 3), // Just include first 3 markets for brevity + uiFeeFactor: uiFeeFactor.toString() } }; } catch (error) { diff --git a/src/Managing.WebApp/src/pages/settingsPage/healthchecks/healthChecks.tsx b/src/Managing.WebApp/src/pages/settingsPage/healthchecks/healthChecks.tsx index ca937e0..78f4e75 100644 --- a/src/Managing.WebApp/src/pages/settingsPage/healthchecks/healthChecks.tsx +++ b/src/Managing.WebApp/src/pages/settingsPage/healthchecks/healthChecks.tsx @@ -151,6 +151,9 @@ const HealthChecks: React.FC = () => { if (gmxData.data.responseTimeMs) { marketDetails['Response Time'] = `${gmxData.data.responseTimeMs}ms`; } + if (gmxData.data.uiFeeFactor) { + marketDetails['UI Fee Factor'] = gmxData.data.uiFeeFactor; + } // Add sample markets info (just count for details section) if (gmxData.data.sampleMarkets && Array.isArray(gmxData.data.sampleMarkets)) { diff --git a/src/Managing.WebApp/src/pages/toolsPage/feeCalculator.tsx b/src/Managing.WebApp/src/pages/toolsPage/feeCalculator.tsx index 064c93c..8e145bd 100644 --- a/src/Managing.WebApp/src/pages/toolsPage/feeCalculator.tsx +++ b/src/Managing.WebApp/src/pages/toolsPage/feeCalculator.tsx @@ -1,9 +1,10 @@ import {useState} from 'react' const FeeCalculator: React.FC = () => { - const [volumeUsd, setVolumeUsd] = useState(1000000) - const [uiFeePercent, setUiFeePercent] = useState(0.002) - const [rebatePercent, setRebatePercent] = useState(10) + const [volumeUsd, setVolumeUsd] = useState(1400000) + const [uiFeePercent, setUiFeePercent] = useState(0.02) + const [rebatePercent, setRebatePercent] = useState(20) + const [fundingFeeRate, setFundingFeeRate] = useState(0.00371) // Based on example: ~0.065 USD for 1751 USD volume // Based on the specific example: 10% rebate on $1751 = $0.0983 // This means at 10% rebate, $1 of volume = $0.0000561393 rebated @@ -18,32 +19,39 @@ const FeeCalculator: React.FC = () => { // For 10% rebate on $1751 resulting in $0.0983 rebate const dailyRebate = volumeUsd * BASE_REBATE_PER_DOLLAR * (rebatePercent / 10) // Normalize to 10% base - // Total revenue is UI fees plus rebates - const dailyTotalRevenue = dailyUiFees + dailyRebate + // Calculate funding fees based on the provided example (0.065 USD for 1751 USD) + const dailyFundingFees = volumeUsd * (fundingFeeRate / 100) + + // Total revenue is UI fees + rebates + funding fees + const dailyTotalRevenue = dailyUiFees + dailyRebate + dailyFundingFees return { daily: { volume: volumeUsd, uiFees: dailyUiFees, rebate: dailyRebate, + fundingFees: dailyFundingFees, totalRevenue: dailyTotalRevenue }, weekly: { volume: volumeUsd * 7, uiFees: dailyUiFees * 7, rebate: dailyRebate * 7, + fundingFees: dailyFundingFees * 7, totalRevenue: dailyTotalRevenue * 7 }, monthly: { volume: volumeUsd * 30, uiFees: dailyUiFees * 30, rebate: dailyRebate * 30, + fundingFees: dailyFundingFees * 30, totalRevenue: dailyTotalRevenue * 30 }, yearly: { volume: volumeUsd * 365, uiFees: dailyUiFees * 365, rebate: dailyRebate * 365, + fundingFees: dailyFundingFees * 365, totalRevenue: dailyTotalRevenue * 365 } } @@ -51,15 +59,16 @@ const FeeCalculator: React.FC = () => { const fees = calculateFees() - // Test calculation: For $1751 with 10% rebate + // Test calculations const testVolume = 1751 const testRebate = testVolume * BASE_REBATE_PER_DOLLAR * (rebatePercent / 10) + const testFundingFee = testVolume * (fundingFeeRate / 100) return (

Platform Revenue Calculator

-
+
+ +
+ + setFundingFeeRate(Number(e.target.value))} + className="input input-bordered" + step="0.0001" + /> +
@@ -108,6 +131,7 @@ const FeeCalculator: React.FC = () => { Volume (USD) UI Fees (USD) Rebates (USD) + Funding Fees (USD) Total Revenue (USD) @@ -117,6 +141,7 @@ const FeeCalculator: React.FC = () => { ${fees.daily.volume.toLocaleString(undefined, {maximumFractionDigits: 0})} ${fees.daily.uiFees.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})} ${fees.daily.rebate.toLocaleString(undefined, {minimumFractionDigits: 4, maximumFractionDigits: 4})} + ${fees.daily.fundingFees.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})} ${fees.daily.totalRevenue.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})} @@ -124,6 +149,7 @@ const FeeCalculator: React.FC = () => { ${fees.weekly.volume.toLocaleString(undefined, {maximumFractionDigits: 0})} ${fees.weekly.uiFees.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})} ${fees.weekly.rebate.toLocaleString(undefined, {minimumFractionDigits: 4, maximumFractionDigits: 4})} + ${fees.weekly.fundingFees.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})} ${fees.weekly.totalRevenue.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})} @@ -131,6 +157,7 @@ const FeeCalculator: React.FC = () => { ${fees.monthly.volume.toLocaleString(undefined, {maximumFractionDigits: 0})} ${fees.monthly.uiFees.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})} ${fees.monthly.rebate.toLocaleString(undefined, {minimumFractionDigits: 4, maximumFractionDigits: 4})} + ${fees.monthly.fundingFees.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})} ${fees.monthly.totalRevenue.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})} @@ -138,6 +165,7 @@ const FeeCalculator: React.FC = () => { ${fees.yearly.volume.toLocaleString(undefined, {maximumFractionDigits: 0})} ${fees.yearly.uiFees.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})} ${fees.yearly.rebate.toLocaleString(undefined, {minimumFractionDigits: 4, maximumFractionDigits: 4})} + ${fees.yearly.fundingFees.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})} ${fees.yearly.totalRevenue.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})} @@ -145,8 +173,12 @@ const FeeCalculator: React.FC = () => {
-
- Verification: For a volume of $1751 with 10% rebate, the calculated rebate is ${testRebate.toFixed(4)} USD +
+
+ Verification: For a volume of $1751: +
+
- With 10% rebate, the calculated rebate is ${testRebate.toFixed(4)} USD
+
- The funding fee is ${testFundingFee.toFixed(2)} USD
@@ -155,7 +187,8 @@ const FeeCalculator: React.FC = () => {
  • UI Fee: Charged on top of the base fee for transactions. For position operations, it's a percentage of the increase/decrease size.
  • Rebate: Based on a ratio where $1 of volume with a 10% rebate generates $0.0000561393 of rebate. The actual rebate scales with both volume and rebate percentage.
  • -
  • Total Revenue: The combined income from both UI fees and rebates.
  • +
  • Funding Fee: Fees generated by agents based on the trading volume. This is a percentage of the total volume.
  • +
  • Total Revenue: The combined income from UI fees, rebates, and funding fees.

Fee Details

@@ -164,7 +197,7 @@ const FeeCalculator: React.FC = () => { The uiFeeFactor is a percentage value over 10^30. For example, if the uiFeeFactor is 2 * 10^25, the percentage charged would be 0.002%.

- Both UI fees and rebates contribute to the platform's revenue stream and can be used to cover on-chain transaction costs and other operational expenses. + Both UI fees, rebates, and funding fees contribute to the platform's revenue stream and can be used to cover on-chain transaction costs and other operational expenses.