using Managing.Api.Models.Requests; using Managing.Api.Models.Responses; using Managing.Application.Abstractions; using Managing.Application.Abstractions.Services; using Managing.Application.Hubs; using Managing.Application.ManageBot.Commands; using Managing.Application.Shared; using Managing.Common; using Managing.Core; using Managing.Core.Exceptions; using Managing.Domain.Accounts; using Managing.Domain.Backtests; using Managing.Domain.Bots; using Managing.Domain.Indicators; using Managing.Domain.MoneyManagements; using Managing.Domain.Scenarios; using Managing.Domain.Strategies; using Managing.Domain.Trades; using Managing.Domain.Users; using MediatR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; using static Managing.Common.Enums; namespace Managing.Api.Controllers; /// /// Controller for bot operations such as starting, stopping, deleting, and managing bots. /// Requires authorization for access and produces JSON responses. /// [ApiController] [Authorize] [Route("[controller]")] [Produces("application/json")] public class BotController : BaseController { private readonly IMediator _mediator; private readonly ILogger _logger; private readonly IHubContext _hubContext; private readonly IBacktester _backtester; private readonly IBotService _botService; private readonly IAccountService _accountService; private readonly IMoneyManagementService _moneyManagementService; private readonly IServiceScopeFactory _scopeFactory; private readonly IAdminConfigurationService _adminService; private readonly IConfiguration _configuration; /// /// Initializes a new instance of the class. /// /// Logger for logging information. /// Mediator for handling commands and requests. /// SignalR hub context for real-time communication. /// Backtester for running backtests on bots. /// /// /// /// /// /// Configuration for accessing environment variables. public BotController(ILogger logger, IMediator mediator, IHubContext hubContext, IBacktester backtester, IBotService botService, IUserService userService, IAccountService accountService, IMoneyManagementService moneyManagementService, IServiceScopeFactory scopeFactory, IAdminConfigurationService adminService, IConfiguration configuration) : base(userService) { _logger = logger; _mediator = mediator; _hubContext = hubContext; _backtester = backtester; _botService = botService; _accountService = accountService; _moneyManagementService = moneyManagementService; _scopeFactory = scopeFactory; _adminService = adminService; _configuration = configuration; } /// /// Checks if the current authenticated user owns the account associated with the specified bot or account name /// /// The identifier of the bot to check /// Optional account name to check when creating a new bot /// True if the user owns the account or is admin, False otherwise private async Task UserOwnsBotAccount(Guid identifier, string accountName = null) { try { var user = await GetUser(); if (user == null) return false; // Admin users can access all bots if (await _adminService.IsUserAdminAsync(user.Name)) return true; if (identifier != default) { // For existing bots, check if the user owns the bot's account var bot = await _botService.GetBotByIdentifier(identifier); if (bot != null) { return bot.User != null && bot.User.Name == user.Name; } } // For new bot creation, check if the user owns the account provided in the request if (!string.IsNullOrEmpty(accountName)) { var account = await ServiceScopeHelpers.WithScopedService(_scopeFactory, async accountService => { return await accountService.GetAccount(accountName, true, false); }); // Compare the user names return account != null && account.User != null && account.User.Name == user.Name; } return false; } catch (Exception ex) { _logger.LogError(ex, "Error checking if user owns bot account"); return false; } } /// /// Starts a bot with the specified parameters. /// /// The request containing bot start parameters. /// A string indicating the result of the start operation. [HttpPost] [Route("Start")] public async Task> Start(StartBotRequest request) { try { var (config, user) = await ValidateAndPrepareBotRequest(request, "start"); var result = await _mediator.Send(new StartBotCommand(config, user, false)); // createOnly = false await NotifyBotSubscriberAsync(); return Ok(result); } catch (InvalidOperationException ex) when (ex.Message.Contains("already have a strategy")) { // Return 400 for validation errors about existing strategies on same ticker return BadRequest(ex.Message); } catch (Exception ex) { _logger.LogError(ex, "Error starting bot"); return StatusCode(500, $"Error starting bot: {ex.Message}"); } } /// /// Starts a copy trading bot that mirrors trades from a master bot. /// /// The request containing copy trading parameters. /// A string indicating the result of the start operation. [HttpPost] [Route("StartCopyTrading")] public async Task> StartCopyTrading(StartCopyTradingRequest request) { try { var user = await GetUser(); if (user == null) { return Unauthorized("User not found"); } var result = await _mediator.Send(new StartCopyTradingCommand(request.MasterBotIdentifier, request.BotTradingBalance, user)); await NotifyBotSubscriberAsync(); return Ok(result); } catch (InvalidOperationException ex) when (ex.Message.Contains("already have a strategy")) { // Return 400 for validation errors about existing strategies on same ticker return BadRequest(ex.Message); } catch (Exception ex) { _logger.LogError(ex, "Error starting copy trading bot"); return StatusCode(500, $"Error starting copy trading bot: {ex.Message}"); } } /// /// Saves a bot configuration without starting it. /// /// The request containing bot configuration parameters. /// A string indicating the result of the save operation. [HttpPost] [Route("Save")] public async Task> Save(SaveBotRequest request) { try { var (config, user) = await ValidateAndPrepareBotRequest(request, "save"); var result = await _mediator.Send(new StartBotCommand(config, user, true)); // createOnly = true return Ok(result); } catch (InvalidOperationException ex) when (ex.Message.Contains("already have a strategy")) { // Return 400 for validation errors about existing strategies on same ticker return BadRequest(ex.Message); } catch (Exception ex) { _logger.LogError(ex, "Error saving bot"); return StatusCode(500, $"Error saving bot: {ex.Message}"); } } /// /// Stops a bot specified by type and name. /// /// The identifier of the bot to stop. /// A string indicating the result of the stop operation. [HttpGet] [Route("Stop")] public async Task> Stop(Guid identifier) { try { // Check if user owns the account if (!await UserOwnsBotAccount(identifier)) { return Forbid("You don't have permission to stop this bot"); } var result = await _mediator.Send(new StopBotCommand(identifier)); _logger.LogInformation($"Bot identifier {identifier} is now {result}"); await NotifyBotSubscriberAsync(); return Ok(result); } catch (ServiceUnavailableException ex) { // ServiceUnavailableException is already user-friendly (e.g., from Orleans exception conversion) _logger.LogWarning(ex, "Service unavailable error stopping bot {Identifier}", identifier); return StatusCode(503, ex.Message); } catch (Exception ex) { _logger.LogError(ex, "Error stopping bot {Identifier}", identifier); // Check if this is an Orleans exception that wasn't caught earlier if (OrleansExceptionHelper.IsOrleansException(ex)) { var userMessage = OrleansExceptionHelper.GetUserFriendlyMessage(ex, "bot stop"); _logger.LogWarning( "Orleans exception detected in controller for bot {Identifier}: {ExceptionType}", identifier, ex.GetType().Name); return StatusCode(503, userMessage); } return StatusCode(500, $"Error stopping bot: {ex.Message}"); } } /// /// Stops all active bots for the connected user. /// /// A boolean indicating the result of the stop all operation. [HttpGet] [Route("StopAll")] public async Task> StopAll() { try { var user = await GetUser(); if (user == null) { return Unauthorized("User not found"); } var result = await _mediator.Send(new StopAllUserBotsCommand(user)); if (result) { await NotifyBotSubscriberAsync(); _logger.LogInformation($"All bots stopped successfully for user {user.Name}"); } else { _logger.LogWarning($"Failed to stop all bots for user {user.Name}"); } return Ok(result); } catch (Exception ex) { _logger.LogError(ex, "Error stopping all bots"); return StatusCode(500, $"Error stopping all bots: {ex.Message}"); } } /// /// Deletes a bot specified by name. /// /// The identifier of the bot to delete. /// A boolean indicating the result of the delete operation. [HttpDelete] [Route("Delete")] public async Task> Delete(Guid identifier) { try { // Check if user owns the account if (!await UserOwnsBotAccount(identifier)) { return Forbid("You don't have permission to delete this bot"); } var result = await _botService.DeleteBot(identifier); await NotifyBotSubscriberAsync(); return result ? Ok(result) : Problem($"Failed to delete bot with identifier {identifier}"); } catch (Exception ex) { _logger.LogError(ex, "Error deleting bot"); return StatusCode(500, $"Error deleting bot: {ex.Message}"); } } /// /// Restarts a bot specified by type and name. /// /// The identifier of the bot to restart. /// A string indicating the result of the restart operation. [HttpGet] [Route("Restart")] public async Task> Restart(Guid identifier) { try { // Check if user owns the account if (!await UserOwnsBotAccount(identifier)) { return Forbid("You don't have permission to restart this bot"); } var result = await _mediator.Send(new RestartBotCommand(identifier)); _logger.LogInformation($"Bot with identifier {identifier} is now {result}"); return Ok(result); } catch (InvalidOperationException ex) when (ex.Message.Contains("already have another strategy")) { // Return 400 for validation errors about existing strategies on same ticker return BadRequest(ex.Message); } catch (Exception ex) { _logger.LogError(ex, "Error restarting bot"); return StatusCode(500, $"Error restarting bot: {ex.Message}"); } } /// /// Retrieves a list of active bots. /// /// A list of active trading bots. [HttpGet] public async Task> GetActiveBots() { return await GetBotsByStatusAsync(BotStatus.Running); } /// /// Retrieves a list of bots by status. /// /// The status to filter bots by (None, Down, Up) /// A list of trading bots with the specified status. [HttpGet] [Route("ByStatus/{status}")] public async Task> GetBotsByStatus(BotStatus status) { return await GetBotsByStatusAsync(status); } /// /// Retrieves a list of saved bots (status None) for the current user. /// /// A list of saved trading bots for the current user. [HttpGet] [Route("GetMySavedBots")] public async Task> GetMySavedBots() { try { var user = await GetUser(); if (user == null) { return new List(); } var result = await _mediator.Send(new GetBotsByUserAndStatusCommand(user.Id, BotStatus.Saved)); return MapBotsToTradingBotResponse(result); } catch (Exception ex) { _logger.LogError(ex, "Error getting saved bots for user"); return new List(); } } /// /// Retrieves a paginated list of bots with filtering and sorting capabilities. /// /// Page number (1-based). Default is 1. /// Number of items per page. Default is 10, maximum is 100. /// Filter by bot status. If null, returns bots of all statuses. /// Filter by bot name (partial match, case-insensitive). If null, no name filtering is applied. /// Filter by ticker (partial match, case-insensitive). If null, no ticker filtering is applied. /// Filter by agent name (partial match, case-insensitive). If null, no agent name filtering is applied. /// Sort field. Valid values: "Name", "Ticker", "Status", "CreateDate", "StartupTime", "Pnl", "WinRate", "AgentName". Default is "CreateDate". /// Sort direction. Default is "Desc". /// A paginated response containing trading bots [HttpGet] [Route("Paginated")] public async Task> GetBotsPaginated( int pageNumber = 1, int pageSize = 10, BotStatus? status = null, string? name = null, string? ticker = null, string? agentName = null, BotSortableColumn sortBy = BotSortableColumn.CreateDate, string sortDirection = "Desc") { try { // Validate parameters if (pageNumber < 1) { pageNumber = 1; } if (pageSize < 1 || pageSize > 100) { pageSize = Math.Min(Math.Max(pageSize, 1), 100); } // Check environment variable for filtering profitable bots only var showOnlyProfitable = _configuration.GetValue("showOnlyProfitable", false); // Get paginated bots from service var (bots, totalCount) = await _botService.GetBotsPaginatedAsync( pageNumber, pageSize, status, name, ticker, agentName, sortBy, sortDirection, showOnlyProfitable); // Map to response objects var tradingBotResponses = MapBotsToTradingBotResponse(bots); // Calculate pagination metadata var totalPages = (int)Math.Ceiling((double)totalCount / pageSize); var hasPreviousPage = pageNumber > 1; var hasNextPage = pageNumber < totalPages; return new PaginatedResponse { Items = tradingBotResponses, TotalCount = totalCount, PageNumber = pageNumber, PageSize = pageSize, TotalPages = totalPages, HasPreviousPage = hasPreviousPage, HasNextPage = hasNextPage }; } catch (Exception ex) { _logger.LogError(ex, "Error getting paginated bots"); return new PaginatedResponse { Items = new List(), TotalCount = 0, PageNumber = pageNumber, PageSize = pageSize, TotalPages = 0, HasPreviousPage = false, HasNextPage = false }; } } /// /// Retrieves a list of bots by status by sending a command to the mediator. /// /// The status to filter bots by /// A list of trading bots. private async Task> GetBotsByStatusAsync(BotStatus status) { var result = await _mediator.Send(new GetBotsByStatusCommand(status)); return MapBotsToTradingBotResponse(result); } /// /// Maps a collection of Bot entities to TradingBotResponse objects. /// /// The collection of bots to map /// A list of TradingBotResponse objects private static List MapBotsToTradingBotResponse(IEnumerable bots) { var list = new List(); foreach (var item in bots) { list.Add(new TradingBotResponse { Status = item.Status.ToString(), WinRate = (item.TradeWins + item.TradeLosses) != 0 ? item.TradeWins / (item.TradeWins + item.TradeLosses) : 0, ProfitAndLoss = item.Pnl, Roi = item.Roi, Identifier = item.Identifier.ToString(), AgentName = item.User.AgentName, MasterAgentName = item.MasterBotUser?.AgentName, CreateDate = item.CreateDate, StartupTime = item.StartupTime, Name = item.Name, Ticker = item.Ticker, }); } return list; } /// /// Notifies subscribers about the current list of bots via SignalR. /// private async Task NotifyBotSubscriberAsync() { var botsList = await GetBotsByStatusAsync(BotStatus.Running); await _hubContext.Clients.All.SendAsync("BotsSubscription", botsList); } /// /// Manually create a signal for a specified bot with the given parameters. /// /// The request containing position parameters. /// A response indicating the result of the operation. [HttpPost] [Route("CreateManualSignal")] public async Task> CreateManualSignalAsync([FromBody] CreateManualSignalRequest request) { try { // Check if user owns the account if (!await UserOwnsBotAccount(request.Identifier)) { return Forbid("You don't have permission to open positions for this bot"); } var bot = await _botService.GetBotByIdentifier(request.Identifier); if (bot == null) { return NotFound($"Bot with identifier {request.Identifier} not found or is not a trading bot"); } if (bot.Status != BotStatus.Running) { return BadRequest($"Bot with identifier {request.Identifier} is not running"); } var signal = await _botService.CreateManualSignalAsync(request.Identifier, request.Direction); return Ok(signal); } catch (Exception ex) { _logger.LogError(ex, "Error creating signal manually"); return StatusCode(500, $"Error creating signal: {ex.Message}, {ex.InnerException?.Message} or {ex.StackTrace}"); } } /// /// Closes a specific position for a trading bot /// /// The request containing the position close parameters /// The closed position or an error [HttpPost] [Route("ClosePosition")] public async Task> ClosePosition([FromBody] ClosePositionRequest request) { try { // Check if user owns the account if (!await UserOwnsBotAccount(request.Identifier)) { return Forbid("You don't have permission to close positions for this bot"); } var position = await _botService.ClosePositionAsync(request.Identifier, request.PositionId); if (position == null) { return NotFound( $"Position with ID {request.PositionId} not found for bot with identifier {request.Identifier}"); } return Ok(position); } catch (Exception ex) { _logger.LogError(ex, "Error closing position"); return StatusCode(500, $"Error closing position: {ex.Message}"); } } /// /// Retrieves the configuration of an existing bot. /// /// The identifier of the bot to get configuration for /// The bot configuration [HttpGet] [Route("GetConfig/{identifier}")] public async Task> GetBotConfig(Guid identifier) { try { // Check if user owns the account if (!await UserOwnsBotAccount(identifier)) { return Forbid("You don't have permission to view this bot's configuration"); } var config = await _botService.GetBotConfig(identifier); if (config == null) { return NotFound($"Bot with identifier {identifier} not found"); } return Ok(config); } catch (Exception ex) { _logger.LogError(ex, "Error getting bot configuration for identifier {Identifier}", identifier); return StatusCode(500, $"Error getting bot configuration: {ex.Message}"); } } /// /// Updates the configuration of an existing bot. /// /// The update request containing the bot identifier and new configuration /// Success message [HttpPut] [Route("UpdateConfig")] public async Task> UpdateBotConfig([FromBody] UpdateBotConfigRequest request) { try { var user = await GetUser(); if (user == null) { return Unauthorized("User not found"); } if (request.Identifier == Guid.Empty) { 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"); } // Get the existing bot to ensure it exists and get current config var existingBot = await _botService.GetBotByIdentifier(request.Identifier); if (existingBot == null) { return NotFound($"Bot with identifier '{request.Identifier}' not found"); } var config = await _botService.GetBotConfig(request.Identifier); // If the account is being changed, verify the user owns the new account too // TODO : Uncomment this for security // if (config.AccountName != request.Config.AccountName) // { // if (!await UserOwnsBotAccount(request.Identifier, request.Config.AccountName)) // { // return Forbid("You don't have permission to use this account"); // } // } // Validate and get the money management LightMoneyManagement moneyManagement = null; if (!string.IsNullOrEmpty(request.MoneyManagementName)) { // Load money management by name var fullMoneyManagement = await _moneyManagementService.GetMoneyMangement(user, request.MoneyManagementName); if (fullMoneyManagement == null) { return BadRequest($"Money management '{request.MoneyManagementName}' not found"); } if (fullMoneyManagement.User?.Name != user.Name) { return Forbid("You don't have permission to use this money management"); } // Convert to LightMoneyManagement moneyManagement = new LightMoneyManagement { Name = fullMoneyManagement.Name, Timeframe = fullMoneyManagement.Timeframe, StopLoss = fullMoneyManagement.StopLoss, TakeProfit = fullMoneyManagement.TakeProfit, Leverage = fullMoneyManagement.Leverage }; } else if (request.Config.MoneyManagement != null) { // Use provided money management object moneyManagement = new LightMoneyManagement { Name = request.Config.Name, Timeframe = request.Config.Timeframe, StopLoss = request.Config.MoneyManagement.StopLoss, TakeProfit = request.Config.MoneyManagement.TakeProfit, Leverage = request.Config.MoneyManagement.Leverage }; } // Validate CloseEarlyWhenProfitable requires MaxPositionTimeHours if (request.Config.CloseEarlyWhenProfitable && !request.Config.MaxPositionTimeHours.HasValue) { return BadRequest("CloseEarlyWhenProfitable requires MaxPositionTimeHours to be set"); } // Handle scenario - either from ScenarioRequest or ScenarioName Scenario scenarioForUpdate = null; if (request.Config.Scenario != null) { // Convert ScenarioRequest to Scenario domain object scenarioForUpdate = new Scenario(request.Config.Scenario.Name, request.Config.Scenario.LoopbackPeriod) { User = user }; // Convert IndicatorRequest objects to Indicator domain objects foreach (var indicatorRequest in request.Config.Scenario.Indicators) { var indicator = new IndicatorBase(indicatorRequest.Name, indicatorRequest.Type) { SignalType = indicatorRequest.SignalType, MinimumHistory = indicatorRequest.MinimumHistory, Period = indicatorRequest.Period, FastPeriods = indicatorRequest.FastPeriods, SlowPeriods = indicatorRequest.SlowPeriods, SignalPeriods = indicatorRequest.SignalPeriods, Multiplier = indicatorRequest.Multiplier, SmoothPeriods = indicatorRequest.SmoothPeriods, StochPeriods = indicatorRequest.StochPeriods, CyclePeriods = indicatorRequest.CyclePeriods, User = user }; scenarioForUpdate.AddIndicator(indicator); } } // Map the request to the full TradingBotConfig var updatedConfig = new TradingBotConfig { MoneyManagement = moneyManagement, Ticker = request.Config.Ticker, Scenario = LightScenario.FromScenario(scenarioForUpdate), // Convert to LightScenario for Orleans ScenarioName = request.Config.ScenarioName, // Fallback to scenario name if scenario object not provided Timeframe = request.Config.Timeframe, IsForWatchingOnly = request.Config.IsForWatchingOnly, BotTradingBalance = request.Config.BotTradingBalance, CooldownPeriod = request.Config.CooldownPeriod ?? 1, MaxLossStreak = request.Config.MaxLossStreak, MaxPositionTimeHours = request.Config.MaxPositionTimeHours, FlipOnlyWhenInProfit = request.Config.FlipOnlyWhenInProfit, CloseEarlyWhenProfitable = request.Config.CloseEarlyWhenProfitable, UseSynthApi = request.Config.UseSynthApi, UseForPositionSizing = request.Config.UseForPositionSizing, UseForSignalFiltering = request.Config.UseForSignalFiltering, UseForDynamicStopLoss = request.Config.UseForDynamicStopLoss, // Set computed/default properties TradingType = TradingType.Futures, FlipPosition = request.Config.FlipPosition, Name = request.Config.Name }; var success = await _botService.UpdateBotConfiguration(request.Identifier, updatedConfig); if (success) { return Ok("Bot configuration updated successfully"); } else { return BadRequest("Failed to update bot configuration"); } } catch (Exception ex) { _logger.LogError(ex, "Error updating bot configuration for identifier {Identifier}", request.Identifier); return StatusCode(500, $"Error updating bot configuration: {ex.Message}"); } } /// /// Validates and prepares the bot request by performing all necessary validations and mapping. /// /// The bot request to validate and prepare /// The operation being performed (start/save) for error messages /// The prepared TradingBotConfig and User /// Thrown when validation fails /// Thrown when user doesn't have permission private async Task<(TradingBotConfig, User)> ValidateAndPrepareBotRequest(StartBotRequest request, string operation) { if (request.Config == null) { throw new ArgumentException("Bot configuration is required"); } // Check if user owns the account specified in the request if (!await UserOwnsBotAccount(Guid.Empty, request.Config.AccountName)) { throw new UnauthorizedAccessException($"You don't have permission to {operation} a bot with this account"); } // Validate that either money management name or object is provided if (string.IsNullOrEmpty(request.Config.MoneyManagementName) && request.Config.MoneyManagement == null) { throw new ArgumentException("Either money management name or money management object is required"); } var cachedUser = await GetUser(); var user = await _userService.GetUserByName(cachedUser.Name); if (string.IsNullOrEmpty(user.AgentName)) { throw new ArgumentException( $"Agent name is required to {operation} a bot. Please configure your agent name in the user profile."); } // Get money management - either by name lookup or use provided object LightMoneyManagement moneyManagement; if (!string.IsNullOrEmpty(request.Config.MoneyManagementName)) { moneyManagement = await _moneyManagementService.GetMoneyMangement(user, request.Config.MoneyManagementName); if (moneyManagement == null) { throw new ArgumentException("Money management not found"); } } else { moneyManagement = Map(request.Config.MoneyManagement); // Format percentage values if using custom money management moneyManagement?.FormatPercentage(); } // Validate initialTradingBalance if (request.Config.BotTradingBalance <= Constants.GMX.Config.MinimumPositionAmount) { throw new ArgumentException( $"Initial trading balance must be greater than {Constants.GMX.Config.MinimumPositionAmount}"); } // Validate cooldown period if (request.Config.CooldownPeriod < 1) { throw new ArgumentException("Cooldown period must be at least 1 candle"); } // Validate max loss streak if (request.Config.MaxLossStreak < 0) { throw new ArgumentException("Max loss streak cannot be negative"); } // Validate max position time hours if (request.Config.MaxPositionTimeHours.HasValue && request.Config.MaxPositionTimeHours.Value <= 0) { throw new ArgumentException("Max position time hours must be greater than 0 if specified"); } // Validate CloseEarlyWhenProfitable consistency if (request.Config.CloseEarlyWhenProfitable && !request.Config.MaxPositionTimeHours.HasValue) { throw new ArgumentException( "CloseEarlyWhenProfitable can only be enabled when MaxPositionTimeHours is set"); } // Handle scenario - either from ScenarioRequest or ScenarioName Scenario scenario = null; if (request.Config.Scenario != null) { // Convert ScenarioRequest to Scenario domain object scenario = new Scenario(request.Config.Scenario.Name, request.Config.Scenario.LoopbackPeriod) { User = user }; // Convert IndicatorRequest objects to Indicator domain objects foreach (var indicatorRequest in request.Config.Scenario.Indicators) { var indicator = new IndicatorBase(indicatorRequest.Name, indicatorRequest.Type) { SignalType = indicatorRequest.SignalType, MinimumHistory = indicatorRequest.MinimumHistory, Period = indicatorRequest.Period, FastPeriods = indicatorRequest.FastPeriods, SlowPeriods = indicatorRequest.SlowPeriods, SignalPeriods = indicatorRequest.SignalPeriods, Multiplier = indicatorRequest.Multiplier, SmoothPeriods = indicatorRequest.SmoothPeriods, StochPeriods = indicatorRequest.StochPeriods, CyclePeriods = indicatorRequest.CyclePeriods, User = user }; scenario.AddIndicator(indicator); } } // Map the request to the full TradingBotConfig var config = new TradingBotConfig { AccountName = request.Config.AccountName, MoneyManagement = moneyManagement, Ticker = request.Config.Ticker, Scenario = LightScenario.FromScenario(scenario), // Convert to LightScenario for Orleans ScenarioName = request.Config.ScenarioName, // Fallback to scenario name if scenario object not provided Timeframe = request.Config.Timeframe, IsForWatchingOnly = request.Config.IsForWatchingOnly, BotTradingBalance = request.Config.BotTradingBalance, CooldownPeriod = request.Config.CooldownPeriod ?? 1, MaxLossStreak = request.Config.MaxLossStreak, MaxPositionTimeHours = request.Config.MaxPositionTimeHours, FlipOnlyWhenInProfit = request.Config.FlipOnlyWhenInProfit, CloseEarlyWhenProfitable = request.Config.CloseEarlyWhenProfitable, UseSynthApi = request.Config.UseSynthApi, UseForPositionSizing = request.Config.UseForPositionSizing, UseForSignalFiltering = request.Config.UseForSignalFiltering, UseForDynamicStopLoss = request.Config.UseForDynamicStopLoss, // Set computed/default properties TradingType = TradingType.Futures, FlipPosition = request.Config.FlipPosition, Name = request.Config.Name }; return (config, user); } public MoneyManagement Map(MoneyManagementRequest moneyManagementRequest) { return new MoneyManagement { Name = moneyManagementRequest.Name, StopLoss = moneyManagementRequest.StopLoss, TakeProfit = moneyManagementRequest.TakeProfit, Leverage = moneyManagementRequest.Leverage, Timeframe = moneyManagementRequest.Timeframe }; } } /// /// Request model for opening a position manually /// public class CreateManualSignalRequest { /// /// The identifier of the bot /// public Guid Identifier { get; set; } /// /// The direction of the position /// public TradeDirection Direction { get; set; } } /// /// Request model for closing a position /// public class ClosePositionRequest { /// /// The identifier of the bot /// public Guid Identifier { get; set; } /// /// The ID of the position to close /// public Guid PositionId { get; set; } } /// /// Request model for starting a bot /// public class StartBotRequest { /// /// The trading bot configuration request with primary properties /// public TradingBotConfigRequest Config { get; set; } } public class SaveBotRequest : StartBotRequest { } /// /// Request model for starting a copy trading bot /// public class StartCopyTradingRequest { /// /// The identifier of the master bot to copy trades from /// public Guid MasterBotIdentifier { get; set; } /// /// The trading balance for the copy trading bot /// public decimal BotTradingBalance { get; set; } }