From 973a8c7c6108d8856dc61e1d450a3eee9840aad1 Mon Sep 17 00:00:00 2001 From: cryptooda Date: Wed, 4 Jun 2025 23:15:50 +0700 Subject: [PATCH] Update config --- .../Controllers/BacktestController.cs | 3 +- src/Managing.Api/Controllers/BotController.cs | 274 ++++---- .../Models/Responses/TradingBot.cs | 40 -- .../Models/Responses/TradingBotResponse.cs | 56 ++ src/Managing.Application.Tests/BotsTests.cs | 35 +- .../StatisticService.cs | 5 +- .../Abstractions/IBotService.cs | 2 +- src/Managing.Application/Bots/TradingBot.cs | 8 +- .../ManageBot/BotService.cs | 23 +- .../ManageBot/StartBotCommandHandler.cs | 3 +- .../BotConfigModal/BotConfigModal.tsx | 619 ++++++++++++++++++ .../organism/Backtest/backtestTable.tsx | 151 +---- .../src/generated/ManagingApi.ts | 36 +- .../src/pages/botsPage/botList.tsx | 83 ++- 14 files changed, 969 insertions(+), 369 deletions(-) delete mode 100644 src/Managing.Api/Models/Responses/TradingBot.cs create mode 100644 src/Managing.Api/Models/Responses/TradingBotResponse.cs create mode 100644 src/Managing.WebApp/src/components/mollecules/BotConfigModal/BotConfigModal.tsx diff --git a/src/Managing.Api/Controllers/BacktestController.cs b/src/Managing.Api/Controllers/BacktestController.cs index 3b76aea..a26e8fc 100644 --- a/src/Managing.Api/Controllers/BacktestController.cs +++ b/src/Managing.Api/Controllers/BacktestController.cs @@ -169,7 +169,8 @@ public class BacktestController : BaseController MaxPositionTimeHours = request.Config.MaxPositionTimeHours, FlipOnlyWhenInProfit = request.Config.FlipOnlyWhenInProfit, FlipPosition = request.Config.FlipPosition, - Name = request.Config.Name ?? $"Backtest-{request.Config.ScenarioName}-{DateTime.UtcNow:yyyyMMdd-HHmmss}" + Name = request.Config.Name ?? $"Backtest-{request.Config.ScenarioName}-{DateTime.UtcNow:yyyyMMdd-HHmmss}", + CloseEarlyWhenProfitable = request.Config.CloseEarlyWhenProfitable }; switch (request.Config.BotType) diff --git a/src/Managing.Api/Controllers/BotController.cs b/src/Managing.Api/Controllers/BotController.cs index 8333497..63f3b4c 100644 --- a/src/Managing.Api/Controllers/BotController.cs +++ b/src/Managing.Api/Controllers/BotController.cs @@ -1,4 +1,6 @@ -using Managing.Application.Abstractions; +using System.ComponentModel.DataAnnotations; +using Managing.Api.Models.Responses; +using Managing.Application.Abstractions; using Managing.Application.Abstractions.Services; using Managing.Application.Hubs; using Managing.Application.ManageBot.Commands; @@ -11,7 +13,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; using static Managing.Common.Enums; -using ApiTradingBot = Managing.Api.Models.Responses.TradingBot; namespace Managing.Api.Controllers; @@ -142,6 +143,30 @@ public class BotController : BaseController $"Initial trading balance must be greater than {Constants.GMX.Config.MinimumPositionAmount}"); } + // Validate cooldown period + if (request.Config.CooldownPeriod < 1) + { + return BadRequest("Cooldown period must be at least 1 candle"); + } + + // Validate max loss streak + if (request.Config.MaxLossStreak < 0) + { + return BadRequest("Max loss streak cannot be negative"); + } + + // Validate max position time hours + if (request.Config.MaxPositionTimeHours.HasValue && request.Config.MaxPositionTimeHours.Value <= 0) + { + return BadRequest("Max position time hours must be greater than 0 if specified"); + } + + // Validate CloseEarlyWhenProfitable consistency + if (request.Config.CloseEarlyWhenProfitable && !request.Config.MaxPositionTimeHours.HasValue) + { + return BadRequest("CloseEarlyWhenProfitable can only be enabled when MaxPositionTimeHours is set"); + } + // Update the config with final money management var config = new TradingBotConfig { @@ -159,7 +184,8 @@ public class BotController : BaseController FlipOnlyWhenInProfit = request.Config.FlipOnlyWhenInProfit, IsForBacktest = false, FlipPosition = request.Config.BotType == BotType.FlippingBot, - Name = request.Config.Name + Name = request.Config.Name, + CloseEarlyWhenProfitable = request.Config.CloseEarlyWhenProfitable }; var result = await _mediator.Send(new StartBotCommand(config, request.Config.Name, user)); @@ -250,11 +276,11 @@ public class BotController : BaseController { var bots = await GetBotList(); // Filter to only include bots owned by the current user - var userBots = new List(); + var userBots = new List(); foreach (var bot in bots) { - var account = await _accountService.GetAccount(bot.AccountName, true, false); + var account = await _accountService.GetAccount(bot.Config.AccountName, true, false); // Compare the user names if (account != null && account.User != null && account.User.Name == user.Name) { @@ -264,7 +290,7 @@ public class BotController : BaseController foreach (var bot in userBots) { - await _mediator.Send(new StopBotCommand(bot.BotType, bot.Identifier)); + await _mediator.Send(new StopBotCommand(bot.Config.BotType, bot.Identifier)); await _hubContext.Clients.All.SendAsync("SendNotification", $"Bot {bot.Identifier} paused by {user.Name}.", "Info"); } @@ -326,12 +352,12 @@ public class BotController : BaseController { var bots = await GetBotList(); // Filter to only include bots owned by the current user - var userBots = new List(); + var userBots = new List(); var accountService = HttpContext.RequestServices.GetRequiredService(); foreach (var bot in bots) { - var account = await accountService.GetAccount(bot.AccountName, true, false); + var account = await accountService.GetAccount(bot.Config.AccountName, true, false); // Compare the user names if (account != null && account.User != null && account.User.Name == user.Name) { @@ -343,7 +369,7 @@ public class BotController : BaseController { // We can't directly restart a bot with just BotType and Name // Instead, stop the bot and then retrieve the backup to start it again - await _mediator.Send(new StopBotCommand(bot.BotType, bot.Identifier)); + await _mediator.Send(new StopBotCommand(bot.Config.BotType, bot.Identifier)); // Get the saved bot backup var backup = _botService.GetBotBackup(bot.Identifier); @@ -401,7 +427,7 @@ public class BotController : BaseController /// /// A list of active trading bots. [HttpGet] - public async Task> GetActiveBots() + public async Task> GetActiveBots() { return await GetBotList(); } @@ -410,33 +436,24 @@ public class BotController : BaseController /// Retrieves a list of active bots by sending a command to the mediator. /// /// A list of trading bots. - private async Task> GetBotList() + private async Task> GetBotList() { var result = await _mediator.Send(new GetActiveBotsCommand()); - var list = new List(); + var list = new List(); foreach (var item in result) { - list.Add(new ApiTradingBot + list.Add(new TradingBotResponse { Status = item.GetStatus(), - Name = item.Name, Signals = item.Signals.ToList(), Positions = item.Positions, Candles = item.Candles.DistinctBy(c => c.Date).ToList(), WinRate = item.GetWinRate(), ProfitAndLoss = item.GetProfitAndLoss(), - Timeframe = item.Config.Timeframe, - Ticker = item.Config.Ticker, - Scenario = item.Config.ScenarioName, - IsForWatchingOnly = item.Config.IsForWatchingOnly, - BotType = item.Config.BotType, - AccountName = item.Config.AccountName, - MoneyManagement = item.Config.MoneyManagement, Identifier = item.Identifier, AgentName = item.User.AgentName, - MaxPositionTimeHours = item.Config.MaxPositionTimeHours, - FlipOnlyWhenInProfit = item.Config.FlipOnlyWhenInProfit + Config = item.Config // Contains all configuration properties }); } @@ -562,97 +579,107 @@ public class BotController : BaseController } /// - /// Updates the configuration of a running bot + /// Updates the configuration of an existing bot. /// - /// The request containing the new bot configuration - /// A response indicating the result of the operation + /// The update request containing the bot identifier and new configuration + /// Success message [HttpPut] [Route("UpdateConfig")] public async Task> UpdateBotConfig([FromBody] UpdateBotConfigRequest request) { try { - // Check if user owns the account + var user = await GetUser(); + if (user == null) + { + return Unauthorized("User not found"); + } + + if (string.IsNullOrEmpty(request.Identifier)) + { + return BadRequest("Bot identifier is required"); + } + + if (request.Config == null) + { + return BadRequest("Bot configuration is required"); + } + + // First, check if the user owns the existing bot if (!await UserOwnsBotAccount(request.Identifier)) { - return Forbid("You don't have permission to update this bot's configuration"); + return Forbid("You don't have permission to update this bot"); } - var activeBots = _botService.GetActiveBots(); - var bot = activeBots.FirstOrDefault(b => b.Identifier == request.Identifier); - - if (bot == null) - { - return NotFound($"Bot with identifier {request.Identifier} not found or is not running"); - } - - // Get the user for validation - var user = await GetUser(); + // Get the existing bot to ensure it exists and get current config + var bots = _botService.GetActiveBots(); + var existingBot = bots.FirstOrDefault(b => b.Identifier == request.Identifier); - // Validate money management if provided - MoneyManagement moneyManagement = null; - if (!string.IsNullOrEmpty(request.MoneyManagementName)) + if (existingBot == null) { - moneyManagement = await _moneyManagementService.GetMoneyMangement(user, request.MoneyManagementName); + return NotFound($"Bot with identifier '{request.Identifier}' not found"); + } + + // If the account is being changed, verify the user owns the new account too + if (existingBot.Config.AccountName != request.Config.AccountName) + { + if (!await UserOwnsBotAccount(null, request.Config.AccountName)) + { + return Forbid("You don't have permission to use this account"); + } + } + + // Validate the money management if provided + if (request.Config.MoneyManagement != null) + { + // Check if the money management belongs to the user + var userMoneyManagement = await _moneyManagementService.GetMoneyMangement(user, request.Config.MoneyManagement.Name); + if (userMoneyManagement != null && userMoneyManagement.User?.Name != user.Name) + { + return Forbid("You don't have permission to use this money management"); + } + } + else if (!string.IsNullOrEmpty(request.MoneyManagementName)) + { + // If MoneyManagement is null but MoneyManagementName is provided, load it + var moneyManagement = await _moneyManagementService.GetMoneyMangement(user, request.MoneyManagementName); if (moneyManagement == null) { - return BadRequest("Money management not found"); + return BadRequest($"Money management '{request.MoneyManagementName}' not found"); } + + if (moneyManagement.User?.Name != user.Name) + { + return Forbid("You don't have permission to use this money management"); + } + + request.Config.MoneyManagement = moneyManagement; + } + + // Validate CloseEarlyWhenProfitable requires MaxPositionTimeHours + if (request.Config.CloseEarlyWhenProfitable && !request.Config.MaxPositionTimeHours.HasValue) + { + return BadRequest("CloseEarlyWhenProfitable requires MaxPositionTimeHours to be set"); + } + + // Update the bot configuration using the new method + var success = await _botService.UpdateBotConfiguration(request.Identifier, request.Config); + + if (success) + { + await _hubContext.Clients.All.SendAsync("SendNotification", + $"Bot {request.Identifier} configuration updated successfully by {user.Name}.", "Info"); + + return Ok("Bot configuration updated successfully"); } else { - // Keep existing money management if not provided - moneyManagement = bot.Config.MoneyManagement; + return BadRequest("Failed to update bot configuration"); } - - // Validate account if provided - if (!string.IsNullOrEmpty(request.AccountName)) - { - var account = await _accountService.GetAccount(request.AccountName, true, false); - if (account == null || account.User?.Name != user.Name) - { - return BadRequest("Account not found or you don't have permission to use this account"); - } - } - - // Create updated configuration - var updatedConfig = new TradingBotConfig - { - AccountName = !string.IsNullOrEmpty(request.AccountName) ? request.AccountName : bot.Config.AccountName, - MoneyManagement = moneyManagement, - Ticker = request.Ticker ?? bot.Config.Ticker, - ScenarioName = !string.IsNullOrEmpty(request.ScenarioName) ? request.ScenarioName : bot.Config.ScenarioName, - Timeframe = request.Timeframe ?? bot.Config.Timeframe, - IsForWatchingOnly = request.IsForWatchingOnly ?? bot.Config.IsForWatchingOnly, - BotTradingBalance = request.BotTradingBalance ?? bot.Config.BotTradingBalance, - BotType = bot.Config.BotType, // Bot type cannot be changed - CooldownPeriod = request.CooldownPeriod ?? bot.Config.CooldownPeriod, - MaxLossStreak = request.MaxLossStreak ?? bot.Config.MaxLossStreak, - MaxPositionTimeHours = request.MaxPositionTimeHours ?? bot.Config.MaxPositionTimeHours, - FlipOnlyWhenInProfit = request.FlipOnlyWhenInProfit ?? bot.Config.FlipOnlyWhenInProfit, - IsForBacktest = bot.Config.IsForBacktest, // Cannot be changed for running bots - FlipPosition = request.FlipPosition ?? bot.Config.FlipPosition, - Name = !string.IsNullOrEmpty(request.Name) ? request.Name : bot.Config.Name - }; - - // Validate the updated configuration - if (updatedConfig.BotTradingBalance <= Constants.GMX.Config.MinimumPositionAmount) - { - return BadRequest($"Bot trading balance must be greater than {Constants.GMX.Config.MinimumPositionAmount}"); - } - - // Update the bot's configuration - var updateCommand = new UpdateBotConfigCommand(request.Identifier, updatedConfig); - var result = await _mediator.Send(updateCommand); - - _logger.LogInformation($"Bot configuration update result for {request.Identifier} by user {user.Name}: {result}"); - - await NotifyBotSubscriberAsync(); - return Ok(result); } catch (Exception ex) { - _logger.LogError(ex, "Error updating bot configuration"); + _logger.LogError(ex, "Error updating bot configuration for identifier {Identifier}", request.Identifier); return StatusCode(500, $"Error updating bot configuration: {ex.Message}"); } } @@ -712,72 +739,19 @@ public class StartBotRequest public class UpdateBotConfigRequest { /// - /// The identifier of the bot to update + /// The unique identifier of the bot to update /// + [Required] public string Identifier { get; set; } /// - /// The account name to use (optional - will keep existing if not provided) + /// The new trading bot configuration /// - public string? AccountName { get; set; } + [Required] + public TradingBotConfig Config { get; set; } /// - /// The money management name to use (optional - will keep existing if not provided) + /// Optional: Money management name to load if Config.MoneyManagement is null /// public string? MoneyManagementName { get; set; } - - /// - /// The ticker to trade (optional - will keep existing if not provided) - /// - public Ticker? Ticker { get; set; } - - /// - /// The scenario to use (optional - will keep existing if not provided) - /// - public string? ScenarioName { get; set; } - - /// - /// The timeframe to use (optional - will keep existing if not provided) - /// - public Timeframe? Timeframe { get; set; } - - /// - /// Whether the bot is for watching only (optional - will keep existing if not provided) - /// - public bool? IsForWatchingOnly { get; set; } - - /// - /// The bot trading balance (optional - will keep existing if not provided) - /// - public decimal? BotTradingBalance { get; set; } - - /// - /// The cooldown period in candles between positions (optional - will keep existing if not provided) - /// - public int? CooldownPeriod { get; set; } - - /// - /// The maximum number of consecutive losses before stopping (optional - will keep existing if not provided) - /// - public int? MaxLossStreak { get; set; } - - /// - /// Maximum time in hours that a position can remain open before being automatically closed (optional - will keep existing if not provided) - /// - public decimal? MaxPositionTimeHours { get; set; } - - /// - /// If true, positions will only be flipped when the current position is in profit (optional - will keep existing if not provided) - /// - public bool? FlipOnlyWhenInProfit { get; set; } - - /// - /// Whether position flipping is enabled (optional - will keep existing if not provided) - /// - public bool? FlipPosition { get; set; } - - /// - /// The name of the bot (optional - will keep existing if not provided) - /// - public string? Name { get; set; } } \ No newline at end of file diff --git a/src/Managing.Api/Models/Responses/TradingBot.cs b/src/Managing.Api/Models/Responses/TradingBot.cs deleted file mode 100644 index d2eabbe..0000000 --- a/src/Managing.Api/Models/Responses/TradingBot.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using Managing.Domain.Candles; -using Managing.Domain.MoneyManagements; -using Managing.Domain.Strategies; -using Managing.Domain.Trades; -using static Managing.Common.Enums; - -namespace Managing.Api.Models.Responses -{ - public class TradingBot - { - [Required] public string Name { get; internal set; } - [Required] public string Status { get; internal set; } - [Required] public List Signals { get; internal set; } - [Required] public List Positions { get; internal set; } - [Required] public List Candles { get; internal set; } - [Required] public int WinRate { get; internal set; } - [Required] public decimal ProfitAndLoss { get; internal set; } - [Required] public Timeframe Timeframe { get; internal set; } - [Required] public Ticker Ticker { get; internal set; } - [Required] public string Scenario { get; internal set; } - [Required] public bool IsForWatchingOnly { get; internal set; } - [Required] public BotType BotType { get; internal set; } - [Required] public string AccountName { get; internal set; } - [Required] public MoneyManagement MoneyManagement { get; internal set; } - [Required] public string Identifier { get; set; } - [Required] public string AgentName { get; set; } - - /// - /// Maximum time in hours that a position can remain open before being automatically closed. - /// If null, time-based position closure is disabled. - /// - [Required] public decimal? MaxPositionTimeHours { get; internal set; } - - /// - /// If true, positions will only be flipped when the current position is in profit. - /// - [Required] public bool FlipOnlyWhenInProfit { get; internal set; } - } -} \ No newline at end of file diff --git a/src/Managing.Api/Models/Responses/TradingBotResponse.cs b/src/Managing.Api/Models/Responses/TradingBotResponse.cs new file mode 100644 index 0000000..c13f742 --- /dev/null +++ b/src/Managing.Api/Models/Responses/TradingBotResponse.cs @@ -0,0 +1,56 @@ +using System.ComponentModel.DataAnnotations; +using Managing.Domain.Bots; +using Managing.Domain.Candles; +using Managing.Domain.Strategies; +using Managing.Domain.Trades; + +namespace Managing.Api.Models.Responses +{ + public class TradingBotResponse + { + /// + /// Current status of the bot (Up, Down, etc.) + /// + [Required] public string Status { get; internal set; } + + /// + /// List of signals generated by the bot + /// + [Required] public List Signals { get; internal set; } + + /// + /// List of positions opened by the bot + /// + [Required] public List Positions { get; internal set; } + + /// + /// Candles used by the bot for analysis + /// + [Required] public List Candles { get; internal set; } + + /// + /// Current win rate percentage + /// + [Required] public int WinRate { get; internal set; } + + /// + /// Current profit and loss + /// + [Required] public decimal ProfitAndLoss { get; internal set; } + + /// + /// Unique identifier for the bot + /// + [Required] public string Identifier { get; set; } + + /// + /// Agent name associated with the bot + /// + [Required] public string AgentName { get; set; } + + /// + /// The full trading bot configuration + /// + [Required] public TradingBotConfig Config { get; internal set; } + } +} \ No newline at end of file diff --git a/src/Managing.Application.Tests/BotsTests.cs b/src/Managing.Application.Tests/BotsTests.cs index 05bce9f..0ed957b 100644 --- a/src/Managing.Application.Tests/BotsTests.cs +++ b/src/Managing.Application.Tests/BotsTests.cs @@ -73,7 +73,10 @@ namespace Managing.Application.Tests CooldownPeriod = 1, MaxLossStreak = 0, FlipPosition = true, - Name = "Test" + Name = "Test", + FlipOnlyWhenInProfit = true, + MaxPositionTimeHours = null, + CloseEarlyWhenProfitable = false }; // Act @@ -120,7 +123,10 @@ namespace Managing.Application.Tests CooldownPeriod = 1, MaxLossStreak = 0, FlipPosition = false, - Name = "Test" + Name = "Test", + FlipOnlyWhenInProfit = true, + MaxPositionTimeHours = null, + CloseEarlyWhenProfitable = false }; // Act @@ -166,7 +172,10 @@ namespace Managing.Application.Tests CooldownPeriod = 1, MaxLossStreak = 0, FlipPosition = false, - Name = "Test" + Name = "Test", + FlipOnlyWhenInProfit = true, + MaxPositionTimeHours = null, + CloseEarlyWhenProfitable = false }; // Act @@ -253,7 +262,10 @@ namespace Managing.Application.Tests CooldownPeriod = 1, MaxLossStreak = 0, FlipPosition = false, - Name = "Test" + Name = "Test", + FlipOnlyWhenInProfit = true, + MaxPositionTimeHours = null, + CloseEarlyWhenProfitable = false }, candles, null).Result, BotType.FlippingBot => _backtester.RunFlippingBotBacktest(new TradingBotConfig { @@ -269,7 +281,10 @@ namespace Managing.Application.Tests CooldownPeriod = 1, MaxLossStreak = 0, FlipPosition = true, - Name = "Test" + Name = "Test", + FlipOnlyWhenInProfit = true, + MaxPositionTimeHours = null, + CloseEarlyWhenProfitable = false }, candles, null).Result, _ => throw new NotImplementedException(), }; @@ -389,7 +404,10 @@ namespace Managing.Application.Tests CooldownPeriod = 1, MaxLossStreak = 0, FlipPosition = false, - Name = "Test" + Name = "Test", + FlipOnlyWhenInProfit = true, + MaxPositionTimeHours = null, + CloseEarlyWhenProfitable = false }, candles, null).Result, BotType.FlippingBot => _backtester.RunFlippingBotBacktest(new TradingBotConfig { @@ -405,7 +423,10 @@ namespace Managing.Application.Tests CooldownPeriod = 1, MaxLossStreak = 0, FlipPosition = true, - Name = "Test" + Name = "Test", + FlipOnlyWhenInProfit = true, + MaxPositionTimeHours = null, + CloseEarlyWhenProfitable = false }, candles, null).Result, _ => throw new NotImplementedException(), }; diff --git a/src/Managing.Application.Workers/StatisticService.cs b/src/Managing.Application.Workers/StatisticService.cs index b72161b..a182122 100644 --- a/src/Managing.Application.Workers/StatisticService.cs +++ b/src/Managing.Application.Workers/StatisticService.cs @@ -277,7 +277,10 @@ public class StatisticService : IStatisticService CooldownPeriod = 1, MaxLossStreak = 0, FlipPosition = false, - Name = "StatisticsBacktest" + Name = "StatisticsBacktest", + FlipOnlyWhenInProfit = true, + MaxPositionTimeHours = null, + CloseEarlyWhenProfitable = false }; var backtest = await _backtester.RunScalpingBotBacktest( diff --git a/src/Managing.Application/Abstractions/IBotService.cs b/src/Managing.Application/Abstractions/IBotService.cs index e61923b..6f2a4cf 100644 --- a/src/Managing.Application/Abstractions/IBotService.cs +++ b/src/Managing.Application/Abstractions/IBotService.cs @@ -1,4 +1,3 @@ -using Managing.Application.Bots; using Managing.Domain.Bots; using Managing.Domain.Users; using Managing.Domain.Workflows; @@ -26,4 +25,5 @@ public interface IBotService Task DeleteBot(string botName); Task RestartBot(string botName); void ToggleIsForWatchingOnly(string botName); + Task UpdateBotConfiguration(string identifier, TradingBotConfig newConfig); } \ No newline at end of file diff --git a/src/Managing.Application/Bots/TradingBot.cs b/src/Managing.Application/Bots/TradingBot.cs index ac14fdd..34863ed 100644 --- a/src/Managing.Application/Bots/TradingBot.cs +++ b/src/Managing.Application/Bots/TradingBot.cs @@ -666,7 +666,7 @@ public class TradingBot : Bot, ITradingBot $"Signal {signal.Identifier} will wait for position to become profitable before flipping."); // Keep signal in waiting status to check again on next execution - SetSignalStatus(signal.Identifier, SignalStatus.WaitingForPosition); + SetSignalStatus(signal.Identifier, SignalStatus.Expired); return; } } @@ -1180,6 +1180,7 @@ public class TradingBot : Bot, ITradingBot MaxLossStreak = Config.MaxLossStreak, MaxPositionTimeHours = Config.MaxPositionTimeHours ?? 0m, FlipOnlyWhenInProfit = Config.FlipOnlyWhenInProfit, + CloseEarlyWhenProfitable = Config.CloseEarlyWhenProfitable, }; BotService.SaveOrUpdateBotBackup(User, Identifier, Config.BotType, Status, JsonConvert.SerializeObject(data)); } @@ -1202,6 +1203,7 @@ public class TradingBot : Bot, ITradingBot MaxLossStreak = data.MaxLossStreak, MaxPositionTimeHours = data.MaxPositionTimeHours == 0m ? null : data.MaxPositionTimeHours, FlipOnlyWhenInProfit = data.FlipOnlyWhenInProfit, + CloseEarlyWhenProfitable = data.CloseEarlyWhenProfitable, Name = data.Name }; @@ -1429,7 +1431,8 @@ public class TradingBot : Bot, ITradingBot MaxPositionTimeHours = Config.MaxPositionTimeHours, FlipOnlyWhenInProfit = Config.FlipOnlyWhenInProfit, FlipPosition = Config.FlipPosition, - Name = Config.Name + Name = Config.Name, + CloseEarlyWhenProfitable = Config.CloseEarlyWhenProfitable }; } } @@ -1453,4 +1456,5 @@ public class TradingBotBackup public int MaxLossStreak { get; set; } public decimal MaxPositionTimeHours { get; set; } public bool FlipOnlyWhenInProfit { get; set; } + public bool CloseEarlyWhenProfitable { get; set; } } \ No newline at end of file diff --git a/src/Managing.Application/ManageBot/BotService.cs b/src/Managing.Application/ManageBot/BotService.cs index c75226a..24739ca 100644 --- a/src/Managing.Application/ManageBot/BotService.cs +++ b/src/Managing.Application/ManageBot/BotService.cs @@ -142,7 +142,8 @@ namespace Managing.Application.ManageBot MaxPositionTimeHours = scalpingBotData.MaxPositionTimeHours == 0m ? null : scalpingBotData.MaxPositionTimeHours, FlipOnlyWhenInProfit = scalpingBotData.FlipOnlyWhenInProfit, IsForBacktest = false, - FlipPosition = false + FlipPosition = false, + CloseEarlyWhenProfitable = scalpingBotData.CloseEarlyWhenProfitable }; bot = CreateScalpingBot(scalpingConfig); @@ -171,7 +172,8 @@ namespace Managing.Application.ManageBot MaxPositionTimeHours = flippingBotData.MaxPositionTimeHours == 0m ? null : flippingBotData.MaxPositionTimeHours, FlipOnlyWhenInProfit = flippingBotData.FlipOnlyWhenInProfit, IsForBacktest = false, - FlipPosition = true + FlipPosition = true, + CloseEarlyWhenProfitable = flippingBotData.CloseEarlyWhenProfitable }; bot = CreateFlippingBot(flippingConfig); @@ -263,6 +265,23 @@ namespace Managing.Application.ManageBot } } + /// + /// Updates the configuration of an existing bot without stopping and restarting it. + /// + /// The bot identifier + /// The new configuration to apply + /// True if the configuration was successfully updated, false otherwise + public async Task UpdateBotConfiguration(string identifier, TradingBotConfig newConfig) + { + if (_botTasks.TryGetValue(identifier, out var botTaskWrapper) && + botTaskWrapper.BotInstance is TradingBot tradingBot) + { + return await tradingBot.UpdateConfiguration(newConfig); + } + + return false; + } + public ITradingBot CreateScalpingBot(TradingBotConfig config) { return new ScalpingBot( diff --git a/src/Managing.Application/ManageBot/StartBotCommandHandler.cs b/src/Managing.Application/ManageBot/StartBotCommandHandler.cs index 3cd3c43..f8b656b 100644 --- a/src/Managing.Application/ManageBot/StartBotCommandHandler.cs +++ b/src/Managing.Application/ManageBot/StartBotCommandHandler.cs @@ -74,7 +74,8 @@ namespace Managing.Application.ManageBot MaxPositionTimeHours = request.Config.MaxPositionTimeHours, // Properly handle nullable value FlipOnlyWhenInProfit = request.Config.FlipOnlyWhenInProfit, FlipPosition = request.Config.FlipPosition, - Name = request.Config.Name ?? request.Name + Name = request.Config.Name ?? request.Name, + CloseEarlyWhenProfitable = request.Config.CloseEarlyWhenProfitable }; switch (configToUse.BotType) diff --git a/src/Managing.WebApp/src/components/mollecules/BotConfigModal/BotConfigModal.tsx b/src/Managing.WebApp/src/components/mollecules/BotConfigModal/BotConfigModal.tsx new file mode 100644 index 0000000..2fe36e0 --- /dev/null +++ b/src/Managing.WebApp/src/components/mollecules/BotConfigModal/BotConfigModal.tsx @@ -0,0 +1,619 @@ +import React, {useEffect, useState} from 'react' +import {useQuery} from '@tanstack/react-query' +import useApiUrlStore from '../../../app/store/apiStore' +import { + AccountClient, + Backtest, + BotClient, + BotType, + MoneyManagement, + MoneyManagementClient, + ScenarioClient, + StartBotRequest, + Ticker, + Timeframe, + TradingBotConfig, + UpdateBotConfigRequest +} from '../../../generated/ManagingApi' +import Toast from '../Toast/Toast' + +interface BotConfigModalProps { + showModal: boolean + onClose: () => void + backtest?: Backtest // When creating from backtest + existingBot?: { + identifier: string + config: TradingBotConfig + } // When updating existing bot + mode: 'create' | 'update' // Explicitly specify the mode +} + +const BotConfigModal: React.FC = ({ + showModal, + onClose, + backtest, + existingBot, + mode +}) => { + const { apiUrl } = useApiUrlStore() + + // Form state + const [formData, setFormData] = useState<{ + name: string + accountName: string + moneyManagementName: string + ticker: Ticker + scenarioName: string + timeframe: Timeframe + isForWatchingOnly: boolean + botTradingBalance: number + botType: BotType + cooldownPeriod: number + maxLossStreak: number + maxPositionTimeHours: number | null + flipOnlyWhenInProfit: boolean + flipPosition: boolean + closeEarlyWhenProfitable: boolean + useCustomMoneyManagement: boolean + customStopLoss: number + customTakeProfit: number + customLeverage: number + }>({ + name: '', + accountName: '', + moneyManagementName: '', + ticker: Ticker.BTC, + scenarioName: '', + timeframe: Timeframe.FifteenMinutes, + isForWatchingOnly: false, + botTradingBalance: 1000, + botType: BotType.ScalpingBot, + cooldownPeriod: 1, + maxLossStreak: 0, + maxPositionTimeHours: null, + flipOnlyWhenInProfit: true, + flipPosition: false, + closeEarlyWhenProfitable: false, + useCustomMoneyManagement: false, + customStopLoss: 0.01, + customTakeProfit: 0.02, + customLeverage: 1 + }) + + // Fetch data + const { data: accounts } = useQuery({ + queryFn: async () => { + const accountClient = new AccountClient({}, apiUrl) + return await accountClient.account_GetAccounts() + }, + queryKey: ['accounts'] + }) + + const { data: moneyManagements } = useQuery({ + queryFn: async () => { + const moneyManagementClient = new MoneyManagementClient({}, apiUrl) + return await moneyManagementClient.moneyManagement_GetMoneyManagements() + }, + queryKey: ['moneyManagements'] + }) + + const { data: scenarios } = useQuery({ + queryFn: async () => { + const scenarioClient = new ScenarioClient({}, apiUrl) + return await scenarioClient.scenario_GetScenarios() + }, + queryKey: ['scenarios'] + }) + + // Initialize form data based on props + useEffect(() => { + if (mode === 'create' && backtest) { + // Initialize from backtest + setFormData({ + name: `Bot-${backtest.config.scenarioName}-${new Date().toISOString().slice(0, 19).replace(/[-:]/g, '')}`, + accountName: backtest.config.accountName, + moneyManagementName: moneyManagements?.[0]?.name || '', + ticker: backtest.config.ticker, + scenarioName: backtest.config.scenarioName, + timeframe: backtest.config.timeframe, + isForWatchingOnly: false, + botTradingBalance: 1000, + botType: backtest.config.botType, + cooldownPeriod: backtest.config.cooldownPeriod, + maxLossStreak: backtest.config.maxLossStreak, + maxPositionTimeHours: backtest.config.maxPositionTimeHours ?? null, + flipOnlyWhenInProfit: backtest.config.flipOnlyWhenInProfit, + flipPosition: backtest.config.flipPosition, + closeEarlyWhenProfitable: backtest.config.closeEarlyWhenProfitable || false, + useCustomMoneyManagement: true, // Default to custom for backtests + customStopLoss: backtest.config.moneyManagement?.stopLoss || 0.01, + customTakeProfit: backtest.config.moneyManagement?.takeProfit || 0.02, + customLeverage: backtest.config.moneyManagement?.leverage || 1 + }) + } else if (mode === 'update' && existingBot) { + // Initialize from existing bot + setFormData({ + name: existingBot.config.name, + accountName: existingBot.config.accountName, + moneyManagementName: existingBot.config.moneyManagement?.name || '', + ticker: existingBot.config.ticker, + scenarioName: existingBot.config.scenarioName, + timeframe: existingBot.config.timeframe, + isForWatchingOnly: existingBot.config.isForWatchingOnly, + botTradingBalance: existingBot.config.botTradingBalance, + botType: existingBot.config.botType, + cooldownPeriod: existingBot.config.cooldownPeriod, + maxLossStreak: existingBot.config.maxLossStreak, + maxPositionTimeHours: existingBot.config.maxPositionTimeHours ?? null, + flipOnlyWhenInProfit: existingBot.config.flipOnlyWhenInProfit, + flipPosition: existingBot.config.flipPosition, + closeEarlyWhenProfitable: existingBot.config.closeEarlyWhenProfitable || false, + useCustomMoneyManagement: false, + customStopLoss: existingBot.config.moneyManagement?.stopLoss || 0.01, + customTakeProfit: existingBot.config.moneyManagement?.takeProfit || 0.02, + customLeverage: existingBot.config.moneyManagement?.leverage || 1 + }) + } else if (mode === 'create' && !backtest) { + // Initialize for new bot creation + setFormData({ + name: `Bot-${new Date().toISOString().slice(0, 19).replace(/[-:]/g, '')}`, + accountName: accounts?.[0]?.name || '', + moneyManagementName: moneyManagements?.[0]?.name || '', + ticker: Ticker.BTC, + scenarioName: scenarios?.[0]?.name || '', + timeframe: Timeframe.FifteenMinutes, + isForWatchingOnly: false, + botTradingBalance: 1000, + botType: BotType.ScalpingBot, + cooldownPeriod: 1, + maxLossStreak: 0, + maxPositionTimeHours: null, + flipOnlyWhenInProfit: true, + flipPosition: false, + closeEarlyWhenProfitable: false, + useCustomMoneyManagement: false, + customStopLoss: 0.01, + customTakeProfit: 0.02, + customLeverage: 1 + }) + } + }, [mode, backtest, existingBot, accounts, moneyManagements, scenarios]) + + // Set default money management when data loads + useEffect(() => { + if (moneyManagements && moneyManagements.length > 0 && !formData.moneyManagementName) { + setFormData(prev => ({ + ...prev, + moneyManagementName: moneyManagements[0].name + })) + } + }, [moneyManagements]) + + // Set default account when data loads + useEffect(() => { + if (accounts && accounts.length > 0 && !formData.accountName) { + setFormData(prev => ({ + ...prev, + accountName: accounts[0].name + })) + } + }, [accounts]) + + // Set default scenario when data loads + useEffect(() => { + if (scenarios && scenarios.length > 0 && !formData.scenarioName) { + setFormData(prev => ({ + ...prev, + scenarioName: scenarios[0].name || '' + })) + } + }, [scenarios]) + + const handleInputChange = (field: string, value: any) => { + setFormData(prev => ({ + ...prev, + [field]: value + })) + } + + const handleSubmit = async () => { + const t = new Toast(mode === 'create' ? 'Creating bot...' : 'Updating bot...') + const client = new BotClient({}, apiUrl) + + try { + // Create the money management object + let moneyManagement: MoneyManagement | undefined = undefined + + if (formData.useCustomMoneyManagement || (mode === 'create' && backtest)) { + // Use custom money management + moneyManagement = { + name: 'custom', + leverage: formData.customLeverage, + stopLoss: formData.customStopLoss, + takeProfit: formData.customTakeProfit, + timeframe: formData.timeframe + } + } else { + // Use saved money management - load the complete object + const selectedMoneyManagement = moneyManagements?.find(mm => mm.name === formData.moneyManagementName) + if (selectedMoneyManagement) { + moneyManagement = selectedMoneyManagement + } else { + t.update('error', 'Selected money management not found') + return + } + } + + if (!moneyManagement) { + t.update('error', 'Money management is required') + return + } + + // Create TradingBotConfig (reused for both create and update) + const tradingBotConfig: TradingBotConfig = { + accountName: formData.accountName, + ticker: formData.ticker, + scenarioName: formData.scenarioName, + timeframe: formData.timeframe, + botType: formData.botType, + isForWatchingOnly: formData.isForWatchingOnly, + isForBacktest: false, + cooldownPeriod: formData.cooldownPeriod, + maxLossStreak: formData.maxLossStreak, + maxPositionTimeHours: formData.maxPositionTimeHours, + flipOnlyWhenInProfit: formData.flipOnlyWhenInProfit, + flipPosition: formData.flipPosition, + name: formData.name, + botTradingBalance: formData.botTradingBalance, + moneyManagement: moneyManagement, + closeEarlyWhenProfitable: formData.closeEarlyWhenProfitable + } + + if (mode === 'create') { + // Create new bot + const request: StartBotRequest = { + config: tradingBotConfig, + moneyManagementName: formData.useCustomMoneyManagement ? undefined : formData.moneyManagementName + } + + await client.bot_Start(request) + t.update('success', 'Bot created successfully!') + } else { + // Update existing bot + const request: UpdateBotConfigRequest = { + identifier: existingBot!.identifier, + config: tradingBotConfig, + moneyManagementName: formData.useCustomMoneyManagement ? undefined : formData.moneyManagementName + } + + await client.bot_UpdateBotConfig(request) + t.update('success', 'Bot updated successfully!') + } + + onClose() + } catch (error: any) { + t.update('error', `Error: ${error.message || error}`) + } + } + + if (!showModal) return null + + return ( +
+
+

+ {mode === 'create' ? 'Create Bot' : 'Update Bot Configuration'} + {backtest && ` from Backtest`} +

+ +
+ {/* Basic Configuration */} +
+ + handleInputChange('name', e.target.value)} + /> +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + handleInputChange('botTradingBalance', parseFloat(e.target.value))} + min="1" + step="0.01" + /> +
+ +
+ + handleInputChange('cooldownPeriod', parseInt(e.target.value))} + min="1" + /> +
+ +
+ + handleInputChange('maxLossStreak', parseInt(e.target.value))} + min="0" + /> +
+ +
+ + handleInputChange('maxPositionTimeHours', e.target.value ? parseFloat(e.target.value) : null)} + min="0.1" + step="0.1" + placeholder="Optional" + /> +
+ + {/* Checkboxes */} +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+ + {/* Money Management Section */} +
Money Management
+ +
+ +
+ + {formData.useCustomMoneyManagement ? ( +
+
+ + handleInputChange('customStopLoss', parseFloat(e.target.value))} + min="0.001" + max="1" + step="0.001" + /> +
+ +
+ + handleInputChange('customTakeProfit', parseFloat(e.target.value))} + min="0.001" + max="1" + step="0.001" + /> +
+ +
+ + handleInputChange('customLeverage', parseInt(e.target.value))} + min="1" + max="100" + /> +
+
+ ) : ( +
+ + +
+ )} + + {/* Validation Messages */} + {formData.closeEarlyWhenProfitable && !formData.maxPositionTimeHours && ( +
+ Close Early When Profitable requires Max Position Time to be set. +
+ )} + +
+ + +
+
+
+ ) +} + +export default BotConfigModal \ No newline at end of file diff --git a/src/Managing.WebApp/src/components/organism/Backtest/backtestTable.tsx b/src/Managing.WebApp/src/components/organism/Backtest/backtestTable.tsx index e9c621f..74bf1fd 100644 --- a/src/Managing.WebApp/src/components/organism/Backtest/backtestTable.tsx +++ b/src/Managing.WebApp/src/components/organism/Backtest/backtestTable.tsx @@ -1,13 +1,13 @@ -import {ChevronDownIcon, ChevronRightIcon, EyeIcon, PlayIcon, TrashIcon} from '@heroicons/react/solid' +import {ChevronDownIcon, ChevronRightIcon, PlayIcon, TrashIcon} from '@heroicons/react/solid' import React, {useEffect, useState} from 'react' -import {useQuery} from '@tanstack/react-query' import useApiUrlStore from '../../../app/store/apiStore' -import type {Backtest, StartBotRequest, Ticker, TradingBotConfig} from '../../../generated/ManagingApi' -import {BacktestClient, BotClient, MoneyManagementClient} from '../../../generated/ManagingApi' +import type {Backtest} from '../../../generated/ManagingApi' +import {BacktestClient} from '../../../generated/ManagingApi' import type {IBacktestCards} from '../../../global/type' -import {CardText, SelectColumnFilter, Table, Toast} from '../../mollecules' -import {BotNameModal} from '../index' +import {CardText, SelectColumnFilter, Table} from '../../mollecules' +import BotConfigModal from '../../mollecules/BotConfigModal/BotConfigModal' +import Toast from '../../mollecules/Toast/Toast' import BacktestRowDetails from './backtestRowDetails' @@ -27,95 +27,19 @@ const BacktestTable: React.FC = ({ list, isFetching, setBacktest averageCooldown: 0, medianCooldown: 0, }) - const [showBotNameModal, setShowBotNameModal] = useState(false) - const [isForWatchOnly, setIsForWatchOnly] = useState(false) - const [currentBacktest, setCurrentBacktest] = useState(null) - const [selectedMoneyManagement, setSelectedMoneyManagement] = useState('') + + // Bot configuration modal state + const [showBotConfigModal, setShowBotConfigModal] = useState(false) + const [selectedBacktest, setSelectedBacktest] = useState(null) - // Fetch money managements - const { data: moneyManagements } = useQuery({ - queryFn: async () => { - const moneyManagementClient = new MoneyManagementClient({}, apiUrl) - return await moneyManagementClient.moneyManagement_GetMoneyManagements() - }, - queryKey: ['moneyManagements'], - }) - - // Set the first money management as default when the data is loaded - useEffect(() => { - if (moneyManagements && moneyManagements.length > 0) { - setSelectedMoneyManagement(moneyManagements[0].name) - } - }, [moneyManagements]) - - async function runBot(botName: string, backtest: Backtest, isForWatchOnly: boolean, moneyManagementName: string, initialTradingBalance: number) { - const t = new Toast('Bot is starting') - const client = new BotClient({}, apiUrl) - - // Check if the money management name is "custom" or contains "custom" - const isCustomMoneyManagement = - !moneyManagementName || - moneyManagementName.toLowerCase() === 'custom' || - moneyManagementName.toLowerCase().includes('custom'); - - // Create TradingBotConfig from the backtest configuration - const tradingBotConfig: TradingBotConfig = { - accountName: backtest.config.accountName, - ticker: backtest.config.ticker, - scenarioName: backtest.config.scenarioName, - timeframe: backtest.config.timeframe, - botType: backtest.config.botType, - isForWatchingOnly: isForWatchOnly, - isForBacktest: false, // This is for running a live bot - cooldownPeriod: backtest.config.cooldownPeriod, - maxLossStreak: backtest.config.maxLossStreak, - maxPositionTimeHours: backtest.config.maxPositionTimeHours, - flipOnlyWhenInProfit: backtest.config.flipOnlyWhenInProfit, - flipPosition: backtest.config.flipPosition, - name: botName, - botTradingBalance: initialTradingBalance, - // Use the money management from backtest if it's custom, otherwise leave null and use moneyManagementName - moneyManagement: isCustomMoneyManagement ? - (backtest.config.moneyManagement || { - name: 'default', - leverage: 1, - stopLoss: 0.01, - takeProfit: 0.02, - timeframe: backtest.config.timeframe - }) : - backtest.config.moneyManagement, // Always provide a valid MoneyManagement object - closeEarlyWhenProfitable: backtest.config.closeEarlyWhenProfitable || false - }; - - const request: StartBotRequest = { - config: tradingBotConfig, - // Only use the money management name if it's not a custom money management - moneyManagementName: isCustomMoneyManagement ? undefined : moneyManagementName - } - - await client - .bot_Start(request) - .then((botStatus: string) => { - t.update('info', 'Bot status: ' + botStatus) - }) - .catch((err) => { - t.update('error', 'Error: ' + err) - }) + const handleOpenBotConfigModal = (backtest: Backtest) => { + setSelectedBacktest(backtest) + setShowBotConfigModal(true) } - const handleOpenBotNameModal = (backtest: Backtest, isForWatchOnly: boolean) => { - setCurrentBacktest(backtest) - setIsForWatchOnly(isForWatchOnly) - setShowBotNameModal(true) - } - - const handleCloseBotNameModal = () => { - setShowBotNameModal(false) - } - - const handleSubmitBotName = (botName: string, backtest: Backtest, isForWatchOnly: boolean, moneyManagementName: string, initialTradingBalance: number) => { - runBot(botName, backtest, isForWatchOnly, moneyManagementName, initialTradingBalance) - setShowBotNameModal(false) + const handleCloseBotConfigModal = () => { + setShowBotConfigModal(false) + setSelectedBacktest(null) } async function deleteBacktest(id: string) { @@ -292,27 +216,10 @@ const BacktestTable: React.FC = ({ list, isFetching, setBacktest { Cell: ({ cell }: any) => ( <> -
+
-
- - ), - Header: '', - accessor: 'watcher', - disableFilters: true, - }, - { - Cell: ({ cell }: any) => ( - <> -
- @@ -475,8 +382,6 @@ const BacktestTable: React.FC = ({ list, isFetching, setBacktest } }, [list]) - - return ( <> {isFetching ? ( @@ -528,18 +433,14 @@ const BacktestTable: React.FC = ({ list, isFetching, setBacktest /> )} /> - {showBotNameModal && currentBacktest && moneyManagements && ( - - handleSubmitBotName(botName, backtest, isForWatchOnly, moneyManagementName, initialTradingBalance) - } - moneyManagements={moneyManagements} - selectedMoneyManagement={selectedMoneyManagement} - setSelectedMoneyManagement={setSelectedMoneyManagement} + + {/* Bot Configuration Modal */} + {selectedBacktest && ( + )} diff --git a/src/Managing.WebApp/src/generated/ManagingApi.ts b/src/Managing.WebApp/src/generated/ManagingApi.ts index d1fa72b..26ebb59 100644 --- a/src/Managing.WebApp/src/generated/ManagingApi.ts +++ b/src/Managing.WebApp/src/generated/ManagingApi.ts @@ -653,7 +653,7 @@ export class BotClient extends AuthorizedApiBase { return Promise.resolve(null as any); } - bot_GetActiveBots(): Promise { + bot_GetActiveBots(): Promise { let url_ = this.baseUrl + "/Bot"; url_ = url_.replace(/[?&]$/, ""); @@ -671,13 +671,13 @@ export class BotClient extends AuthorizedApiBase { }); } - protected processBot_GetActiveBots(response: Response): Promise { + protected processBot_GetActiveBots(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 TradingBot[]; + result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as TradingBotResponse[]; return result200; }); } else if (status !== 200 && status !== 204) { @@ -685,7 +685,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_OpenPositionManually(request: OpenPositionManuallyRequest): Promise { @@ -3118,25 +3118,16 @@ export interface StartBotRequest { moneyManagementName?: string | null; } -export interface TradingBot { - name: string; +export interface TradingBotResponse { status: string; signals: Signal[]; positions: Position[]; candles: Candle[]; winRate: number; profitAndLoss: number; - timeframe: Timeframe; - ticker: Ticker; - scenario: string; - isForWatchingOnly: boolean; - botType: BotType; - accountName: string; - moneyManagement: MoneyManagement; identifier: string; agentName: string; - maxPositionTimeHours: number; - flipOnlyWhenInProfit: boolean; + config: TradingBotConfig; } export interface OpenPositionManuallyRequest { @@ -3150,20 +3141,9 @@ export interface ClosePositionRequest { } export interface UpdateBotConfigRequest { - identifier?: string | null; - accountName?: string | null; + identifier: string; + config: TradingBotConfig; moneyManagementName?: string | null; - ticker?: Ticker | null; - scenarioName?: string | null; - timeframe?: Timeframe | null; - isForWatchingOnly?: boolean | null; - botTradingBalance?: number | null; - cooldownPeriod?: number | null; - maxLossStreak?: number | null; - maxPositionTimeHours?: number | null; - flipOnlyWhenInProfit?: boolean | null; - flipPosition?: boolean | null; - name?: string | null; } export interface TickerInfos { diff --git a/src/Managing.WebApp/src/pages/botsPage/botList.tsx b/src/Managing.WebApp/src/pages/botsPage/botList.tsx index 677c9d4..3184abc 100644 --- a/src/Managing.WebApp/src/pages/botsPage/botList.tsx +++ b/src/Managing.WebApp/src/pages/botsPage/botList.tsx @@ -1,12 +1,13 @@ -import {ChartBarIcon, EyeIcon, PlayIcon, PlusCircleIcon, StopIcon, TrashIcon} from '@heroicons/react/solid' +import {ChartBarIcon, CogIcon, EyeIcon, PlayIcon, PlusCircleIcon, StopIcon, TrashIcon} from '@heroicons/react/solid' import React, {useState} from 'react' import useApiUrlStore from '../../app/store/apiStore' import {CardPosition, CardSignal, CardText, Toast,} from '../../components/mollecules' import ManualPositionModal from '../../components/mollecules/ManualPositionModal' import TradesModal from '../../components/mollecules/TradesModal/TradesModal' +import BotConfigModal from '../../components/mollecules/BotConfigModal/BotConfigModal' import {TradeChart} from '../../components/organism' -import type {BotType, MoneyManagement, TradingBot,} from '../../generated/ManagingApi' +import type {BotType, MoneyManagement, Position, TradingBotResponse} from '../../generated/ManagingApi' import {BotClient} from '../../generated/ManagingApi' import type {IBotList} from '../../global/type' import MoneyManagementModal from '../settingsPage/moneymanagement/moneyManagementModal' @@ -38,6 +39,12 @@ const BotList: React.FC = ({ list }) => { const [selectedBotForManualPosition, setSelectedBotForManualPosition] = useState(null) const [showTradesModal, setShowTradesModal] = useState(false) const [selectedBotForTrades, setSelectedBotForTrades] = useState<{ identifier: string; agentName: string } | null>(null) + const [showBotConfigModal, setShowBotConfigModal] = useState(false) + const [botConfigModalMode, setBotConfigModalMode] = useState<'create' | 'update'>('create') + const [selectedBotForUpdate, setSelectedBotForUpdate] = useState<{ + identifier: string + config: any + } | null>(null) function getIsForWatchingBadge(isForWatchingOnly: boolean, identifier: string) { const classes = @@ -188,9 +195,53 @@ const BotList: React.FC = ({ list }) => { }) } + function getUpdateBotBadge(bot: TradingBotResponse) { + const classes = baseBadgeClass() + ' bg-warning' + return ( + + ) + } + + function getCreateBotBadge() { + const classes = baseBadgeClass() + ' bg-success' + return ( + + ) + } + + function openCreateBotModal() { + setBotConfigModalMode('create') + setSelectedBotForUpdate(null) + setShowBotConfigModal(true) + } + + function openUpdateBotModal(bot: TradingBotResponse) { + setBotConfigModalMode('update') + setSelectedBotForUpdate({ + identifier: bot.identifier, + config: bot.config + }) + setShowBotConfigModal(true) + } + return (
- {list.map((bot: TradingBot, index) => ( +
+
+ {getCreateBotBadge()} +
+
+ + {list.map((bot: TradingBotResponse, index) => (
= ({ list }) => {

- {bot.ticker} - {getMoneyManagementBadge(bot.moneyManagement)} - {getIsForWatchingBadge(bot.isForWatchingOnly, bot.identifier)} - {getToggleBotStatusBadge(bot.status, bot.identifier, bot.botType)} + {bot.config.ticker} + {getMoneyManagementBadge(bot.config.moneyManagement)} + {getIsForWatchingBadge(bot.config.isForWatchingOnly, bot.identifier)} + {getToggleBotStatusBadge(bot.status, bot.identifier, bot.config.botType)} + {getUpdateBotBadge(bot)} {getManualPositionBadge(bot.identifier)} {getDeleteBadge(bot.identifier)}

@@ -223,26 +275,26 @@ const BotList: React.FC = ({ list }) => {
- +
{ + positions={bot.positions.filter((p: Position) => { const realized = p.profitAndLoss?.realized ?? 0 return realized > 0 ? p : null })} > { + positions={bot.positions.filter((p: Position) => { const realized = p.profitAndLoss?.realized ?? 0 return realized <= 0 ? p : null })} @@ -287,6 +339,15 @@ const BotList: React.FC = ({ list }) => { setSelectedBotForTrades(null) }} /> + { + setShowBotConfigModal(false) + setSelectedBotForUpdate(null) + }} + />
) }