Update account/position and platform summary

This commit is contained in:
2025-09-26 01:18:59 +07:00
parent b2e38811ed
commit bcfeb693ce
32 changed files with 3301 additions and 151 deletions

View File

@@ -96,7 +96,8 @@ public class TradingController : BaseController
public async Task<ActionResult<Position>> ClosePosition(Guid identifier) public async Task<ActionResult<Position>> ClosePosition(Guid identifier)
{ {
var position = await _tradingService.GetPositionByIdentifierAsync(identifier); var position = await _tradingService.GetPositionByIdentifierAsync(identifier);
var result = await _closeTradeCommandHandler.Handle(new ClosePositionCommand(position));
var result = await _closeTradeCommandHandler.Handle(new ClosePositionCommand(position, position.AccountId));
return Ok(result); return Ok(result);
} }

View File

@@ -5,6 +5,7 @@ namespace Managing.Application.Abstractions.Repositories;
public interface IAccountRepository public interface IAccountRepository
{ {
Task<Account> GetAccountByNameAsync(string name); Task<Account> GetAccountByNameAsync(string name);
Task<Account> GetAccountByIdAsync(int id);
Task<Account> GetAccountByKeyAsync(string key); Task<Account> GetAccountByKeyAsync(string key);
Task InsertAccountAsync(Account account); Task InsertAccountAsync(Account account);
Task UpdateAccountAsync(Account account); Task UpdateAccountAsync(Account account);

View File

@@ -24,6 +24,15 @@ public interface IAccountService
/// <returns>The found account or null if not found</returns> /// <returns>The found account or null if not found</returns>
Task<Account> GetAccountByAccountName(string accountName, bool hideSecrets = true, bool getBalance = false); Task<Account> GetAccountByAccountName(string accountName, bool hideSecrets = true, bool getBalance = false);
/// <summary>
/// Gets an account by ID directly from the repository.
/// </summary>
/// <param name="accountId">The ID of the account to find</param>
/// <param name="hideSecrets">Whether to hide sensitive information</param>
/// <param name="getBalance">Whether to fetch the current balance</param>
/// <returns>The found account or null if not found</returns>
Task<Account> GetAccountById(int accountId, bool hideSecrets = true, bool getBalance = false);
IEnumerable<Account> GetAccountsBalancesByUser(User user, bool hideSecrets = true); IEnumerable<Account> GetAccountsBalancesByUser(User user, bool hideSecrets = true);
Task<IEnumerable<Account>> GetAccountsBalancesByUserAsync(User user, bool hideSecrets = true); Task<IEnumerable<Account>> GetAccountsBalancesByUserAsync(User user, bool hideSecrets = true);
Task<GmxClaimableSummary> GetGmxClaimableSummaryAsync(User user, string accountName); Task<GmxClaimableSummary> GetGmxClaimableSummaryAsync(User user, string accountName);

View File

@@ -46,13 +46,13 @@ public class PositionTests : BaseTests
// _ = new GetAccountPositioqwnInfoListOutputDTO().DecodeOutput(hexPositions).d // _ = new GetAccountPositioqwnInfoListOutputDTO().DecodeOutput(hexPositions).d
// //
var openTrade = await _exchangeService.GetTrade(_account, "", Ticker.GMX); var openTrade = await _exchangeService.GetTrade(_account, "", Ticker.GMX);
var position = new Position(Guid.NewGuid(), "", TradeDirection.Long, Ticker.GMX, MoneyManagement, var position = new Position(Guid.NewGuid(), 1, TradeDirection.Long, Ticker.GMX, MoneyManagement,
PositionInitiator.User, PositionInitiator.User,
DateTime.UtcNow, new User()) DateTime.UtcNow, new User())
{ {
Open = openTrade Open = openTrade
}; };
var command = new ClosePositionCommand(position); var command = new ClosePositionCommand(position, 1);
_ = _tradingService.Setup(m => m.GetPositionByIdentifierAsync(It.IsAny<Guid>())).ReturnsAsync(position); _ = _tradingService.Setup(m => m.GetPositionByIdentifierAsync(It.IsAny<Guid>())).ReturnsAsync(position);
_ = _tradingService.Setup(m => m.GetPositionByIdentifierAsync(It.IsAny<Guid>())).ReturnsAsync(position); _ = _tradingService.Setup(m => m.GetPositionByIdentifierAsync(It.IsAny<Guid>())).ReturnsAsync(position);

View File

@@ -214,7 +214,7 @@ namespace Managing.Application.Tests
private static Position GetFakeShortPosition() private static Position GetFakeShortPosition()
{ {
return new Position(Guid.NewGuid(), "FakeAccount", TradeDirection.Short, Ticker.BTC, null, return new Position(Guid.NewGuid(), 1, TradeDirection.Short, Ticker.BTC, null,
PositionInitiator.PaperTrading, DateTime.UtcNow, new User()) PositionInitiator.PaperTrading, DateTime.UtcNow, new User())
{ {
Open = new Trade(DateTime.Now, TradeDirection.Short, TradeStatus.Filled, Open = new Trade(DateTime.Now, TradeDirection.Short, TradeStatus.Filled,
@@ -230,7 +230,7 @@ namespace Managing.Application.Tests
private static Position GetSolanaLongPosition() private static Position GetSolanaLongPosition()
{ {
return new Position(Guid.NewGuid(), "FakeAccount", TradeDirection.Long, Ticker.BTC, null, return new Position(Guid.NewGuid(), 1, TradeDirection.Long, Ticker.BTC, null,
PositionInitiator.PaperTrading, DateTime.UtcNow, new User()) PositionInitiator.PaperTrading, DateTime.UtcNow, new User())
{ {
Open = new Trade(DateTime.Now, TradeDirection.Long, TradeStatus.Filled, Open = new Trade(DateTime.Now, TradeDirection.Long, TradeStatus.Filled,
@@ -250,7 +250,7 @@ namespace Managing.Application.Tests
private static Position GetFakeLongPosition() private static Position GetFakeLongPosition()
{ {
return new Position(Guid.NewGuid(), "FakeAccount", TradeDirection.Long, Ticker.BTC, null, return new Position(Guid.NewGuid(), 1, TradeDirection.Long, Ticker.BTC, null,
PositionInitiator.PaperTrading, DateTime.UtcNow, new User()) PositionInitiator.PaperTrading, DateTime.UtcNow, new User())
{ {
Open = new Trade(DateTime.Now, TradeDirection.Short, TradeStatus.Filled, Open = new Trade(DateTime.Now, TradeDirection.Short, TradeStatus.Filled,

View File

@@ -149,6 +149,25 @@ public class AccountService : IAccountService
return account; return account;
} }
public async Task<Account> GetAccountById(int accountId, bool hideSecrets = true, bool getBalance = false)
{
var account = await _accountRepository.GetAccountByIdAsync(accountId);
if (account == null)
{
throw new ArgumentException($"Account with ID '{accountId}' not found");
}
await ManagePropertiesAsync(hideSecrets, getBalance, account);
if (account.User != null)
{
account.User = await _userRepository.GetUserByNameAsync(account.User.Name);
}
return account;
}
public async Task<IEnumerable<Account>> GetAccounts(bool hideSecrets, bool getBalance) public async Task<IEnumerable<Account>> GetAccounts(bool hideSecrets, bool getBalance)
{ {
return await GetAccountsAsync(hideSecrets, getBalance); return await GetAccountsAsync(hideSecrets, getBalance);

View File

@@ -398,8 +398,10 @@ public class TradingBotBase : ITradingBot
var brokerPosition = brokerPositions.FirstOrDefault(p => p.Ticker == Config.Ticker); var brokerPosition = brokerPositions.FirstOrDefault(p => p.Ticker == Config.Ticker);
if (brokerPosition != null) if (brokerPosition != null)
{ {
UpdatePositionPnl(positionForSignal.Identifier, brokerPosition.ProfitAndLoss.Realized); // Calculate net PnL after fees for broker position
internalPosition.ProfitAndLoss = brokerPosition.ProfitAndLoss; var brokerNetPnL = brokerPosition.GetNetPnL();
UpdatePositionPnl(positionForSignal.Identifier, brokerNetPnL);
internalPosition.ProfitAndLoss = new ProfitAndLoss { Realized = brokerNetPnL };
internalPosition.Status = PositionStatus.Filled; internalPosition.Status = PositionStatus.Filled;
// Update Open trade status when position is found on broker // Update Open trade status when position is found on broker
@@ -510,7 +512,9 @@ public class TradingBotBase : ITradingBot
await LogInformation( await LogInformation(
$"✅ **Position Found on Broker**\nPosition is already open on broker\nUpdating position status to Filled"); $"✅ **Position Found on Broker**\nPosition is already open on broker\nUpdating position status to Filled");
UpdatePositionPnl(positionForSignal.Identifier, brokerPosition.ProfitAndLoss.Realized); // 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 // Update Open trade status when position is found on broker with 2 orders
if (internalPosition.Open != null) if (internalPosition.Open != null)
@@ -779,7 +783,12 @@ public class TradingBotBase : ITradingBot
{ {
// Update the internal position with broker data // Update the internal position with broker data
internalPosition.Status = PositionStatus.Filled; internalPosition.Status = PositionStatus.Filled;
internalPosition.ProfitAndLoss = internalPosition.ProfitAndLoss; // Apply fees to the internal position PnL before saving
if (internalPosition.ProfitAndLoss != null)
{
var totalFees = internalPosition.CalculateTotalFees();
internalPosition.ProfitAndLoss.Realized = internalPosition.ProfitAndLoss.Realized - totalFees;
}
// Update Open trade status when position is updated to Filled // Update Open trade status when position is updated to Filled
if (internalPosition.Open != null) if (internalPosition.Open != null)
@@ -1129,7 +1138,7 @@ public class TradingBotBase : ITradingBot
} }
else else
{ {
var command = new ClosePositionCommand(position, lastPrice, isForBacktest: Config.IsForBacktest); var command = new ClosePositionCommand(position, position.AccountId, lastPrice, isForBacktest: Config.IsForBacktest);
try try
{ {
Position closedPosition = null; Position closedPosition = null;
@@ -1375,6 +1384,9 @@ public class TradingBotBase : ITradingBot
{ {
position.ProfitAndLoss.Realized = pnl; position.ProfitAndLoss.Realized = pnl;
} }
// Fees are now tracked separately in UiFees and GasFees properties
// No need to subtract fees from PnL as they're tracked separately
} }
await SetPositionStatus(position.SignalIdentifier, PositionStatus.Finished); await SetPositionStatus(position.SignalIdentifier, PositionStatus.Finished);
@@ -1536,14 +1548,15 @@ public class TradingBotBase : ITradingBot
public decimal GetProfitAndLoss() public decimal GetProfitAndLoss()
{ {
var pnl = Positions.Values.Where(p => p.ProfitAndLoss != null && p.IsFinished()) // Calculate net PnL after deducting fees for each position
.Sum(p => p.ProfitAndLoss.Realized); var netPnl = Positions.Values.Where(p => p.ProfitAndLoss != null && p.IsFinished())
return pnl - GetTotalFees(); .Sum(p => p.GetNetPnL());
return netPnl;
} }
/// <summary> /// <summary>
/// Calculates the total fees paid by the trading bot for each position. /// Calculates the total fees paid by the trading bot for each position.
/// Includes UI fees (0.1% of position size) and network fees ($0.50 for opening). /// Includes UI fees (0.1% of position size) and network fees ($0.15 for opening).
/// Closing fees are handled by oracle, so no network fee for closing. /// Closing fees are handled by oracle, so no network fee for closing.
/// </summary> /// </summary>
/// <returns>Returns the total fees paid as a decimal value.</returns> /// <returns>Returns the total fees paid as a decimal value.</returns>
@@ -1553,63 +1566,12 @@ public class TradingBotBase : ITradingBot
foreach (var position in Positions.Values.Where(p => p.Open.Price > 0 && p.Open.Quantity > 0)) foreach (var position in Positions.Values.Where(p => p.Open.Price > 0 && p.Open.Quantity > 0))
{ {
totalFees += CalculatePositionFees(position); totalFees += TradingHelpers.CalculatePositionFees(position);
} }
return totalFees; return totalFees;
} }
/// <summary>
/// Calculates the total fees for a specific position based on GMX V2 fee structure
/// </summary>
/// <param name="position">The position to calculate fees for</param>
/// <returns>The total fees for the position</returns>
private decimal CalculatePositionFees(Position position)
{
decimal fees = 0;
// Calculate position size in USD (leverage is already included in quantity calculation)
var positionSizeUsd = (position.Open.Price * position.Open.Quantity) * position.Open.Leverage;
// UI Fee: 0.1% of position size paid on opening
var uiFeeRate = 0.001m; // 0.1%
var uiFeeOpen = positionSizeUsd * uiFeeRate; // Fee paid on opening
fees += uiFeeOpen;
// UI Fee: 0.1% of position size paid on closing - only if position was actually closed
// Check which closing trade was executed (StopLoss, TakeProfit1, or TakeProfit2)
// Calculate closing fee based on the actual executed trade's price and quantity
if (position.StopLoss?.Status == TradeStatus.Filled)
{
var stopLossPositionSizeUsd =
(position.StopLoss.Price * position.StopLoss.Quantity) * position.StopLoss.Leverage;
var uiFeeClose = stopLossPositionSizeUsd * uiFeeRate; // Fee paid on closing via StopLoss
fees += uiFeeClose;
}
else if (position.TakeProfit1?.Status == TradeStatus.Filled)
{
var takeProfit1PositionSizeUsd = (position.TakeProfit1.Price * position.TakeProfit1.Quantity) *
position.TakeProfit1.Leverage;
var uiFeeClose = takeProfit1PositionSizeUsd * uiFeeRate; // Fee paid on closing via TakeProfit1
fees += uiFeeClose;
}
else if (position.TakeProfit2?.Status == TradeStatus.Filled)
{
var takeProfit2PositionSizeUsd = (position.TakeProfit2.Price * position.TakeProfit2.Quantity) *
position.TakeProfit2.Leverage;
var uiFeeClose = takeProfit2PositionSizeUsd * uiFeeRate; // Fee paid on closing via TakeProfit2
fees += uiFeeClose;
}
// Network Fee: $0.50 for opening position only
// Closing is handled by oracle, so no network fee for closing
var networkFeeForOpening = 0.15m;
fees += networkFeeForOpening;
return fees;
}
public async Task ToggleIsForWatchOnly() public async Task ToggleIsForWatchOnly()
{ {
Config.IsForWatchingOnly = !Config.IsForWatchingOnly; Config.IsForWatchingOnly = !Config.IsForWatchingOnly;

View File

@@ -265,26 +265,15 @@ namespace Managing.Application.ManageBot
// Calculate statistics using TradingBox helpers // Calculate statistics using TradingBox helpers
var (tradeWins, tradeLosses) = TradingBox.GetWinLossCount(botData.Positions); var (tradeWins, tradeLosses) = TradingBox.GetWinLossCount(botData.Positions);
var pnl = botData.ProfitAndLoss; var pnl = botData.ProfitAndLoss;
var fees = botData.Positions.Values.Sum(p => var fees = botData.Positions.Values.Sum(p => p.CalculateTotalFees());
{
if (p.Open.Price > 0 && p.Open.Quantity > 0)
{
var positionSizeUsd = (p.Open.Price * p.Open.Quantity) * p.Open.Leverage;
var uiFeeRate = 0.001m; // 0.1%
var uiFeeOpen = positionSizeUsd * uiFeeRate;
var networkFeeForOpening = 0.50m;
return uiFeeOpen + networkFeeForOpening;
}
return 0;
});
var volume = TradingBox.GetTotalVolumeTraded(botData.Positions); var volume = TradingBox.GetTotalVolumeTraded(botData.Positions);
// Calculate ROI based on total investment // Calculate ROI based on total investment
var totalInvestment = botData.Positions.Values var totalInvestment = botData.Positions.Values
.Where(p => p.IsFinished()) .Where(p => p.IsFinished())
.Sum(p => p.Open.Quantity * p.Open.Price); .Sum(p => p.Open.Quantity * p.Open.Price);
var roi = totalInvestment > 0 ? (pnl / totalInvestment) * 100 : 0; var netPnl = pnl - fees;
var roi = totalInvestment > 0 ? (netPnl / totalInvestment) * 100 : 0;
// Update bot statistics // Update bot statistics
existingBot.TradeWins = tradeWins; existingBot.TradeWins = tradeWins;

View File

@@ -94,7 +94,6 @@ public class MessengerService : IMessengerService
var message = $"🎯 Position {status}\n" + var message = $"🎯 Position {status}\n" +
$"Symbol: {position.Ticker}\n" + $"Symbol: {position.Ticker}\n" +
$"Direction: {direction}\n" + $"Direction: {direction}\n" +
$"Account: {position.AccountName}\n" +
$"Identifier: {position.Identifier}\n" + $"Identifier: {position.Identifier}\n" +
$"Initiator: {position.Initiator}\n" + $"Initiator: {position.Initiator}\n" +
$"Date: {position.Date:yyyy-MM-dd HH:mm:ss}"; $"Date: {position.Date:yyyy-MM-dd HH:mm:ss}";

View File

@@ -5,14 +5,16 @@ namespace Managing.Application.Trading.Commands
{ {
public class ClosePositionCommand : IRequest<Position> public class ClosePositionCommand : IRequest<Position>
{ {
public ClosePositionCommand(Position position, decimal? executionPrice = null, bool isForBacktest = false) public ClosePositionCommand(Position position, int accountId, decimal? executionPrice = null, bool isForBacktest = false)
{ {
Position = position; Position = position;
AccountId = accountId;
ExecutionPrice = executionPrice; ExecutionPrice = executionPrice;
IsForBacktest = isForBacktest; IsForBacktest = isForBacktest;
} }
public Position Position { get; } public Position Position { get; }
public int AccountId { get; }
public decimal? ExecutionPrice { get; set; } public decimal? ExecutionPrice { get; set; }
public bool IsForBacktest { get; set; } public bool IsForBacktest { get; set; }
} }

View File

@@ -22,7 +22,7 @@ public class ClosePositionCommandHandler(
try try
{ {
// Get Trade // Get Trade
var account = await accountService.GetAccount(request.Position.AccountName, false, false); var account = await accountService.GetAccountById(request.AccountId, false, false);
if (request.Position == null) if (request.Position == null)
{ {
_ = await exchangeService.CancelOrder(account, request.Position.Ticker); _ = await exchangeService.CancelOrder(account, request.Position.Ticker);
@@ -48,6 +48,12 @@ public class ClosePositionCommandHandler(
request.Position.ProfitAndLoss = request.Position.ProfitAndLoss =
TradingBox.GetProfitAndLoss(request.Position, request.Position.Open.Quantity, lastPrice, TradingBox.GetProfitAndLoss(request.Position, request.Position.Open.Quantity, lastPrice,
request.Position.Open.Leverage); request.Position.Open.Leverage);
// Add UI fees for closing the position (broker closed it)
var closingPositionSizeUsd = (lastPrice * request.Position.Open.Quantity) * request.Position.Open.Leverage;
var closingUiFees = TradingHelpers.CalculateClosingUiFees(closingPositionSizeUsd);
request.Position.AddUiFees(closingUiFees);
await tradingService.UpdatePositionAsync(request.Position); await tradingService.UpdatePositionAsync(request.Position);
return request.Position; return request.Position;
} }
@@ -68,6 +74,11 @@ public class ClosePositionCommandHandler(
TradingBox.GetProfitAndLoss(request.Position, closedPosition.Quantity, lastPrice, TradingBox.GetProfitAndLoss(request.Position, closedPosition.Quantity, lastPrice,
request.Position.Open.Leverage); request.Position.Open.Leverage);
// Add UI fees for closing the position
var closingPositionSizeUsd = (lastPrice * closedPosition.Quantity) * request.Position.Open.Leverage;
var closingUiFees = TradingHelpers.CalculateClosingUiFees(closingPositionSizeUsd);
request.Position.AddUiFees(closingUiFees);
if (!request.IsForBacktest) if (!request.IsForBacktest)
await tradingService.UpdatePositionAsync(request.Position); await tradingService.UpdatePositionAsync(request.Position);
} }

View File

@@ -21,7 +21,7 @@ namespace Managing.Application.Trading.Handlers
var account = await accountService.GetAccount(request.AccountName, hideSecrets: false, getBalance: false); var account = await accountService.GetAccount(request.AccountName, hideSecrets: false, getBalance: false);
var initiator = request.IsForPaperTrading ? PositionInitiator.PaperTrading : request.Initiator; var initiator = request.IsForPaperTrading ? PositionInitiator.PaperTrading : request.Initiator;
var position = new Position(Guid.NewGuid(), request.AccountName, request.Direction, var position = new Position(Guid.NewGuid(), account.Id, request.Direction,
request.Ticker, request.Ticker,
request.MoneyManagement, request.MoneyManagement,
initiator, request.Date, request.User); initiator, request.Date, request.User);
@@ -45,9 +45,10 @@ namespace Managing.Application.Trading.Handlers
} }
// Gas fee check for EVM exchanges // Gas fee check for EVM exchanges
decimal gasFeeUsd = 0;
if (account.Exchange == TradingExchanges.Evm || account.Exchange == TradingExchanges.GmxV2) if (account.Exchange == TradingExchanges.Evm || account.Exchange == TradingExchanges.GmxV2)
{ {
var gasFeeUsd = await exchangeService.GetFee(account); gasFeeUsd = await exchangeService.GetFee(account);
if (gasFeeUsd > Constants.GMX.Config.MaximumGasFeeUsd) if (gasFeeUsd > Constants.GMX.Config.MaximumGasFeeUsd)
{ {
throw new InsufficientFundsException( throw new InsufficientFundsException(
@@ -84,6 +85,22 @@ namespace Managing.Application.Trading.Handlers
position.Open = trade; position.Open = trade;
// Calculate and set fees for the position
var positionSizeUsd = (position.Open.Price * position.Open.Quantity) * position.Open.Leverage;
// Set gas fees (only for EVM exchanges)
if (account.Exchange == TradingExchanges.Evm || account.Exchange == TradingExchanges.GmxV2)
{
position.GasFees = gasFeeUsd;
}
else
{
position.GasFees = TradingHelpers.CalculateOpeningGasFees();
}
// Set UI fees for opening
position.UiFees = TradingHelpers.CalculateOpeningUiFees(positionSizeUsd);
var closeDirection = request.Direction == TradeDirection.Long var closeDirection = request.Direction == TradeDirection.Long
? TradeDirection.Short ? TradeDirection.Short
: TradeDirection.Long; : TradeDirection.Long;

View File

@@ -112,6 +112,10 @@ namespace Managing.Common
public static decimal MinimumSwapEthBalanceUsd = 1m; public static decimal MinimumSwapEthBalanceUsd = 1m;
public const decimal MaximumGasFeeUsd = 1.5m; public const decimal MaximumGasFeeUsd = 1.5m;
public const double AutoSwapAmount = 3; public const double AutoSwapAmount = 3;
// Fee Configuration
public const decimal UiFeeRate = 0.001m; // 0.1% UI fee rate
public const decimal GasFeePerTransaction = 0.15m; // $0.15 gas fee per transaction
} }
public class TokenAddress public class TokenAddress

View File

@@ -9,27 +9,30 @@ namespace Managing.Domain.Accounts;
public class Account public class Account
{ {
[Id(0)] [Id(0)]
[Required] public string Name { get; set; } public int Id { get; set; }
[Id(1)] [Id(1)]
[Required] public TradingExchanges Exchange { get; set; } [Required] public string Name { get; set; }
[Id(2)] [Id(2)]
[Required] public AccountType Type { get; set; } [Required] public TradingExchanges Exchange { get; set; }
[Id(3)] [Id(3)]
public string Key { get; set; } [Required] public AccountType Type { get; set; }
[Id(4)] [Id(4)]
public string Secret { get; set; } public string Key { get; set; }
[Id(5)] [Id(5)]
public User User { get; set; } public string Secret { get; set; }
[Id(6)] [Id(6)]
public List<Balance> Balances { get; set; } public User User { get; set; }
[Id(7)] [Id(7)]
public List<Balance> Balances { get; set; }
[Id(8)]
public bool IsGmxInitialized { get; set; } = false; public bool IsGmxInitialized { get; set; } = false;
public bool IsPrivyWallet => Type == AccountType.Privy; public bool IsPrivyWallet => Type == AccountType.Privy;

View File

@@ -1,7 +1,9 @@
using Exilion.TradingAtomics; using Exilion.TradingAtomics;
using Managing.Common;
using Managing.Domain.Accounts; using Managing.Domain.Accounts;
using Managing.Domain.Candles; using Managing.Domain.Candles;
using Managing.Domain.Statistics; using Managing.Domain.Statistics;
using Managing.Domain.Trades;
using static Managing.Common.Enums; using static Managing.Common.Enums;
namespace Managing.Domain.Shared.Helpers; namespace Managing.Domain.Shared.Helpers;
@@ -97,4 +99,94 @@ public static class TradingHelpers
return traders; return traders;
} }
/// <summary>
/// Calculates the total fees for a position based on GMX V2 fee structure
/// </summary>
/// <param name="position">The position to calculate fees for</param>
/// <returns>The total fees for the position</returns>
public static decimal CalculatePositionFees(Position position)
{
var (uiFees, gasFees) = CalculatePositionFeesBreakdown(position);
return uiFees + gasFees;
}
/// <summary>
/// Calculates the UI and Gas fees breakdown for a position based on GMX V2 fee structure
/// </summary>
/// <param name="position">The position to calculate fees for</param>
/// <returns>A tuple containing (uiFees, gasFees)</returns>
public static (decimal uiFees, decimal gasFees) CalculatePositionFeesBreakdown(Position position)
{
decimal uiFees = 0;
decimal gasFees = 0;
if (position?.Open?.Price <= 0 || position?.Open?.Quantity <= 0)
{
return (uiFees, gasFees); // Return 0 if position data is invalid
}
// Calculate position size in USD (leverage is already included in quantity calculation)
var positionSizeUsd = (position.Open.Price * position.Open.Quantity) * position.Open.Leverage;
// UI Fee: 0.1% of position size paid on opening
var uiFeeOpen = positionSizeUsd * Constants.GMX.Config.UiFeeRate; // Fee paid on opening
uiFees += uiFeeOpen;
// UI Fee: 0.1% of position size paid on closing - only if position was actually closed
// Check which closing trade was executed (StopLoss, TakeProfit1, or TakeProfit2)
if (position.StopLoss?.Status == TradeStatus.Filled)
{
var stopLossPositionSizeUsd = (position.StopLoss.Price * position.StopLoss.Quantity) * position.StopLoss.Leverage;
var uiFeeClose = stopLossPositionSizeUsd * Constants.GMX.Config.UiFeeRate; // Fee paid on closing via StopLoss
uiFees += uiFeeClose;
}
else if (position.TakeProfit1?.Status == TradeStatus.Filled)
{
var takeProfit1PositionSizeUsd = (position.TakeProfit1.Price * position.TakeProfit1.Quantity) * position.TakeProfit1.Leverage;
var uiFeeClose = takeProfit1PositionSizeUsd * Constants.GMX.Config.UiFeeRate; // Fee paid on closing via TakeProfit1
uiFees += uiFeeClose;
}
else if (position.TakeProfit2?.Status == TradeStatus.Filled)
{
var takeProfit2PositionSizeUsd = (position.TakeProfit2.Price * position.TakeProfit2.Quantity) * position.TakeProfit2.Leverage;
var uiFeeClose = takeProfit2PositionSizeUsd * Constants.GMX.Config.UiFeeRate; // Fee paid on closing via TakeProfit2
uiFees += uiFeeClose;
}
// Gas Fee: $0.15 for opening position only
// Closing is handled by oracle, so no gas fee for closing
gasFees += Constants.GMX.Config.GasFeePerTransaction;
return (uiFees, gasFees);
}
/// <summary>
/// Calculates UI fees for opening a position
/// </summary>
/// <param name="positionSizeUsd">The position size in USD</param>
/// <returns>The UI fees for opening</returns>
public static decimal CalculateOpeningUiFees(decimal positionSizeUsd)
{
return positionSizeUsd * Constants.GMX.Config.UiFeeRate;
}
/// <summary>
/// Calculates UI fees for closing a position
/// </summary>
/// <param name="positionSizeUsd">The position size in USD</param>
/// <returns>The UI fees for closing</returns>
public static decimal CalculateClosingUiFees(decimal positionSizeUsd)
{
return positionSizeUsd * Constants.GMX.Config.UiFeeRate;
}
/// <summary>
/// Calculates gas fees for opening a position
/// </summary>
/// <returns>The gas fees for opening (fixed at $0.15)</returns>
public static decimal CalculateOpeningGasFees()
{
return Constants.GMX.Config.GasFeePerTransaction;
}
} }

View File

@@ -9,11 +9,11 @@ namespace Managing.Domain.Trades
[GenerateSerializer] [GenerateSerializer]
public class Position public class Position
{ {
public Position(Guid identifier, string accountName, TradeDirection originDirection, Ticker ticker, public Position(Guid identifier, int accountId, TradeDirection originDirection, Ticker ticker,
LightMoneyManagement moneyManagement, PositionInitiator initiator, DateTime date, User user) LightMoneyManagement moneyManagement, PositionInitiator initiator, DateTime date, User user)
{ {
Identifier = identifier; Identifier = identifier;
AccountName = accountName; AccountId = accountId;
OriginDirection = originDirection; OriginDirection = originDirection;
Ticker = ticker; Ticker = ticker;
MoneyManagement = moneyManagement; MoneyManagement = moneyManagement;
@@ -23,9 +23,9 @@ namespace Managing.Domain.Trades
User = user; User = user;
} }
[Id(0)] [Required] public string AccountName { get; set; } [Id(0)] [Required] public DateTime Date { get; set; }
[Id(1)] [Required] public DateTime Date { get; set; } [Id(1)] [Required] public int AccountId { get; set; }
[Id(2)] [Required] public TradeDirection OriginDirection { get; set; } [Id(2)] [Required] public TradeDirection OriginDirection { get; set; }
@@ -56,20 +56,24 @@ namespace Managing.Domain.Trades
[JsonPropertyName("ProfitAndLoss")] [JsonPropertyName("ProfitAndLoss")]
public ProfitAndLoss ProfitAndLoss { get; set; } public ProfitAndLoss ProfitAndLoss { get; set; }
[Id(10)] [Required] public PositionStatus Status { get; set; } [Id(10)] public decimal UiFees { get; set; }
[Id(11)] public string SignalIdentifier { get; set; } [Id(11)] public decimal GasFees { get; set; }
[Id(12)] [Required] public Guid Identifier { get; set; } [Id(12)] [Required] public PositionStatus Status { get; set; }
[Id(13)] [Required] public PositionInitiator Initiator { get; set; } [Id(13)] public string SignalIdentifier { get; set; }
[Id(14)] [Required] public User User { get; set; } [Id(14)] [Required] public Guid Identifier { get; set; }
[Id(15)] [Required] public PositionInitiator Initiator { get; set; }
[Id(16)] [Required] public User User { get; set; }
/// <summary> /// <summary>
/// Identifier of the bot or entity that initiated this position /// Identifier of the bot or entity that initiated this position
/// </summary> /// </summary>
[Id(15)] [Required] public Guid InitiatorIdentifier { get; set; } [Id(17)] [Required] public Guid InitiatorIdentifier { get; set; }
public bool IsFinished() public bool IsFinished()
{ {
@@ -80,5 +84,46 @@ namespace Managing.Domain.Trades
_ => false _ => false
}; };
} }
/// <summary>
/// Calculates the total fees for this position based on GMX V2 fee structure
/// </summary>
/// <returns>The total fees for the position</returns>
public decimal CalculateTotalFees()
{
return UiFees + GasFees;
}
/// <summary>
/// Gets the net PnL after deducting fees
/// </summary>
/// <returns>The net PnL after fees</returns>
public decimal GetNetPnL()
{
if (ProfitAndLoss?.Realized == null)
{
return 0;
}
return ProfitAndLoss.Realized - CalculateTotalFees();
}
/// <summary>
/// Updates the UI fees for this position
/// </summary>
/// <param name="uiFees">The UI fees to add</param>
public void AddUiFees(decimal uiFees)
{
UiFees += uiFees;
}
/// <summary>
/// Updates the gas fees for this position
/// </summary>
/// <param name="gasFees">The gas fees to add</param>
public void AddGasFees(decimal gasFees)
{
GasFees += gasFees;
}
} }
} }

View File

@@ -0,0 +1,63 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Managing.Infrastructure.Databases.Migrations
{
/// <inheritdoc />
public partial class RemoveAccountNameFromPositions : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Fee",
table: "Trades");
migrationBuilder.DropColumn(
name: "AccountName",
table: "Positions");
migrationBuilder.AddColumn<decimal>(
name: "GasFees",
table: "Positions",
type: "numeric(18,8)",
nullable: false,
defaultValue: 0m);
migrationBuilder.AddColumn<decimal>(
name: "UiFees",
table: "Positions",
type: "numeric(18,8)",
nullable: false,
defaultValue: 0m);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "GasFees",
table: "Positions");
migrationBuilder.DropColumn(
name: "UiFees",
table: "Positions");
migrationBuilder.AddColumn<decimal>(
name: "Fee",
table: "Trades",
type: "numeric(18,8)",
nullable: false,
defaultValue: 0m);
migrationBuilder.AddColumn<string>(
name: "AccountName",
table: "Positions",
type: "character varying(255)",
maxLength: 255,
nullable: false,
defaultValue: "");
}
}
}

View File

@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Managing.Infrastructure.Databases.Migrations
{
/// <inheritdoc />
public partial class AddAccountIdToPositions : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "AccountId",
table: "Positions",
type: "integer",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "AccountId",
table: "Positions");
}
}
}

View File

@@ -666,10 +666,8 @@ namespace Managing.Infrastructure.Databases.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("uuid"); .HasColumnType("uuid");
b.Property<string>("AccountName") b.Property<int>("AccountId")
.IsRequired() .HasColumnType("integer");
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<DateTime>("CreatedAt") b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
@@ -677,6 +675,9 @@ namespace Managing.Infrastructure.Databases.Migrations
b.Property<DateTime>("Date") b.Property<DateTime>("Date")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
b.Property<decimal>("GasFees")
.HasColumnType("decimal(18,8)");
b.Property<string>("Initiator") b.Property<string>("Initiator")
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasColumnType("text");
@@ -719,6 +720,9 @@ namespace Managing.Infrastructure.Databases.Migrations
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasColumnType("text");
b.Property<decimal>("UiFees")
.HasColumnType("decimal(18,8)");
b.Property<DateTime>("UpdatedAt") b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
@@ -1083,9 +1087,6 @@ namespace Managing.Infrastructure.Databases.Migrations
.HasMaxLength(255) .HasMaxLength(255)
.HasColumnType("character varying(255)"); .HasColumnType("character varying(255)");
b.Property<decimal>("Fee")
.HasColumnType("decimal(18,8)");
b.Property<decimal>("Leverage") b.Property<decimal>("Leverage")
.HasColumnType("decimal(18,8)"); .HasColumnType("decimal(18,8)");

View File

@@ -11,16 +11,20 @@ public class PositionEntity
public DateTime Date { get; set; } public DateTime Date { get; set; }
[Required] public int AccountId { get; set; }
[Column(TypeName = "decimal(18,8)")] public decimal ProfitAndLoss { get; set; } [Column(TypeName = "decimal(18,8)")] public decimal ProfitAndLoss { get; set; }
[Column(TypeName = "decimal(18,8)")] public decimal UiFees { get; set; }
[Column(TypeName = "decimal(18,8)")] public decimal GasFees { get; set; }
public TradeDirection OriginDirection { get; set; } public TradeDirection OriginDirection { get; set; }
public PositionStatus Status { get; set; } public PositionStatus Status { get; set; }
public Ticker Ticker { get; set; } public Ticker Ticker { get; set; }
public PositionInitiator Initiator { get; set; } public PositionInitiator Initiator { get; set; }
[MaxLength(255)] public string SignalIdentifier { get; set; } [MaxLength(255)] public string? SignalIdentifier { get; set; }
[MaxLength(255)] public string AccountName { get; set; }
public int? UserId { get; set; } public int? UserId { get; set; }

View File

@@ -16,9 +16,6 @@ public class TradeEntity
public TradeType TradeType { get; set; } public TradeType TradeType { get; set; }
public Ticker Ticker { get; set; } public Ticker Ticker { get; set; }
[Column(TypeName = "decimal(18,8)")]
public decimal Fee { get; set; }
[Column(TypeName = "decimal(18,8)")] [Column(TypeName = "decimal(18,8)")]
public decimal Quantity { get; set; } public decimal Quantity { get; set; }

View File

@@ -301,7 +301,6 @@ public class ManagingDbContext : DbContext
entity.Property(e => e.Ticker).IsRequired().HasConversion<string>(); entity.Property(e => e.Ticker).IsRequired().HasConversion<string>();
entity.Property(e => e.Initiator).IsRequired().HasConversion<string>(); entity.Property(e => e.Initiator).IsRequired().HasConversion<string>();
entity.Property(e => e.SignalIdentifier).IsRequired().HasMaxLength(255); entity.Property(e => e.SignalIdentifier).IsRequired().HasMaxLength(255);
entity.Property(e => e.AccountName).IsRequired().HasMaxLength(255);
entity.Property(e => e.UserId); entity.Property(e => e.UserId);
entity.Property(e => e.InitiatorIdentifier).IsRequired(); entity.Property(e => e.InitiatorIdentifier).IsRequired();
entity.Property(e => e.MoneyManagementJson).HasColumnType("text"); entity.Property(e => e.MoneyManagementJson).HasColumnType("text");
@@ -351,7 +350,6 @@ public class ManagingDbContext : DbContext
entity.Property(e => e.Status).IsRequired().HasConversion<string>(); entity.Property(e => e.Status).IsRequired().HasConversion<string>();
entity.Property(e => e.TradeType).IsRequired().HasConversion<string>(); entity.Property(e => e.TradeType).IsRequired().HasConversion<string>();
entity.Property(e => e.Ticker).IsRequired().HasConversion<string>(); entity.Property(e => e.Ticker).IsRequired().HasConversion<string>();
entity.Property(e => e.Fee).HasColumnType("decimal(18,8)");
entity.Property(e => e.Quantity).HasColumnType("decimal(18,8)"); entity.Property(e => e.Quantity).HasColumnType("decimal(18,8)");
entity.Property(e => e.Price).HasColumnType("decimal(18,8)"); entity.Property(e => e.Price).HasColumnType("decimal(18,8)");
entity.Property(e => e.Leverage).HasColumnType("decimal(18,8)"); entity.Property(e => e.Leverage).HasColumnType("decimal(18,8)");
@@ -561,8 +559,6 @@ public class ManagingDbContext : DbContext
.HasForeignKey(e => e.UserId) .HasForeignKey(e => e.UserId)
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
}); });
} }
/// <summary> /// <summary>

View File

@@ -82,6 +82,27 @@ public class PostgreSqlAccountRepository : IAccountRepository
} }
} }
public async Task<Account> GetAccountByIdAsync(int id)
{
try
{
await PostgreSqlConnectionHelper.EnsureConnectionOpenAsync(_context);
var accountEntity = await _context.Accounts
.AsNoTracking()
.Include(a => a.User)
.FirstOrDefaultAsync(a => a.Id == id)
.ConfigureAwait(false);
return PostgreSqlMappers.Map(accountEntity);
}
finally
{
// Always ensure the connection is closed after the operation
await PostgreSqlConnectionHelper.SafeCloseConnectionAsync(_context);
}
}
public async Task<IEnumerable<Account>> GetAccountsAsync() public async Task<IEnumerable<Account>> GetAccountsAsync()
{ {
try try

View File

@@ -28,6 +28,7 @@ public static class PostgreSqlMappers
return new Account return new Account
{ {
Id = entity.Id,
Name = entity.Name, Name = entity.Name,
Exchange = entity.Exchange, Exchange = entity.Exchange,
Type = entity.Type, Type = entity.Type,
@@ -45,6 +46,7 @@ public static class PostgreSqlMappers
return new AccountEntity return new AccountEntity
{ {
Id = account.Id,
Name = account.Name, Name = account.Name,
Exchange = account.Exchange, Exchange = account.Exchange,
Type = account.Type, Type = account.Type,
@@ -554,7 +556,7 @@ public static class PostgreSqlMappers
var position = new Position( var position = new Position(
entity.Identifier, entity.Identifier,
entity.AccountName, entity.AccountId,
entity.OriginDirection, entity.OriginDirection,
entity.Ticker, entity.Ticker,
moneyManagement, moneyManagement,
@@ -570,6 +572,10 @@ public static class PostgreSqlMappers
// Set ProfitAndLoss with proper type // Set ProfitAndLoss with proper type
position.ProfitAndLoss = new ProfitAndLoss { Realized = entity.ProfitAndLoss }; position.ProfitAndLoss = new ProfitAndLoss { Realized = entity.ProfitAndLoss };
// Set fee properties
position.UiFees = entity.UiFees;
position.GasFees = entity.GasFees;
// Map related trades // Map related trades
if (entity.OpenTrade != null) if (entity.OpenTrade != null)
position.Open = Map(entity.OpenTrade); position.Open = Map(entity.OpenTrade);
@@ -591,13 +597,15 @@ public static class PostgreSqlMappers
{ {
Identifier = position.Identifier, Identifier = position.Identifier,
Date = position.Date, Date = position.Date,
AccountId = position.AccountId,
ProfitAndLoss = position.ProfitAndLoss?.Realized ?? 0, ProfitAndLoss = position.ProfitAndLoss?.Realized ?? 0,
UiFees = position.UiFees,
GasFees = position.GasFees,
OriginDirection = position.OriginDirection, OriginDirection = position.OriginDirection,
Status = position.Status, Status = position.Status,
Ticker = position.Ticker, Ticker = position.Ticker,
Initiator = position.Initiator, Initiator = position.Initiator,
SignalIdentifier = position.SignalIdentifier, SignalIdentifier = position.SignalIdentifier,
AccountName = position.AccountName,
UserId = position.User?.Id ?? 0, UserId = position.User?.Id ?? 0,
InitiatorIdentifier = position.InitiatorIdentifier, InitiatorIdentifier = position.InitiatorIdentifier,
MoneyManagementJson = position.MoneyManagement != null MoneyManagementJson = position.MoneyManagement != null
@@ -621,10 +629,7 @@ public static class PostgreSqlMappers
entity.Price, entity.Price,
entity.Leverage, entity.Leverage,
entity.ExchangeOrderId, entity.ExchangeOrderId,
entity.Message) entity.Message);
{
Fee = entity.Fee
};
} }
public static TradeEntity Map(Trade trade) public static TradeEntity Map(Trade trade)
@@ -638,7 +643,6 @@ public static class PostgreSqlMappers
Status = trade.Status, Status = trade.Status,
TradeType = trade.TradeType, TradeType = trade.TradeType,
Ticker = trade.Ticker, Ticker = trade.Ticker,
Fee = trade.Fee,
Quantity = trade.Quantity, Quantity = trade.Quantity,
Price = trade.Price, Price = trade.Price,
Leverage = trade.Leverage, Leverage = trade.Leverage,

View File

@@ -395,6 +395,8 @@ public class PostgreSqlTradingRepository : ITradingRepository
{ {
entity.Date = position.Date; entity.Date = position.Date;
entity.ProfitAndLoss = position.ProfitAndLoss?.Realized ?? 0; entity.ProfitAndLoss = position.ProfitAndLoss?.Realized ?? 0;
entity.UiFees = position.UiFees;
entity.GasFees = position.GasFees;
entity.Status = position.Status; entity.Status = position.Status;
entity.SignalIdentifier = position.SignalIdentifier; entity.SignalIdentifier = position.SignalIdentifier;
entity.MoneyManagementJson = position.MoneyManagement != null entity.MoneyManagementJson = position.MoneyManagement != null
@@ -440,7 +442,6 @@ public class PostgreSqlTradingRepository : ITradingRepository
entity.Status = trade.Status; entity.Status = trade.Status;
entity.TradeType = trade.TradeType; entity.TradeType = trade.TradeType;
entity.Ticker = trade.Ticker; entity.Ticker = trade.Ticker;
entity.Fee = trade.Fee;
entity.Quantity = trade.Quantity; entity.Quantity = trade.Quantity;
entity.Price = trade.Price; entity.Price = trade.Price;
entity.Leverage = trade.Leverage; entity.Leverage = trade.Leverage;
@@ -468,7 +469,8 @@ public class PostgreSqlTradingRepository : ITradingRepository
return PostgreSqlMappers.Map(positions); return PostgreSqlMappers.Map(positions);
} }
public async Task<IEnumerable<Position>> GetPositionsByInitiatorIdentifiersAsync(IEnumerable<Guid> initiatorIdentifiers) public async Task<IEnumerable<Position>> GetPositionsByInitiatorIdentifiersAsync(
IEnumerable<Guid> initiatorIdentifiers)
{ {
var identifiersList = initiatorIdentifiers.ToList(); var identifiersList = initiatorIdentifiers.ToList();
if (!identifiersList.Any()) if (!identifiersList.Any())

View File

@@ -305,7 +305,8 @@ namespace Managing.Infrastructure.Messengers.Discord
await component.RespondAsync("Alright, let met few seconds to close this position"); await component.RespondAsync("Alright, let met few seconds to close this position");
var position = await tradingService.GetPositionByIdentifierAsync(Guid.Parse(parameters[1])); var position = await tradingService.GetPositionByIdentifierAsync(Guid.Parse(parameters[1]));
var command = new ClosePositionCommand(position);
var command = new ClosePositionCommand(position, position.AccountId);
var result = var result =
await new ClosePositionCommandHandler(exchangeService, accountService, tradingService, scopeFactory) await new ClosePositionCommandHandler(exchangeService, accountService, tradingService, scopeFactory)
.Handle(command); .Handle(command);
@@ -336,7 +337,7 @@ namespace Managing.Infrastructure.Messengers.Discord
IsInline = true IsInline = true
}, },
}; };
var embed = DiscordHelpers.GetEmbed(position.AccountName, $"Position status is now {result.Status}", fields, var embed = DiscordHelpers.GetEmbed($"Position {position.Identifier}", $"Position status is now {result.Status}", fields,
position.ProfitAndLoss.Net > 0 ? Color.Green : Color.Red); position.ProfitAndLoss.Net > 0 ? Color.Green : Color.Red);
await component.Channel.SendMessageAsync("", embed: embed); await component.Channel.SendMessageAsync("", embed: embed);
} }

View File

@@ -164,7 +164,7 @@ internal static class GmxV2Mappers
{ {
var direction = MiscExtensions.ParseEnum<TradeDirection>(gmxPosition.Direction); var direction = MiscExtensions.ParseEnum<TradeDirection>(gmxPosition.Direction);
var ticker = MiscExtensions.ParseEnum<Ticker>(gmxPosition.Ticker); var ticker = MiscExtensions.ParseEnum<Ticker>(gmxPosition.Ticker);
var position = new Position(Guid.NewGuid(), "", var position = new Position(Guid.NewGuid(), 1,
direction, direction,
ticker, ticker,
new MoneyManagement(), new MoneyManagement(),

View File

@@ -27,7 +27,7 @@
"@sentry/node": "^8.55.0", "@sentry/node": "^8.55.0",
"@sinclair/typebox": "^0.34.11", "@sinclair/typebox": "^0.34.11",
"canonicalize": "^2.0.0", "canonicalize": "^2.0.0",
"concurrently": "^9.0.1", "concurrently": "^9.2.1",
"cross-fetch": "^4.1.0", "cross-fetch": "^4.1.0",
"csv-stringify": "^6.5.2", "csv-stringify": "^6.5.2",
"ethers": "^6.13.5", "ethers": "^6.13.5",
@@ -2854,16 +2854,17 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/concurrently": { "node_modules/concurrently": {
"version": "9.1.2", "version": "9.2.1",
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz",
"integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"chalk": "^4.1.2", "chalk": "4.1.2",
"lodash": "^4.17.21", "rxjs": "7.8.2",
"rxjs": "^7.8.1", "shell-quote": "1.8.3",
"shell-quote": "^1.8.1", "supports-color": "8.1.1",
"supports-color": "^8.1.1", "tree-kill": "1.2.2",
"tree-kill": "^1.2.2", "yargs": "17.7.2"
"yargs": "^17.7.2"
}, },
"bin": { "bin": {
"conc": "dist/bin/concurrently.js", "conc": "dist/bin/concurrently.js",
@@ -7059,7 +7060,9 @@
} }
}, },
"node_modules/shell-quote": { "node_modules/shell-quote": {
"version": "1.8.2", "version": "1.8.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
"integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"

View File

@@ -46,7 +46,7 @@
"@sentry/node": "^8.55.0", "@sentry/node": "^8.55.0",
"@sinclair/typebox": "^0.34.11", "@sinclair/typebox": "^0.34.11",
"canonicalize": "^2.0.0", "canonicalize": "^2.0.0",
"concurrently": "^9.0.1", "concurrently": "^9.2.1",
"cross-fetch": "^4.1.0", "cross-fetch": "^4.1.0",
"csv-stringify": "^6.5.2", "csv-stringify": "^6.5.2",
"ethers": "^6.13.5", "ethers": "^6.13.5",

View File

@@ -56,7 +56,7 @@ const MAX_CACHE_SIZE = 5; // Limit cache size to prevent memory issues
const OPERATION_TIMEOUT = 30000; // 30 seconds timeout for operations const OPERATION_TIMEOUT = 30000; // 30 seconds timeout for operations
const MEMORY_WARNING_THRESHOLD = 0.8; // Warn when memory usage exceeds 80% const MEMORY_WARNING_THRESHOLD = 0.8; // Warn when memory usage exceeds 80%
const MAX_GAS_FEE_USD = 1; // Maximum gas fee in USD (1 USDC) const MAX_GAS_FEE_USD = 1.5; // Maximum gas fee in USD (1 USDC)
// Memory monitoring function // Memory monitoring function
function checkMemoryUsage() { function checkMemoryUsage() {