Update config

This commit is contained in:
2025-06-04 23:15:50 +07:00
parent 756cd5fb11
commit 973a8c7c61
14 changed files with 969 additions and 369 deletions

View File

@@ -169,7 +169,8 @@ public class BacktestController : BaseController
MaxPositionTimeHours = request.Config.MaxPositionTimeHours,
FlipOnlyWhenInProfit = request.Config.FlipOnlyWhenInProfit,
FlipPosition = request.Config.FlipPosition,
Name = request.Config.Name ?? $"Backtest-{request.Config.ScenarioName}-{DateTime.UtcNow:yyyyMMdd-HHmmss}"
Name = request.Config.Name ?? $"Backtest-{request.Config.ScenarioName}-{DateTime.UtcNow:yyyyMMdd-HHmmss}",
CloseEarlyWhenProfitable = request.Config.CloseEarlyWhenProfitable
};
switch (request.Config.BotType)

View File

@@ -1,4 +1,6 @@
using Managing.Application.Abstractions;
using System.ComponentModel.DataAnnotations;
using Managing.Api.Models.Responses;
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Services;
using Managing.Application.Hubs;
using Managing.Application.ManageBot.Commands;
@@ -11,7 +13,6 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using static Managing.Common.Enums;
using ApiTradingBot = Managing.Api.Models.Responses.TradingBot;
namespace Managing.Api.Controllers;
@@ -142,6 +143,30 @@ public class BotController : BaseController
$"Initial trading balance must be greater than {Constants.GMX.Config.MinimumPositionAmount}");
}
// Validate cooldown period
if (request.Config.CooldownPeriod < 1)
{
return BadRequest("Cooldown period must be at least 1 candle");
}
// Validate max loss streak
if (request.Config.MaxLossStreak < 0)
{
return BadRequest("Max loss streak cannot be negative");
}
// Validate max position time hours
if (request.Config.MaxPositionTimeHours.HasValue && request.Config.MaxPositionTimeHours.Value <= 0)
{
return BadRequest("Max position time hours must be greater than 0 if specified");
}
// Validate CloseEarlyWhenProfitable consistency
if (request.Config.CloseEarlyWhenProfitable && !request.Config.MaxPositionTimeHours.HasValue)
{
return BadRequest("CloseEarlyWhenProfitable can only be enabled when MaxPositionTimeHours is set");
}
// Update the config with final money management
var config = new TradingBotConfig
{
@@ -159,7 +184,8 @@ public class BotController : BaseController
FlipOnlyWhenInProfit = request.Config.FlipOnlyWhenInProfit,
IsForBacktest = false,
FlipPosition = request.Config.BotType == BotType.FlippingBot,
Name = request.Config.Name
Name = request.Config.Name,
CloseEarlyWhenProfitable = request.Config.CloseEarlyWhenProfitable
};
var result = await _mediator.Send(new StartBotCommand(config, request.Config.Name, user));
@@ -250,11 +276,11 @@ public class BotController : BaseController
{
var bots = await GetBotList();
// Filter to only include bots owned by the current user
var userBots = new List<ApiTradingBot>();
var userBots = new List<TradingBotResponse>();
foreach (var bot in bots)
{
var account = await _accountService.GetAccount(bot.AccountName, true, false);
var account = await _accountService.GetAccount(bot.Config.AccountName, true, false);
// Compare the user names
if (account != null && account.User != null && account.User.Name == user.Name)
{
@@ -264,7 +290,7 @@ public class BotController : BaseController
foreach (var bot in userBots)
{
await _mediator.Send(new StopBotCommand(bot.BotType, bot.Identifier));
await _mediator.Send(new StopBotCommand(bot.Config.BotType, bot.Identifier));
await _hubContext.Clients.All.SendAsync("SendNotification",
$"Bot {bot.Identifier} paused by {user.Name}.", "Info");
}
@@ -326,12 +352,12 @@ public class BotController : BaseController
{
var bots = await GetBotList();
// Filter to only include bots owned by the current user
var userBots = new List<ApiTradingBot>();
var userBots = new List<TradingBotResponse>();
var accountService = HttpContext.RequestServices.GetRequiredService<IAccountService>();
foreach (var bot in bots)
{
var account = await accountService.GetAccount(bot.AccountName, true, false);
var account = await accountService.GetAccount(bot.Config.AccountName, true, false);
// Compare the user names
if (account != null && account.User != null && account.User.Name == user.Name)
{
@@ -343,7 +369,7 @@ public class BotController : BaseController
{
// We can't directly restart a bot with just BotType and Name
// Instead, stop the bot and then retrieve the backup to start it again
await _mediator.Send(new StopBotCommand(bot.BotType, bot.Identifier));
await _mediator.Send(new StopBotCommand(bot.Config.BotType, bot.Identifier));
// Get the saved bot backup
var backup = _botService.GetBotBackup(bot.Identifier);
@@ -401,7 +427,7 @@ public class BotController : BaseController
/// </summary>
/// <returns>A list of active trading bots.</returns>
[HttpGet]
public async Task<List<ApiTradingBot>> GetActiveBots()
public async Task<List<TradingBotResponse>> GetActiveBots()
{
return await GetBotList();
}
@@ -410,33 +436,24 @@ public class BotController : BaseController
/// Retrieves a list of active bots by sending a command to the mediator.
/// </summary>
/// <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 list = new List<ApiTradingBot>();
var list = new List<TradingBotResponse>();
foreach (var item in result)
{
list.Add(new ApiTradingBot
list.Add(new TradingBotResponse
{
Status = item.GetStatus(),
Name = item.Name,
Signals = item.Signals.ToList(),
Positions = item.Positions,
Candles = item.Candles.DistinctBy(c => c.Date).ToList(),
WinRate = item.GetWinRate(),
ProfitAndLoss = item.GetProfitAndLoss(),
Timeframe = item.Config.Timeframe,
Ticker = item.Config.Ticker,
Scenario = item.Config.ScenarioName,
IsForWatchingOnly = item.Config.IsForWatchingOnly,
BotType = item.Config.BotType,
AccountName = item.Config.AccountName,
MoneyManagement = item.Config.MoneyManagement,
Identifier = item.Identifier,
AgentName = item.User.AgentName,
MaxPositionTimeHours = item.Config.MaxPositionTimeHours,
FlipOnlyWhenInProfit = item.Config.FlipOnlyWhenInProfit
Config = item.Config // Contains all configuration properties
});
}
@@ -562,97 +579,107 @@ public class BotController : BaseController
}
/// <summary>
/// Updates the configuration of a running bot
/// Updates the configuration of an existing bot.
/// </summary>
/// <param name="request">The request containing the new bot configuration</param>
/// <returns>A response indicating the result of the operation</returns>
/// <param name="request">The update request containing the bot identifier and new configuration</param>
/// <returns>Success message</returns>
[HttpPut]
[Route("UpdateConfig")]
public async Task<ActionResult<string>> UpdateBotConfig([FromBody] UpdateBotConfigRequest request)
{
try
{
// Check if user owns the account
var user = await GetUser();
if (user == null)
{
return Unauthorized("User not found");
}
if (string.IsNullOrEmpty(request.Identifier))
{
return BadRequest("Bot identifier is required");
}
if (request.Config == null)
{
return BadRequest("Bot configuration is required");
}
// First, check if the user owns the existing bot
if (!await UserOwnsBotAccount(request.Identifier))
{
return Forbid("You don't have permission to update this bot's configuration");
return Forbid("You don't have permission to update this bot");
}
var activeBots = _botService.GetActiveBots();
var bot = activeBots.FirstOrDefault(b => b.Identifier == request.Identifier);
if (bot == null)
{
return NotFound($"Bot with identifier {request.Identifier} not found or is not running");
}
// Get the user for validation
var user = await GetUser();
// Get the existing bot to ensure it exists and get current config
var bots = _botService.GetActiveBots();
var existingBot = bots.FirstOrDefault(b => b.Identifier == request.Identifier);
// Validate money management if provided
MoneyManagement moneyManagement = null;
if (!string.IsNullOrEmpty(request.MoneyManagementName))
if (existingBot == null)
{
moneyManagement = await _moneyManagementService.GetMoneyMangement(user, request.MoneyManagementName);
return NotFound($"Bot with identifier '{request.Identifier}' not found");
}
// If the account is being changed, verify the user owns the new account too
if (existingBot.Config.AccountName != request.Config.AccountName)
{
if (!await UserOwnsBotAccount(null, request.Config.AccountName))
{
return Forbid("You don't have permission to use this account");
}
}
// Validate the money management if provided
if (request.Config.MoneyManagement != null)
{
// Check if the money management belongs to the user
var userMoneyManagement = await _moneyManagementService.GetMoneyMangement(user, request.Config.MoneyManagement.Name);
if (userMoneyManagement != null && userMoneyManagement.User?.Name != user.Name)
{
return Forbid("You don't have permission to use this money management");
}
}
else if (!string.IsNullOrEmpty(request.MoneyManagementName))
{
// If MoneyManagement is null but MoneyManagementName is provided, load it
var moneyManagement = await _moneyManagementService.GetMoneyMangement(user, request.MoneyManagementName);
if (moneyManagement == null)
{
return BadRequest("Money management not found");
return BadRequest($"Money management '{request.MoneyManagementName}' not found");
}
if (moneyManagement.User?.Name != user.Name)
{
return Forbid("You don't have permission to use this money management");
}
request.Config.MoneyManagement = moneyManagement;
}
// Validate CloseEarlyWhenProfitable requires MaxPositionTimeHours
if (request.Config.CloseEarlyWhenProfitable && !request.Config.MaxPositionTimeHours.HasValue)
{
return BadRequest("CloseEarlyWhenProfitable requires MaxPositionTimeHours to be set");
}
// Update the bot configuration using the new method
var success = await _botService.UpdateBotConfiguration(request.Identifier, request.Config);
if (success)
{
await _hubContext.Clients.All.SendAsync("SendNotification",
$"Bot {request.Identifier} configuration updated successfully by {user.Name}.", "Info");
return Ok("Bot configuration updated successfully");
}
else
{
// Keep existing money management if not provided
moneyManagement = bot.Config.MoneyManagement;
return BadRequest("Failed to update bot configuration");
}
// Validate account if provided
if (!string.IsNullOrEmpty(request.AccountName))
{
var account = await _accountService.GetAccount(request.AccountName, true, false);
if (account == null || account.User?.Name != user.Name)
{
return BadRequest("Account not found or you don't have permission to use this account");
}
}
// Create updated configuration
var updatedConfig = new TradingBotConfig
{
AccountName = !string.IsNullOrEmpty(request.AccountName) ? request.AccountName : bot.Config.AccountName,
MoneyManagement = moneyManagement,
Ticker = request.Ticker ?? bot.Config.Ticker,
ScenarioName = !string.IsNullOrEmpty(request.ScenarioName) ? request.ScenarioName : bot.Config.ScenarioName,
Timeframe = request.Timeframe ?? bot.Config.Timeframe,
IsForWatchingOnly = request.IsForWatchingOnly ?? bot.Config.IsForWatchingOnly,
BotTradingBalance = request.BotTradingBalance ?? bot.Config.BotTradingBalance,
BotType = bot.Config.BotType, // Bot type cannot be changed
CooldownPeriod = request.CooldownPeriod ?? bot.Config.CooldownPeriod,
MaxLossStreak = request.MaxLossStreak ?? bot.Config.MaxLossStreak,
MaxPositionTimeHours = request.MaxPositionTimeHours ?? bot.Config.MaxPositionTimeHours,
FlipOnlyWhenInProfit = request.FlipOnlyWhenInProfit ?? bot.Config.FlipOnlyWhenInProfit,
IsForBacktest = bot.Config.IsForBacktest, // Cannot be changed for running bots
FlipPosition = request.FlipPosition ?? bot.Config.FlipPosition,
Name = !string.IsNullOrEmpty(request.Name) ? request.Name : bot.Config.Name
};
// Validate the updated configuration
if (updatedConfig.BotTradingBalance <= Constants.GMX.Config.MinimumPositionAmount)
{
return BadRequest($"Bot trading balance must be greater than {Constants.GMX.Config.MinimumPositionAmount}");
}
// Update the bot's configuration
var updateCommand = new UpdateBotConfigCommand(request.Identifier, updatedConfig);
var result = await _mediator.Send(updateCommand);
_logger.LogInformation($"Bot configuration update result for {request.Identifier} by user {user.Name}: {result}");
await NotifyBotSubscriberAsync();
return Ok(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error updating bot configuration");
_logger.LogError(ex, "Error updating bot configuration for identifier {Identifier}", request.Identifier);
return StatusCode(500, $"Error updating bot configuration: {ex.Message}");
}
}
@@ -712,72 +739,19 @@ public class StartBotRequest
public class UpdateBotConfigRequest
{
/// <summary>
/// The identifier of the bot to update
/// The unique identifier of the bot to update
/// </summary>
[Required]
public string Identifier { get; set; }
/// <summary>
/// The account name to use (optional - will keep existing if not provided)
/// The new trading bot configuration
/// </summary>
public string? AccountName { get; set; }
[Required]
public TradingBotConfig Config { get; set; }
/// <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>
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; }
}

View File

@@ -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; }
}
}

View 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; }
}
}

View File

@@ -73,7 +73,10 @@ namespace Managing.Application.Tests
CooldownPeriod = 1,
MaxLossStreak = 0,
FlipPosition = true,
Name = "Test"
Name = "Test",
FlipOnlyWhenInProfit = true,
MaxPositionTimeHours = null,
CloseEarlyWhenProfitable = false
};
// Act
@@ -120,7 +123,10 @@ namespace Managing.Application.Tests
CooldownPeriod = 1,
MaxLossStreak = 0,
FlipPosition = false,
Name = "Test"
Name = "Test",
FlipOnlyWhenInProfit = true,
MaxPositionTimeHours = null,
CloseEarlyWhenProfitable = false
};
// Act
@@ -166,7 +172,10 @@ namespace Managing.Application.Tests
CooldownPeriod = 1,
MaxLossStreak = 0,
FlipPosition = false,
Name = "Test"
Name = "Test",
FlipOnlyWhenInProfit = true,
MaxPositionTimeHours = null,
CloseEarlyWhenProfitable = false
};
// Act
@@ -253,7 +262,10 @@ namespace Managing.Application.Tests
CooldownPeriod = 1,
MaxLossStreak = 0,
FlipPosition = false,
Name = "Test"
Name = "Test",
FlipOnlyWhenInProfit = true,
MaxPositionTimeHours = null,
CloseEarlyWhenProfitable = false
}, candles, null).Result,
BotType.FlippingBot => _backtester.RunFlippingBotBacktest(new TradingBotConfig
{
@@ -269,7 +281,10 @@ namespace Managing.Application.Tests
CooldownPeriod = 1,
MaxLossStreak = 0,
FlipPosition = true,
Name = "Test"
Name = "Test",
FlipOnlyWhenInProfit = true,
MaxPositionTimeHours = null,
CloseEarlyWhenProfitable = false
}, candles, null).Result,
_ => throw new NotImplementedException(),
};
@@ -389,7 +404,10 @@ namespace Managing.Application.Tests
CooldownPeriod = 1,
MaxLossStreak = 0,
FlipPosition = false,
Name = "Test"
Name = "Test",
FlipOnlyWhenInProfit = true,
MaxPositionTimeHours = null,
CloseEarlyWhenProfitable = false
}, candles, null).Result,
BotType.FlippingBot => _backtester.RunFlippingBotBacktest(new TradingBotConfig
{
@@ -405,7 +423,10 @@ namespace Managing.Application.Tests
CooldownPeriod = 1,
MaxLossStreak = 0,
FlipPosition = true,
Name = "Test"
Name = "Test",
FlipOnlyWhenInProfit = true,
MaxPositionTimeHours = null,
CloseEarlyWhenProfitable = false
}, candles, null).Result,
_ => throw new NotImplementedException(),
};

View File

@@ -277,7 +277,10 @@ public class StatisticService : IStatisticService
CooldownPeriod = 1,
MaxLossStreak = 0,
FlipPosition = false,
Name = "StatisticsBacktest"
Name = "StatisticsBacktest",
FlipOnlyWhenInProfit = true,
MaxPositionTimeHours = null,
CloseEarlyWhenProfitable = false
};
var backtest = await _backtester.RunScalpingBotBacktest(

View File

@@ -1,4 +1,3 @@
using Managing.Application.Bots;
using Managing.Domain.Bots;
using Managing.Domain.Users;
using Managing.Domain.Workflows;
@@ -26,4 +25,5 @@ public interface IBotService
Task<bool> DeleteBot(string botName);
Task<string> RestartBot(string botName);
void ToggleIsForWatchingOnly(string botName);
Task<bool> UpdateBotConfiguration(string identifier, TradingBotConfig newConfig);
}

View File

@@ -666,7 +666,7 @@ public class TradingBot : Bot, ITradingBot
$"Signal {signal.Identifier} will wait for position to become profitable before flipping.");
// Keep signal in waiting status to check again on next execution
SetSignalStatus(signal.Identifier, SignalStatus.WaitingForPosition);
SetSignalStatus(signal.Identifier, SignalStatus.Expired);
return;
}
}
@@ -1180,6 +1180,7 @@ public class TradingBot : Bot, ITradingBot
MaxLossStreak = Config.MaxLossStreak,
MaxPositionTimeHours = Config.MaxPositionTimeHours ?? 0m,
FlipOnlyWhenInProfit = Config.FlipOnlyWhenInProfit,
CloseEarlyWhenProfitable = Config.CloseEarlyWhenProfitable,
};
BotService.SaveOrUpdateBotBackup(User, Identifier, Config.BotType, Status, JsonConvert.SerializeObject(data));
}
@@ -1202,6 +1203,7 @@ public class TradingBot : Bot, ITradingBot
MaxLossStreak = data.MaxLossStreak,
MaxPositionTimeHours = data.MaxPositionTimeHours == 0m ? null : data.MaxPositionTimeHours,
FlipOnlyWhenInProfit = data.FlipOnlyWhenInProfit,
CloseEarlyWhenProfitable = data.CloseEarlyWhenProfitable,
Name = data.Name
};
@@ -1429,7 +1431,8 @@ public class TradingBot : Bot, ITradingBot
MaxPositionTimeHours = Config.MaxPositionTimeHours,
FlipOnlyWhenInProfit = Config.FlipOnlyWhenInProfit,
FlipPosition = Config.FlipPosition,
Name = Config.Name
Name = Config.Name,
CloseEarlyWhenProfitable = Config.CloseEarlyWhenProfitable
};
}
}
@@ -1453,4 +1456,5 @@ public class TradingBotBackup
public int MaxLossStreak { get; set; }
public decimal MaxPositionTimeHours { get; set; }
public bool FlipOnlyWhenInProfit { get; set; }
public bool CloseEarlyWhenProfitable { get; set; }
}

View File

@@ -142,7 +142,8 @@ namespace Managing.Application.ManageBot
MaxPositionTimeHours = scalpingBotData.MaxPositionTimeHours == 0m ? null : scalpingBotData.MaxPositionTimeHours,
FlipOnlyWhenInProfit = scalpingBotData.FlipOnlyWhenInProfit,
IsForBacktest = false,
FlipPosition = false
FlipPosition = false,
CloseEarlyWhenProfitable = scalpingBotData.CloseEarlyWhenProfitable
};
bot = CreateScalpingBot(scalpingConfig);
@@ -171,7 +172,8 @@ namespace Managing.Application.ManageBot
MaxPositionTimeHours = flippingBotData.MaxPositionTimeHours == 0m ? null : flippingBotData.MaxPositionTimeHours,
FlipOnlyWhenInProfit = flippingBotData.FlipOnlyWhenInProfit,
IsForBacktest = false,
FlipPosition = true
FlipPosition = true,
CloseEarlyWhenProfitable = flippingBotData.CloseEarlyWhenProfitable
};
bot = CreateFlippingBot(flippingConfig);
@@ -263,6 +265,23 @@ namespace Managing.Application.ManageBot
}
}
/// <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)
{
return new ScalpingBot(

View File

@@ -74,7 +74,8 @@ namespace Managing.Application.ManageBot
MaxPositionTimeHours = request.Config.MaxPositionTimeHours, // Properly handle nullable value
FlipOnlyWhenInProfit = request.Config.FlipOnlyWhenInProfit,
FlipPosition = request.Config.FlipPosition,
Name = request.Config.Name ?? request.Name
Name = request.Config.Name ?? request.Name,
CloseEarlyWhenProfitable = request.Config.CloseEarlyWhenProfitable
};
switch (configToUse.BotType)

View File

@@ -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

View File

@@ -1,13 +1,13 @@
import {ChevronDownIcon, ChevronRightIcon, EyeIcon, PlayIcon, TrashIcon} from '@heroicons/react/solid'
import {ChevronDownIcon, ChevronRightIcon, PlayIcon, TrashIcon} from '@heroicons/react/solid'
import React, {useEffect, useState} from 'react'
import {useQuery} from '@tanstack/react-query'
import useApiUrlStore from '../../../app/store/apiStore'
import type {Backtest, StartBotRequest, Ticker, TradingBotConfig} from '../../../generated/ManagingApi'
import {BacktestClient, BotClient, MoneyManagementClient} from '../../../generated/ManagingApi'
import type {Backtest} from '../../../generated/ManagingApi'
import {BacktestClient} from '../../../generated/ManagingApi'
import type {IBacktestCards} from '../../../global/type'
import {CardText, SelectColumnFilter, Table, Toast} from '../../mollecules'
import {BotNameModal} from '../index'
import {CardText, SelectColumnFilter, Table} from '../../mollecules'
import BotConfigModal from '../../mollecules/BotConfigModal/BotConfigModal'
import Toast from '../../mollecules/Toast/Toast'
import BacktestRowDetails from './backtestRowDetails'
@@ -27,95 +27,19 @@ const BacktestTable: React.FC<IBacktestCards> = ({ list, isFetching, setBacktest
averageCooldown: 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>('')
// Bot configuration modal state
const [showBotConfigModal, setShowBotConfigModal] = useState(false)
const [selectedBacktest, setSelectedBacktest] = useState<Backtest | null>(null)
// Fetch money managements
const { data: moneyManagements } = useQuery({
queryFn: async () => {
const moneyManagementClient = new MoneyManagementClient({}, apiUrl)
return await moneyManagementClient.moneyManagement_GetMoneyManagements()
},
queryKey: ['moneyManagements'],
})
// Set the first money management as default when the data is loaded
useEffect(() => {
if (moneyManagements && moneyManagements.length > 0) {
setSelectedMoneyManagement(moneyManagements[0].name)
}
}, [moneyManagements])
async function runBot(botName: string, backtest: Backtest, isForWatchOnly: boolean, moneyManagementName: string, initialTradingBalance: number) {
const t = new Toast('Bot is starting')
const client = new BotClient({}, apiUrl)
// Check if the money management name is "custom" or contains "custom"
const isCustomMoneyManagement =
!moneyManagementName ||
moneyManagementName.toLowerCase() === 'custom' ||
moneyManagementName.toLowerCase().includes('custom');
// Create TradingBotConfig from the backtest configuration
const tradingBotConfig: TradingBotConfig = {
accountName: backtest.config.accountName,
ticker: backtest.config.ticker,
scenarioName: backtest.config.scenarioName,
timeframe: backtest.config.timeframe,
botType: backtest.config.botType,
isForWatchingOnly: isForWatchOnly,
isForBacktest: false, // This is for running a live bot
cooldownPeriod: backtest.config.cooldownPeriod,
maxLossStreak: backtest.config.maxLossStreak,
maxPositionTimeHours: backtest.config.maxPositionTimeHours,
flipOnlyWhenInProfit: backtest.config.flipOnlyWhenInProfit,
flipPosition: backtest.config.flipPosition,
name: botName,
botTradingBalance: initialTradingBalance,
// Use the money management from backtest if it's custom, otherwise leave null and use moneyManagementName
moneyManagement: isCustomMoneyManagement ?
(backtest.config.moneyManagement || {
name: 'default',
leverage: 1,
stopLoss: 0.01,
takeProfit: 0.02,
timeframe: backtest.config.timeframe
}) :
backtest.config.moneyManagement, // Always provide a valid MoneyManagement object
closeEarlyWhenProfitable: backtest.config.closeEarlyWhenProfitable || false
};
const request: StartBotRequest = {
config: tradingBotConfig,
// Only use the money management name if it's not a custom money management
moneyManagementName: isCustomMoneyManagement ? undefined : moneyManagementName
}
await client
.bot_Start(request)
.then((botStatus: string) => {
t.update('info', 'Bot status: ' + botStatus)
})
.catch((err) => {
t.update('error', 'Error: ' + err)
})
const handleOpenBotConfigModal = (backtest: Backtest) => {
setSelectedBacktest(backtest)
setShowBotConfigModal(true)
}
const handleOpenBotNameModal = (backtest: Backtest, isForWatchOnly: boolean) => {
setCurrentBacktest(backtest)
setIsForWatchOnly(isForWatchOnly)
setShowBotNameModal(true)
}
const handleCloseBotNameModal = () => {
setShowBotNameModal(false)
}
const handleSubmitBotName = (botName: string, backtest: Backtest, isForWatchOnly: boolean, moneyManagementName: string, initialTradingBalance: number) => {
runBot(botName, backtest, isForWatchOnly, moneyManagementName, initialTradingBalance)
setShowBotNameModal(false)
const handleCloseBotConfigModal = () => {
setShowBotConfigModal(false)
setSelectedBacktest(null)
}
async function deleteBacktest(id: string) {
@@ -292,27 +216,10 @@ const BacktestTable: React.FC<IBacktestCards> = ({ list, isFetching, setBacktest
{
Cell: ({ cell }: any) => (
<>
<div className="tooltip" data-tip="Run in watch-only mode">
<div className="tooltip" data-tip="Create bot from backtest">
<button
data-value={cell.row.values.name}
onClick={() => handleOpenBotNameModal(cell.row.original as Backtest, true)}
>
<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)}
onClick={() => handleOpenBotConfigModal(cell.row.original as Backtest)}
>
<PlayIcon className="text-primary w-4"></PlayIcon>
</button>
@@ -475,8 +382,6 @@ const BacktestTable: React.FC<IBacktestCards> = ({ list, isFetching, setBacktest
}
}, [list])
return (
<>
{isFetching ? (
@@ -528,18 +433,14 @@ const BacktestTable: React.FC<IBacktestCards> = ({ list, isFetching, setBacktest
/>
)}
/>
{showBotNameModal && currentBacktest && moneyManagements && (
<BotNameModal
showModal={showBotNameModal}
onClose={handleCloseBotNameModal}
backtest={currentBacktest}
isForWatchOnly={isForWatchOnly}
onSubmitBotName={(botName, backtest, isForWatchOnly, moneyManagementName, initialTradingBalance) =>
handleSubmitBotName(botName, backtest, isForWatchOnly, moneyManagementName, initialTradingBalance)
}
moneyManagements={moneyManagements}
selectedMoneyManagement={selectedMoneyManagement}
setSelectedMoneyManagement={setSelectedMoneyManagement}
{/* Bot Configuration Modal */}
{selectedBacktest && (
<BotConfigModal
showModal={showBotConfigModal}
mode="create"
backtest={selectedBacktest}
onClose={handleCloseBotConfigModal}
/>
)}
</>

View File

@@ -653,7 +653,7 @@ export class BotClient extends AuthorizedApiBase {
return Promise.resolve<string>(null as any);
}
bot_GetActiveBots(): Promise<TradingBot[]> {
bot_GetActiveBots(): Promise<TradingBotResponse[]> {
let url_ = this.baseUrl + "/Bot";
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;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as TradingBot[];
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as TradingBotResponse[];
return result200;
});
} else if (status !== 200 && status !== 204) {
@@ -685,7 +685,7 @@ export class BotClient extends AuthorizedApiBase {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<TradingBot[]>(null as any);
return Promise.resolve<TradingBotResponse[]>(null as any);
}
bot_OpenPositionManually(request: OpenPositionManuallyRequest): Promise<Position> {
@@ -3118,25 +3118,16 @@ export interface StartBotRequest {
moneyManagementName?: string | null;
}
export interface TradingBot {
name: string;
export interface TradingBotResponse {
status: string;
signals: Signal[];
positions: Position[];
candles: Candle[];
winRate: number;
profitAndLoss: number;
timeframe: Timeframe;
ticker: Ticker;
scenario: string;
isForWatchingOnly: boolean;
botType: BotType;
accountName: string;
moneyManagement: MoneyManagement;
identifier: string;
agentName: string;
maxPositionTimeHours: number;
flipOnlyWhenInProfit: boolean;
config: TradingBotConfig;
}
export interface OpenPositionManuallyRequest {
@@ -3150,20 +3141,9 @@ export interface ClosePositionRequest {
}
export interface UpdateBotConfigRequest {
identifier?: string | null;
accountName?: string | null;
identifier: string;
config: TradingBotConfig;
moneyManagementName?: string | null;
ticker?: Ticker | null;
scenarioName?: string | null;
timeframe?: Timeframe | null;
isForWatchingOnly?: boolean | null;
botTradingBalance?: number | null;
cooldownPeriod?: number | null;
maxLossStreak?: number | null;
maxPositionTimeHours?: number | null;
flipOnlyWhenInProfit?: boolean | null;
flipPosition?: boolean | null;
name?: string | null;
}
export interface TickerInfos {

View File

@@ -1,12 +1,13 @@
import {ChartBarIcon, EyeIcon, PlayIcon, PlusCircleIcon, StopIcon, TrashIcon} from '@heroicons/react/solid'
import {ChartBarIcon, CogIcon, EyeIcon, PlayIcon, PlusCircleIcon, StopIcon, TrashIcon} from '@heroicons/react/solid'
import React, {useState} from 'react'
import useApiUrlStore from '../../app/store/apiStore'
import {CardPosition, CardSignal, CardText, Toast,} from '../../components/mollecules'
import ManualPositionModal from '../../components/mollecules/ManualPositionModal'
import TradesModal from '../../components/mollecules/TradesModal/TradesModal'
import BotConfigModal from '../../components/mollecules/BotConfigModal/BotConfigModal'
import {TradeChart} from '../../components/organism'
import type {BotType, MoneyManagement, TradingBot,} from '../../generated/ManagingApi'
import type {BotType, MoneyManagement, Position, TradingBotResponse} from '../../generated/ManagingApi'
import {BotClient} from '../../generated/ManagingApi'
import type {IBotList} from '../../global/type'
import MoneyManagementModal from '../settingsPage/moneymanagement/moneyManagementModal'
@@ -38,6 +39,12 @@ const BotList: React.FC<IBotList> = ({ list }) => {
const [selectedBotForManualPosition, setSelectedBotForManualPosition] = useState<string | null>(null)
const [showTradesModal, setShowTradesModal] = useState(false)
const [selectedBotForTrades, setSelectedBotForTrades] = useState<{ identifier: string; agentName: string } | null>(null)
const [showBotConfigModal, setShowBotConfigModal] = useState(false)
const [botConfigModalMode, setBotConfigModalMode] = useState<'create' | 'update'>('create')
const [selectedBotForUpdate, setSelectedBotForUpdate] = useState<{
identifier: string
config: any
} | null>(null)
function getIsForWatchingBadge(isForWatchingOnly: boolean, identifier: string) {
const classes =
@@ -188,9 +195,53 @@ const BotList: React.FC<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 (
<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
key={index.toString()}
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>
<div className="card-body">
<h2 className="card-title text-sm">
{bot.ticker}
{getMoneyManagementBadge(bot.moneyManagement)}
{getIsForWatchingBadge(bot.isForWatchingOnly, bot.identifier)}
{getToggleBotStatusBadge(bot.status, bot.identifier, bot.botType)}
{bot.config.ticker}
{getMoneyManagementBadge(bot.config.moneyManagement)}
{getIsForWatchingBadge(bot.config.isForWatchingOnly, bot.identifier)}
{getToggleBotStatusBadge(bot.status, bot.identifier, bot.config.botType)}
{getUpdateBotBadge(bot)}
{getManualPositionBadge(bot.identifier)}
{getDeleteBadge(bot.identifier)}
</h2>
@@ -223,26 +275,26 @@ const BotList: React.FC<IBotList> = ({ list }) => {
<div>
<CardText
title="Scenario"
content={bot.scenario}
content={bot.config.scenarioName}
></CardText>
</div>
</div>
</div>
<div className="columns-2">
<CardSignal signals={bot.signals}></CardSignal>
<CardText title="Type" content={bot.botType}></CardText>
<CardText title="Type" content={bot.config.botType}></CardText>
</div>
<div className="columns-2">
<CardPosition
positivePosition={true}
positions={bot.positions.filter((p) => {
positions={bot.positions.filter((p: Position) => {
const realized = p.profitAndLoss?.realized ?? 0
return realized > 0 ? p : null
})}
></CardPosition>
<CardPosition
positivePosition={false}
positions={bot.positions.filter((p) => {
positions={bot.positions.filter((p: Position) => {
const realized = p.profitAndLoss?.realized ?? 0
return realized <= 0 ? p : null
})}
@@ -287,6 +339,15 @@ const BotList: React.FC<IBotList> = ({ list }) => {
setSelectedBotForTrades(null)
}}
/>
<BotConfigModal
showModal={showBotConfigModal}
mode={botConfigModalMode}
existingBot={selectedBotForUpdate || undefined}
onClose={() => {
setShowBotConfigModal(false)
setSelectedBotForUpdate(null)
}}
/>
</div>
)
}