From 67065469a63f8640ee3d32916eeb80a6cd6e7237 Mon Sep 17 00:00:00 2001 From: cryptooda Date: Wed, 8 Oct 2025 02:52:11 +0700 Subject: [PATCH] Fix config update + remove messages + Summary fix for not open position --- src/Managing.Api/Controllers/BotController.cs | 36 +++--- .../Grains/ILiveTradingBotGrain.cs | 3 +- .../Abstractions/IBotService.cs | 3 +- .../Abstractions/ITradingBot.cs | 2 +- .../Bots/Grains/LiveTradingBotGrain.cs | 8 +- .../Bots/TradingBotBase.cs | 115 +++++++++--------- .../Grains/PlatformSummaryGrain.cs | 83 ++++++++----- .../ManageBot/BotService.cs | 5 +- src/Managing.Domain/Trades/Position.cs | 16 ++- .../EvmManager.cs | 2 +- .../src/routes/api/gmx/index.ts | 2 +- .../test/plugins/close-position.test.ts | 4 +- .../mollecules/ManualPositionModal.tsx | 12 +- .../CustomMoneyManagement.tsx | 17 ++- .../UnifiedTradingModal.tsx | 44 +++---- .../src/generated/ManagingApi.ts | 14 +-- .../src/generated/ManagingApiTypes.ts | 2 +- 17 files changed, 209 insertions(+), 159 deletions(-) diff --git a/src/Managing.Api/Controllers/BotController.cs b/src/Managing.Api/Controllers/BotController.cs index 1aa72375..d5fa1926 100644 --- a/src/Managing.Api/Controllers/BotController.cs +++ b/src/Managing.Api/Controllers/BotController.cs @@ -10,6 +10,7 @@ using Managing.Core; using Managing.Domain.Accounts; using Managing.Domain.Backtests; using Managing.Domain.Bots; +using Managing.Domain.Indicators; using Managing.Domain.MoneyManagements; using Managing.Domain.Scenarios; using Managing.Domain.Strategies; @@ -479,13 +480,13 @@ public class BotController : BaseController } /// - /// Manually opens a position for a specified bot with the given parameters. + /// Manually create a signal for a specified bot with the given parameters. /// /// The request containing position parameters. /// A response indicating the result of the operation. [HttpPost] - [Route("OpenPosition")] - public async Task> OpenPositionManually([FromBody] OpenPositionManuallyRequest request) + [Route("CreateManualSignal")] + public async Task> CreateManualSignalAsync([FromBody] CreateManualSignalRequest request) { try { @@ -507,16 +508,15 @@ public class BotController : BaseController return BadRequest($"Bot with identifier {request.Identifier} is not running"); } - var position = await _botService.OpenPositionManuallyAsync(request.Identifier, request.Direction); + var signal = await _botService.CreateManualSignalAsync(request.Identifier, request.Direction); - await NotifyBotSubscriberAsync(); - return Ok(position); + return Ok(signal); } catch (Exception ex) { - _logger.LogError(ex, "Error opening position manually"); + _logger.LogError(ex, "Error creating signal manually"); return StatusCode(500, - $"Error opening position: {ex.Message}, {ex.InnerException?.Message} or {ex.StackTrace}"); + $"Error creating signal: {ex.Message}, {ex.InnerException?.Message} or {ex.StackTrace}"); } } @@ -664,17 +664,17 @@ public class BotController : BaseController Leverage = fullMoneyManagement.Leverage }; } - else if (request.MoneyManagement != null) + else if (request.Config.MoneyManagement != null) { // Use provided money management object - moneyManagement = request.MoneyManagement; - // Format percentage values if using custom money management - moneyManagement.FormatPercentage(); - } - else - { - // Use existing bot's money management if no new one is provided - moneyManagement = config.MoneyManagement; + moneyManagement = new LightMoneyManagement + { + Name = request.Config.Name, + Timeframe = request.Config.Timeframe, + StopLoss = request.Config.MoneyManagement.StopLoss, + TakeProfit = request.Config.MoneyManagement.TakeProfit, + Leverage = request.Config.MoneyManagement.Leverage + }; } // Validate CloseEarlyWhenProfitable requires MaxPositionTimeHours @@ -921,7 +921,7 @@ public class BotController : BaseController /// /// Request model for opening a position manually /// -public class OpenPositionManuallyRequest +public class CreateManualSignalRequest { /// /// The identifier of the bot diff --git a/src/Managing.Application.Abstractions/Grains/ILiveTradingBotGrain.cs b/src/Managing.Application.Abstractions/Grains/ILiveTradingBotGrain.cs index 71a762ab..a6753a57 100644 --- a/src/Managing.Application.Abstractions/Grains/ILiveTradingBotGrain.cs +++ b/src/Managing.Application.Abstractions/Grains/ILiveTradingBotGrain.cs @@ -1,5 +1,6 @@ using Managing.Domain.Accounts; using Managing.Domain.Bots; +using Managing.Domain.Indicators; using Managing.Domain.Trades; using Managing.Domain.Users; using Orleans; @@ -18,7 +19,7 @@ public interface ILiveTradingBotGrain : IGrainWithGuidKey /// /// The direction of the trade (Long/Short) /// The created Position object - Task OpenPositionManuallyAsync(TradeDirection direction); + Task CreateManualSignalAsync(TradeDirection direction); /// /// Gets comprehensive bot data including positions, signals, and performance metrics diff --git a/src/Managing.Application/Abstractions/IBotService.cs b/src/Managing.Application/Abstractions/IBotService.cs index 0bb8e5e1..572b9fd9 100644 --- a/src/Managing.Application/Abstractions/IBotService.cs +++ b/src/Managing.Application/Abstractions/IBotService.cs @@ -1,6 +1,7 @@ using Managing.Application.Bots.Models; using Managing.Domain.Accounts; using Managing.Domain.Bots; +using Managing.Domain.Indicators; using Managing.Domain.Trades; using static Managing.Common.Enums; @@ -19,7 +20,7 @@ public interface IBotService Task> GetBotsByIdsAsync(IEnumerable botIds); Task GetBotByName(string name); Task GetBotByIdentifier(Guid identifier); - Task OpenPositionManuallyAsync(Guid identifier, TradeDirection direction); + Task CreateManualSignalAsync(Guid identifier, TradeDirection direction); Task ClosePositionAsync(Guid identifier, Guid positionId); Task GetBotConfig(Guid identifier); Task> GetBotConfigsByIdsAsync(IEnumerable botIds); diff --git a/src/Managing.Application/Abstractions/ITradingBot.cs b/src/Managing.Application/Abstractions/ITradingBot.cs index 7f95cc03..4ab06689 100644 --- a/src/Managing.Application/Abstractions/ITradingBot.cs +++ b/src/Managing.Application/Abstractions/ITradingBot.cs @@ -25,7 +25,7 @@ namespace Managing.Application.Abstractions decimal GetTotalFees(); Task LoadAccount(); Task LoadLastCandle(); - Task OpenPositionManually(TradeDirection direction); + Task CreateManualSignal(TradeDirection direction); Task CloseTrade(LightSignal signal, Position position, Trade tradeToClose, decimal lastPrice, bool tradeClosingPosition = false); diff --git a/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs b/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs index c3178028..54dd59bc 100644 --- a/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs +++ b/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs @@ -6,6 +6,7 @@ using Managing.Application.Shared; using Managing.Core; using Managing.Domain.Accounts; using Managing.Domain.Bots; +using Managing.Domain.Indicators; using Managing.Domain.Shared.Helpers; using Managing.Domain.Trades; using Managing.Domain.Users; @@ -277,7 +278,8 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable var hasOpenPositions = await HasOpenPositionsInDatabaseAsync(); if (hasOpenPositions) { - _logger.LogWarning("Stopping bot {Name} while it still has open positions in database. Trading loop will stop but positions remain managed by system.", + _logger.LogWarning( + "Stopping bot {Name} while it still has open positions in database. Trading loop will stop but positions remain managed by system.", _tradingBot?.Config.Name); throw new InvalidOperationException( "Cannot stop bot while it has open positions. Please close all positions first."); @@ -442,7 +444,7 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable } - public async Task OpenPositionManuallyAsync(TradeDirection direction) + public async Task CreateManualSignalAsync(TradeDirection direction) { try { @@ -462,7 +464,7 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable await _state.WriteStateAsync(); } - return await _tradingBot.OpenPositionManually(direction); + return await _tradingBot.CreateManualSignal(direction); } catch (Exception ex) { diff --git a/src/Managing.Application/Bots/TradingBotBase.cs b/src/Managing.Application/Bots/TradingBotBase.cs index 5dd0ee35..86ff580a 100644 --- a/src/Managing.Application/Bots/TradingBotBase.cs +++ b/src/Managing.Application/Bots/TradingBotBase.cs @@ -339,11 +339,6 @@ public class TradingBotBase : ITradingBot { Positions[newlyCreatedPosition.Identifier] = newlyCreatedPosition; } - else - { - await LogWarning( - $"⚠️ Position Creation Failed\nSignal: `{signal.Identifier}`\nPosition creation returned null"); - } } } @@ -369,7 +364,7 @@ public class TradingBotBase : ITradingBot try { // Skip processing if position is already canceled or rejected (never filled) - if (positionForSignal.Status == PositionStatus.Canceled || + if (positionForSignal.Status == PositionStatus.Canceled || positionForSignal.Status == PositionStatus.Rejected) { Logger.LogDebug( @@ -486,7 +481,7 @@ public class TradingBotBase : ITradingBot if (timeSinceRequest.TotalMinutes >= waitTimeMinutes) { await LogWarning( - $"⚠️ Order Cleanup\nToo many open orders: `{orders.Count()}`\nPosition: `{positionForSignal.Identifier}`\nTime elapsed: `{waitTimeMinutes}min`\nCanceling all orders..."); + $"⚠️ Orders Cleanup\nTime elapsed: {waitTimeMinutes}min\nCanceling all orders..."); try { await ServiceScopeHelpers.WithScopedService(_scopeFactory, @@ -495,7 +490,7 @@ public class TradingBotBase : ITradingBot await exchangeService.CancelOrder(Account, Config.Ticker); }); await LogInformation( - $"✅ Orders Canceled\nSuccessfully canceled all orders for: `{Config.Ticker}`"); + $"✅ Orders for {internalPosition.OriginDirection} {Config.Ticker} successfully canceled"); } catch (Exception ex) { @@ -504,6 +499,13 @@ public class TradingBotBase : ITradingBot await SetPositionStatus(signal.Identifier, PositionStatus.Canceled); SetSignalStatus(signal.Identifier, SignalStatus.Expired); + + positionForSignal.Status = PositionStatus.Canceled; + positionForSignal.Open.SetStatus(TradeStatus.Cancelled); + positionForSignal.StopLoss.SetStatus(TradeStatus.Cancelled); + positionForSignal.TakeProfit1.SetStatus(TradeStatus.Cancelled); + + await UpdatePositionDatabase(positionForSignal); return; } else @@ -577,7 +579,7 @@ public class TradingBotBase : ITradingBot { await LogWarning( $"❌ Position Never Filled\nNo position on exchange and no orders\nSignal: `{signal.Identifier}`\nPosition was never filled and will be marked as canceled."); - + // Position was never filled (still in New status), so just mark it as canceled // Don't call HandleClosedPosition as that would incorrectly add volume/PnL await SetPositionStatus(signal.Identifier, PositionStatus.Canceled); @@ -762,16 +764,16 @@ public class TradingBotBase : ITradingBot } } } - else if (internalPosition.Status == PositionStatus.Rejected || - internalPosition.Status == PositionStatus.Canceled) - { - await LogWarning($"Open position trade is rejected for signal {signal.Identifier}"); - if (signal.Status == SignalStatus.PositionOpen) - { - Logger.LogInformation($"Try to re-open position"); - await OpenPosition(signal); - } - } + // else if (internalPosition.Status == PositionStatus.Rejected || + // internalPosition.Status == PositionStatus.Canceled) + // { + // await LogWarning($"Open position trade is rejected for signal {signal.Identifier}"); + // if (signal.Status == SignalStatus.PositionOpen) + // { + // Logger.LogInformation($"Try to re-open position"); + // await OpenPosition(signal); + // } + // } if (Config.UseSynthApi && !Config.IsForBacktest && positionForSignal.Status == PositionStatus.Filled) @@ -926,24 +928,26 @@ public class TradingBotBase : ITradingBot if (position != null) { - if (position.Open.Status != TradeStatus.Cancelled) + if (position.Open.Status != TradeStatus.Cancelled && position.Status != PositionStatus.Rejected) { SetSignalStatus(signal.Identifier, SignalStatus.PositionOpen); if (!Config.IsForBacktest) { await ServiceScopeHelpers.WithScopedService(_scopeFactory, - async messengerService => { await messengerService.SendClosedPosition(position, Account.User); }); + async messengerService => { await messengerService.SendPosition(position); }); } Logger.LogInformation($"Position requested"); - return position; // Return the created position without adding to list + return position; } else { await SetPositionStatus(signal.Identifier, PositionStatus.Rejected); + position.Status = PositionStatus.Rejected; + await UpdatePositionDatabase(position); SetSignalStatus(signal.Identifier, SignalStatus.Expired); - return null; + return position; } } @@ -964,7 +968,7 @@ public class TradingBotBase : ITradingBot catch (Exception ex) { SetSignalStatus(signal.Identifier, SignalStatus.Expired); - await LogWarning($"Cannot open trade : {ex.Message}, stackTrace : {ex.StackTrace}"); + SentrySdk.CaptureException(ex); return null; } } @@ -1247,7 +1251,7 @@ public class TradingBotBase : ITradingBot // We use this for reconciliation with the bot's own calculations var totalBotFees = position.GasFees + position.UiFees; var gmxNetPnl = gmxPosition.ProfitAndLoss.Realized; // This is already after GMX fees - + position.ProfitAndLoss = new ProfitAndLoss { // GMX's realized PnL is already after their fees @@ -1530,7 +1534,7 @@ public class TradingBotBase : ITradingBot // No need to subtract fees from PnL as they're tracked separately } - SkipCandleBasedCalculation: + SkipCandleBasedCalculation: await SetPositionStatus(position.SignalIdentifier, PositionStatus.Finished); // Update position in database with all trade changes @@ -1538,13 +1542,13 @@ public class TradingBotBase : ITradingBot { position.Status = PositionStatus.Finished; await UpdatePositionDatabase(position); - + // Only send PositionClosed notification if the position was actually filled // Check if Open trade was filled (means position was opened on the broker) if (position.Open?.Status == TradeStatus.Filled) { await NotifyAgentAndPlatformGrainAsync(NotificationEventType.PositionClosed, position); - + // Update the last position closing time for cooldown period tracking // Only update if position was actually filled LastPositionClosingTime = Config.IsForBacktest ? currentCandle.Date : DateTime.UtcNow; @@ -1586,10 +1590,7 @@ public class TradingBotBase : ITradingBot if (!Config.IsForBacktest) { await ServiceScopeHelpers.WithScopedService(_scopeFactory, - async messengerService => - { - await messengerService.SendClosedPosition(position, Account.User); - }); + async messengerService => { await messengerService.SendClosedPosition(position, Account.User); }); } await CancelAllOrders(); @@ -1810,7 +1811,7 @@ public class TradingBotBase : ITradingBot /// The direction of the trade (Long/Short). /// The created Position object. /// Throws if no candles are available or position opening fails. - public async Task OpenPositionManually(TradeDirection direction) + public async Task CreateManualSignal(TradeDirection direction) { if (LastCandle == null) { @@ -1828,22 +1829,7 @@ public class TradingBotBase : ITradingBot // Add the signal to our collection await AddSignal(signal); - // Open the position using the generated signal (SL/TP handled by MoneyManagement) - var position = await OpenPosition(signal); - - if (position == null) - { - // Clean up the signal if position creation failed - SetSignalStatus(signal.Identifier, SignalStatus.Expired); - throw new Exception("Failed to open position"); - } - - // Add the position to the list after successful creation - Positions[position.Identifier] = position; - - Logger.LogInformation( - $"👤 Manual Position Opened\nPosition: `{position.Identifier}`\nSignal: `{signal.Identifier}`\nAdded to positions list"); - return position; + return signal; } public async Task AddSignal(LightSignal signal) @@ -1866,7 +1852,7 @@ public class TradingBotBase : ITradingBot $"🆔 Signal ID: `{signal.Identifier}`"; // Apply Synth-based signal filtering if enabled - if ((Config.UseSynthApi || !Config.IsForBacktest) && ExecutionCount > 0) + if (Config.UseSynthApi && !Config.IsForBacktest && ExecutionCount > 0) { await ServiceScopeHelpers.WithScopedServices(_scopeFactory, async (tradingService, exchangeService) => @@ -2052,11 +2038,26 @@ public class TradingBotBase : ITradingBot changes.Add($"👀 Watch Only: {oldWatch} → {newWatch}"); } - if (Config.MoneyManagement?.GetType().Name != newConfig.MoneyManagement?.GetType().Name) + // Check for changes in individual MoneyManagement properties + if (Config.MoneyManagement?.StopLoss != newConfig.MoneyManagement?.StopLoss) { - var oldMM = Config.MoneyManagement?.GetType().Name ?? "None"; - var newMM = newConfig.MoneyManagement?.GetType().Name ?? "None"; - changes.Add($"💰 Money Management: {oldMM} → {newMM}"); + var oldStopLoss = Config.MoneyManagement?.StopLoss.ToString("P2") ?? "None"; + var newStopLoss = newConfig.MoneyManagement?.StopLoss.ToString("P2") ?? "None"; + changes.Add($"🛑 Stop Loss: {oldStopLoss} → {newStopLoss}"); + } + + if (Config.MoneyManagement?.TakeProfit != newConfig.MoneyManagement?.TakeProfit) + { + var oldTakeProfit = Config.MoneyManagement?.TakeProfit.ToString("P2") ?? "None"; + var newTakeProfit = newConfig.MoneyManagement?.TakeProfit.ToString("P2") ?? "None"; + changes.Add($"🎯 Take Profit: {oldTakeProfit} → {newTakeProfit}"); + } + + if (Config.MoneyManagement?.Leverage != newConfig.MoneyManagement?.Leverage) + { + var oldLeverage = Config.MoneyManagement?.Leverage.ToString("F1") + "x" ?? "None"; + var newLeverage = newConfig.MoneyManagement?.Leverage.ToString("F1") + "x" ?? "None"; + changes.Add($"⚡ Leverage: {oldLeverage} → {newLeverage}"); } if (Config.RiskManagement != newConfig.RiskManagement) @@ -2260,13 +2261,13 @@ public class TradingBotBase : ITradingBot { var grainKey = CandleHelpers.GetCandleStoreGrainKey(Account.Exchange, Config.Ticker, Config.Timeframe); var grain = grainFactory.GetGrain(grainKey); - + var lastCandles = await grain.GetLastCandle(1); LastCandle = lastCandles.FirstOrDefault(); - + if (LastCandle != null) { - Logger.LogDebug("Successfully refreshed last candle for {Ticker} at {Date}", + Logger.LogDebug("Successfully refreshed last candle for {Ticker} at {Date}", Config.Ticker, LastCandle.Date); } else diff --git a/src/Managing.Application/Grains/PlatformSummaryGrain.cs b/src/Managing.Application/Grains/PlatformSummaryGrain.cs index a08bec7c..8120deab 100644 --- a/src/Managing.Application/Grains/PlatformSummaryGrain.cs +++ b/src/Managing.Application/Grains/PlatformSummaryGrain.cs @@ -50,18 +50,18 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable // Daily reminder - runs at midnight (00:00 UTC) var nextDailyTime = CandleHelpers.GetNextExpectedCandleTime(Timeframe.OneDay, now); var timeUntilNextDay = nextDailyTime - now; - + // Ensure dueTime is never negative - if it is, schedule for next day if (timeUntilNextDay <= TimeSpan.Zero) { timeUntilNextDay = TimeSpan.FromDays(1).Add(TimeSpan.FromMinutes(3)); _logger.LogWarning("Due time was negative or zero, scheduling reminder for next day instead"); } - + await this.RegisterOrUpdateReminder(_dailySnapshotReminder, timeUntilNextDay, TimeSpan.FromDays(1).Add(TimeSpan.FromMinutes(3))); - _logger.LogInformation("Daily reminder scheduled - Next daily: {NextDaily}, Due time: {DueTime}", + _logger.LogInformation("Daily reminder scheduled - Next daily: {NextDaily}, Due time: {DueTime}", nextDailyTime, timeUntilNextDay); // Wipe daily snapshots except for the first day @@ -136,12 +136,14 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable foreach (var position in positions) { + if (!position.IsValidForMetrics()) continue; + // Calculate volume using the dedicated method var positionVolume = TradingHelpers.GetVolumeForPosition(position); totalVolume += positionVolume; // Add to open interest for active positions only (only opening volume) - if (!position.IsFinished()) + if (position.Status.Equals(PositionStatus.Filled)) { var openingVolume = position.Open.Price * position.Open.Quantity * position.Open.Leverage; totalOpenInterest += openingVolume; @@ -175,7 +177,7 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable _state.State.PositionCountByAsset[ticker]++; // Position count breakdown by direction - only count finished positions - if (!position.IsFinished()) + if (position.IsValidForMetrics()) { if (!_state.State.PositionCountByDirection.ContainsKey(direction)) { @@ -201,6 +203,7 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable _state.State.PositionCountByDirection.GetValueOrDefault(TradeDirection.Short, 0)); _state.State.LastUpdated = DateTime.UtcNow; + await RefreshAgentCountAsync(); await _state.WriteStateAsync(); _logger.LogInformation("Platform summary data refreshed successfully"); @@ -344,7 +347,7 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable } var originalCount = _state.State.DailySnapshots.Count; - + // Keep only the first day snapshot var firstSnapshot = _state.State.DailySnapshots.OrderBy(s => s.Date).First(); _state.State.DailySnapshots.Clear(); @@ -354,8 +357,8 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable _state.State.LastSnapshot = firstSnapshot.Date; await _state.WriteStateAsync(); - - _logger.LogInformation("Wiped {WipedCount} daily snapshots, kept first snapshot from {FirstDate}", + + _logger.LogInformation("Wiped {WipedCount} daily snapshots, kept first snapshot from {FirstDate}", originalCount - 1, firstSnapshot.Date); } catch (Exception ex) @@ -375,7 +378,7 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable // Get all positions to calculate missing snapshots var positions = await _tradingService.GetAllDatabasePositionsAsync(); - + if (!positions.Any()) { _logger.LogInformation("No positions found, skipping gap filling"); @@ -388,7 +391,7 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable var today = DateTime.UtcNow.Date; // Determine the start date for gap filling - var startDate = _state.State.DailySnapshots.Any() + var startDate = _state.State.DailySnapshots.Any() ? _state.State.DailySnapshots.Max(s => s.Date).AddDays(1) : earliestPositionDate; @@ -419,8 +422,9 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable { var snapshot = await CalculateDailySnapshotFromPositionsAsync(positions.ToList(), missingDate); _state.State.DailySnapshots.Add(snapshot); - - _logger.LogInformation("Created missing daily snapshot for {Date}: Volume={Volume}, PnL={PnL}, Positions={Positions}", + + _logger.LogInformation( + "Created missing daily snapshot for {Date}: Volume={Volume}, PnL={PnL}, Positions={Positions}", missingDate, snapshot.TotalVolume, snapshot.TotalPnL, snapshot.TotalLifetimePositionCount); } @@ -448,7 +452,8 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable /// All positions to analyze /// The date to calculate the snapshot up to /// A cumulative daily snapshot for the specified date - private async Task CalculateDailySnapshotFromPositionsAsync(List positions, DateTime targetDate) + private async Task CalculateDailySnapshotFromPositionsAsync(List positions, + DateTime targetDate) { var dayStart = targetDate; var dayEnd = targetDate.AddDays(1); @@ -468,7 +473,7 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable // Calculate open interest at different points during the day to find the maximum var hourlyOpenInterest = new List(); - + // Check open interest at each hour of the day (0-23) for (int hour = 0; hour < 24; hour++) { @@ -478,11 +483,15 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable foreach (var position in positions) { // Check if position was active at this hour - var wasActiveAtThisHour = position.Date <= hourDateTime && - (!position.IsFinished() || - (position.StopLoss.Status == TradeStatus.Filled && position.StopLoss.Date > hourDateTime) || - (position.TakeProfit1.Status == TradeStatus.Filled && position.TakeProfit1.Date > hourDateTime) || - (position.TakeProfit2 != null && position.TakeProfit2.Status == TradeStatus.Filled && position.TakeProfit2.Date > hourDateTime)); + var wasActiveAtThisHour = position.Date <= hourDateTime && + (!position.IsFinished() || + (position.StopLoss.Status == TradeStatus.Filled && + position.StopLoss.Date > hourDateTime) || + (position.TakeProfit1.Status == TradeStatus.Filled && + position.TakeProfit1.Date > hourDateTime) || + (position.TakeProfit2 != null && + position.TakeProfit2.Status == TradeStatus.Filled && + position.TakeProfit2.Date > hourDateTime)); if (wasActiveAtThisHour) { @@ -491,7 +500,7 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable hourlyOI += openingVolume; } } - + hourlyOpenInterest.Add(hourlyOI); } @@ -502,16 +511,18 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable { // Calculate CUMULATIVE volume up to this point in time // Include all positions that were opened on or before the target date - _logger.LogDebug("Checking position {PositionId}: Position.Date={PositionDate}, TargetDate={TargetDate}, Position.Date.Date={PositionDateOnly}", + _logger.LogDebug( + "Checking position {PositionId}: Position.Date={PositionDate}, TargetDate={TargetDate}, Position.Date.Date={PositionDateOnly}", position.Identifier, position.Date, targetDate, position.Date.Date); - + // Add opening volume if position was opened on or before this day // Use more flexible date comparison to handle timezone differences if (position.Date.Date <= targetDate) { var openingVolume = position.Open.Price * position.Open.Quantity * position.Open.Leverage; totalVolume += openingVolume; - _logger.LogDebug("Position {PositionId} opened on/before {TargetDate}: Opening volume = {OpeningVolume}", + _logger.LogDebug( + "Position {PositionId} opened on/before {TargetDate}: Opening volume = {OpeningVolume}", position.Identifier, targetDate, openingVolume); } @@ -520,21 +531,29 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable { if (position.StopLoss.Status == TradeStatus.Filled && position.StopLoss.Date.Date <= targetDate) { - var closingVolume = position.StopLoss.Price * position.StopLoss.Quantity * position.StopLoss.Leverage; + var closingVolume = position.StopLoss.Price * position.StopLoss.Quantity * + position.StopLoss.Leverage; totalVolume += closingVolume; - _logger.LogDebug("Position {PositionId} closed on/before {TargetDate} via StopLoss: Closing volume = {ClosingVolume}", + _logger.LogDebug( + "Position {PositionId} closed on/before {TargetDate} via StopLoss: Closing volume = {ClosingVolume}", position.Identifier, targetDate, closingVolume); } + if (position.TakeProfit1.Status == TradeStatus.Filled && position.TakeProfit1.Date.Date <= targetDate) { - var closingVolume = position.TakeProfit1.Price * position.TakeProfit1.Quantity * position.TakeProfit1.Leverage; + var closingVolume = position.TakeProfit1.Price * position.TakeProfit1.Quantity * + position.TakeProfit1.Leverage; totalVolume += closingVolume; - _logger.LogDebug("Position {PositionId} closed on/before {TargetDate} via TakeProfit1: Closing volume = {ClosingVolume}", + _logger.LogDebug( + "Position {PositionId} closed on/before {TargetDate} via TakeProfit1: Closing volume = {ClosingVolume}", position.Identifier, targetDate, closingVolume); } - if (position.TakeProfit2 != null && position.TakeProfit2.Status == TradeStatus.Filled && position.TakeProfit2.Date.Date <= targetDate) + + if (position.TakeProfit2 != null && position.TakeProfit2.Status == TradeStatus.Filled && + position.TakeProfit2.Date.Date <= targetDate) { - var closingVolume = position.TakeProfit2.Price * position.TakeProfit2.Quantity * position.TakeProfit2.Leverage; + var closingVolume = position.TakeProfit2.Price * position.TakeProfit2.Quantity * + position.TakeProfit2.Leverage; totalVolume += closingVolume; } } @@ -543,7 +562,8 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable var wasClosedOnOrBeforeThisDay = position.IsFinished() && ( (position.StopLoss.Status == TradeStatus.Filled && position.StopLoss.Date.Date <= targetDate) || (position.TakeProfit1.Status == TradeStatus.Filled && position.TakeProfit1.Date.Date <= targetDate) || - (position.TakeProfit2 != null && position.TakeProfit2.Status == TradeStatus.Filled && position.TakeProfit2.Date.Date <= targetDate) + (position.TakeProfit2 != null && position.TakeProfit2.Status == TradeStatus.Filled && + position.TakeProfit2.Date.Date <= targetDate) ); if (wasClosedOnOrBeforeThisDay) @@ -563,7 +583,8 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable var totalAgents = await _agentService.GetTotalAgentCount(); var totalStrategies = _state.State.TotalActiveStrategies; - _logger.LogInformation("Calculated CUMULATIVE daily snapshot for {TargetDate}: CumVolume={TotalVolume}, MaxOpenInterest={MaxOpenInterest}, CumPositionCount={TotalPositionCount}", + _logger.LogInformation( + "Calculated CUMULATIVE daily snapshot for {TargetDate}: CumVolume={TotalVolume}, MaxOpenInterest={MaxOpenInterest}, CumPositionCount={TotalPositionCount}", targetDate, totalVolume, maxOpenInterest, totalPositionCount); return new DailySnapshot diff --git a/src/Managing.Application/ManageBot/BotService.cs b/src/Managing.Application/ManageBot/BotService.cs index 20c6c6e7..532e1947 100644 --- a/src/Managing.Application/ManageBot/BotService.cs +++ b/src/Managing.Application/ManageBot/BotService.cs @@ -8,6 +8,7 @@ using Managing.Common; using Managing.Core; using Managing.Domain.Accounts; using Managing.Domain.Bots; +using Managing.Domain.Indicators; using Managing.Domain.Scenarios; using Managing.Domain.Shared.Helpers; using Managing.Domain.Trades; @@ -264,10 +265,10 @@ namespace Managing.Application.ManageBot return await _botRepository.GetBotByIdentifierAsync(identifier); } - public async Task OpenPositionManuallyAsync(Guid identifier, TradeDirection direction) + public async Task CreateManualSignalAsync(Guid identifier, TradeDirection direction) { var grain = _grainFactory.GetGrain(identifier); - return await grain.OpenPositionManuallyAsync(direction); + return await grain.CreateManualSignalAsync(direction); } public async Task ClosePositionAsync(Guid identifier, Guid positionId) diff --git a/src/Managing.Domain/Trades/Position.cs b/src/Managing.Domain/Trades/Position.cs index 2304f155..8b5f808e 100644 --- a/src/Managing.Domain/Trades/Position.cs +++ b/src/Managing.Domain/Trades/Position.cs @@ -73,7 +73,9 @@ namespace Managing.Domain.Trades /// /// Identifier of the bot or entity that initiated this position /// - [Id(17)] [Required] public Guid InitiatorIdentifier { get; set; } + [Id(17)] + [Required] + public Guid InitiatorIdentifier { get; set; } public bool IsFinished() { @@ -85,6 +87,18 @@ namespace Managing.Domain.Trades }; } + public bool IsValidForMetrics() + { + return Status switch + { + PositionStatus.Filled => true, + PositionStatus.Finished => true, + PositionStatus.Flipped => true, + PositionStatus.Updating => true, + _ => false + }; + } + /// /// Calculates the total fees for this position based on GMX V2 fee structure /// diff --git a/src/Managing.Infrastructure.Web3/EvmManager.cs b/src/Managing.Infrastructure.Web3/EvmManager.cs index 57538308..efc1d719 100644 --- a/src/Managing.Infrastructure.Web3/EvmManager.cs +++ b/src/Managing.Infrastructure.Web3/EvmManager.cs @@ -671,7 +671,7 @@ public class EvmManager : IEvmManager } catch (Exception e) { - Console.WriteLine(e); + SentrySdk.CaptureException(e); throw; } } diff --git a/src/Managing.Web3Proxy/src/routes/api/gmx/index.ts b/src/Managing.Web3Proxy/src/routes/api/gmx/index.ts index bd547706..17490f96 100644 --- a/src/Managing.Web3Proxy/src/routes/api/gmx/index.ts +++ b/src/Managing.Web3Proxy/src/routes/api/gmx/index.ts @@ -36,7 +36,7 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => { } }, async (request, reply) => { const { account, tradeType, ticker, direction, price, quantity, leverage, stopLossPrice, takeProfitPrice } = request.body - + // Call the openPosition method from the GMX plugin return request.openGmxPosition( reply, diff --git a/src/Managing.Web3Proxy/test/plugins/close-position.test.ts b/src/Managing.Web3Proxy/test/plugins/close-position.test.ts index e2429763..653e167e 100644 --- a/src/Managing.Web3Proxy/test/plugins/close-position.test.ts +++ b/src/Managing.Web3Proxy/test/plugins/close-position.test.ts @@ -9,8 +9,8 @@ test('GMX Position Closing', async (t) => { const result = await closeGmxPositionImpl( sdk, - "DOGE", - TradeDirection.Short + "ADA", + TradeDirection.Long ) console.log('Position closing result:', result) assert.ok(result, 'Position closing result should be defined') diff --git a/src/Managing.WebApp/src/components/mollecules/ManualPositionModal.tsx b/src/Managing.WebApp/src/components/mollecules/ManualPositionModal.tsx index c0372008..13262906 100644 --- a/src/Managing.WebApp/src/components/mollecules/ManualPositionModal.tsx +++ b/src/Managing.WebApp/src/components/mollecules/ManualPositionModal.tsx @@ -30,17 +30,17 @@ function ManualPositionModal({ showModal, botName, onClose }: ManualPositionModa const onSubmit = async (data: ManualPositionFormValues) => { if (!botName) return - const t = new Toast('Opening position...') + const t = new Toast('Creating signal...') try { - await client.bot_OpenPositionManually({ + await client.bot_CreateManualSignal({ identifier: botName, direction: data.direction, }) - t.update('success', 'Position opened successfully') + t.update('success', 'Signal created successfully') reset() onClose() } catch (error: any) { - t.update('error', `Failed to open position: ${error.message || error}`) + t.update('error', `Failed to create signal: ${error.message || error}`) } } @@ -49,7 +49,7 @@ function ManualPositionModal({ showModal, botName, onClose }: ManualPositionModa return (
-

Open Position Manually for {botName}

+

Create Signal Manually

- +
diff --git a/src/Managing.WebApp/src/components/organism/CustomMoneyManagement/CustomMoneyManagement.tsx b/src/Managing.WebApp/src/components/organism/CustomMoneyManagement/CustomMoneyManagement.tsx index e8c11adf..5952345c 100644 --- a/src/Managing.WebApp/src/components/organism/CustomMoneyManagement/CustomMoneyManagement.tsx +++ b/src/Managing.WebApp/src/components/organism/CustomMoneyManagement/CustomMoneyManagement.tsx @@ -18,15 +18,15 @@ const CustomMoneyManagement: React.FC = ({ const { moneyManagement, setCustomMoneyManagement } = useCustomMoneyManagement() const [leverage, setLeverage] = useState(moneyManagement?.leverage || 1) - const [takeProfit, setTakeProfit] = useState(moneyManagement?.takeProfit || 2) - const [stopLoss, setStopLoss] = useState(moneyManagement?.stopLoss || 1) + const [takeProfit, setTakeProfit] = useState((moneyManagement?.takeProfit || 0.02) * 100) // Convert decimal to percentage + const [stopLoss, setStopLoss] = useState((moneyManagement?.stopLoss || 0.01) * 100) // Convert decimal to percentage const handleCreateMoneyManagement = () => { const moneyManagement: MoneyManagement = { leverage, name: 'custom', - stopLoss, - takeProfit, + stopLoss: stopLoss / 100, // Convert percentage to decimal + takeProfit: takeProfit / 100, // Convert percentage to decimal timeframe, } onCreateMoneyManagement(moneyManagement) @@ -37,6 +37,15 @@ const CustomMoneyManagement: React.FC = ({ handleCreateMoneyManagement() }, [leverage, takeProfit, stopLoss]) + // Update local state when global moneyManagement changes + useEffect(() => { + if (moneyManagement) { + setLeverage(moneyManagement.leverage) + setTakeProfit(moneyManagement.takeProfit * 100) // Convert decimal to percentage + setStopLoss(moneyManagement.stopLoss * 100) // Convert decimal to percentage + } + }, [moneyManagement]) + return ( <> {showCustomMoneyManagement ? ( diff --git a/src/Managing.WebApp/src/components/organism/UnifiedTradingModal/UnifiedTradingModal.tsx b/src/Managing.WebApp/src/components/organism/UnifiedTradingModal/UnifiedTradingModal.tsx index 4673a9f4..0a76f353 100644 --- a/src/Managing.WebApp/src/components/organism/UnifiedTradingModal/UnifiedTradingModal.tsx +++ b/src/Managing.WebApp/src/components/organism/UnifiedTradingModal/UnifiedTradingModal.tsx @@ -7,25 +7,25 @@ import useBacktestStore from '../../../app/store/backtestStore' import {useCustomMoneyManagement} from '../../../app/store/customMoneyManagement' import {useCustomScenario} from '../../../app/store/customScenario' import { - AccountClient, - BacktestClient, - BotClient, - DataClient, - LightBacktestResponse, - LightScenario, - MoneyManagement, - MoneyManagementClient, - RiskManagement, - RiskToleranceLevel, - RunBacktestRequest, - ScenarioClient, - ScenarioRequest, - SignalType, - StartBotRequest, - Ticker, - Timeframe, - TradingBotConfigRequest, - UpdateBotConfigRequest, + AccountClient, + BacktestClient, + BotClient, + DataClient, + LightBacktestResponse, + LightScenario, + MoneyManagement, + MoneyManagementClient, + RiskManagement, + RiskToleranceLevel, + RunBacktestRequest, + ScenarioClient, + ScenarioRequest, + SignalType, + StartBotRequest, + Ticker, + Timeframe, + TradingBotConfigRequest, + UpdateBotConfigRequest, } from '../../../generated/ManagingApi' import type {IUnifiedTradingConfigInput, UnifiedTradingModalProps} from '../../../global/type' import {Loader} from '../../atoms' @@ -142,7 +142,7 @@ const UnifiedTradingModal: React.FC = ({ const { apiUrl } = useApiUrlStore(); const { addBacktest } = useBacktestStore(); - const { setCustomMoneyManagement: setGlobalCustomMoneyManagement } = useCustomMoneyManagement(); + const { moneyManagement: globalCustomMoneyManagement, setCustomMoneyManagement: setGlobalCustomMoneyManagement } = useCustomMoneyManagement(); const { setCustomScenario: setGlobalCustomScenario } = useCustomScenario(); // API clients @@ -620,7 +620,7 @@ const UnifiedTradingModal: React.FC = ({ let moneyManagement: MoneyManagement | undefined = undefined; if (showCustomMoneyManagement || (mode === 'createBot' && backtest)) { - moneyManagement = customMoneyManagement; + moneyManagement = globalCustomMoneyManagement || customMoneyManagement; } else { const selectedMM = moneyManagements?.find(mm => mm.name === selectedMoneyManagement); if (selectedMM) { @@ -712,7 +712,7 @@ const UnifiedTradingModal: React.FC = ({ useForSignalFiltering: form.useForSignalFiltering ?? true, useForDynamicStopLoss: form.useForDynamicStopLoss ?? true, moneyManagementName: showCustomMoneyManagement ? undefined : selectedMoneyManagement, - moneyManagement: customMoneyManagement, + moneyManagement: globalCustomMoneyManagement || customMoneyManagement, flipPosition: form.flipPosition || false, }; diff --git a/src/Managing.WebApp/src/generated/ManagingApi.ts b/src/Managing.WebApp/src/generated/ManagingApi.ts index 0b7241da..474c214a 100644 --- a/src/Managing.WebApp/src/generated/ManagingApi.ts +++ b/src/Managing.WebApp/src/generated/ManagingApi.ts @@ -1591,8 +1591,8 @@ export class BotClient extends AuthorizedApiBase { return Promise.resolve(null as any); } - bot_OpenPositionManually(request: OpenPositionManuallyRequest): Promise { - let url_ = this.baseUrl + "/Bot/OpenPosition"; + bot_CreateManualSignal(request: CreateManualSignalRequest): Promise { + let url_ = this.baseUrl + "/Bot/CreateManualSignal"; url_ = url_.replace(/[?&]$/, ""); const content_ = JSON.stringify(request); @@ -1609,17 +1609,17 @@ export class BotClient extends AuthorizedApiBase { return this.transformOptions(options_).then(transformedOptions_ => { return this.http.fetch(url_, transformedOptions_); }).then((_response: Response) => { - return this.processBot_OpenPositionManually(_response); + return this.processBot_CreateManualSignal(_response); }); } - protected processBot_OpenPositionManually(response: Response): Promise { + protected processBot_CreateManualSignal(response: Response): Promise { const status = response.status; let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; if (status === 200) { return response.text().then((_responseText) => { let result200: any = null; - result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as Position; + result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as LightSignal; return result200; }); } else if (status !== 200 && status !== 204) { @@ -1627,7 +1627,7 @@ export class BotClient extends AuthorizedApiBase { return throwException("An unexpected server error occurred.", status, _responseText, _headers); }); } - return Promise.resolve(null as any); + return Promise.resolve(null as any); } bot_ClosePosition(request: ClosePositionRequest): Promise { @@ -4309,7 +4309,7 @@ export enum BotSortableColumn { AgentName = "AgentName", } -export interface OpenPositionManuallyRequest { +export interface CreateManualSignalRequest { identifier?: string; direction?: TradeDirection; } diff --git a/src/Managing.WebApp/src/generated/ManagingApiTypes.ts b/src/Managing.WebApp/src/generated/ManagingApiTypes.ts index 14ea3e1e..49559ee7 100644 --- a/src/Managing.WebApp/src/generated/ManagingApiTypes.ts +++ b/src/Managing.WebApp/src/generated/ManagingApiTypes.ts @@ -784,7 +784,7 @@ export enum BotSortableColumn { AgentName = "AgentName", } -export interface OpenPositionManuallyRequest { +export interface CreateManualSignalRequest { identifier?: string; direction?: TradeDirection; }