Update config
This commit is contained in:
@@ -169,7 +169,8 @@ public class BacktestController : BaseController
|
|||||||
MaxPositionTimeHours = request.Config.MaxPositionTimeHours,
|
MaxPositionTimeHours = request.Config.MaxPositionTimeHours,
|
||||||
FlipOnlyWhenInProfit = request.Config.FlipOnlyWhenInProfit,
|
FlipOnlyWhenInProfit = request.Config.FlipOnlyWhenInProfit,
|
||||||
FlipPosition = request.Config.FlipPosition,
|
FlipPosition = request.Config.FlipPosition,
|
||||||
Name = request.Config.Name ?? $"Backtest-{request.Config.ScenarioName}-{DateTime.UtcNow:yyyyMMdd-HHmmss}"
|
Name = request.Config.Name ?? $"Backtest-{request.Config.ScenarioName}-{DateTime.UtcNow:yyyyMMdd-HHmmss}",
|
||||||
|
CloseEarlyWhenProfitable = request.Config.CloseEarlyWhenProfitable
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (request.Config.BotType)
|
switch (request.Config.BotType)
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using Managing.Application.Abstractions;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Managing.Api.Models.Responses;
|
||||||
|
using Managing.Application.Abstractions;
|
||||||
using Managing.Application.Abstractions.Services;
|
using Managing.Application.Abstractions.Services;
|
||||||
using Managing.Application.Hubs;
|
using Managing.Application.Hubs;
|
||||||
using Managing.Application.ManageBot.Commands;
|
using Managing.Application.ManageBot.Commands;
|
||||||
@@ -11,7 +13,6 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using static Managing.Common.Enums;
|
using static Managing.Common.Enums;
|
||||||
using ApiTradingBot = Managing.Api.Models.Responses.TradingBot;
|
|
||||||
|
|
||||||
namespace Managing.Api.Controllers;
|
namespace Managing.Api.Controllers;
|
||||||
|
|
||||||
@@ -142,6 +143,30 @@ public class BotController : BaseController
|
|||||||
$"Initial trading balance must be greater than {Constants.GMX.Config.MinimumPositionAmount}");
|
$"Initial trading balance must be greater than {Constants.GMX.Config.MinimumPositionAmount}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate cooldown period
|
||||||
|
if (request.Config.CooldownPeriod < 1)
|
||||||
|
{
|
||||||
|
return BadRequest("Cooldown period must be at least 1 candle");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate max loss streak
|
||||||
|
if (request.Config.MaxLossStreak < 0)
|
||||||
|
{
|
||||||
|
return BadRequest("Max loss streak cannot be negative");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate max position time hours
|
||||||
|
if (request.Config.MaxPositionTimeHours.HasValue && request.Config.MaxPositionTimeHours.Value <= 0)
|
||||||
|
{
|
||||||
|
return BadRequest("Max position time hours must be greater than 0 if specified");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate CloseEarlyWhenProfitable consistency
|
||||||
|
if (request.Config.CloseEarlyWhenProfitable && !request.Config.MaxPositionTimeHours.HasValue)
|
||||||
|
{
|
||||||
|
return BadRequest("CloseEarlyWhenProfitable can only be enabled when MaxPositionTimeHours is set");
|
||||||
|
}
|
||||||
|
|
||||||
// Update the config with final money management
|
// Update the config with final money management
|
||||||
var config = new TradingBotConfig
|
var config = new TradingBotConfig
|
||||||
{
|
{
|
||||||
@@ -159,7 +184,8 @@ public class BotController : BaseController
|
|||||||
FlipOnlyWhenInProfit = request.Config.FlipOnlyWhenInProfit,
|
FlipOnlyWhenInProfit = request.Config.FlipOnlyWhenInProfit,
|
||||||
IsForBacktest = false,
|
IsForBacktest = false,
|
||||||
FlipPosition = request.Config.BotType == BotType.FlippingBot,
|
FlipPosition = request.Config.BotType == BotType.FlippingBot,
|
||||||
Name = request.Config.Name
|
Name = request.Config.Name,
|
||||||
|
CloseEarlyWhenProfitable = request.Config.CloseEarlyWhenProfitable
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = await _mediator.Send(new StartBotCommand(config, request.Config.Name, user));
|
var result = await _mediator.Send(new StartBotCommand(config, request.Config.Name, user));
|
||||||
@@ -250,11 +276,11 @@ public class BotController : BaseController
|
|||||||
{
|
{
|
||||||
var bots = await GetBotList();
|
var bots = await GetBotList();
|
||||||
// Filter to only include bots owned by the current user
|
// Filter to only include bots owned by the current user
|
||||||
var userBots = new List<ApiTradingBot>();
|
var userBots = new List<TradingBotResponse>();
|
||||||
|
|
||||||
foreach (var bot in bots)
|
foreach (var bot in bots)
|
||||||
{
|
{
|
||||||
var account = await _accountService.GetAccount(bot.AccountName, true, false);
|
var account = await _accountService.GetAccount(bot.Config.AccountName, true, false);
|
||||||
// Compare the user names
|
// Compare the user names
|
||||||
if (account != null && account.User != null && account.User.Name == user.Name)
|
if (account != null && account.User != null && account.User.Name == user.Name)
|
||||||
{
|
{
|
||||||
@@ -264,7 +290,7 @@ public class BotController : BaseController
|
|||||||
|
|
||||||
foreach (var bot in userBots)
|
foreach (var bot in userBots)
|
||||||
{
|
{
|
||||||
await _mediator.Send(new StopBotCommand(bot.BotType, bot.Identifier));
|
await _mediator.Send(new StopBotCommand(bot.Config.BotType, bot.Identifier));
|
||||||
await _hubContext.Clients.All.SendAsync("SendNotification",
|
await _hubContext.Clients.All.SendAsync("SendNotification",
|
||||||
$"Bot {bot.Identifier} paused by {user.Name}.", "Info");
|
$"Bot {bot.Identifier} paused by {user.Name}.", "Info");
|
||||||
}
|
}
|
||||||
@@ -326,12 +352,12 @@ public class BotController : BaseController
|
|||||||
{
|
{
|
||||||
var bots = await GetBotList();
|
var bots = await GetBotList();
|
||||||
// Filter to only include bots owned by the current user
|
// Filter to only include bots owned by the current user
|
||||||
var userBots = new List<ApiTradingBot>();
|
var userBots = new List<TradingBotResponse>();
|
||||||
var accountService = HttpContext.RequestServices.GetRequiredService<IAccountService>();
|
var accountService = HttpContext.RequestServices.GetRequiredService<IAccountService>();
|
||||||
|
|
||||||
foreach (var bot in bots)
|
foreach (var bot in bots)
|
||||||
{
|
{
|
||||||
var account = await accountService.GetAccount(bot.AccountName, true, false);
|
var account = await accountService.GetAccount(bot.Config.AccountName, true, false);
|
||||||
// Compare the user names
|
// Compare the user names
|
||||||
if (account != null && account.User != null && account.User.Name == user.Name)
|
if (account != null && account.User != null && account.User.Name == user.Name)
|
||||||
{
|
{
|
||||||
@@ -343,7 +369,7 @@ public class BotController : BaseController
|
|||||||
{
|
{
|
||||||
// We can't directly restart a bot with just BotType and Name
|
// We can't directly restart a bot with just BotType and Name
|
||||||
// Instead, stop the bot and then retrieve the backup to start it again
|
// Instead, stop the bot and then retrieve the backup to start it again
|
||||||
await _mediator.Send(new StopBotCommand(bot.BotType, bot.Identifier));
|
await _mediator.Send(new StopBotCommand(bot.Config.BotType, bot.Identifier));
|
||||||
|
|
||||||
// Get the saved bot backup
|
// Get the saved bot backup
|
||||||
var backup = _botService.GetBotBackup(bot.Identifier);
|
var backup = _botService.GetBotBackup(bot.Identifier);
|
||||||
@@ -401,7 +427,7 @@ public class BotController : BaseController
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>A list of active trading bots.</returns>
|
/// <returns>A list of active trading bots.</returns>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<List<ApiTradingBot>> GetActiveBots()
|
public async Task<List<TradingBotResponse>> GetActiveBots()
|
||||||
{
|
{
|
||||||
return await GetBotList();
|
return await GetBotList();
|
||||||
}
|
}
|
||||||
@@ -410,33 +436,24 @@ public class BotController : BaseController
|
|||||||
/// Retrieves a list of active bots by sending a command to the mediator.
|
/// Retrieves a list of active bots by sending a command to the mediator.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>A list of trading bots.</returns>
|
/// <returns>A list of trading bots.</returns>
|
||||||
private async Task<List<ApiTradingBot>> GetBotList()
|
private async Task<List<TradingBotResponse>> GetBotList()
|
||||||
{
|
{
|
||||||
var result = await _mediator.Send(new GetActiveBotsCommand());
|
var result = await _mediator.Send(new GetActiveBotsCommand());
|
||||||
var list = new List<ApiTradingBot>();
|
var list = new List<TradingBotResponse>();
|
||||||
|
|
||||||
foreach (var item in result)
|
foreach (var item in result)
|
||||||
{
|
{
|
||||||
list.Add(new ApiTradingBot
|
list.Add(new TradingBotResponse
|
||||||
{
|
{
|
||||||
Status = item.GetStatus(),
|
Status = item.GetStatus(),
|
||||||
Name = item.Name,
|
|
||||||
Signals = item.Signals.ToList(),
|
Signals = item.Signals.ToList(),
|
||||||
Positions = item.Positions,
|
Positions = item.Positions,
|
||||||
Candles = item.Candles.DistinctBy(c => c.Date).ToList(),
|
Candles = item.Candles.DistinctBy(c => c.Date).ToList(),
|
||||||
WinRate = item.GetWinRate(),
|
WinRate = item.GetWinRate(),
|
||||||
ProfitAndLoss = item.GetProfitAndLoss(),
|
ProfitAndLoss = item.GetProfitAndLoss(),
|
||||||
Timeframe = item.Config.Timeframe,
|
|
||||||
Ticker = item.Config.Ticker,
|
|
||||||
Scenario = item.Config.ScenarioName,
|
|
||||||
IsForWatchingOnly = item.Config.IsForWatchingOnly,
|
|
||||||
BotType = item.Config.BotType,
|
|
||||||
AccountName = item.Config.AccountName,
|
|
||||||
MoneyManagement = item.Config.MoneyManagement,
|
|
||||||
Identifier = item.Identifier,
|
Identifier = item.Identifier,
|
||||||
AgentName = item.User.AgentName,
|
AgentName = item.User.AgentName,
|
||||||
MaxPositionTimeHours = item.Config.MaxPositionTimeHours,
|
Config = item.Config // Contains all configuration properties
|
||||||
FlipOnlyWhenInProfit = item.Config.FlipOnlyWhenInProfit
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -562,97 +579,107 @@ public class BotController : BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates the configuration of a running bot
|
/// Updates the configuration of an existing bot.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="request">The request containing the new bot configuration</param>
|
/// <param name="request">The update request containing the bot identifier and new configuration</param>
|
||||||
/// <returns>A response indicating the result of the operation</returns>
|
/// <returns>Success message</returns>
|
||||||
[HttpPut]
|
[HttpPut]
|
||||||
[Route("UpdateConfig")]
|
[Route("UpdateConfig")]
|
||||||
public async Task<ActionResult<string>> UpdateBotConfig([FromBody] UpdateBotConfigRequest request)
|
public async Task<ActionResult<string>> UpdateBotConfig([FromBody] UpdateBotConfigRequest request)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Check if user owns the account
|
var user = await GetUser();
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return Unauthorized("User not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(request.Identifier))
|
||||||
|
{
|
||||||
|
return BadRequest("Bot identifier is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.Config == null)
|
||||||
|
{
|
||||||
|
return BadRequest("Bot configuration is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, check if the user owns the existing bot
|
||||||
if (!await UserOwnsBotAccount(request.Identifier))
|
if (!await UserOwnsBotAccount(request.Identifier))
|
||||||
{
|
{
|
||||||
return Forbid("You don't have permission to update this bot's configuration");
|
return Forbid("You don't have permission to update this bot");
|
||||||
}
|
}
|
||||||
|
|
||||||
var activeBots = _botService.GetActiveBots();
|
// Get the existing bot to ensure it exists and get current config
|
||||||
var bot = activeBots.FirstOrDefault(b => b.Identifier == request.Identifier);
|
var bots = _botService.GetActiveBots();
|
||||||
|
var existingBot = bots.FirstOrDefault(b => b.Identifier == request.Identifier);
|
||||||
|
|
||||||
if (bot == null)
|
if (existingBot == null)
|
||||||
{
|
{
|
||||||
return NotFound($"Bot with identifier {request.Identifier} not found or is not running");
|
return NotFound($"Bot with identifier '{request.Identifier}' not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the user for validation
|
// If the account is being changed, verify the user owns the new account too
|
||||||
var user = await GetUser();
|
if (existingBot.Config.AccountName != request.Config.AccountName)
|
||||||
|
|
||||||
// Validate money management if provided
|
|
||||||
MoneyManagement moneyManagement = null;
|
|
||||||
if (!string.IsNullOrEmpty(request.MoneyManagementName))
|
|
||||||
{
|
{
|
||||||
moneyManagement = await _moneyManagementService.GetMoneyMangement(user, request.MoneyManagementName);
|
if (!await UserOwnsBotAccount(null, request.Config.AccountName))
|
||||||
|
{
|
||||||
|
return Forbid("You don't have permission to use this account");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the money management if provided
|
||||||
|
if (request.Config.MoneyManagement != null)
|
||||||
|
{
|
||||||
|
// Check if the money management belongs to the user
|
||||||
|
var userMoneyManagement = await _moneyManagementService.GetMoneyMangement(user, request.Config.MoneyManagement.Name);
|
||||||
|
if (userMoneyManagement != null && userMoneyManagement.User?.Name != user.Name)
|
||||||
|
{
|
||||||
|
return Forbid("You don't have permission to use this money management");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(request.MoneyManagementName))
|
||||||
|
{
|
||||||
|
// If MoneyManagement is null but MoneyManagementName is provided, load it
|
||||||
|
var moneyManagement = await _moneyManagementService.GetMoneyMangement(user, request.MoneyManagementName);
|
||||||
if (moneyManagement == null)
|
if (moneyManagement == null)
|
||||||
{
|
{
|
||||||
return BadRequest("Money management not found");
|
return BadRequest($"Money management '{request.MoneyManagementName}' not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (moneyManagement.User?.Name != user.Name)
|
||||||
|
{
|
||||||
|
return Forbid("You don't have permission to use this money management");
|
||||||
|
}
|
||||||
|
|
||||||
|
request.Config.MoneyManagement = moneyManagement;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate CloseEarlyWhenProfitable requires MaxPositionTimeHours
|
||||||
|
if (request.Config.CloseEarlyWhenProfitable && !request.Config.MaxPositionTimeHours.HasValue)
|
||||||
|
{
|
||||||
|
return BadRequest("CloseEarlyWhenProfitable requires MaxPositionTimeHours to be set");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the bot configuration using the new method
|
||||||
|
var success = await _botService.UpdateBotConfiguration(request.Identifier, request.Config);
|
||||||
|
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
await _hubContext.Clients.All.SendAsync("SendNotification",
|
||||||
|
$"Bot {request.Identifier} configuration updated successfully by {user.Name}.", "Info");
|
||||||
|
|
||||||
|
return Ok("Bot configuration updated successfully");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Keep existing money management if not provided
|
return BadRequest("Failed to update bot configuration");
|
||||||
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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error updating bot configuration");
|
_logger.LogError(ex, "Error updating bot configuration for identifier {Identifier}", request.Identifier);
|
||||||
return StatusCode(500, $"Error updating bot configuration: {ex.Message}");
|
return StatusCode(500, $"Error updating bot configuration: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -712,72 +739,19 @@ public class StartBotRequest
|
|||||||
public class UpdateBotConfigRequest
|
public class UpdateBotConfigRequest
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The identifier of the bot to update
|
/// The unique identifier of the bot to update
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Required]
|
||||||
public string Identifier { get; set; }
|
public string Identifier { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The account name to use (optional - will keep existing if not provided)
|
/// The new trading bot configuration
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? AccountName { get; set; }
|
[Required]
|
||||||
|
public TradingBotConfig Config { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The money management name to use (optional - will keep existing if not provided)
|
/// Optional: Money management name to load if Config.MoneyManagement is null
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? MoneyManagementName { get; set; }
|
public string? MoneyManagementName { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The ticker to trade (optional - will keep existing if not provided)
|
|
||||||
/// </summary>
|
|
||||||
public Ticker? Ticker { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The scenario to use (optional - will keep existing if not provided)
|
|
||||||
/// </summary>
|
|
||||||
public string? ScenarioName { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The timeframe to use (optional - will keep existing if not provided)
|
|
||||||
/// </summary>
|
|
||||||
public Timeframe? Timeframe { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether the bot is for watching only (optional - will keep existing if not provided)
|
|
||||||
/// </summary>
|
|
||||||
public bool? IsForWatchingOnly { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The bot trading balance (optional - will keep existing if not provided)
|
|
||||||
/// </summary>
|
|
||||||
public decimal? BotTradingBalance { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The cooldown period in candles between positions (optional - will keep existing if not provided)
|
|
||||||
/// </summary>
|
|
||||||
public int? CooldownPeriod { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The maximum number of consecutive losses before stopping (optional - will keep existing if not provided)
|
|
||||||
/// </summary>
|
|
||||||
public int? MaxLossStreak { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Maximum time in hours that a position can remain open before being automatically closed (optional - will keep existing if not provided)
|
|
||||||
/// </summary>
|
|
||||||
public decimal? MaxPositionTimeHours { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// If true, positions will only be flipped when the current position is in profit (optional - will keep existing if not provided)
|
|
||||||
/// </summary>
|
|
||||||
public bool? FlipOnlyWhenInProfit { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether position flipping is enabled (optional - will keep existing if not provided)
|
|
||||||
/// </summary>
|
|
||||||
public bool? FlipPosition { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The name of the bot (optional - will keep existing if not provided)
|
|
||||||
/// </summary>
|
|
||||||
public string? Name { get; set; }
|
|
||||||
}
|
}
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using Managing.Domain.Candles;
|
|
||||||
using Managing.Domain.MoneyManagements;
|
|
||||||
using Managing.Domain.Strategies;
|
|
||||||
using Managing.Domain.Trades;
|
|
||||||
using static Managing.Common.Enums;
|
|
||||||
|
|
||||||
namespace Managing.Api.Models.Responses
|
|
||||||
{
|
|
||||||
public class TradingBot
|
|
||||||
{
|
|
||||||
[Required] public string Name { get; internal set; }
|
|
||||||
[Required] public string Status { get; internal set; }
|
|
||||||
[Required] public List<Signal> Signals { get; internal set; }
|
|
||||||
[Required] public List<Position> Positions { get; internal set; }
|
|
||||||
[Required] public List<Candle> Candles { get; internal set; }
|
|
||||||
[Required] public int WinRate { get; internal set; }
|
|
||||||
[Required] public decimal ProfitAndLoss { get; internal set; }
|
|
||||||
[Required] public Timeframe Timeframe { get; internal set; }
|
|
||||||
[Required] public Ticker Ticker { get; internal set; }
|
|
||||||
[Required] public string Scenario { get; internal set; }
|
|
||||||
[Required] public bool IsForWatchingOnly { get; internal set; }
|
|
||||||
[Required] public BotType BotType { get; internal set; }
|
|
||||||
[Required] public string AccountName { get; internal set; }
|
|
||||||
[Required] public MoneyManagement MoneyManagement { get; internal set; }
|
|
||||||
[Required] public string Identifier { get; set; }
|
|
||||||
[Required] public string AgentName { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Maximum time in hours that a position can remain open before being automatically closed.
|
|
||||||
/// If null, time-based position closure is disabled.
|
|
||||||
/// </summary>
|
|
||||||
[Required] public decimal? MaxPositionTimeHours { get; internal set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// If true, positions will only be flipped when the current position is in profit.
|
|
||||||
/// </summary>
|
|
||||||
[Required] public bool FlipOnlyWhenInProfit { get; internal set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
56
src/Managing.Api/Models/Responses/TradingBotResponse.cs
Normal file
56
src/Managing.Api/Models/Responses/TradingBotResponse.cs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Managing.Domain.Bots;
|
||||||
|
using Managing.Domain.Candles;
|
||||||
|
using Managing.Domain.Strategies;
|
||||||
|
using Managing.Domain.Trades;
|
||||||
|
|
||||||
|
namespace Managing.Api.Models.Responses
|
||||||
|
{
|
||||||
|
public class TradingBotResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Current status of the bot (Up, Down, etc.)
|
||||||
|
/// </summary>
|
||||||
|
[Required] public string Status { get; internal set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// List of signals generated by the bot
|
||||||
|
/// </summary>
|
||||||
|
[Required] public List<Signal> Signals { get; internal set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// List of positions opened by the bot
|
||||||
|
/// </summary>
|
||||||
|
[Required] public List<Position> Positions { get; internal set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Candles used by the bot for analysis
|
||||||
|
/// </summary>
|
||||||
|
[Required] public List<Candle> Candles { get; internal set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current win rate percentage
|
||||||
|
/// </summary>
|
||||||
|
[Required] public int WinRate { get; internal set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current profit and loss
|
||||||
|
/// </summary>
|
||||||
|
[Required] public decimal ProfitAndLoss { get; internal set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unique identifier for the bot
|
||||||
|
/// </summary>
|
||||||
|
[Required] public string Identifier { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Agent name associated with the bot
|
||||||
|
/// </summary>
|
||||||
|
[Required] public string AgentName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The full trading bot configuration
|
||||||
|
/// </summary>
|
||||||
|
[Required] public TradingBotConfig Config { get; internal set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -73,7 +73,10 @@ namespace Managing.Application.Tests
|
|||||||
CooldownPeriod = 1,
|
CooldownPeriod = 1,
|
||||||
MaxLossStreak = 0,
|
MaxLossStreak = 0,
|
||||||
FlipPosition = true,
|
FlipPosition = true,
|
||||||
Name = "Test"
|
Name = "Test",
|
||||||
|
FlipOnlyWhenInProfit = true,
|
||||||
|
MaxPositionTimeHours = null,
|
||||||
|
CloseEarlyWhenProfitable = false
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -120,7 +123,10 @@ namespace Managing.Application.Tests
|
|||||||
CooldownPeriod = 1,
|
CooldownPeriod = 1,
|
||||||
MaxLossStreak = 0,
|
MaxLossStreak = 0,
|
||||||
FlipPosition = false,
|
FlipPosition = false,
|
||||||
Name = "Test"
|
Name = "Test",
|
||||||
|
FlipOnlyWhenInProfit = true,
|
||||||
|
MaxPositionTimeHours = null,
|
||||||
|
CloseEarlyWhenProfitable = false
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -166,7 +172,10 @@ namespace Managing.Application.Tests
|
|||||||
CooldownPeriod = 1,
|
CooldownPeriod = 1,
|
||||||
MaxLossStreak = 0,
|
MaxLossStreak = 0,
|
||||||
FlipPosition = false,
|
FlipPosition = false,
|
||||||
Name = "Test"
|
Name = "Test",
|
||||||
|
FlipOnlyWhenInProfit = true,
|
||||||
|
MaxPositionTimeHours = null,
|
||||||
|
CloseEarlyWhenProfitable = false
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -253,7 +262,10 @@ namespace Managing.Application.Tests
|
|||||||
CooldownPeriod = 1,
|
CooldownPeriod = 1,
|
||||||
MaxLossStreak = 0,
|
MaxLossStreak = 0,
|
||||||
FlipPosition = false,
|
FlipPosition = false,
|
||||||
Name = "Test"
|
Name = "Test",
|
||||||
|
FlipOnlyWhenInProfit = true,
|
||||||
|
MaxPositionTimeHours = null,
|
||||||
|
CloseEarlyWhenProfitable = false
|
||||||
}, candles, null).Result,
|
}, candles, null).Result,
|
||||||
BotType.FlippingBot => _backtester.RunFlippingBotBacktest(new TradingBotConfig
|
BotType.FlippingBot => _backtester.RunFlippingBotBacktest(new TradingBotConfig
|
||||||
{
|
{
|
||||||
@@ -269,7 +281,10 @@ namespace Managing.Application.Tests
|
|||||||
CooldownPeriod = 1,
|
CooldownPeriod = 1,
|
||||||
MaxLossStreak = 0,
|
MaxLossStreak = 0,
|
||||||
FlipPosition = true,
|
FlipPosition = true,
|
||||||
Name = "Test"
|
Name = "Test",
|
||||||
|
FlipOnlyWhenInProfit = true,
|
||||||
|
MaxPositionTimeHours = null,
|
||||||
|
CloseEarlyWhenProfitable = false
|
||||||
}, candles, null).Result,
|
}, candles, null).Result,
|
||||||
_ => throw new NotImplementedException(),
|
_ => throw new NotImplementedException(),
|
||||||
};
|
};
|
||||||
@@ -389,7 +404,10 @@ namespace Managing.Application.Tests
|
|||||||
CooldownPeriod = 1,
|
CooldownPeriod = 1,
|
||||||
MaxLossStreak = 0,
|
MaxLossStreak = 0,
|
||||||
FlipPosition = false,
|
FlipPosition = false,
|
||||||
Name = "Test"
|
Name = "Test",
|
||||||
|
FlipOnlyWhenInProfit = true,
|
||||||
|
MaxPositionTimeHours = null,
|
||||||
|
CloseEarlyWhenProfitable = false
|
||||||
}, candles, null).Result,
|
}, candles, null).Result,
|
||||||
BotType.FlippingBot => _backtester.RunFlippingBotBacktest(new TradingBotConfig
|
BotType.FlippingBot => _backtester.RunFlippingBotBacktest(new TradingBotConfig
|
||||||
{
|
{
|
||||||
@@ -405,7 +423,10 @@ namespace Managing.Application.Tests
|
|||||||
CooldownPeriod = 1,
|
CooldownPeriod = 1,
|
||||||
MaxLossStreak = 0,
|
MaxLossStreak = 0,
|
||||||
FlipPosition = true,
|
FlipPosition = true,
|
||||||
Name = "Test"
|
Name = "Test",
|
||||||
|
FlipOnlyWhenInProfit = true,
|
||||||
|
MaxPositionTimeHours = null,
|
||||||
|
CloseEarlyWhenProfitable = false
|
||||||
}, candles, null).Result,
|
}, candles, null).Result,
|
||||||
_ => throw new NotImplementedException(),
|
_ => throw new NotImplementedException(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -277,7 +277,10 @@ public class StatisticService : IStatisticService
|
|||||||
CooldownPeriod = 1,
|
CooldownPeriod = 1,
|
||||||
MaxLossStreak = 0,
|
MaxLossStreak = 0,
|
||||||
FlipPosition = false,
|
FlipPosition = false,
|
||||||
Name = "StatisticsBacktest"
|
Name = "StatisticsBacktest",
|
||||||
|
FlipOnlyWhenInProfit = true,
|
||||||
|
MaxPositionTimeHours = null,
|
||||||
|
CloseEarlyWhenProfitable = false
|
||||||
};
|
};
|
||||||
|
|
||||||
var backtest = await _backtester.RunScalpingBotBacktest(
|
var backtest = await _backtester.RunScalpingBotBacktest(
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
using Managing.Application.Bots;
|
|
||||||
using Managing.Domain.Bots;
|
using Managing.Domain.Bots;
|
||||||
using Managing.Domain.Users;
|
using Managing.Domain.Users;
|
||||||
using Managing.Domain.Workflows;
|
using Managing.Domain.Workflows;
|
||||||
@@ -26,4 +25,5 @@ public interface IBotService
|
|||||||
Task<bool> DeleteBot(string botName);
|
Task<bool> DeleteBot(string botName);
|
||||||
Task<string> RestartBot(string botName);
|
Task<string> RestartBot(string botName);
|
||||||
void ToggleIsForWatchingOnly(string botName);
|
void ToggleIsForWatchingOnly(string botName);
|
||||||
|
Task<bool> UpdateBotConfiguration(string identifier, TradingBotConfig newConfig);
|
||||||
}
|
}
|
||||||
@@ -666,7 +666,7 @@ public class TradingBot : Bot, ITradingBot
|
|||||||
$"Signal {signal.Identifier} will wait for position to become profitable before flipping.");
|
$"Signal {signal.Identifier} will wait for position to become profitable before flipping.");
|
||||||
|
|
||||||
// Keep signal in waiting status to check again on next execution
|
// Keep signal in waiting status to check again on next execution
|
||||||
SetSignalStatus(signal.Identifier, SignalStatus.WaitingForPosition);
|
SetSignalStatus(signal.Identifier, SignalStatus.Expired);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1180,6 +1180,7 @@ public class TradingBot : Bot, ITradingBot
|
|||||||
MaxLossStreak = Config.MaxLossStreak,
|
MaxLossStreak = Config.MaxLossStreak,
|
||||||
MaxPositionTimeHours = Config.MaxPositionTimeHours ?? 0m,
|
MaxPositionTimeHours = Config.MaxPositionTimeHours ?? 0m,
|
||||||
FlipOnlyWhenInProfit = Config.FlipOnlyWhenInProfit,
|
FlipOnlyWhenInProfit = Config.FlipOnlyWhenInProfit,
|
||||||
|
CloseEarlyWhenProfitable = Config.CloseEarlyWhenProfitable,
|
||||||
};
|
};
|
||||||
BotService.SaveOrUpdateBotBackup(User, Identifier, Config.BotType, Status, JsonConvert.SerializeObject(data));
|
BotService.SaveOrUpdateBotBackup(User, Identifier, Config.BotType, Status, JsonConvert.SerializeObject(data));
|
||||||
}
|
}
|
||||||
@@ -1202,6 +1203,7 @@ public class TradingBot : Bot, ITradingBot
|
|||||||
MaxLossStreak = data.MaxLossStreak,
|
MaxLossStreak = data.MaxLossStreak,
|
||||||
MaxPositionTimeHours = data.MaxPositionTimeHours == 0m ? null : data.MaxPositionTimeHours,
|
MaxPositionTimeHours = data.MaxPositionTimeHours == 0m ? null : data.MaxPositionTimeHours,
|
||||||
FlipOnlyWhenInProfit = data.FlipOnlyWhenInProfit,
|
FlipOnlyWhenInProfit = data.FlipOnlyWhenInProfit,
|
||||||
|
CloseEarlyWhenProfitable = data.CloseEarlyWhenProfitable,
|
||||||
Name = data.Name
|
Name = data.Name
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1429,7 +1431,8 @@ public class TradingBot : Bot, ITradingBot
|
|||||||
MaxPositionTimeHours = Config.MaxPositionTimeHours,
|
MaxPositionTimeHours = Config.MaxPositionTimeHours,
|
||||||
FlipOnlyWhenInProfit = Config.FlipOnlyWhenInProfit,
|
FlipOnlyWhenInProfit = Config.FlipOnlyWhenInProfit,
|
||||||
FlipPosition = Config.FlipPosition,
|
FlipPosition = Config.FlipPosition,
|
||||||
Name = Config.Name
|
Name = Config.Name,
|
||||||
|
CloseEarlyWhenProfitable = Config.CloseEarlyWhenProfitable
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1453,4 +1456,5 @@ public class TradingBotBackup
|
|||||||
public int MaxLossStreak { get; set; }
|
public int MaxLossStreak { get; set; }
|
||||||
public decimal MaxPositionTimeHours { get; set; }
|
public decimal MaxPositionTimeHours { get; set; }
|
||||||
public bool FlipOnlyWhenInProfit { get; set; }
|
public bool FlipOnlyWhenInProfit { get; set; }
|
||||||
|
public bool CloseEarlyWhenProfitable { get; set; }
|
||||||
}
|
}
|
||||||
@@ -142,7 +142,8 @@ namespace Managing.Application.ManageBot
|
|||||||
MaxPositionTimeHours = scalpingBotData.MaxPositionTimeHours == 0m ? null : scalpingBotData.MaxPositionTimeHours,
|
MaxPositionTimeHours = scalpingBotData.MaxPositionTimeHours == 0m ? null : scalpingBotData.MaxPositionTimeHours,
|
||||||
FlipOnlyWhenInProfit = scalpingBotData.FlipOnlyWhenInProfit,
|
FlipOnlyWhenInProfit = scalpingBotData.FlipOnlyWhenInProfit,
|
||||||
IsForBacktest = false,
|
IsForBacktest = false,
|
||||||
FlipPosition = false
|
FlipPosition = false,
|
||||||
|
CloseEarlyWhenProfitable = scalpingBotData.CloseEarlyWhenProfitable
|
||||||
};
|
};
|
||||||
|
|
||||||
bot = CreateScalpingBot(scalpingConfig);
|
bot = CreateScalpingBot(scalpingConfig);
|
||||||
@@ -171,7 +172,8 @@ namespace Managing.Application.ManageBot
|
|||||||
MaxPositionTimeHours = flippingBotData.MaxPositionTimeHours == 0m ? null : flippingBotData.MaxPositionTimeHours,
|
MaxPositionTimeHours = flippingBotData.MaxPositionTimeHours == 0m ? null : flippingBotData.MaxPositionTimeHours,
|
||||||
FlipOnlyWhenInProfit = flippingBotData.FlipOnlyWhenInProfit,
|
FlipOnlyWhenInProfit = flippingBotData.FlipOnlyWhenInProfit,
|
||||||
IsForBacktest = false,
|
IsForBacktest = false,
|
||||||
FlipPosition = true
|
FlipPosition = true,
|
||||||
|
CloseEarlyWhenProfitable = flippingBotData.CloseEarlyWhenProfitable
|
||||||
};
|
};
|
||||||
|
|
||||||
bot = CreateFlippingBot(flippingConfig);
|
bot = CreateFlippingBot(flippingConfig);
|
||||||
@@ -263,6 +265,23 @@ namespace Managing.Application.ManageBot
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the configuration of an existing bot without stopping and restarting it.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="identifier">The bot identifier</param>
|
||||||
|
/// <param name="newConfig">The new configuration to apply</param>
|
||||||
|
/// <returns>True if the configuration was successfully updated, false otherwise</returns>
|
||||||
|
public async Task<bool> UpdateBotConfiguration(string identifier, TradingBotConfig newConfig)
|
||||||
|
{
|
||||||
|
if (_botTasks.TryGetValue(identifier, out var botTaskWrapper) &&
|
||||||
|
botTaskWrapper.BotInstance is TradingBot tradingBot)
|
||||||
|
{
|
||||||
|
return await tradingBot.UpdateConfiguration(newConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public ITradingBot CreateScalpingBot(TradingBotConfig config)
|
public ITradingBot CreateScalpingBot(TradingBotConfig config)
|
||||||
{
|
{
|
||||||
return new ScalpingBot(
|
return new ScalpingBot(
|
||||||
|
|||||||
@@ -74,7 +74,8 @@ namespace Managing.Application.ManageBot
|
|||||||
MaxPositionTimeHours = request.Config.MaxPositionTimeHours, // Properly handle nullable value
|
MaxPositionTimeHours = request.Config.MaxPositionTimeHours, // Properly handle nullable value
|
||||||
FlipOnlyWhenInProfit = request.Config.FlipOnlyWhenInProfit,
|
FlipOnlyWhenInProfit = request.Config.FlipOnlyWhenInProfit,
|
||||||
FlipPosition = request.Config.FlipPosition,
|
FlipPosition = request.Config.FlipPosition,
|
||||||
Name = request.Config.Name ?? request.Name
|
Name = request.Config.Name ?? request.Name,
|
||||||
|
CloseEarlyWhenProfitable = request.Config.CloseEarlyWhenProfitable
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (configToUse.BotType)
|
switch (configToUse.BotType)
|
||||||
|
|||||||
@@ -0,0 +1,619 @@
|
|||||||
|
import React, {useEffect, useState} from 'react'
|
||||||
|
import {useQuery} from '@tanstack/react-query'
|
||||||
|
import useApiUrlStore from '../../../app/store/apiStore'
|
||||||
|
import {
|
||||||
|
AccountClient,
|
||||||
|
Backtest,
|
||||||
|
BotClient,
|
||||||
|
BotType,
|
||||||
|
MoneyManagement,
|
||||||
|
MoneyManagementClient,
|
||||||
|
ScenarioClient,
|
||||||
|
StartBotRequest,
|
||||||
|
Ticker,
|
||||||
|
Timeframe,
|
||||||
|
TradingBotConfig,
|
||||||
|
UpdateBotConfigRequest
|
||||||
|
} from '../../../generated/ManagingApi'
|
||||||
|
import Toast from '../Toast/Toast'
|
||||||
|
|
||||||
|
interface BotConfigModalProps {
|
||||||
|
showModal: boolean
|
||||||
|
onClose: () => void
|
||||||
|
backtest?: Backtest // When creating from backtest
|
||||||
|
existingBot?: {
|
||||||
|
identifier: string
|
||||||
|
config: TradingBotConfig
|
||||||
|
} // When updating existing bot
|
||||||
|
mode: 'create' | 'update' // Explicitly specify the mode
|
||||||
|
}
|
||||||
|
|
||||||
|
const BotConfigModal: React.FC<BotConfigModalProps> = ({
|
||||||
|
showModal,
|
||||||
|
onClose,
|
||||||
|
backtest,
|
||||||
|
existingBot,
|
||||||
|
mode
|
||||||
|
}) => {
|
||||||
|
const { apiUrl } = useApiUrlStore()
|
||||||
|
|
||||||
|
// Form state
|
||||||
|
const [formData, setFormData] = useState<{
|
||||||
|
name: string
|
||||||
|
accountName: string
|
||||||
|
moneyManagementName: string
|
||||||
|
ticker: Ticker
|
||||||
|
scenarioName: string
|
||||||
|
timeframe: Timeframe
|
||||||
|
isForWatchingOnly: boolean
|
||||||
|
botTradingBalance: number
|
||||||
|
botType: BotType
|
||||||
|
cooldownPeriod: number
|
||||||
|
maxLossStreak: number
|
||||||
|
maxPositionTimeHours: number | null
|
||||||
|
flipOnlyWhenInProfit: boolean
|
||||||
|
flipPosition: boolean
|
||||||
|
closeEarlyWhenProfitable: boolean
|
||||||
|
useCustomMoneyManagement: boolean
|
||||||
|
customStopLoss: number
|
||||||
|
customTakeProfit: number
|
||||||
|
customLeverage: number
|
||||||
|
}>({
|
||||||
|
name: '',
|
||||||
|
accountName: '',
|
||||||
|
moneyManagementName: '',
|
||||||
|
ticker: Ticker.BTC,
|
||||||
|
scenarioName: '',
|
||||||
|
timeframe: Timeframe.FifteenMinutes,
|
||||||
|
isForWatchingOnly: false,
|
||||||
|
botTradingBalance: 1000,
|
||||||
|
botType: BotType.ScalpingBot,
|
||||||
|
cooldownPeriod: 1,
|
||||||
|
maxLossStreak: 0,
|
||||||
|
maxPositionTimeHours: null,
|
||||||
|
flipOnlyWhenInProfit: true,
|
||||||
|
flipPosition: false,
|
||||||
|
closeEarlyWhenProfitable: false,
|
||||||
|
useCustomMoneyManagement: false,
|
||||||
|
customStopLoss: 0.01,
|
||||||
|
customTakeProfit: 0.02,
|
||||||
|
customLeverage: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
// Fetch data
|
||||||
|
const { data: accounts } = useQuery({
|
||||||
|
queryFn: async () => {
|
||||||
|
const accountClient = new AccountClient({}, apiUrl)
|
||||||
|
return await accountClient.account_GetAccounts()
|
||||||
|
},
|
||||||
|
queryKey: ['accounts']
|
||||||
|
})
|
||||||
|
|
||||||
|
const { data: moneyManagements } = useQuery({
|
||||||
|
queryFn: async () => {
|
||||||
|
const moneyManagementClient = new MoneyManagementClient({}, apiUrl)
|
||||||
|
return await moneyManagementClient.moneyManagement_GetMoneyManagements()
|
||||||
|
},
|
||||||
|
queryKey: ['moneyManagements']
|
||||||
|
})
|
||||||
|
|
||||||
|
const { data: scenarios } = useQuery({
|
||||||
|
queryFn: async () => {
|
||||||
|
const scenarioClient = new ScenarioClient({}, apiUrl)
|
||||||
|
return await scenarioClient.scenario_GetScenarios()
|
||||||
|
},
|
||||||
|
queryKey: ['scenarios']
|
||||||
|
})
|
||||||
|
|
||||||
|
// Initialize form data based on props
|
||||||
|
useEffect(() => {
|
||||||
|
if (mode === 'create' && backtest) {
|
||||||
|
// Initialize from backtest
|
||||||
|
setFormData({
|
||||||
|
name: `Bot-${backtest.config.scenarioName}-${new Date().toISOString().slice(0, 19).replace(/[-:]/g, '')}`,
|
||||||
|
accountName: backtest.config.accountName,
|
||||||
|
moneyManagementName: moneyManagements?.[0]?.name || '',
|
||||||
|
ticker: backtest.config.ticker,
|
||||||
|
scenarioName: backtest.config.scenarioName,
|
||||||
|
timeframe: backtest.config.timeframe,
|
||||||
|
isForWatchingOnly: false,
|
||||||
|
botTradingBalance: 1000,
|
||||||
|
botType: backtest.config.botType,
|
||||||
|
cooldownPeriod: backtest.config.cooldownPeriod,
|
||||||
|
maxLossStreak: backtest.config.maxLossStreak,
|
||||||
|
maxPositionTimeHours: backtest.config.maxPositionTimeHours ?? null,
|
||||||
|
flipOnlyWhenInProfit: backtest.config.flipOnlyWhenInProfit,
|
||||||
|
flipPosition: backtest.config.flipPosition,
|
||||||
|
closeEarlyWhenProfitable: backtest.config.closeEarlyWhenProfitable || false,
|
||||||
|
useCustomMoneyManagement: true, // Default to custom for backtests
|
||||||
|
customStopLoss: backtest.config.moneyManagement?.stopLoss || 0.01,
|
||||||
|
customTakeProfit: backtest.config.moneyManagement?.takeProfit || 0.02,
|
||||||
|
customLeverage: backtest.config.moneyManagement?.leverage || 1
|
||||||
|
})
|
||||||
|
} else if (mode === 'update' && existingBot) {
|
||||||
|
// Initialize from existing bot
|
||||||
|
setFormData({
|
||||||
|
name: existingBot.config.name,
|
||||||
|
accountName: existingBot.config.accountName,
|
||||||
|
moneyManagementName: existingBot.config.moneyManagement?.name || '',
|
||||||
|
ticker: existingBot.config.ticker,
|
||||||
|
scenarioName: existingBot.config.scenarioName,
|
||||||
|
timeframe: existingBot.config.timeframe,
|
||||||
|
isForWatchingOnly: existingBot.config.isForWatchingOnly,
|
||||||
|
botTradingBalance: existingBot.config.botTradingBalance,
|
||||||
|
botType: existingBot.config.botType,
|
||||||
|
cooldownPeriod: existingBot.config.cooldownPeriod,
|
||||||
|
maxLossStreak: existingBot.config.maxLossStreak,
|
||||||
|
maxPositionTimeHours: existingBot.config.maxPositionTimeHours ?? null,
|
||||||
|
flipOnlyWhenInProfit: existingBot.config.flipOnlyWhenInProfit,
|
||||||
|
flipPosition: existingBot.config.flipPosition,
|
||||||
|
closeEarlyWhenProfitable: existingBot.config.closeEarlyWhenProfitable || false,
|
||||||
|
useCustomMoneyManagement: false,
|
||||||
|
customStopLoss: existingBot.config.moneyManagement?.stopLoss || 0.01,
|
||||||
|
customTakeProfit: existingBot.config.moneyManagement?.takeProfit || 0.02,
|
||||||
|
customLeverage: existingBot.config.moneyManagement?.leverage || 1
|
||||||
|
})
|
||||||
|
} else if (mode === 'create' && !backtest) {
|
||||||
|
// Initialize for new bot creation
|
||||||
|
setFormData({
|
||||||
|
name: `Bot-${new Date().toISOString().slice(0, 19).replace(/[-:]/g, '')}`,
|
||||||
|
accountName: accounts?.[0]?.name || '',
|
||||||
|
moneyManagementName: moneyManagements?.[0]?.name || '',
|
||||||
|
ticker: Ticker.BTC,
|
||||||
|
scenarioName: scenarios?.[0]?.name || '',
|
||||||
|
timeframe: Timeframe.FifteenMinutes,
|
||||||
|
isForWatchingOnly: false,
|
||||||
|
botTradingBalance: 1000,
|
||||||
|
botType: BotType.ScalpingBot,
|
||||||
|
cooldownPeriod: 1,
|
||||||
|
maxLossStreak: 0,
|
||||||
|
maxPositionTimeHours: null,
|
||||||
|
flipOnlyWhenInProfit: true,
|
||||||
|
flipPosition: false,
|
||||||
|
closeEarlyWhenProfitable: false,
|
||||||
|
useCustomMoneyManagement: false,
|
||||||
|
customStopLoss: 0.01,
|
||||||
|
customTakeProfit: 0.02,
|
||||||
|
customLeverage: 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [mode, backtest, existingBot, accounts, moneyManagements, scenarios])
|
||||||
|
|
||||||
|
// Set default money management when data loads
|
||||||
|
useEffect(() => {
|
||||||
|
if (moneyManagements && moneyManagements.length > 0 && !formData.moneyManagementName) {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
moneyManagementName: moneyManagements[0].name
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}, [moneyManagements])
|
||||||
|
|
||||||
|
// Set default account when data loads
|
||||||
|
useEffect(() => {
|
||||||
|
if (accounts && accounts.length > 0 && !formData.accountName) {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
accountName: accounts[0].name
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}, [accounts])
|
||||||
|
|
||||||
|
// Set default scenario when data loads
|
||||||
|
useEffect(() => {
|
||||||
|
if (scenarios && scenarios.length > 0 && !formData.scenarioName) {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
scenarioName: scenarios[0].name || ''
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}, [scenarios])
|
||||||
|
|
||||||
|
const handleInputChange = (field: string, value: any) => {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[field]: value
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
const t = new Toast(mode === 'create' ? 'Creating bot...' : 'Updating bot...')
|
||||||
|
const client = new BotClient({}, apiUrl)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create the money management object
|
||||||
|
let moneyManagement: MoneyManagement | undefined = undefined
|
||||||
|
|
||||||
|
if (formData.useCustomMoneyManagement || (mode === 'create' && backtest)) {
|
||||||
|
// Use custom money management
|
||||||
|
moneyManagement = {
|
||||||
|
name: 'custom',
|
||||||
|
leverage: formData.customLeverage,
|
||||||
|
stopLoss: formData.customStopLoss,
|
||||||
|
takeProfit: formData.customTakeProfit,
|
||||||
|
timeframe: formData.timeframe
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Use saved money management - load the complete object
|
||||||
|
const selectedMoneyManagement = moneyManagements?.find(mm => mm.name === formData.moneyManagementName)
|
||||||
|
if (selectedMoneyManagement) {
|
||||||
|
moneyManagement = selectedMoneyManagement
|
||||||
|
} else {
|
||||||
|
t.update('error', 'Selected money management not found')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!moneyManagement) {
|
||||||
|
t.update('error', 'Money management is required')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create TradingBotConfig (reused for both create and update)
|
||||||
|
const tradingBotConfig: TradingBotConfig = {
|
||||||
|
accountName: formData.accountName,
|
||||||
|
ticker: formData.ticker,
|
||||||
|
scenarioName: formData.scenarioName,
|
||||||
|
timeframe: formData.timeframe,
|
||||||
|
botType: formData.botType,
|
||||||
|
isForWatchingOnly: formData.isForWatchingOnly,
|
||||||
|
isForBacktest: false,
|
||||||
|
cooldownPeriod: formData.cooldownPeriod,
|
||||||
|
maxLossStreak: formData.maxLossStreak,
|
||||||
|
maxPositionTimeHours: formData.maxPositionTimeHours,
|
||||||
|
flipOnlyWhenInProfit: formData.flipOnlyWhenInProfit,
|
||||||
|
flipPosition: formData.flipPosition,
|
||||||
|
name: formData.name,
|
||||||
|
botTradingBalance: formData.botTradingBalance,
|
||||||
|
moneyManagement: moneyManagement,
|
||||||
|
closeEarlyWhenProfitable: formData.closeEarlyWhenProfitable
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === 'create') {
|
||||||
|
// Create new bot
|
||||||
|
const request: StartBotRequest = {
|
||||||
|
config: tradingBotConfig,
|
||||||
|
moneyManagementName: formData.useCustomMoneyManagement ? undefined : formData.moneyManagementName
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.bot_Start(request)
|
||||||
|
t.update('success', 'Bot created successfully!')
|
||||||
|
} else {
|
||||||
|
// Update existing bot
|
||||||
|
const request: UpdateBotConfigRequest = {
|
||||||
|
identifier: existingBot!.identifier,
|
||||||
|
config: tradingBotConfig,
|
||||||
|
moneyManagementName: formData.useCustomMoneyManagement ? undefined : formData.moneyManagementName
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.bot_UpdateBotConfig(request)
|
||||||
|
t.update('success', 'Bot updated successfully!')
|
||||||
|
}
|
||||||
|
|
||||||
|
onClose()
|
||||||
|
} catch (error: any) {
|
||||||
|
t.update('error', `Error: ${error.message || error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!showModal) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="modal modal-open">
|
||||||
|
<div className="modal-box w-11/12 max-w-4xl">
|
||||||
|
<h3 className="font-bold text-lg mb-4">
|
||||||
|
{mode === 'create' ? 'Create Bot' : 'Update Bot Configuration'}
|
||||||
|
{backtest && ` from Backtest`}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{/* Basic Configuration */}
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Bot Name</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="input input-bordered"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => handleInputChange('name', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Account</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className="select select-bordered"
|
||||||
|
value={formData.accountName}
|
||||||
|
onChange={(e) => handleInputChange('accountName', e.target.value)}
|
||||||
|
>
|
||||||
|
{accounts?.map((account) => (
|
||||||
|
<option key={account.name} value={account.name}>
|
||||||
|
{account.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Ticker</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className="select select-bordered"
|
||||||
|
value={formData.ticker}
|
||||||
|
onChange={(e) => handleInputChange('ticker', e.target.value as Ticker)}
|
||||||
|
>
|
||||||
|
{Object.values(Ticker).map((ticker) => (
|
||||||
|
<option key={ticker} value={ticker}>
|
||||||
|
{ticker}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Scenario</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className="select select-bordered"
|
||||||
|
value={formData.scenarioName}
|
||||||
|
onChange={(e) => handleInputChange('scenarioName', e.target.value)}
|
||||||
|
>
|
||||||
|
{scenarios?.map((scenario) => (
|
||||||
|
<option key={scenario.name || ''} value={scenario.name || ''}>
|
||||||
|
{scenario.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Timeframe</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className="select select-bordered"
|
||||||
|
value={formData.timeframe}
|
||||||
|
onChange={(e) => handleInputChange('timeframe', e.target.value as Timeframe)}
|
||||||
|
>
|
||||||
|
{Object.values(Timeframe).map((timeframe) => (
|
||||||
|
<option key={timeframe} value={timeframe}>
|
||||||
|
{timeframe}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Bot Type</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className="select select-bordered"
|
||||||
|
value={formData.botType}
|
||||||
|
onChange={(e) => handleInputChange('botType', e.target.value as BotType)}
|
||||||
|
disabled={mode === 'update'} // Can't change bot type for existing bots
|
||||||
|
>
|
||||||
|
{Object.values(BotType).map((botType) => (
|
||||||
|
<option key={botType} value={botType}>
|
||||||
|
{botType}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Trading Balance</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="input input-bordered"
|
||||||
|
value={formData.botTradingBalance}
|
||||||
|
onChange={(e) => handleInputChange('botTradingBalance', parseFloat(e.target.value))}
|
||||||
|
min="1"
|
||||||
|
step="0.01"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Cooldown Period (candles)</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="input input-bordered"
|
||||||
|
value={formData.cooldownPeriod}
|
||||||
|
onChange={(e) => handleInputChange('cooldownPeriod', parseInt(e.target.value))}
|
||||||
|
min="1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Max Loss Streak</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="input input-bordered"
|
||||||
|
value={formData.maxLossStreak}
|
||||||
|
onChange={(e) => handleInputChange('maxLossStreak', parseInt(e.target.value))}
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Max Position Time (hours)</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="input input-bordered"
|
||||||
|
value={formData.maxPositionTimeHours || ''}
|
||||||
|
onChange={(e) => handleInputChange('maxPositionTimeHours', e.target.value ? parseFloat(e.target.value) : null)}
|
||||||
|
min="0.1"
|
||||||
|
step="0.1"
|
||||||
|
placeholder="Optional"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Checkboxes */}
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label cursor-pointer">
|
||||||
|
<span className="label-text">Watch Only Mode</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="checkbox"
|
||||||
|
checked={formData.isForWatchingOnly}
|
||||||
|
onChange={(e) => handleInputChange('isForWatchingOnly', e.target.checked)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label cursor-pointer">
|
||||||
|
<span className="label-text">Flip Only When In Profit</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="checkbox"
|
||||||
|
checked={formData.flipOnlyWhenInProfit}
|
||||||
|
onChange={(e) => handleInputChange('flipOnlyWhenInProfit', e.target.checked)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label cursor-pointer">
|
||||||
|
<span className="label-text">Enable Position Flipping</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="checkbox"
|
||||||
|
checked={formData.flipPosition}
|
||||||
|
onChange={(e) => handleInputChange('flipPosition', e.target.checked)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label cursor-pointer">
|
||||||
|
<span className="label-text">Close Early When Profitable</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="checkbox"
|
||||||
|
checked={formData.closeEarlyWhenProfitable}
|
||||||
|
onChange={(e) => handleInputChange('closeEarlyWhenProfitable', e.target.checked)}
|
||||||
|
disabled={!formData.maxPositionTimeHours}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Money Management Section */}
|
||||||
|
<div className="divider">Money Management</div>
|
||||||
|
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label cursor-pointer">
|
||||||
|
<span className="label-text">Use Custom Money Management</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="checkbox"
|
||||||
|
checked={formData.useCustomMoneyManagement}
|
||||||
|
onChange={(e) => handleInputChange('useCustomMoneyManagement', e.target.checked)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{formData.useCustomMoneyManagement ? (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-4">
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Stop Loss (%)</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="input input-bordered"
|
||||||
|
value={formData.customStopLoss}
|
||||||
|
onChange={(e) => handleInputChange('customStopLoss', parseFloat(e.target.value))}
|
||||||
|
min="0.001"
|
||||||
|
max="1"
|
||||||
|
step="0.001"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Take Profit (%)</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="input input-bordered"
|
||||||
|
value={formData.customTakeProfit}
|
||||||
|
onChange={(e) => handleInputChange('customTakeProfit', parseFloat(e.target.value))}
|
||||||
|
min="0.001"
|
||||||
|
max="1"
|
||||||
|
step="0.001"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Leverage</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="input input-bordered"
|
||||||
|
value={formData.customLeverage}
|
||||||
|
onChange={(e) => handleInputChange('customLeverage', parseInt(e.target.value))}
|
||||||
|
min="1"
|
||||||
|
max="100"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="form-control mt-4">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Money Management</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className="select select-bordered"
|
||||||
|
value={formData.moneyManagementName}
|
||||||
|
onChange={(e) => handleInputChange('moneyManagementName', e.target.value)}
|
||||||
|
>
|
||||||
|
{moneyManagements?.map((mm) => (
|
||||||
|
<option key={mm.name} value={mm.name}>
|
||||||
|
{mm.name} (SL: {(mm.stopLoss * 100).toFixed(2)}%, TP: {(mm.takeProfit * 100).toFixed(2)}%)
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Validation Messages */}
|
||||||
|
{formData.closeEarlyWhenProfitable && !formData.maxPositionTimeHours && (
|
||||||
|
<div className="alert alert-warning mt-4">
|
||||||
|
<span>Close Early When Profitable requires Max Position Time to be set.</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="modal-action">
|
||||||
|
<button className="btn" onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={formData.closeEarlyWhenProfitable && !formData.maxPositionTimeHours}
|
||||||
|
>
|
||||||
|
{mode === 'create' ? 'Create Bot' : 'Update Bot'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BotConfigModal
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import {ChevronDownIcon, ChevronRightIcon, EyeIcon, PlayIcon, TrashIcon} from '@heroicons/react/solid'
|
import {ChevronDownIcon, ChevronRightIcon, PlayIcon, TrashIcon} from '@heroicons/react/solid'
|
||||||
import React, {useEffect, useState} from 'react'
|
import React, {useEffect, useState} from 'react'
|
||||||
import {useQuery} from '@tanstack/react-query'
|
|
||||||
|
|
||||||
import useApiUrlStore from '../../../app/store/apiStore'
|
import useApiUrlStore from '../../../app/store/apiStore'
|
||||||
import type {Backtest, StartBotRequest, Ticker, TradingBotConfig} from '../../../generated/ManagingApi'
|
import type {Backtest} from '../../../generated/ManagingApi'
|
||||||
import {BacktestClient, BotClient, MoneyManagementClient} from '../../../generated/ManagingApi'
|
import {BacktestClient} from '../../../generated/ManagingApi'
|
||||||
import type {IBacktestCards} from '../../../global/type'
|
import type {IBacktestCards} from '../../../global/type'
|
||||||
import {CardText, SelectColumnFilter, Table, Toast} from '../../mollecules'
|
import {CardText, SelectColumnFilter, Table} from '../../mollecules'
|
||||||
import {BotNameModal} from '../index'
|
import BotConfigModal from '../../mollecules/BotConfigModal/BotConfigModal'
|
||||||
|
import Toast from '../../mollecules/Toast/Toast'
|
||||||
|
|
||||||
import BacktestRowDetails from './backtestRowDetails'
|
import BacktestRowDetails from './backtestRowDetails'
|
||||||
|
|
||||||
@@ -27,95 +27,19 @@ const BacktestTable: React.FC<IBacktestCards> = ({ list, isFetching, setBacktest
|
|||||||
averageCooldown: 0,
|
averageCooldown: 0,
|
||||||
medianCooldown: 0,
|
medianCooldown: 0,
|
||||||
})
|
})
|
||||||
const [showBotNameModal, setShowBotNameModal] = useState(false)
|
|
||||||
const [isForWatchOnly, setIsForWatchOnly] = useState(false)
|
|
||||||
const [currentBacktest, setCurrentBacktest] = useState<Backtest | null>(null)
|
|
||||||
const [selectedMoneyManagement, setSelectedMoneyManagement] = useState<string>('')
|
|
||||||
|
|
||||||
// Fetch money managements
|
// Bot configuration modal state
|
||||||
const { data: moneyManagements } = useQuery({
|
const [showBotConfigModal, setShowBotConfigModal] = useState(false)
|
||||||
queryFn: async () => {
|
const [selectedBacktest, setSelectedBacktest] = useState<Backtest | null>(null)
|
||||||
const moneyManagementClient = new MoneyManagementClient({}, apiUrl)
|
|
||||||
return await moneyManagementClient.moneyManagement_GetMoneyManagements()
|
|
||||||
},
|
|
||||||
queryKey: ['moneyManagements'],
|
|
||||||
})
|
|
||||||
|
|
||||||
// Set the first money management as default when the data is loaded
|
const handleOpenBotConfigModal = (backtest: Backtest) => {
|
||||||
useEffect(() => {
|
setSelectedBacktest(backtest)
|
||||||
if (moneyManagements && moneyManagements.length > 0) {
|
setShowBotConfigModal(true)
|
||||||
setSelectedMoneyManagement(moneyManagements[0].name)
|
|
||||||
}
|
|
||||||
}, [moneyManagements])
|
|
||||||
|
|
||||||
async function runBot(botName: string, backtest: Backtest, isForWatchOnly: boolean, moneyManagementName: string, initialTradingBalance: number) {
|
|
||||||
const t = new Toast('Bot is starting')
|
|
||||||
const client = new BotClient({}, apiUrl)
|
|
||||||
|
|
||||||
// Check if the money management name is "custom" or contains "custom"
|
|
||||||
const isCustomMoneyManagement =
|
|
||||||
!moneyManagementName ||
|
|
||||||
moneyManagementName.toLowerCase() === 'custom' ||
|
|
||||||
moneyManagementName.toLowerCase().includes('custom');
|
|
||||||
|
|
||||||
// Create TradingBotConfig from the backtest configuration
|
|
||||||
const tradingBotConfig: TradingBotConfig = {
|
|
||||||
accountName: backtest.config.accountName,
|
|
||||||
ticker: backtest.config.ticker,
|
|
||||||
scenarioName: backtest.config.scenarioName,
|
|
||||||
timeframe: backtest.config.timeframe,
|
|
||||||
botType: backtest.config.botType,
|
|
||||||
isForWatchingOnly: isForWatchOnly,
|
|
||||||
isForBacktest: false, // This is for running a live bot
|
|
||||||
cooldownPeriod: backtest.config.cooldownPeriod,
|
|
||||||
maxLossStreak: backtest.config.maxLossStreak,
|
|
||||||
maxPositionTimeHours: backtest.config.maxPositionTimeHours,
|
|
||||||
flipOnlyWhenInProfit: backtest.config.flipOnlyWhenInProfit,
|
|
||||||
flipPosition: backtest.config.flipPosition,
|
|
||||||
name: botName,
|
|
||||||
botTradingBalance: initialTradingBalance,
|
|
||||||
// Use the money management from backtest if it's custom, otherwise leave null and use moneyManagementName
|
|
||||||
moneyManagement: isCustomMoneyManagement ?
|
|
||||||
(backtest.config.moneyManagement || {
|
|
||||||
name: 'default',
|
|
||||||
leverage: 1,
|
|
||||||
stopLoss: 0.01,
|
|
||||||
takeProfit: 0.02,
|
|
||||||
timeframe: backtest.config.timeframe
|
|
||||||
}) :
|
|
||||||
backtest.config.moneyManagement, // Always provide a valid MoneyManagement object
|
|
||||||
closeEarlyWhenProfitable: backtest.config.closeEarlyWhenProfitable || false
|
|
||||||
};
|
|
||||||
|
|
||||||
const request: StartBotRequest = {
|
|
||||||
config: tradingBotConfig,
|
|
||||||
// Only use the money management name if it's not a custom money management
|
|
||||||
moneyManagementName: isCustomMoneyManagement ? undefined : moneyManagementName
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await client
|
const handleCloseBotConfigModal = () => {
|
||||||
.bot_Start(request)
|
setShowBotConfigModal(false)
|
||||||
.then((botStatus: string) => {
|
setSelectedBacktest(null)
|
||||||
t.update('info', 'Bot status: ' + botStatus)
|
|
||||||
})
|
|
||||||
.catch((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 deleteBacktest(id: string) {
|
async function deleteBacktest(id: string) {
|
||||||
@@ -292,27 +216,10 @@ const BacktestTable: React.FC<IBacktestCards> = ({ list, isFetching, setBacktest
|
|||||||
{
|
{
|
||||||
Cell: ({ cell }: any) => (
|
Cell: ({ cell }: any) => (
|
||||||
<>
|
<>
|
||||||
<div className="tooltip" data-tip="Run in watch-only mode">
|
<div className="tooltip" data-tip="Create bot from backtest">
|
||||||
<button
|
<button
|
||||||
data-value={cell.row.values.name}
|
data-value={cell.row.values.name}
|
||||||
onClick={() => handleOpenBotNameModal(cell.row.original as Backtest, true)}
|
onClick={() => handleOpenBotConfigModal(cell.row.original as Backtest)}
|
||||||
>
|
|
||||||
<EyeIcon className="text-primary w-4"></EyeIcon>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
Header: '',
|
|
||||||
accessor: 'watcher',
|
|
||||||
disableFilters: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Cell: ({ cell }: any) => (
|
|
||||||
<>
|
|
||||||
<div className="tooltip" data-tip="Run bot">
|
|
||||||
<button
|
|
||||||
data-value={cell.row.values.name}
|
|
||||||
onClick={() => handleOpenBotNameModal(cell.row.original as Backtest, false)}
|
|
||||||
>
|
>
|
||||||
<PlayIcon className="text-primary w-4"></PlayIcon>
|
<PlayIcon className="text-primary w-4"></PlayIcon>
|
||||||
</button>
|
</button>
|
||||||
@@ -475,8 +382,6 @@ const BacktestTable: React.FC<IBacktestCards> = ({ list, isFetching, setBacktest
|
|||||||
}
|
}
|
||||||
}, [list])
|
}, [list])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isFetching ? (
|
{isFetching ? (
|
||||||
@@ -528,18 +433,14 @@ const BacktestTable: React.FC<IBacktestCards> = ({ list, isFetching, setBacktest
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{showBotNameModal && currentBacktest && moneyManagements && (
|
|
||||||
<BotNameModal
|
{/* Bot Configuration Modal */}
|
||||||
showModal={showBotNameModal}
|
{selectedBacktest && (
|
||||||
onClose={handleCloseBotNameModal}
|
<BotConfigModal
|
||||||
backtest={currentBacktest}
|
showModal={showBotConfigModal}
|
||||||
isForWatchOnly={isForWatchOnly}
|
mode="create"
|
||||||
onSubmitBotName={(botName, backtest, isForWatchOnly, moneyManagementName, initialTradingBalance) =>
|
backtest={selectedBacktest}
|
||||||
handleSubmitBotName(botName, backtest, isForWatchOnly, moneyManagementName, initialTradingBalance)
|
onClose={handleCloseBotConfigModal}
|
||||||
}
|
|
||||||
moneyManagements={moneyManagements}
|
|
||||||
selectedMoneyManagement={selectedMoneyManagement}
|
|
||||||
setSelectedMoneyManagement={setSelectedMoneyManagement}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -653,7 +653,7 @@ export class BotClient extends AuthorizedApiBase {
|
|||||||
return Promise.resolve<string>(null as any);
|
return Promise.resolve<string>(null as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
bot_GetActiveBots(): Promise<TradingBot[]> {
|
bot_GetActiveBots(): Promise<TradingBotResponse[]> {
|
||||||
let url_ = this.baseUrl + "/Bot";
|
let url_ = this.baseUrl + "/Bot";
|
||||||
url_ = url_.replace(/[?&]$/, "");
|
url_ = url_.replace(/[?&]$/, "");
|
||||||
|
|
||||||
@@ -671,13 +671,13 @@ export class BotClient extends AuthorizedApiBase {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected processBot_GetActiveBots(response: Response): Promise<TradingBot[]> {
|
protected processBot_GetActiveBots(response: Response): Promise<TradingBotResponse[]> {
|
||||||
const status = response.status;
|
const status = response.status;
|
||||||
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||||
if (status === 200) {
|
if (status === 200) {
|
||||||
return response.text().then((_responseText) => {
|
return response.text().then((_responseText) => {
|
||||||
let result200: any = null;
|
let result200: any = null;
|
||||||
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as TradingBot[];
|
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as TradingBotResponse[];
|
||||||
return result200;
|
return result200;
|
||||||
});
|
});
|
||||||
} else if (status !== 200 && status !== 204) {
|
} else if (status !== 200 && status !== 204) {
|
||||||
@@ -685,7 +685,7 @@ export class BotClient extends AuthorizedApiBase {
|
|||||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return Promise.resolve<TradingBot[]>(null as any);
|
return Promise.resolve<TradingBotResponse[]>(null as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
bot_OpenPositionManually(request: OpenPositionManuallyRequest): Promise<Position> {
|
bot_OpenPositionManually(request: OpenPositionManuallyRequest): Promise<Position> {
|
||||||
@@ -3118,25 +3118,16 @@ export interface StartBotRequest {
|
|||||||
moneyManagementName?: string | null;
|
moneyManagementName?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TradingBot {
|
export interface TradingBotResponse {
|
||||||
name: string;
|
|
||||||
status: string;
|
status: string;
|
||||||
signals: Signal[];
|
signals: Signal[];
|
||||||
positions: Position[];
|
positions: Position[];
|
||||||
candles: Candle[];
|
candles: Candle[];
|
||||||
winRate: number;
|
winRate: number;
|
||||||
profitAndLoss: number;
|
profitAndLoss: number;
|
||||||
timeframe: Timeframe;
|
|
||||||
ticker: Ticker;
|
|
||||||
scenario: string;
|
|
||||||
isForWatchingOnly: boolean;
|
|
||||||
botType: BotType;
|
|
||||||
accountName: string;
|
|
||||||
moneyManagement: MoneyManagement;
|
|
||||||
identifier: string;
|
identifier: string;
|
||||||
agentName: string;
|
agentName: string;
|
||||||
maxPositionTimeHours: number;
|
config: TradingBotConfig;
|
||||||
flipOnlyWhenInProfit: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OpenPositionManuallyRequest {
|
export interface OpenPositionManuallyRequest {
|
||||||
@@ -3150,20 +3141,9 @@ export interface ClosePositionRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateBotConfigRequest {
|
export interface UpdateBotConfigRequest {
|
||||||
identifier?: string | null;
|
identifier: string;
|
||||||
accountName?: string | null;
|
config: TradingBotConfig;
|
||||||
moneyManagementName?: 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 {
|
export interface TickerInfos {
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import {ChartBarIcon, EyeIcon, PlayIcon, PlusCircleIcon, StopIcon, TrashIcon} from '@heroicons/react/solid'
|
import {ChartBarIcon, CogIcon, EyeIcon, PlayIcon, PlusCircleIcon, StopIcon, TrashIcon} from '@heroicons/react/solid'
|
||||||
import React, {useState} from 'react'
|
import React, {useState} from 'react'
|
||||||
|
|
||||||
import useApiUrlStore from '../../app/store/apiStore'
|
import useApiUrlStore from '../../app/store/apiStore'
|
||||||
import {CardPosition, CardSignal, CardText, Toast,} from '../../components/mollecules'
|
import {CardPosition, CardSignal, CardText, Toast,} from '../../components/mollecules'
|
||||||
import ManualPositionModal from '../../components/mollecules/ManualPositionModal'
|
import ManualPositionModal from '../../components/mollecules/ManualPositionModal'
|
||||||
import TradesModal from '../../components/mollecules/TradesModal/TradesModal'
|
import TradesModal from '../../components/mollecules/TradesModal/TradesModal'
|
||||||
|
import BotConfigModal from '../../components/mollecules/BotConfigModal/BotConfigModal'
|
||||||
import {TradeChart} from '../../components/organism'
|
import {TradeChart} from '../../components/organism'
|
||||||
import type {BotType, MoneyManagement, TradingBot,} from '../../generated/ManagingApi'
|
import type {BotType, MoneyManagement, Position, TradingBotResponse} from '../../generated/ManagingApi'
|
||||||
import {BotClient} from '../../generated/ManagingApi'
|
import {BotClient} from '../../generated/ManagingApi'
|
||||||
import type {IBotList} from '../../global/type'
|
import type {IBotList} from '../../global/type'
|
||||||
import MoneyManagementModal from '../settingsPage/moneymanagement/moneyManagementModal'
|
import MoneyManagementModal from '../settingsPage/moneymanagement/moneyManagementModal'
|
||||||
@@ -38,6 +39,12 @@ const BotList: React.FC<IBotList> = ({ list }) => {
|
|||||||
const [selectedBotForManualPosition, setSelectedBotForManualPosition] = useState<string | null>(null)
|
const [selectedBotForManualPosition, setSelectedBotForManualPosition] = useState<string | null>(null)
|
||||||
const [showTradesModal, setShowTradesModal] = useState(false)
|
const [showTradesModal, setShowTradesModal] = useState(false)
|
||||||
const [selectedBotForTrades, setSelectedBotForTrades] = useState<{ identifier: string; agentName: string } | null>(null)
|
const [selectedBotForTrades, setSelectedBotForTrades] = useState<{ identifier: string; agentName: string } | null>(null)
|
||||||
|
const [showBotConfigModal, setShowBotConfigModal] = useState(false)
|
||||||
|
const [botConfigModalMode, setBotConfigModalMode] = useState<'create' | 'update'>('create')
|
||||||
|
const [selectedBotForUpdate, setSelectedBotForUpdate] = useState<{
|
||||||
|
identifier: string
|
||||||
|
config: any
|
||||||
|
} | null>(null)
|
||||||
|
|
||||||
function getIsForWatchingBadge(isForWatchingOnly: boolean, identifier: string) {
|
function getIsForWatchingBadge(isForWatchingOnly: boolean, identifier: string) {
|
||||||
const classes =
|
const classes =
|
||||||
@@ -188,9 +195,53 @@ const BotList: React.FC<IBotList> = ({ list }) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getUpdateBotBadge(bot: TradingBotResponse) {
|
||||||
|
const classes = baseBadgeClass() + ' bg-warning'
|
||||||
|
return (
|
||||||
|
<button className={classes} onClick={() => openUpdateBotModal(bot)}>
|
||||||
|
<p className="text-primary-content flex">
|
||||||
|
<CogIcon width={15}></CogIcon>
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCreateBotBadge() {
|
||||||
|
const classes = baseBadgeClass() + ' bg-success'
|
||||||
|
return (
|
||||||
|
<button className={classes} onClick={() => openCreateBotModal()}>
|
||||||
|
<p className="text-primary-content flex">
|
||||||
|
<PlusCircleIcon width={15}></PlusCircleIcon>
|
||||||
|
Create Bot
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreateBotModal() {
|
||||||
|
setBotConfigModalMode('create')
|
||||||
|
setSelectedBotForUpdate(null)
|
||||||
|
setShowBotConfigModal(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function openUpdateBotModal(bot: TradingBotResponse) {
|
||||||
|
setBotConfigModalMode('update')
|
||||||
|
setSelectedBotForUpdate({
|
||||||
|
identifier: bot.identifier,
|
||||||
|
config: bot.config
|
||||||
|
})
|
||||||
|
setShowBotConfigModal(true)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap m-4 -mx-4">
|
<div className="flex flex-wrap m-4 -mx-4">
|
||||||
{list.map((bot: TradingBot, index) => (
|
<div className="w-full p-2 mb-4">
|
||||||
|
<div className="flex justify-end">
|
||||||
|
{getCreateBotBadge()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{list.map((bot: TradingBotResponse, index) => (
|
||||||
<div
|
<div
|
||||||
key={index.toString()}
|
key={index.toString()}
|
||||||
className="sm:w-1 md:w-1/2 xl:w-1/2 w-full p-2"
|
className="sm:w-1 md:w-1/2 xl:w-1/2 w-full p-2"
|
||||||
@@ -209,10 +260,11 @@ const BotList: React.FC<IBotList> = ({ list }) => {
|
|||||||
</figure>
|
</figure>
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<h2 className="card-title text-sm">
|
<h2 className="card-title text-sm">
|
||||||
{bot.ticker}
|
{bot.config.ticker}
|
||||||
{getMoneyManagementBadge(bot.moneyManagement)}
|
{getMoneyManagementBadge(bot.config.moneyManagement)}
|
||||||
{getIsForWatchingBadge(bot.isForWatchingOnly, bot.identifier)}
|
{getIsForWatchingBadge(bot.config.isForWatchingOnly, bot.identifier)}
|
||||||
{getToggleBotStatusBadge(bot.status, bot.identifier, bot.botType)}
|
{getToggleBotStatusBadge(bot.status, bot.identifier, bot.config.botType)}
|
||||||
|
{getUpdateBotBadge(bot)}
|
||||||
{getManualPositionBadge(bot.identifier)}
|
{getManualPositionBadge(bot.identifier)}
|
||||||
{getDeleteBadge(bot.identifier)}
|
{getDeleteBadge(bot.identifier)}
|
||||||
</h2>
|
</h2>
|
||||||
@@ -223,26 +275,26 @@ const BotList: React.FC<IBotList> = ({ list }) => {
|
|||||||
<div>
|
<div>
|
||||||
<CardText
|
<CardText
|
||||||
title="Scenario"
|
title="Scenario"
|
||||||
content={bot.scenario}
|
content={bot.config.scenarioName}
|
||||||
></CardText>
|
></CardText>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="columns-2">
|
<div className="columns-2">
|
||||||
<CardSignal signals={bot.signals}></CardSignal>
|
<CardSignal signals={bot.signals}></CardSignal>
|
||||||
<CardText title="Type" content={bot.botType}></CardText>
|
<CardText title="Type" content={bot.config.botType}></CardText>
|
||||||
</div>
|
</div>
|
||||||
<div className="columns-2">
|
<div className="columns-2">
|
||||||
<CardPosition
|
<CardPosition
|
||||||
positivePosition={true}
|
positivePosition={true}
|
||||||
positions={bot.positions.filter((p) => {
|
positions={bot.positions.filter((p: Position) => {
|
||||||
const realized = p.profitAndLoss?.realized ?? 0
|
const realized = p.profitAndLoss?.realized ?? 0
|
||||||
return realized > 0 ? p : null
|
return realized > 0 ? p : null
|
||||||
})}
|
})}
|
||||||
></CardPosition>
|
></CardPosition>
|
||||||
<CardPosition
|
<CardPosition
|
||||||
positivePosition={false}
|
positivePosition={false}
|
||||||
positions={bot.positions.filter((p) => {
|
positions={bot.positions.filter((p: Position) => {
|
||||||
const realized = p.profitAndLoss?.realized ?? 0
|
const realized = p.profitAndLoss?.realized ?? 0
|
||||||
return realized <= 0 ? p : null
|
return realized <= 0 ? p : null
|
||||||
})}
|
})}
|
||||||
@@ -287,6 +339,15 @@ const BotList: React.FC<IBotList> = ({ list }) => {
|
|||||||
setSelectedBotForTrades(null)
|
setSelectedBotForTrades(null)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<BotConfigModal
|
||||||
|
showModal={showBotConfigModal}
|
||||||
|
mode={botConfigModalMode}
|
||||||
|
existingBot={selectedBotForUpdate || undefined}
|
||||||
|
onClose={() => {
|
||||||
|
setShowBotConfigModal(false)
|
||||||
|
setSelectedBotForUpdate(null)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user