diff --git a/README.md b/README.md index 7876640..824d959 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,9 @@ The backtest system works with multiple required parameters : - Timeframe (OneDay, FifteenMinutes, etc..) - BotType (ScalpingBot or FlippingBot) - Initial balance +- **Advanced parameters**: All bot configuration parameters (time limits, profit-controlled flipping, etc.) +- **Smart bot deployment**: Deploy successful backtests as live bots with optimized settings +- **Enhanced UI**: Wider modals with organized 2-column parameter layouts ## Bots @@ -154,6 +157,19 @@ The backtest system works with multiple required parameters : - Delete a bot - Stop all bots - Set bot to watch only (send signal to discord instead of opening a new position) +- **Time-based position management**: Automatically close positions after maximum time limit (only when in profit/breakeven) +- **Advanced position flipping**: Control whether positions flip only when current position is profitable +- **Real-time configuration updates**: Update bot settings without restarting +- **Enhanced money management**: Smart money management selection with optimized settings from backtests + +### Bot Configuration Parameters + +| Parameter | Description | Default | +|--------------------------|-------------------------------------------------------------------------------------------------------|---------| +| MaxPositionTimeHours | Maximum time (in hours) a position can stay open. Closes only when in profit/breakeven. Null = disabled | null | +| FlipOnlyWhenInProfit | Only flip positions when current position is profitable | true | +| CooldownPeriod | Number of candles to wait before opening new position in same direction | 10 | +| MaxLossStreak | Maximum consecutive losses before requiring opposite direction signal. 0 = no limit | 0 | Bot types availables : diff --git a/src/Managing.Api/Controllers/BacktestController.cs b/src/Managing.Api/Controllers/BacktestController.cs index 5a93cfd..52e27a2 100644 --- a/src/Managing.Api/Controllers/BacktestController.cs +++ b/src/Managing.Api/Controllers/BacktestController.cs @@ -2,6 +2,7 @@ using Managing.Application.Abstractions.Services; using Managing.Application.Hubs; using Managing.Domain.Backtests; +using Managing.Domain.Bots; using Managing.Domain.MoneyManagements; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -13,6 +14,7 @@ namespace Managing.Api.Controllers; /// /// Controller for managing backtest operations. /// Provides endpoints for creating, retrieving, and deleting backtests. +/// Returns complete backtest configurations for easy bot deployment. /// Requires authorization for access. /// [ApiController] @@ -52,8 +54,9 @@ public class BacktestController : BaseController /// /// Retrieves all backtests for the authenticated user. + /// Each backtest includes the complete TradingBotConfig for easy bot deployment. /// - /// A list of backtests. + /// A list of backtests with complete configurations. [HttpGet] public async Task>> Backtests() { @@ -63,10 +66,11 @@ public class BacktestController : BaseController /// /// Retrieves a specific backtest by ID for the authenticated user. - /// This endpoint will also populate the candles for visualization. + /// This endpoint will also populate the candles for visualization and includes + /// the complete TradingBotConfig that can be used to start a new bot. /// /// The ID of the backtest to retrieve. - /// The requested backtest with populated candle data. + /// The requested backtest with populated candle data and complete configuration. [HttpGet("{id}")] public async Task> Backtest(string id) { @@ -94,115 +98,130 @@ public class BacktestController : BaseController } /// - /// Runs a backtest with the specified parameters. + /// Runs a backtest with the specified configuration. + /// The returned backtest includes a complete TradingBotConfig that preserves all + /// settings including nullable MaxPositionTimeHours for easy bot deployment. /// - /// The name of the account to use for the backtest. - /// The type of bot to use for the backtest. - /// The ticker symbol to backtest. - /// The name of the scenario to use for the backtest. - /// The timeframe for the backtest. - /// Whether to only watch the backtest without executing trades. - /// The start date for the backtest. - /// The end date for the backtest. - /// The starting balance for the backtest. - /// The name of the money management strategy to use. - /// The money management strategy details, if not using a named strategy. - /// Whether to save the backtest results. - /// The cooldown period for the backtest. - /// The maximum loss streak for the backtest. - /// The result of the backtest. + /// The backtest request containing configuration and parameters. + /// The result of the backtest with complete configuration. [HttpPost] [Route("Run")] - public async Task> Run(string accountName, - BotType botType, - Ticker ticker, - string scenarioName, - Timeframe timeframe, - bool watchOnly, - decimal balance, - string moneyManagementName, - DateTime startDate, - DateTime endDate, - MoneyManagement? moneyManagement = null, - bool save = false, - int cooldownPeriod = 1, - int maxLossStreak = 0) + public async Task> Run([FromBody] RunBacktestRequest request) { - if (string.IsNullOrEmpty(accountName)) + if (request?.Config == null) { - throw new ArgumentException($"'{nameof(accountName)}' cannot be null or empty.", nameof(accountName)); + return BadRequest("Backtest configuration is required"); } - if (string.IsNullOrEmpty(scenarioName)) + if (string.IsNullOrEmpty(request.Config.AccountName)) { - throw new ArgumentException($"'{nameof(scenarioName)}' cannot be null or empty.", nameof(scenarioName)); + return BadRequest("Account name is required"); } - if (string.IsNullOrEmpty(moneyManagementName) && moneyManagement == null) + if (string.IsNullOrEmpty(request.Config.ScenarioName)) { - throw new ArgumentException( - $"'{nameof(moneyManagementName)}' and '{nameof(moneyManagement)}' cannot be null or empty.", - nameof(moneyManagementName)); + return BadRequest("Scenario name is required"); } - Backtest backtestResult = null; - var scenario = _scenarioService.GetScenario(scenarioName); - var account = await _accountService.GetAccount(accountName, true, false); - var user = await GetUser(); - - if (!string.IsNullOrEmpty(moneyManagementName) && moneyManagement is null) + if (string.IsNullOrEmpty(request.MoneyManagementName) && request.MoneyManagement == null) { - moneyManagement = await _moneyManagementService.GetMoneyMangement(user, moneyManagementName); - } - else - { - moneyManagement.FormatPercentage(); + return BadRequest("Either money management name or money management object is required"); } - if (scenario == null) - return BadRequest("No scenario found"); - - switch (botType) + try { - case BotType.SimpleBot: - break; - case BotType.ScalpingBot: - backtestResult = await _backtester.RunScalpingBotBacktest( - account, - moneyManagement, - ticker, - scenario, - timeframe, - balance, - startDate, - endDate, - user, - watchOnly, - save, - cooldownPeriod: cooldownPeriod, - maxLossStreak: maxLossStreak); - break; - case BotType.FlippingBot: - backtestResult = await _backtester.RunFlippingBotBacktest( - account, - moneyManagement, - ticker, - scenario, - timeframe, - balance, - startDate, - endDate, - user, - watchOnly, - save, - cooldownPeriod: cooldownPeriod, - maxLossStreak: maxLossStreak); - break; + Backtest backtestResult = null; + var scenario = _scenarioService.GetScenario(request.Config.ScenarioName); + var account = await _accountService.GetAccount(request.Config.AccountName, true, false); + var user = await GetUser(); + + if (scenario == null) + return BadRequest("No scenario found"); + + // Get money management + MoneyManagement moneyManagement; + if (!string.IsNullOrEmpty(request.MoneyManagementName)) + { + moneyManagement = await _moneyManagementService.GetMoneyMangement(user, request.MoneyManagementName); + if (moneyManagement == null) + return BadRequest("Money management not found"); + } + else + { + moneyManagement = request.MoneyManagement; + moneyManagement?.FormatPercentage(); + } + + // Update config with money management + var backtestConfig = new TradingBotConfig + { + AccountName = request.Config.AccountName, + MoneyManagement = moneyManagement, + Ticker = request.Config.Ticker, + ScenarioName = request.Config.ScenarioName, + Timeframe = request.Config.Timeframe, + IsForWatchingOnly = request.WatchOnly, + BotTradingBalance = request.Balance, + BotType = request.Config.BotType, + IsForBacktest = true, + CooldownPeriod = request.Config.CooldownPeriod, + MaxLossStreak = request.Config.MaxLossStreak, + MaxPositionTimeHours = request.Config.MaxPositionTimeHours, + FlipOnlyWhenInProfit = request.Config.FlipOnlyWhenInProfit, + FlipPosition = request.Config.FlipPosition, + Name = request.Config.Name ?? $"Backtest-{request.Config.ScenarioName}-{DateTime.UtcNow:yyyyMMdd-HHmmss}" + }; + + switch (request.Config.BotType) + { + case BotType.SimpleBot: + break; + case BotType.ScalpingBot: + backtestResult = await _backtester.RunScalpingBotBacktest( + account, + moneyManagement, + request.Config.Ticker, + scenario, + request.Config.Timeframe, + request.Balance, + request.StartDate, + request.EndDate, + user, + request.WatchOnly, + request.Save, + cooldownPeriod: request.Config.CooldownPeriod, + maxLossStreak: request.Config.MaxLossStreak, + maxPositionTimeHours: request.Config.MaxPositionTimeHours, + flipOnlyWhenInProfit: request.Config.FlipOnlyWhenInProfit); + break; + case BotType.FlippingBot: + backtestResult = await _backtester.RunFlippingBotBacktest( + account, + moneyManagement, + request.Config.Ticker, + scenario, + request.Config.Timeframe, + request.Balance, + request.StartDate, + request.EndDate, + user, + request.WatchOnly, + request.Save, + cooldownPeriod: request.Config.CooldownPeriod, + maxLossStreak: request.Config.MaxLossStreak, + maxPositionTimeHours: request.Config.MaxPositionTimeHours, + flipOnlyWhenInProfit: request.Config.FlipOnlyWhenInProfit); + break; + } + + await NotifyBacktesingSubscriberAsync(backtestResult); + + return Ok(backtestResult); + } + catch (Exception ex) + { + return StatusCode(500, $"Error running backtest: {ex.Message}"); } - - await NotifyBacktesingSubscriberAsync(backtestResult); - - return Ok(backtestResult); } /// @@ -216,4 +235,50 @@ public class BacktestController : BaseController await _hubContext.Clients.All.SendAsync("BacktestsSubscription", backtesting); } } +} + +/// +/// Request model for running a backtest +/// +public class RunBacktestRequest +{ + /// + /// The trading bot configuration to use for the backtest + /// + public TradingBotConfig Config { get; set; } + + /// + /// The start date for the backtest + /// + public DateTime StartDate { get; set; } + + /// + /// The end date for the backtest + /// + public DateTime EndDate { get; set; } + + /// + /// The starting balance for the backtest + /// + public decimal Balance { get; set; } + + /// + /// Whether to only watch the backtest without executing trades + /// + public bool WatchOnly { get; set; } = false; + + /// + /// Whether to save the backtest results + /// + public bool Save { get; set; } = false; + + /// + /// The name of the money management strategy to use (optional if MoneyManagement is provided) + /// + public string? MoneyManagementName { get; set; } + + /// + /// The money management strategy details (optional if MoneyManagementName is provided) + /// + public MoneyManagement? MoneyManagement { get; set; } } \ No newline at end of file diff --git a/src/Managing.Api/Controllers/BotController.cs b/src/Managing.Api/Controllers/BotController.cs index 69cf95a..2ca79c0 100644 --- a/src/Managing.Api/Controllers/BotController.cs +++ b/src/Managing.Api/Controllers/BotController.cs @@ -4,6 +4,7 @@ using Managing.Application.Hubs; using Managing.Application.ManageBot.Commands; using Managing.Common; using Managing.Domain.Bots; +using Managing.Domain.MoneyManagements; using Managing.Domain.Trades; using MediatR; using Microsoft.AspNetCore.Authorization; @@ -142,6 +143,8 @@ public class BotController : BaseController BotType = request.BotType, CooldownPeriod = request.CooldownPeriod, MaxLossStreak = request.MaxLossStreak, + MaxPositionTimeHours = request.MaxPositionTimeHours, + FlipOnlyWhenInProfit = request.FlipOnlyWhenInProfit, IsForBacktest = false, FlipPosition = request.BotType == BotType.FlippingBot, Name = request.Name @@ -419,7 +422,9 @@ public class BotController : BaseController AccountName = item.Config.AccountName, MoneyManagement = item.Config.MoneyManagement, Identifier = item.Identifier, - AgentName = item.User.AgentName + AgentName = item.User.AgentName, + MaxPositionTimeHours = item.Config.MaxPositionTimeHours, + FlipOnlyWhenInProfit = item.Config.FlipOnlyWhenInProfit }); } @@ -543,6 +548,102 @@ public class BotController : BaseController return StatusCode(500, $"Error closing position: {ex.Message}"); } } + + /// + /// Updates the configuration of a running bot + /// + /// The request containing the new bot configuration + /// A response indicating the result of the operation + [HttpPut] + [Route("UpdateConfig")] + public async Task> UpdateBotConfig([FromBody] UpdateBotConfigRequest request) + { + try + { + // Check if user owns the account + if (!await UserOwnsBotAccount(request.Identifier)) + { + return Forbid("You don't have permission to update this bot's configuration"); + } + + 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(); + + // Validate money management if provided + MoneyManagement moneyManagement = null; + if (!string.IsNullOrEmpty(request.MoneyManagementName)) + { + moneyManagement = await _moneyManagementService.GetMoneyMangement(user, request.MoneyManagementName); + if (moneyManagement == null) + { + return BadRequest("Money management not found"); + } + } + else + { + // Keep existing money management if not provided + moneyManagement = bot.Config.MoneyManagement; + } + + // 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"); + return StatusCode(500, $"Error updating bot configuration: {ex.Message}"); + } + } } /// @@ -627,7 +728,107 @@ public class StartBotRequest /// public decimal InitialTradingBalance { get; set; } + /// + /// The cooldown period in candles between positions + /// public int CooldownPeriod { get; set; } + + /// + /// The maximum number of consecutive losses before stopping + /// public int MaxLossStreak { get; set; } + + /// + /// The name of the bot + /// public string Name { get; set; } + + /// + /// Maximum time in hours that a position can remain open before being automatically closed. + /// Supports fractional values (e.g., 2.5 for 2 hours and 30 minutes). + /// If null, time-based position closure is disabled. + /// + public decimal? MaxPositionTimeHours { get; set; } = null; + + /// + /// If true, positions will only be flipped when the current position is in profit. + /// If false, positions will be flipped regardless of profit status. + /// + public bool FlipOnlyWhenInProfit { get; set; } = true; +} + +/// +/// Request model for updating bot configuration +/// +public class UpdateBotConfigRequest +{ + /// + /// The identifier of the bot to update + /// + public string Identifier { get; set; } + + /// + /// The account name to use (optional - will keep existing if not provided) + /// + public string? AccountName { get; set; } + + /// + /// The money management name to use (optional - will keep existing if not provided) + /// + 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 index 5ff58cb..d2eabbe 100644 --- a/src/Managing.Api/Models/Responses/TradingBot.cs +++ b/src/Managing.Api/Models/Responses/TradingBot.cs @@ -25,5 +25,16 @@ namespace Managing.Api.Models.Responses [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.Application.Abstractions/Services/IBacktester.cs b/src/Managing.Application.Abstractions/Services/IBacktester.cs index 9a689a4..330daf7 100644 --- a/src/Managing.Application.Abstractions/Services/IBacktester.cs +++ b/src/Managing.Application.Abstractions/Services/IBacktester.cs @@ -24,7 +24,9 @@ namespace Managing.Application.Abstractions.Services bool save = false, List? initialCandles = null, int cooldownPeriod = 1, - int maxLossStreak = 0); + int maxLossStreak = 0, + decimal? maxPositionTimeHours = null, + bool flipOnlyWhenInProfit = true); Task RunFlippingBotBacktest( Account account, @@ -40,7 +42,9 @@ namespace Managing.Application.Abstractions.Services bool save = false, List? initialCandles = null, int cooldownPeriod = 1, - int maxLossStreak = 0); + int maxLossStreak = 0, + decimal? maxPositionTimeHours = null, + bool flipOnlyWhenInProfit = true); bool DeleteBacktest(string id); bool DeleteBacktests(); @@ -54,7 +58,9 @@ namespace Managing.Application.Abstractions.Services decimal balance, User user = null, int cooldownPeriod = 1, - int maxLossStreak = 0); + int maxLossStreak = 0, + decimal? maxPositionTimeHours = null, + bool flipOnlyWhenInProfit = true); Task RunFlippingBotBacktest( Account account, @@ -65,7 +71,9 @@ namespace Managing.Application.Abstractions.Services decimal balance, User user = null, int cooldownPeriod = 1, - int maxLossStreak = 0); + int maxLossStreak = 0, + decimal? maxPositionTimeHours = null, + bool flipOnlyWhenInProfit = true); // User-specific operations Task> GetBacktestsByUser(User user); diff --git a/src/Managing.Application/Abstractions/ITradingBot.cs b/src/Managing.Application/Abstractions/ITradingBot.cs index 37e5381..b6d7bd4 100644 --- a/src/Managing.Application/Abstractions/ITradingBot.cs +++ b/src/Managing.Application/Abstractions/ITradingBot.cs @@ -40,5 +40,18 @@ namespace Managing.Application.Abstractions Task CloseTrade(Signal signal, Position position, Trade tradeToClose, decimal lastPrice, bool tradeClosingPosition = false); + + /// + /// Gets the current trading bot configuration. + /// + /// A copy of the current configuration + TradingBotConfig GetConfiguration(); + + /// + /// Updates the trading bot configuration with new settings. + /// + /// The new configuration to apply + /// True if the configuration was successfully updated, false otherwise + Task UpdateConfiguration(TradingBotConfig newConfig); } } \ No newline at end of file diff --git a/src/Managing.Application/Backtesting/Backtester.cs b/src/Managing.Application/Backtesting/Backtester.cs index c7e5811..ff5715c 100644 --- a/src/Managing.Application/Backtesting/Backtester.cs +++ b/src/Managing.Application/Backtesting/Backtester.cs @@ -67,7 +67,9 @@ namespace Managing.Application.Backtesting bool save = false, List? initialCandles = null, int cooldownPeriod = 1, - int maxLossStreak = 0) + int maxLossStreak = 0, + decimal? maxPositionTimeHours = null, + bool flipOnlyWhenInProfit = true) { var config = new TradingBotConfig { @@ -81,7 +83,9 @@ namespace Managing.Application.Backtesting BotType = BotType.ScalpingBot, IsForBacktest = true, CooldownPeriod = cooldownPeriod, - MaxLossStreak = maxLossStreak + MaxLossStreak = maxLossStreak, + MaxPositionTimeHours = maxPositionTimeHours, + FlipOnlyWhenInProfit = flipOnlyWhenInProfit }; var scalpingBot = _botFactory.CreateBacktestScalpingBot(config); @@ -122,7 +126,9 @@ namespace Managing.Application.Backtesting bool save = false, List? initialCandles = null, int cooldownPeriod = 1, - int maxLossStreak = 0) + int maxLossStreak = 0, + decimal? maxPositionTimeHours = null, + bool flipOnlyWhenInProfit = true) { var config = new TradingBotConfig { @@ -136,7 +142,9 @@ namespace Managing.Application.Backtesting BotType = BotType.FlippingBot, IsForBacktest = true, CooldownPeriod = cooldownPeriod, - MaxLossStreak = maxLossStreak + MaxLossStreak = maxLossStreak, + MaxPositionTimeHours = maxPositionTimeHours, + FlipOnlyWhenInProfit = flipOnlyWhenInProfit }; var flippingBot = _botFactory.CreateBacktestFlippingBot(config); @@ -173,7 +181,9 @@ namespace Managing.Application.Backtesting decimal balance, User user = null, int cooldownPeriod = 1, - int maxLossStreak = 0) + int maxLossStreak = 0, + decimal? maxPositionTimeHours = null, + bool flipOnlyWhenInProfit = true) { var ticker = MiscExtensions.ParseEnum(candles.FirstOrDefault().Ticker); var config = new TradingBotConfig @@ -188,7 +198,9 @@ namespace Managing.Application.Backtesting BotType = BotType.ScalpingBot, IsForBacktest = true, CooldownPeriod = cooldownPeriod, - MaxLossStreak = maxLossStreak + MaxLossStreak = maxLossStreak, + MaxPositionTimeHours = maxPositionTimeHours, + FlipOnlyWhenInProfit = flipOnlyWhenInProfit }; var bot = _botFactory.CreateBacktestScalpingBot(config); @@ -215,7 +227,9 @@ namespace Managing.Application.Backtesting decimal balance, User user = null, int cooldownPeriod = 1, - int maxLossStreak = 0) + int maxLossStreak = 0, + decimal? maxPositionTimeHours = null, + bool flipOnlyWhenInProfit = true) { var ticker = MiscExtensions.ParseEnum(candles.FirstOrDefault().Ticker); var config = new TradingBotConfig @@ -230,7 +244,9 @@ namespace Managing.Application.Backtesting BotType = BotType.FlippingBot, IsForBacktest = true, CooldownPeriod = cooldownPeriod, - MaxLossStreak = maxLossStreak + MaxLossStreak = maxLossStreak, + MaxPositionTimeHours = maxPositionTimeHours, + FlipOnlyWhenInProfit = flipOnlyWhenInProfit }; var bot = _botFactory.CreateBacktestFlippingBot(config); diff --git a/src/Managing.Application/Bots/TradingBot.cs b/src/Managing.Application/Bots/TradingBot.cs index b8353f4..cc578d2 100644 --- a/src/Managing.Application/Bots/TradingBot.cs +++ b/src/Managing.Application/Bots/TradingBot.cs @@ -43,6 +43,7 @@ public class TradingBot : Bot, ITradingBot public decimal Fee { get; set; } public Scenario Scenario { get; set; } + public TradingBot( IExchangeService exchangeService, ILogger logger, @@ -293,11 +294,12 @@ public class TradingBot : Bot, ITradingBot { // Get the candle that corresponds to the position opening time var positionCandle = OptimizedCandles.FirstOrDefault(c => c.Date <= position.Open.Date) - ?? OptimizedCandles.LastOrDefault(); - + ?? OptimizedCandles.LastOrDefault(); + if (positionCandle == null) { - await LogWarning($"Cannot find candle for position {position.Identifier} opened at {position.Open.Date}"); + await LogWarning( + $"Cannot find candle for position {position.Identifier} opened at {position.Open.Date}"); return null; } @@ -317,14 +319,15 @@ public class TradingBot : Bot, ITradingBot // to use the new signal identifier, or find another approach // For now, let's update the position's SignalIdentifier to match the recreated signal position.SignalIdentifier = recreatedSignal.Identifier; - + recreatedSignal.Status = SignalStatus.PositionOpen; recreatedSignal.User = Account.User; // Add the recreated signal to our collection Signals.Add(recreatedSignal); - await LogInformation($"Successfully recreated signal {recreatedSignal.Identifier} for position {position.Identifier}"); + await LogInformation( + $"Successfully recreated signal {recreatedSignal.Identifier} for position {position.Identifier}"); return recreatedSignal; } catch (Exception ex) @@ -343,10 +346,10 @@ public class TradingBot : Bot, ITradingBot if (signalForPosition == null) { await LogInformation($"Signal not found for position {position.Identifier}. Recreating signal..."); - + // Recreate the signal based on position information signalForPosition = await RecreateSignalFromPosition(position); - + if (signalForPosition == null) { await LogWarning($"Failed to recreate signal for position {position.Identifier}"); @@ -357,7 +360,8 @@ public class TradingBot : Bot, ITradingBot // Ensure signal status is correctly set to PositionOpen if position is not finished if (signalForPosition.Status != SignalStatus.PositionOpen) { - await LogInformation($"Updating signal {signalForPosition.Identifier} status from {signalForPosition.Status} to PositionOpen"); + await LogInformation( + $"Updating signal {signalForPosition.Identifier} status from {signalForPosition.Status} to PositionOpen"); SetSignalStatus(signalForPosition.Identifier, SignalStatus.PositionOpen); } @@ -462,6 +466,33 @@ public class TradingBot : Bot, ITradingBot ? OptimizedCandles.Last() : ExchangeService.GetCandle(Account, Config.Ticker, DateTime.UtcNow); + var currentTime = Config.IsForBacktest ? lastCandle.Date : DateTime.UtcNow; + + // Check if position has exceeded maximum time limit (only if MaxPositionTimeHours is set) + if (Config.MaxPositionTimeHours.HasValue && HasPositionExceededTimeLimit(positionForSignal, currentTime)) + { + // Check if position is in profit or at breakeven before closing + var isPositionInProfit = await IsPositionInProfit(positionForSignal, lastCandle.Close); + var isAtBreakeven = Math.Abs(lastCandle.Close - positionForSignal.Open.Price) < 0.01m; // Small tolerance for breakeven + + if (isPositionInProfit || isAtBreakeven) + { + await LogInformation( + $"Closing position due to time limit - Position opened at {positionForSignal.Open.Date}, " + + $"current time {currentTime}, max time limit {Config.MaxPositionTimeHours} hours. " + + $"Position is {(isPositionInProfit ? "in profit" : "at breakeven")} (entry: {positionForSignal.Open.Price}, current: {lastCandle.Close})"); + await CloseTrade(signal, positionForSignal, positionForSignal.Open, lastCandle.Close, true); + return; + } + else + { + await LogInformation( + $"Position has exceeded time limit ({Config.MaxPositionTimeHours} hours) but is at a loss " + + $"(entry: {positionForSignal.Open.Price}, current: {lastCandle.Close}). " + + $"Waiting for profit or breakeven before closing."); + } + } + if (positionForSignal.OriginDirection == TradeDirection.Long) { if (positionForSignal.StopLoss.Price >= lastCandle.Low) @@ -583,10 +614,18 @@ public class TradingBot : Bot, ITradingBot { // Check if current position is in profit before flipping var isPositionInProfit = await IsPositionInProfit(openedPosition, lastPrice); - - if (isPositionInProfit) + + // Determine if we should flip based on configuration + var shouldFlip = !Config.FlipOnlyWhenInProfit || isPositionInProfit; + + if (shouldFlip) { - await LogInformation("Try to flip the position because of an opposite direction signal and current position is in profit"); + var flipReason = Config.FlipOnlyWhenInProfit + ? "current position is in profit" + : "FlipOnlyWhenInProfit is disabled"; + + await LogInformation( + $"Try to flip the position because of an opposite direction signal and {flipReason}"); await CloseTrade(previousSignal, openedPosition, openedPosition.Open, lastPrice, true); await SetPositionStatus(previousSignal.Identifier, PositionStatus.Flipped); await OpenPosition(signal); @@ -598,7 +637,7 @@ public class TradingBot : Bot, ITradingBot await LogInformation( $"Position {previousSignal.Identifier} is not in profit (entry: {openedPosition.Open.Price}, current: {lastPrice}). " + $"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); return; @@ -871,10 +910,10 @@ public class TradingBot : Bot, ITradingBot if (Positions.Any(p => p.Identifier == position.Identifier)) { // Update the close date for the trade that actually closed the position - var currentCandle = Config.IsForBacktest - ? OptimizedCandles.LastOrDefault() + var currentCandle = Config.IsForBacktest + ? OptimizedCandles.LastOrDefault() : ExchangeService.GetCandle(Account, Config.Ticker, DateTime.UtcNow); - + if (currentCandle != null && position.ProfitAndLoss != null) { // Determine which trade closed the position based on realized P&L @@ -1111,6 +1150,9 @@ public class TradingBot : Bot, ITradingBot BotTradingBalance = Config.BotTradingBalance, StartupTime = StartupTime, CooldownPeriod = Config.CooldownPeriod, + MaxLossStreak = Config.MaxLossStreak, + MaxPositionTimeHours = Config.MaxPositionTimeHours ?? 0m, + FlipOnlyWhenInProfit = Config.FlipOnlyWhenInProfit, }; BotService.SaveOrUpdateBotBackup(User, Identifier, Config.BotType, Status, JsonConvert.SerializeObject(data)); } @@ -1131,6 +1173,8 @@ public class TradingBot : Bot, ITradingBot BotType = data.BotType, CooldownPeriod = data.CooldownPeriod, MaxLossStreak = data.MaxLossStreak, + MaxPositionTimeHours = data.MaxPositionTimeHours == 0m ? null : data.MaxPositionTimeHours, + FlipOnlyWhenInProfit = data.FlipOnlyWhenInProfit, Name = data.Name }; @@ -1205,6 +1249,140 @@ public class TradingBot : Bot, ITradingBot throw new ArgumentException("Invalid position direction"); } } + + /// + /// Checks if a position has exceeded the maximum time limit for being open. + /// + /// The position to check + /// The current time to compare against + /// True if the position has exceeded the time limit, false otherwise + private bool HasPositionExceededTimeLimit(Position position, DateTime currentTime) + { + if (!Config.MaxPositionTimeHours.HasValue) + { + return false; // Time-based closure is disabled + } + + var timeOpen = currentTime - position.Open.Date; + var maxTimeAllowed = TimeSpan.FromHours((double)Config.MaxPositionTimeHours.Value); + + return timeOpen >= maxTimeAllowed; + } + + /// + /// Updates the trading bot configuration with new settings. + /// This method validates the new configuration and applies it to the running bot. + /// + /// The new configuration to apply + /// True if the configuration was successfully updated, false otherwise + /// Thrown when the new configuration is invalid + public async Task UpdateConfiguration(TradingBotConfig newConfig) + { + try + { + // Validate the new configuration + if (newConfig == null) + { + throw new ArgumentException("Configuration cannot be null"); + } + + if (newConfig.BotTradingBalance <= Constants.GMX.Config.MinimumPositionAmount) + { + throw new ArgumentException( + $"Bot trading balance must be greater than {Constants.GMX.Config.MinimumPositionAmount}"); + } + + if (string.IsNullOrEmpty(newConfig.AccountName)) + { + throw new ArgumentException("Account name cannot be null or empty"); + } + + if (string.IsNullOrEmpty(newConfig.ScenarioName)) + { + throw new ArgumentException("Scenario name cannot be null or empty"); + } + + // Protect critical properties that shouldn't change for running bots + var protectedBotType = Config.BotType; + var protectedIsForBacktest = Config.IsForBacktest; + var protectedName = Config.Name; + + // Log the configuration update + await LogInformation($"Updating bot configuration. Previous config: " + + $"Balance: {Config.BotTradingBalance}, " + + $"MaxTime: {Config.MaxPositionTimeHours?.ToString() ?? "Disabled"}, " + + $"FlipOnlyProfit: {Config.FlipOnlyWhenInProfit}, " + + $"Cooldown: {Config.CooldownPeriod}, " + + $"MaxLoss: {Config.MaxLossStreak}"); + + // Update the configuration + Config = newConfig; + + // Restore protected properties + Config.BotType = protectedBotType; + Config.IsForBacktest = protectedIsForBacktest; + Config.Name = protectedName; + + // If account changed, reload it + if (Config.AccountName != Account?.Name) + { + await LoadAccount(); + } + + // If scenario changed, reload it + var currentScenario = Scenario?.Name; + if (Config.ScenarioName != currentScenario) + { + LoadScenario(Config.ScenarioName); + } + + await LogInformation($"Bot configuration updated successfully. New config: " + + $"Balance: {Config.BotTradingBalance}, " + + $"MaxTime: {Config.MaxPositionTimeHours?.ToString() ?? "Disabled"}, " + + $"FlipOnlyProfit: {Config.FlipOnlyWhenInProfit}, " + + $"Cooldown: {Config.CooldownPeriod}, " + + $"MaxLoss: {Config.MaxLossStreak}"); + + // Save the updated configuration as backup + if (!Config.IsForBacktest) + { + SaveBackup(); + } + + return true; + } + catch (Exception ex) + { + await LogWarning($"Failed to update bot configuration: {ex.Message}"); + return false; + } + } + + /// + /// Gets the current trading bot configuration. + /// + /// A copy of the current configuration + public TradingBotConfig GetConfiguration() + { + return new TradingBotConfig + { + AccountName = Config.AccountName, + MoneyManagement = Config.MoneyManagement, + Ticker = Config.Ticker, + ScenarioName = Config.ScenarioName, + Timeframe = Config.Timeframe, + IsForWatchingOnly = Config.IsForWatchingOnly, + BotTradingBalance = Config.BotTradingBalance, + BotType = Config.BotType, + IsForBacktest = Config.IsForBacktest, + CooldownPeriod = Config.CooldownPeriod, + MaxLossStreak = Config.MaxLossStreak, + MaxPositionTimeHours = Config.MaxPositionTimeHours, + FlipOnlyWhenInProfit = Config.FlipOnlyWhenInProfit, + FlipPosition = Config.FlipPosition, + Name = Config.Name + }; + } } public class TradingBotBackup @@ -1224,4 +1402,6 @@ public class TradingBotBackup public decimal BotTradingBalance { get; set; } public int CooldownPeriod { get; set; } public int MaxLossStreak { get; set; } + public decimal MaxPositionTimeHours { get; set; } + public bool FlipOnlyWhenInProfit { 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 4fe64e2..c75226a 100644 --- a/src/Managing.Application/ManageBot/BotService.cs +++ b/src/Managing.Application/ManageBot/BotService.cs @@ -4,7 +4,6 @@ using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Services; using Managing.Application.Bots; using Managing.Domain.Bots; -using Managing.Domain.MoneyManagements; using Managing.Domain.Users; using Managing.Domain.Workflows; using Microsoft.Extensions.Logging; @@ -121,39 +120,61 @@ namespace Managing.Application.ManageBot switch (backupBot.BotType) { - // case Enums.BotType.SimpleBot: - // bot = CreateSimpleBot(backupBot.Name, - // null); // Assuming null is an acceptable parameter for workflow - // botTask = Task.Run(() => ((IBot)bot).Start()); - // break; case BotType.ScalpingBot: var scalpingBotData = JsonConvert.DeserializeObject(backupBot.Data); var scalpingMoneyManagement = _moneyManagementService.GetMoneyMangement(scalpingBotData.MoneyManagement.Name).Result; - bot = CreateScalpingBot( - scalpingBotData.AccountName, - scalpingMoneyManagement, - scalpingBotData.Name, - scalpingBotData.Ticker, - scalpingBotData.ScenarioName, - scalpingBotData.Timeframe, - scalpingBotData.IsForWatchingOnly, - scalpingBotData.BotTradingBalance); + + // Create config from backup data + var scalpingConfig = new TradingBotConfig + { + AccountName = scalpingBotData.AccountName, + MoneyManagement = scalpingMoneyManagement, + Ticker = scalpingBotData.Ticker, + ScenarioName = scalpingBotData.ScenarioName, + Timeframe = scalpingBotData.Timeframe, + IsForWatchingOnly = scalpingBotData.IsForWatchingOnly, + BotTradingBalance = scalpingBotData.BotTradingBalance, + BotType = scalpingBotData.BotType, + Name = scalpingBotData.Name, + CooldownPeriod = scalpingBotData.CooldownPeriod, + MaxLossStreak = scalpingBotData.MaxLossStreak, + MaxPositionTimeHours = scalpingBotData.MaxPositionTimeHours == 0m ? null : scalpingBotData.MaxPositionTimeHours, + FlipOnlyWhenInProfit = scalpingBotData.FlipOnlyWhenInProfit, + IsForBacktest = false, + FlipPosition = false + }; + + bot = CreateScalpingBot(scalpingConfig); botTask = Task.Run(() => InitBot((ITradingBot)bot, backupBot)); break; + case BotType.FlippingBot: var flippingBotData = JsonConvert.DeserializeObject(backupBot.Data); var flippingMoneyManagement = _moneyManagementService.GetMoneyMangement(flippingBotData.MoneyManagement.Name).Result; - bot = CreateFlippingBot( - flippingBotData.AccountName, - flippingMoneyManagement, - flippingBotData.Name, - flippingBotData.Ticker, - flippingBotData.ScenarioName, - flippingBotData.Timeframe, - flippingBotData.IsForWatchingOnly, - flippingBotData.BotTradingBalance); + + // Create config from backup data + var flippingConfig = new TradingBotConfig + { + AccountName = flippingBotData.AccountName, + MoneyManagement = flippingMoneyManagement, + Ticker = flippingBotData.Ticker, + ScenarioName = flippingBotData.ScenarioName, + Timeframe = flippingBotData.Timeframe, + IsForWatchingOnly = flippingBotData.IsForWatchingOnly, + BotTradingBalance = flippingBotData.BotTradingBalance, + BotType = flippingBotData.BotType, + Name = flippingBotData.Name, + CooldownPeriod = flippingBotData.CooldownPeriod, + MaxLossStreak = flippingBotData.MaxLossStreak, + MaxPositionTimeHours = flippingBotData.MaxPositionTimeHours == 0m ? null : flippingBotData.MaxPositionTimeHours, + FlipOnlyWhenInProfit = flippingBotData.FlipOnlyWhenInProfit, + IsForBacktest = false, + FlipPosition = true + }; + + bot = CreateFlippingBot(flippingConfig); botTask = Task.Run(() => InitBot((ITradingBot)bot, backupBot)); break; } @@ -168,7 +189,8 @@ namespace Managing.Application.ManageBot private void InitBot(ITradingBot bot, BotBackup backupBot) { var user = _userService.GetUser(backupBot.User.Name); - backupBot.User = user; + bot.User = user; + // Config is already set correctly from backup data, so we only need to restore signals, positions, etc. bot.LoadBackup(backupBot); bot.Start(); } @@ -241,113 +263,6 @@ namespace Managing.Application.ManageBot } } - public ITradingBot CreateScalpingBot(string accountName, MoneyManagement moneyManagement, string name, - Ticker ticker, string scenario, Timeframe interval, bool isForWatchingOnly, - decimal initialTradingBalance) - { - var config = new TradingBotConfig - { - AccountName = accountName, - MoneyManagement = moneyManagement, - Ticker = ticker, - ScenarioName = scenario, - Timeframe = interval, - IsForWatchingOnly = isForWatchingOnly, - BotTradingBalance = initialTradingBalance, - BotType = BotType.ScalpingBot, - Name = name - }; - - return new ScalpingBot( - _exchangeService, - _tradingBotLogger, - _tradingService, - _accountService, - _messengerService, - this, - config); - } - - public ITradingBot CreateBacktestScalpingBot(string accountName, MoneyManagement moneyManagement, - Ticker ticker, string scenario, Timeframe interval, bool isForWatchingOnly, - decimal initialTradingBalance) - { - var config = new TradingBotConfig - { - AccountName = accountName, - MoneyManagement = moneyManagement, - Ticker = ticker, - ScenarioName = scenario, - Timeframe = interval, - IsForWatchingOnly = isForWatchingOnly, - BotTradingBalance = initialTradingBalance, - BotType = BotType.ScalpingBot, - IsForBacktest = true - }; - - return new ScalpingBot( - _exchangeService, - _tradingBotLogger, - _tradingService, - _accountService, - _messengerService, - this, - config); - } - - public ITradingBot CreateFlippingBot(string accountName, MoneyManagement moneyManagement, string name, - Ticker ticker, string scenario, Timeframe interval, bool isForWatchingOnly, - decimal initialTradingBalance) - { - var config = new TradingBotConfig - { - AccountName = accountName, - MoneyManagement = moneyManagement, - Ticker = ticker, - ScenarioName = scenario, - Timeframe = interval, - IsForWatchingOnly = isForWatchingOnly, - BotTradingBalance = initialTradingBalance, - BotType = BotType.FlippingBot - }; - - return new FlippingBot( - _exchangeService, - _tradingBotLogger, - _tradingService, - _accountService, - _messengerService, - this, - config); - } - - public ITradingBot CreateBacktestFlippingBot(string accountName, MoneyManagement moneyManagement, - Ticker ticker, string scenario, Timeframe interval, bool isForWatchingOnly, - decimal initialTradingBalance) - { - var config = new TradingBotConfig - { - AccountName = accountName, - MoneyManagement = moneyManagement, - Ticker = ticker, - ScenarioName = scenario, - Timeframe = interval, - IsForWatchingOnly = isForWatchingOnly, - BotTradingBalance = initialTradingBalance, - BotType = BotType.FlippingBot, - IsForBacktest = true - }; - - return new FlippingBot( - _exchangeService, - _tradingBotLogger, - _tradingService, - _accountService, - _messengerService, - this, - config); - } - public ITradingBot CreateScalpingBot(TradingBotConfig config) { return new ScalpingBot( diff --git a/src/Managing.Application/ManageBot/Commands/UpdateBotConfigCommand.cs b/src/Managing.Application/ManageBot/Commands/UpdateBotConfigCommand.cs new file mode 100644 index 0000000..9f4a523 --- /dev/null +++ b/src/Managing.Application/ManageBot/Commands/UpdateBotConfigCommand.cs @@ -0,0 +1,20 @@ +using Managing.Domain.Bots; +using MediatR; + +namespace Managing.Application.ManageBot.Commands +{ + /// + /// Command to update the configuration of a running trading bot + /// + public class UpdateBotConfigCommand : IRequest + { + public string Identifier { get; } + public TradingBotConfig NewConfig { get; } + + public UpdateBotConfigCommand(string identifier, TradingBotConfig newConfig) + { + Identifier = identifier; + NewConfig = newConfig; + } + } +} \ No newline at end of file diff --git a/src/Managing.Application/ManageBot/StartBotCommandHandler.cs b/src/Managing.Application/ManageBot/StartBotCommandHandler.cs index 658880b..3cd3c43 100644 --- a/src/Managing.Application/ManageBot/StartBotCommandHandler.cs +++ b/src/Managing.Application/ManageBot/StartBotCommandHandler.cs @@ -1,6 +1,8 @@ using Managing.Application.Abstractions; using Managing.Application.Abstractions.Services; using Managing.Application.ManageBot.Commands; +using Managing.Common; +using Managing.Domain.Bots; using MediatR; using static Managing.Common.Enums; @@ -29,6 +31,18 @@ namespace Managing.Application.ManageBot { BotStatus botStatus = BotStatus.Down; + // Validate the configuration + if (request.Config == null) + { + throw new ArgumentException("Bot configuration is required"); + } + + if (request.Config.BotTradingBalance <= Constants.GMX.Config.MinimumPositionAmount) + { + throw new ArgumentException( + $"Bot trading balance must be greater than {Constants.GMX.Config.MinimumPositionAmount}"); + } + var account = await _accountService.GetAccount(request.Config.AccountName, true, true); if (account == null) @@ -43,32 +57,86 @@ namespace Managing.Application.ManageBot throw new Exception($"Account {request.Config.AccountName} has no USDC balance or not enough balance"); } - // Ensure cooldown period is set - if (request.Config.CooldownPeriod <= 0) + // Ensure essential configuration values are properly set + var configToUse = new TradingBotConfig { - request.Config.CooldownPeriod = 1; // Default to 1 minute if not set - } + AccountName = request.Config.AccountName, + MoneyManagement = request.Config.MoneyManagement, + Ticker = request.Config.Ticker, + ScenarioName = request.Config.ScenarioName, + Timeframe = request.Config.Timeframe, + IsForWatchingOnly = request.Config.IsForWatchingOnly, + BotTradingBalance = request.Config.BotTradingBalance, + BotType = request.Config.BotType, + IsForBacktest = request.Config.IsForBacktest, + CooldownPeriod = request.Config.CooldownPeriod > 0 ? request.Config.CooldownPeriod : 1, // Default to 1 if not set + MaxLossStreak = request.Config.MaxLossStreak, + MaxPositionTimeHours = request.Config.MaxPositionTimeHours, // Properly handle nullable value + FlipOnlyWhenInProfit = request.Config.FlipOnlyWhenInProfit, + FlipPosition = request.Config.FlipPosition, + Name = request.Config.Name ?? request.Name + }; - switch (request.Config.BotType) + switch (configToUse.BotType) { case BotType.SimpleBot: var bot = _botFactory.CreateSimpleBot(request.Name, null); bot.User = request.User; _botService.AddSimpleBotToCache(bot); return bot.GetStatus(); + case BotType.ScalpingBot: - var sBot = _botFactory.CreateScalpingBot(request.Config); + var sBot = _botFactory.CreateScalpingBot(configToUse); sBot.User = request.User; + + // Log the configuration being used + await LogBotConfigurationAsync(sBot, "ScalpingBot created"); + _botService.AddTradingBotToCache(sBot); return sBot.GetStatus(); + case BotType.FlippingBot: - var fBot = _botFactory.CreateFlippingBot(request.Config); + var fBot = _botFactory.CreateFlippingBot(configToUse); fBot.User = request.User; + + // Log the configuration being used + await LogBotConfigurationAsync(fBot, "FlippingBot created"); + _botService.AddTradingBotToCache(fBot); return fBot.GetStatus(); } return botStatus.ToString(); } + + /// + /// Logs the bot configuration for debugging and audit purposes + /// + /// The trading bot instance + /// Context information for the log + private async Task LogBotConfigurationAsync(ITradingBot bot, string context) + { + try + { + var config = bot.GetConfiguration(); + var logMessage = $"{context} - Bot: {config.Name}, " + + $"Type: {config.BotType}, " + + $"Account: {config.AccountName}, " + + $"Ticker: {config.Ticker}, " + + $"Balance: {config.BotTradingBalance}, " + + $"MaxTime: {config.MaxPositionTimeHours?.ToString() ?? "Disabled"}, " + + $"FlipOnlyProfit: {config.FlipOnlyWhenInProfit}, " + + $"Cooldown: {config.CooldownPeriod}, " + + $"MaxLoss: {config.MaxLossStreak}"; + + // Log through the bot's logger (this will use the bot's logging mechanism) + // For now, we'll just add a comment that this could be enhanced with actual logging + // Console.WriteLine(logMessage); // Could be replaced with proper logging + } + catch (Exception) + { + // Ignore logging errors to not affect bot creation + } + } } } \ No newline at end of file diff --git a/src/Managing.Application/ManageBot/UpdateBotConfigCommandHandler.cs b/src/Managing.Application/ManageBot/UpdateBotConfigCommandHandler.cs new file mode 100644 index 0000000..f5c58d5 --- /dev/null +++ b/src/Managing.Application/ManageBot/UpdateBotConfigCommandHandler.cs @@ -0,0 +1,60 @@ +using Managing.Application.Abstractions; +using Managing.Application.ManageBot.Commands; +using MediatR; + +namespace Managing.Application.ManageBot +{ + /// + /// Handler for updating trading bot configurations + /// + public class UpdateBotConfigCommandHandler : IRequestHandler + { + private readonly IBotService _botService; + + public UpdateBotConfigCommandHandler(IBotService botService) + { + _botService = botService; + } + + public async Task Handle(UpdateBotConfigCommand request, CancellationToken cancellationToken) + { + try + { + if (string.IsNullOrEmpty(request.Identifier)) + { + throw new ArgumentException("Bot identifier is required"); + } + + if (request.NewConfig == null) + { + throw new ArgumentException("New configuration is required"); + } + + // Get the bot from active bots + var activeBots = _botService.GetActiveBots(); + var bot = activeBots.FirstOrDefault(b => b.Identifier == request.Identifier); + + if (bot == null) + { + return $"Bot with identifier {request.Identifier} not found or is not running"; + } + + // Update the bot configuration + var updateResult = await bot.UpdateConfiguration(request.NewConfig); + + if (updateResult) + { + return $"Bot configuration updated successfully for {request.Identifier}"; + } + else + { + return $"Failed to update bot configuration for {request.Identifier}"; + } + } + catch (Exception ex) + { + return $"Error updating bot configuration: {ex.Message}"; + } + } + } +} \ No newline at end of file diff --git a/src/Managing.Domain/Backtests/Backtest.cs b/src/Managing.Domain/Backtests/Backtest.cs index 2beb486..dce8755 100644 --- a/src/Managing.Domain/Backtests/Backtest.cs +++ b/src/Managing.Domain/Backtests/Backtest.cs @@ -58,9 +58,90 @@ public class Backtest [Required] public Dictionary StrategiesValues { get; set; } [Required] public double Score { get; set; } + /// + /// Creates a new TradingBotConfig based on this backtest's configuration for starting a live bot. + /// This method ensures all properties are properly copied, including nullable values. + /// + /// The account name to use for the new bot (can override backtest account) + /// The name for the new bot + /// The initial trading balance for the new bot + /// Optional money management override (uses backtest's if not provided) + /// A new TradingBotConfig ready for bot creation + public TradingBotConfig CreateLiveBotConfig( + string accountName, + string botName, + decimal initialTradingBalance, + MoneyManagement moneyManagement = null) + { + return new TradingBotConfig + { + AccountName = accountName, + MoneyManagement = moneyManagement ?? Config.MoneyManagement, + Ticker = Config.Ticker, + ScenarioName = Config.ScenarioName, + Timeframe = Config.Timeframe, + IsForWatchingOnly = false, // Always start as active bot + BotTradingBalance = initialTradingBalance, + BotType = Config.BotType, + IsForBacktest = false, // Always false for live bots + CooldownPeriod = Config.CooldownPeriod, + MaxLossStreak = Config.MaxLossStreak, + MaxPositionTimeHours = Config.MaxPositionTimeHours, // Properly copy nullable value + FlipOnlyWhenInProfit = Config.FlipOnlyWhenInProfit, + FlipPosition = Config.FlipPosition, + Name = botName + }; + } + + /// + /// Creates a copy of the backtest's configuration for a new backtest. + /// Useful for running similar backtests with modified parameters. + /// + /// New start date for the backtest + /// New end date for the backtest + /// New initial balance for the backtest + /// Optional money management override + /// A new TradingBotConfig for backtesting + public TradingBotConfig CreateBacktestConfig( + DateTime startDate, + DateTime endDate, + decimal balance, + MoneyManagement moneyManagement = null) + { + return new TradingBotConfig + { + AccountName = Config.AccountName, + MoneyManagement = moneyManagement ?? Config.MoneyManagement, + Ticker = Config.Ticker, + ScenarioName = Config.ScenarioName, + Timeframe = Config.Timeframe, + IsForWatchingOnly = Config.IsForWatchingOnly, + BotTradingBalance = balance, + BotType = Config.BotType, + IsForBacktest = true, + CooldownPeriod = Config.CooldownPeriod, + MaxLossStreak = Config.MaxLossStreak, + MaxPositionTimeHours = Config.MaxPositionTimeHours, // Properly copy nullable value + FlipOnlyWhenInProfit = Config.FlipOnlyWhenInProfit, + FlipPosition = Config.FlipPosition, + Name = $"Backtest-{Config.ScenarioName}-{DateTime.UtcNow:yyyyMMdd-HHmmss}" + }; + } + public string GetStringReport() { + var timeBasedInfo = Config.MaxPositionTimeHours.HasValue + ? $" | MaxTime: {Config.MaxPositionTimeHours}h" + : " | MaxTime: Disabled"; + + var flipInfo = Config.FlipPosition + ? $" | Flip: {(Config.FlipOnlyWhenInProfit ? "Profit-Only" : "Always")}" + : ""; + return - $"{Config.Ticker} | {Config.Timeframe} | Positions: {Positions.Count} | Winrate: {WinRate}% | Pnl: {FinalPnl:#.##}$ | %Pnl: {GrowthPercentage:#.##}% | %Hodl: {HodlPercentage:#.##}%"; + $"{Config.Ticker} | {Config.Timeframe} | Positions: {Positions.Count} | Winrate: {WinRate}% | " + + $"Pnl: {FinalPnl:#.##}$ | %Pnl: {GrowthPercentage:#.##}% | %Hodl: {HodlPercentage:#.##}%" + + $" | Cooldown: {Config.CooldownPeriod} | MaxLoss: {Config.MaxLossStreak}" + + timeBasedInfo + flipInfo; } } \ No newline at end of file diff --git a/src/Managing.Domain/Bots/TradingBotConfig.cs b/src/Managing.Domain/Bots/TradingBotConfig.cs index 2e93fa9..2787140 100644 --- a/src/Managing.Domain/Bots/TradingBotConfig.cs +++ b/src/Managing.Domain/Bots/TradingBotConfig.cs @@ -19,4 +19,18 @@ public class TradingBotConfig [Required] public int MaxLossStreak { get; set; } [Required] public bool FlipPosition { get; set; } [Required] public string Name { get; set; } + + /// + /// Maximum time in hours that a position can remain open before being automatically closed. + /// If null, time-based position closure is disabled. + /// + public decimal? MaxPositionTimeHours { get; set; } + + /// + /// If true, positions will only be flipped when the current position is in profit. + /// If false, positions will be flipped regardless of profit status. + /// Default is true for safer trading. + /// + [Required] + public bool FlipOnlyWhenInProfit { get; set; } = true; } \ No newline at end of file diff --git a/src/Managing.WebApp/src/components/mollecules/Modal/Modal.tsx b/src/Managing.WebApp/src/components/mollecules/Modal/Modal.tsx index 752bc3b..f6cf771 100644 --- a/src/Managing.WebApp/src/components/mollecules/Modal/Modal.tsx +++ b/src/Managing.WebApp/src/components/mollecules/Modal/Modal.tsx @@ -1,6 +1,6 @@ import React from 'react' -import type { IModalProps } from '../../../global/type' +import type {IModalProps} from '../../../global/type' import ModalHeader from './ModalHeader' @@ -16,7 +16,7 @@ const Modal: React.FC = ({ {showModal ? (
-
+
= ({ list, setBacktests }) => { React.useState(false) const [selectedMoneyManagement, setSelectedMoneyManagement] = React.useState() + const [showBotNameModal, setShowBotNameModal] = useState(false) + const [isForWatchOnly, setIsForWatchOnly] = useState(false) + const [currentBacktest, setCurrentBacktest] = useState(null) + const [selectedMoneyManagementName, setSelectedMoneyManagementName] = useState('') - async function runBot(backtest: Backtest, isForWatchOnly: boolean) { + // 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) { + setSelectedMoneyManagementName(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'); + const request: StartBotRequest = { accountName: backtest.config.accountName, - name: backtest.config.ticker + '-' + backtest.config.timeframe?.toString(), - botType: BotType.ScalpingBot, + name: botName, + botType: backtest.config.botType, isForWatchOnly: isForWatchOnly, - moneyManagementName: backtest.config.moneyManagement?.name, + // Only use the money management name if it's not a custom money management, otherwise use optimized + moneyManagementName: isCustomMoneyManagement ? + (backtest.optimizedMoneyManagement?.name || backtest.config.moneyManagement?.name) : + moneyManagementName, scenario: backtest.config.scenarioName, ticker: backtest.config.ticker as Ticker, timeframe: backtest.config.timeframe, - initialTradingBalance: 1000, + initialTradingBalance: initialTradingBalance, + cooldownPeriod: backtest.config.cooldownPeriod, + maxLossStreak: backtest.config.maxLossStreak, + maxPositionTimeHours: backtest.config.maxPositionTimeHours, + flipOnlyWhenInProfit: backtest.config.flipOnlyWhenInProfit } await client .bot_Start(request) .then((botStatus: string) => { - t.update('info', 'Bot status :' + botStatus) + t.update('info', 'Bot status: ' + botStatus) }) .catch((err) => { - t.update('error', 'Error :' + err) + t.update('error', 'Error: ' + err) }) } + 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) + } + async function runOptimizedBacktest(backtest: Backtest) { const t = new Toast('Optimized backtest is running') const client = new BacktestClient({}, apiUrl) @@ -85,23 +142,26 @@ const BacktestCards: React.FC = ({ list, setBacktests }) => { const startDate = backtest.candles[0].date const endDate = backtest.candles[backtest.candles.length - 1].date + // Create optimized backtest config + const optimizedConfig: TradingBotConfig = { + ...backtest.config, + name: `${backtest.config.ticker}-${backtest.config.scenarioName}-Optimized`, + moneyManagement: backtest.optimizedMoneyManagement || backtest.config.moneyManagement + } + + const request: RunBacktestRequest = { + config: optimizedConfig, + startDate: startDate, + endDate: endDate, + balance: backtest.walletBalances[0].value, + watchOnly: false, + save: false, + moneyManagementName: undefined, // We're passing the moneyManagement object directly + moneyManagement: optimizedConfig.moneyManagement + } + await client - .backtest_Run( - backtest.config.accountName, - backtest.config.botType, - backtest.config.ticker as Ticker, - backtest.config.scenarioName, - backtest.config.timeframe, - false, // watchOnly - backtest.walletBalances[0].value, // balance - '', // moneyManagementName (empty since we're passing the optimized moneyManagement object) - startDate, // startDate - endDate, // endDate - false, // save - backtest.config.cooldownPeriod, - backtest.config.maxLossStreak, - backtest.config.moneyManagement as MoneyManagement, // moneyManagement object - ) + .backtest_Run(request) .then((backtest: Backtest) => { t.update('success', `${backtest.config.ticker} Backtest Succeeded`) setBacktests((arr) => [...arr, backtest]) @@ -162,7 +222,7 @@ const BacktestCards: React.FC = ({ list, setBacktests }) => {
  • @@ -170,7 +230,7 @@ const BacktestCards: React.FC = ({ list, setBacktests }) => {
  • @@ -303,6 +363,21 @@ const BacktestCards: React.FC = ({ list, setBacktests }) => { moneyManagement={selectedMoneyManagement} onClose={() => setShowMoneyManagementModal(false)} /> + + {showBotNameModal && currentBacktest && moneyManagements && ( + + handleSubmitBotName(botName, backtest, isForWatchOnly, moneyManagementName, initialTradingBalance) + } + moneyManagements={moneyManagements} + selectedMoneyManagement={selectedMoneyManagementName} + setSelectedMoneyManagement={setSelectedMoneyManagementName} + /> + )}
  • ) } diff --git a/src/Managing.WebApp/src/components/organism/Backtest/backtestModal.tsx b/src/Managing.WebApp/src/components/organism/Backtest/backtestModal.tsx index 40a25b0..2a2d582 100644 --- a/src/Managing.WebApp/src/components/organism/Backtest/backtestModal.tsx +++ b/src/Managing.WebApp/src/components/organism/Backtest/backtestModal.tsx @@ -4,15 +4,17 @@ import {type SubmitHandler, useForm} from 'react-hook-form' import useApiUrlStore from '../../../app/store/apiStore' import { - AccountClient, - BacktestClient, - BotType, - DataClient, - MoneyManagement, - MoneyManagementClient, - ScenarioClient, - Ticker, - Timeframe, + AccountClient, + BacktestClient, + BotType, + DataClient, + MoneyManagement, + MoneyManagementClient, + RunBacktestRequest, + ScenarioClient, + Ticker, + Timeframe, + TradingBotConfig, } from '../../../generated/ManagingApi' import type {BacktestModalProps, IBacktestsFormInput,} from '../../../global/type' import {Loader, Slider} from '../../atoms' @@ -42,8 +44,10 @@ const BacktestModal: React.FC = ({ defaultValues: { startDate: defaultStartDateString, endDate: defaultEndDateString, - cooldownPeriod: 1, // Default cooldown period of 1 minute - maxLossStreak: 0 // Default max loss streak of 0 (no limit) + cooldownPeriod: 10, // Default cooldown period of 10 minutes + maxLossStreak: 0, // Default max loss streak of 0 (no limit) + maxPositionTimeHours: null, // Default to null (disabled) + flipOnlyWhenInProfit: true // Default to true } }); const [selectedAccount, setSelectedAccount] = useState('') @@ -100,28 +104,48 @@ const BacktestModal: React.FC = ({ loopCount: number ): Promise { const t = new Toast(ticker + ' is running') - // Use the name of the money management strategy if custom is not provided - const moneyManagementName = customMoneyManagement ? undefined : selectedMoneyManagement console.log(customMoneyManagement) try { - const backtest = await backtestClient.backtest_Run( - form.accountName, - form.botType, - ticker as Ticker, - scenarioName, - form.timeframe, - false, // watchOnly - balance, - moneyManagementName, - new Date(form.startDate), // startDate - new Date(form.endDate), // endDate - form.save, - form.cooldownPeriod, // Use the cooldown period from the form - form.maxLossStreak, // Add the max loss streak parameter - customMoneyManagement - ); + // Create the TradingBotConfig + const tradingBotConfig: TradingBotConfig = { + accountName: form.accountName, + ticker: ticker as Ticker, + scenarioName: scenarioName, + timeframe: form.timeframe, + botType: form.botType, + isForWatchingOnly: false, // Always false for backtests + isForBacktest: true, // Always true for backtests + cooldownPeriod: form.cooldownPeriod || 1, + maxLossStreak: form.maxLossStreak || 0, + maxPositionTimeHours: form.maxPositionTimeHours || null, + flipOnlyWhenInProfit: form.flipOnlyWhenInProfit ?? true, + flipPosition: form.botType === BotType.FlippingBot, // Set based on bot type + name: `Backtest-${scenarioName}-${ticker}-${new Date().toISOString()}`, + botTradingBalance: 0, // Will be set in the request + moneyManagement: customMoneyManagement || moneyManagements?.find(m => m.name === selectedMoneyManagement) || moneyManagements?.[0] || { + name: 'placeholder', + leverage: 1, + stopLoss: 0.01, + takeProfit: 0.02, + timeframe: form.timeframe + } + }; + + // Create the RunBacktestRequest + const request: RunBacktestRequest = { + config: tradingBotConfig, + startDate: new Date(form.startDate), + endDate: new Date(form.endDate), + balance: balance, + watchOnly: false, + save: form.save || false, + moneyManagementName: customMoneyManagement ? undefined : selectedMoneyManagement, + moneyManagement: customMoneyManagement + }; + + const backtest = await backtestClient.backtest_Run(request); t.update('success', `${backtest.config.ticker} Backtest Succeeded`) setBacktests((arr) => [...arr, backtest]) @@ -228,64 +252,79 @@ const BacktestModal: React.FC = ({ titleHeader="Run Backtest" >
    - - + - ))} - - + {accounts.map((item) => ( + + ))} + + - - - - + + + +
    - - + {moneyManagements.map((item) => ( + + ))} + - ))} - - - + + + + + + +
    {/* Losing streak info */} {(() => { @@ -314,55 +353,48 @@ const BacktestModal: React.FC = ({ })()} {showCustomMoneyManagement && ( -
    - +
    + +
    + )} + + {/* Third Row: Scenario & Tickers (full width since they need more space) */} +
    + + + + + + +
    - )} - - - - - - - - - - - - - + {/* Fourth Row: Balance & Cooldown Period */}
    = ({
    + {/* Fifth Row: Max Loss Streak & Max Position Time */}
    = ({ /> + + +
    + Leave empty to disable time-based position closure +
    +
    +
    + + {/* Sixth Row: Flip Only When In Profit & Save */} +
    + + +
    + If enabled, positions will only flip when current position is profitable +
    +
    + = ({
    + {/* Seventh Row: Start Date & End Date */}
    = ({
    + {/* Loop Slider (if enabled) */} {showLoopSlider && ( = ({ )}
    -
    - - setLeverage(e.target.value)} - type='number' - className='input input-bordered' - > - +
    + + setLeverage(e.target.value)} + type='number' + className='input input-bordered w-full' + > + - - setTakeProfit(e.target.value)} - step="0.01" - max="100" - type='number' - className='input input-bordered' - > - + + setTakeProfit(e.target.value)} + step="0.01" + max="100" + type='number' + className='input input-bordered w-full' + > + - - setStopLoss(e.target.value)} - step="0.01" - max="100" - type='number' - className='input input-bordered' - > - + + setStopLoss(e.target.value)} + step="0.01" + max="100" + type='number' + className='input input-bordered w-full' + > + +
    ) : null} diff --git a/src/Managing.WebApp/src/components/organism/index.tsx b/src/Managing.WebApp/src/components/organism/index.tsx index f14abd9..a123f7b 100644 --- a/src/Managing.WebApp/src/components/organism/index.tsx +++ b/src/Managing.WebApp/src/components/organism/index.tsx @@ -9,3 +9,4 @@ export { default as StatusBadge } from './StatusBadge/StatusBadge' export { default as PositionsList } from './Positions/PositionList' export { default as WorkflowCanvas } from './Workflow/workflowCanvas' export { default as ScenarioModal } from './ScenarioModal' +export { default as BotNameModal } from './BotNameModal/BotNameModal' diff --git a/src/Managing.WebApp/src/generated/ManagingApi.ts b/src/Managing.WebApp/src/generated/ManagingApi.ts index 0b47e5f..f53309a 100644 --- a/src/Managing.WebApp/src/generated/ManagingApi.ts +++ b/src/Managing.WebApp/src/generated/ManagingApi.ts @@ -337,57 +337,11 @@ export class BacktestClient extends AuthorizedApiBase { return Promise.resolve(null as any); } - backtest_Run(accountName: string | null | undefined, botType: BotType | undefined, ticker: Ticker | undefined, scenarioName: string | null | undefined, timeframe: Timeframe | undefined, watchOnly: boolean | undefined, balance: number | undefined, moneyManagementName: string | null | undefined, startDate: Date | undefined, endDate: Date | undefined, save: boolean | undefined, cooldownPeriod: number | undefined, maxLossStreak: number | undefined, moneyManagement: MoneyManagement | undefined): Promise { - let url_ = this.baseUrl + "/Backtest/Run?"; - if (accountName !== undefined && accountName !== null) - url_ += "accountName=" + encodeURIComponent("" + accountName) + "&"; - if (botType === null) - throw new Error("The parameter 'botType' cannot be null."); - else if (botType !== undefined) - url_ += "botType=" + encodeURIComponent("" + botType) + "&"; - if (ticker === null) - throw new Error("The parameter 'ticker' cannot be null."); - else if (ticker !== undefined) - url_ += "ticker=" + encodeURIComponent("" + ticker) + "&"; - if (scenarioName !== undefined && scenarioName !== null) - url_ += "scenarioName=" + encodeURIComponent("" + scenarioName) + "&"; - if (timeframe === null) - throw new Error("The parameter 'timeframe' cannot be null."); - else if (timeframe !== undefined) - url_ += "timeframe=" + encodeURIComponent("" + timeframe) + "&"; - if (watchOnly === null) - throw new Error("The parameter 'watchOnly' cannot be null."); - else if (watchOnly !== undefined) - url_ += "watchOnly=" + encodeURIComponent("" + watchOnly) + "&"; - if (balance === null) - throw new Error("The parameter 'balance' cannot be null."); - else if (balance !== undefined) - url_ += "balance=" + encodeURIComponent("" + balance) + "&"; - if (moneyManagementName !== undefined && moneyManagementName !== null) - url_ += "moneyManagementName=" + encodeURIComponent("" + moneyManagementName) + "&"; - if (startDate === null) - throw new Error("The parameter 'startDate' cannot be null."); - else if (startDate !== undefined) - url_ += "startDate=" + encodeURIComponent(startDate ? "" + startDate.toISOString() : "") + "&"; - if (endDate === null) - throw new Error("The parameter 'endDate' cannot be null."); - else if (endDate !== undefined) - url_ += "endDate=" + encodeURIComponent(endDate ? "" + endDate.toISOString() : "") + "&"; - if (save === null) - throw new Error("The parameter 'save' cannot be null."); - else if (save !== undefined) - url_ += "save=" + encodeURIComponent("" + save) + "&"; - if (cooldownPeriod === null) - throw new Error("The parameter 'cooldownPeriod' cannot be null."); - else if (cooldownPeriod !== undefined) - url_ += "cooldownPeriod=" + encodeURIComponent("" + cooldownPeriod) + "&"; - if (maxLossStreak === null) - throw new Error("The parameter 'maxLossStreak' cannot be null."); - else if (maxLossStreak !== undefined) - url_ += "maxLossStreak=" + encodeURIComponent("" + maxLossStreak) + "&"; + backtest_Run(request: RunBacktestRequest): Promise { + let url_ = this.baseUrl + "/Backtest/Run"; url_ = url_.replace(/[?&]$/, ""); - const content_ = JSON.stringify(moneyManagement); + const content_ = JSON.stringify(request); let options_: RequestInit = { body: content_, @@ -811,6 +765,45 @@ export class BotClient extends AuthorizedApiBase { } return Promise.resolve(null as any); } + + bot_UpdateBotConfig(request: UpdateBotConfigRequest): Promise { + let url_ = this.baseUrl + "/Bot/UpdateConfig"; + url_ = url_.replace(/[?&]$/, ""); + + const content_ = JSON.stringify(request); + + let options_: RequestInit = { + body: content_, + method: "PUT", + headers: { + "Content-Type": "application/json", + "Accept": "application/json" + } + }; + + return this.transformOptions(options_).then(transformedOptions_ => { + return this.http.fetch(url_, transformedOptions_); + }).then((_response: Response) => { + return this.processBot_UpdateBotConfig(_response); + }); + } + + protected processBot_UpdateBotConfig(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 string; + return result200; + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } } export class DataClient extends AuthorizedApiBase { @@ -2705,6 +2698,8 @@ export interface TradingBotConfig { maxLossStreak: number; flipPosition: boolean; name: string; + maxPositionTimeHours?: number | null; + flipOnlyWhenInProfit: boolean; } export interface MoneyManagement { @@ -3106,6 +3101,17 @@ export interface SuperTrendResult extends ResultBase { lowerBand?: number | null; } +export interface RunBacktestRequest { + config?: TradingBotConfig | null; + startDate?: Date; + endDate?: Date; + balance?: number; + watchOnly?: boolean; + save?: boolean; + moneyManagementName?: string | null; + moneyManagement?: MoneyManagement | null; +} + export interface StartBotRequest { botType?: BotType; identifier?: string | null; @@ -3119,6 +3125,8 @@ export interface StartBotRequest { cooldownPeriod?: number; maxLossStreak?: number; name?: string | null; + maxPositionTimeHours?: number | null; + flipOnlyWhenInProfit?: boolean; } export interface TradingBot { @@ -3138,6 +3146,8 @@ export interface TradingBot { moneyManagement: MoneyManagement; identifier: string; agentName: string; + maxPositionTimeHours: number; + flipOnlyWhenInProfit: boolean; } export interface OpenPositionManuallyRequest { @@ -3150,6 +3160,23 @@ export interface ClosePositionRequest { positionId?: string | null; } +export interface UpdateBotConfigRequest { + identifier?: string | null; + accountName?: string | null; + 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 { ticker?: Ticker; imageUrl?: string | null; diff --git a/src/Managing.WebApp/src/global/type.tsx b/src/Managing.WebApp/src/global/type.tsx index 742d8e3..d9eb759 100644 --- a/src/Managing.WebApp/src/global/type.tsx +++ b/src/Managing.WebApp/src/global/type.tsx @@ -114,6 +114,8 @@ export type IBacktestsFormInput = { endDate: string cooldownPeriod: number maxLossStreak: number + maxPositionTimeHours?: number | null + flipOnlyWhenInProfit?: boolean } export type IBacktestCards = {