Push merge conflict

This commit is contained in:
2025-07-04 11:02:53 +07:00
parent 88f195c0ca
commit 59c5de7df7
27 changed files with 1133 additions and 1118 deletions

View File

@@ -155,11 +155,10 @@ The `TradingBotConfig` class defines all configuration parameters for trading bo
| `Timeframe` | `Timeframe` | Candle timeframe for analysis | | `Timeframe` | `Timeframe` | Candle timeframe for analysis |
| `IsForWatchingOnly` | `bool` | If true, bot only sends signals without trading | | `IsForWatchingOnly` | `bool` | If true, bot only sends signals without trading |
| `BotTradingBalance` | `decimal` | Initial trading balance for the bot | | `BotTradingBalance` | `decimal` | Initial trading balance for the bot |
| `BotType` | `BotType` | Type of trading bot behavior |
| `IsForBacktest` | `bool` | Whether this config is for backtesting | | `IsForBacktest` | `bool` | Whether this config is for backtesting |
| `CooldownPeriod` | `int` | Number of candles to wait before opening new position in same direction | | `CooldownPeriod` | `int` | Number of candles to wait before opening new position in same direction |
| `MaxLossStreak` | `int` | Maximum consecutive losses before requiring opposite direction signal (0 = no limit) | | `MaxLossStreak` | `int` | Maximum consecutive losses before requiring opposite direction signal (0 = no limit) |
| `FlipPosition` | `bool` | Whether the bot can flip positions | | `FlipPosition` | `bool` | Whether the bot can flip positions (default: false) |
| `Name` | `string` | Unique identifier/name for the bot | | `Name` | `string` | Unique identifier/name for the bot |
| `FlipOnlyWhenInProfit` | `bool` | Only flip positions when current position is profitable (default: true) | | `FlipOnlyWhenInProfit` | `bool` | Only flip positions when current position is profitable (default: true) |
@@ -183,8 +182,10 @@ The `TradingBotConfig` class defines all configuration parameters for trading bo
- Only closes when position is in profit or at breakeven (never closes at a loss due to time) - Only closes when position is in profit or at breakeven (never closes at a loss due to time)
- `CloseEarlyWhenProfitable` allows immediate closure when profitable instead of waiting full duration - `CloseEarlyWhenProfitable` allows immediate closure when profitable instead of waiting full duration
**Profit-Controlled Flipping:** **Bot Behavior Control:**
- `FlipOnlyWhenInProfit` ensures safer trading by only flipping profitable positions - `IsForWatchingOnly` controls whether the bot executes actual trades or only analyzes and generates signals
- `FlipPosition` controls whether the bot can flip positions when opposite signals occur (default: false)
- `FlipOnlyWhenInProfit` ensures safer trading by only flipping profitable positions when flipping is enabled
- Helps prevent cascading losses in volatile markets - Helps prevent cascading losses in volatile markets
**Synth API Integration:** **Synth API Integration:**
@@ -213,27 +214,22 @@ The `TradingBotConfig` class defines all configuration parameters for trading bo
| CloseEarlyWhenProfitable | Close positions early when profitable (requires MaxPositionTimeHours) | false | | CloseEarlyWhenProfitable | Close positions early when profitable (requires MaxPositionTimeHours) | false |
| BotTradingBalance | Initial trading balance for the bot | Required | | BotTradingBalance | Initial trading balance for the bot | Required |
### Bot Types ### Bot Behavior Configuration
The `BotType` enum in `TradingBotConfig` defines the following trading bot behaviors: Trading bots support flexible behavior configuration through boolean flags rather than predefined types:
| Type | Description | #### Watch-Only Mode
|-------------|----------------------------------------------------------------------------------------| - **`IsForWatchingOnly`**: When `true`, the bot analyzes market conditions and generates signals but does not execute actual trades
| SimpleBot | Basic bot implementation for simple trading strategies | - Perfect for strategy testing, signal validation, or paper trading
| ScalpingBot | Opens positions and waits for cooldown period before opening new ones in same direction | - Useful for developing confidence in a strategy before committing real capital
| FlippingBot | Advanced bot that can flip positions when opposite signals are triggered |
#### Flipping Mode Configuration #### Position Flipping
- **`FlipPosition`**: When `true`, enables the bot to flip positions when opposite signals are triggered
The flipping behavior is controlled by several `TradingBotConfig` properties:
- **`BotType`**: Set to `FlippingBot` to enable position flipping capabilities
- **`FlipPosition`**: Boolean flag that enables/disables position flipping (automatically set based on BotType)
- **`FlipOnlyWhenInProfit`**: Safety feature that only allows flipping when current position is profitable (default: true) - **`FlipOnlyWhenInProfit`**: Safety feature that only allows flipping when current position is profitable (default: true)
#### How Flipping Works #### How Position Flipping Works
**FlippingBot Behavior:** **When Flipping is Enabled (`FlipPosition = true`):**
1. Opens initial position based on scenario signals 1. Opens initial position based on scenario signals
2. Monitors for opposite direction signals from the same scenario 2. Monitors for opposite direction signals from the same scenario
3. When opposite signal occurs: 3. When opposite signal occurs:
@@ -242,11 +238,12 @@ The flipping behavior is controlled by several `TradingBotConfig` properties:
4. Closes current position and immediately opens new position in opposite direction 4. Closes current position and immediately opens new position in opposite direction
5. Continues this cycle for the duration of the bot's operation 5. Continues this cycle for the duration of the bot's operation
**ScalpingBot vs FlippingBot:** **When Flipping is Disabled (`FlipPosition = false`):**
- **ScalpingBot**: Opens position → Waits for exit signal → Closes → Cooldown → Opens new position - Opens position → Waits for exit signal → Closes → Cooldown → Opens new position in same direction
- **FlippingBot**: Opens position → Monitors for opposite signals → Flips immediately (no cooldown between flips) - More conservative approach with cooldown periods between trades
- Reduces frequency of trades and potential whipsaw losses
This configuration allows for more aggressive trading strategies while maintaining risk management through the profit-controlled flipping mechanism. This simplified configuration provides clear control over bot behavior while maintaining risk management through the profit-controlled flipping mechanism.
## Backtesting (`BacktestController`) ## Backtesting (`BacktestController`)

View File

@@ -10,7 +10,6 @@ using Managing.Domain.Strategies;
using Microsoft.AspNetCore.Authorization; 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;
namespace Managing.Api.Controllers; namespace Managing.Api.Controllers;
@@ -192,15 +191,14 @@ public class BacktestController : BaseController
ScenarioName = request.Config.ScenarioName, ScenarioName = request.Config.ScenarioName,
Scenario = scenario, // Use the converted scenario object Scenario = scenario, // Use the converted scenario object
Timeframe = request.Config.Timeframe, Timeframe = request.Config.Timeframe,
IsForWatchingOnly = request.WatchOnly, IsForWatchingOnly = request.Config.IsForWatchingOnly,
BotTradingBalance = request.Balance, BotTradingBalance = request.Config.BotTradingBalance,
BotType = request.Config.BotType,
IsForBacktest = true, IsForBacktest = true,
CooldownPeriod = request.Config.CooldownPeriod, CooldownPeriod = request.Config.CooldownPeriod,
MaxLossStreak = request.Config.MaxLossStreak, MaxLossStreak = request.Config.MaxLossStreak,
MaxPositionTimeHours = request.Config.MaxPositionTimeHours, MaxPositionTimeHours = request.Config.MaxPositionTimeHours,
FlipOnlyWhenInProfit = request.Config.FlipOnlyWhenInProfit, FlipOnlyWhenInProfit = request.Config.FlipOnlyWhenInProfit,
FlipPosition = request.Config.BotType == BotType.FlippingBot, // Computed based on BotType FlipPosition = request.Config.FlipPosition, // Computed based on BotType
Name = request.Config.Name ?? Name = request.Config.Name ??
$"Backtest-{request.Config.ScenarioName ?? request.Config.Scenario?.Name ?? "Custom"}-{DateTime.UtcNow:yyyyMMdd-HHmmss}", $"Backtest-{request.Config.ScenarioName ?? request.Config.Scenario?.Name ?? "Custom"}-{DateTime.UtcNow:yyyyMMdd-HHmmss}",
CloseEarlyWhenProfitable = request.Config.CloseEarlyWhenProfitable, CloseEarlyWhenProfitable = request.Config.CloseEarlyWhenProfitable,
@@ -210,21 +208,12 @@ public class BacktestController : BaseController
UseForDynamicStopLoss = request.Config.UseForDynamicStopLoss UseForDynamicStopLoss = request.Config.UseForDynamicStopLoss
}; };
switch (request.Config.BotType) backtestResult = await _backtester.RunTradingBotBacktest(
{ backtestConfig,
case BotType.SimpleBot: request.StartDate,
// SimpleBot backtest not implemented yet request.EndDate,
break; user,
case BotType.ScalpingBot: request.Save);
case BotType.FlippingBot:
backtestResult = await _backtester.RunTradingBotBacktest(
backtestConfig,
request.StartDate,
request.EndDate,
user,
request.Save);
break;
}
await NotifyBacktesingSubscriberAsync(backtestResult); await NotifyBacktesingSubscriberAsync(backtestResult);
@@ -281,16 +270,6 @@ public class RunBacktestRequest
/// </summary> /// </summary>
public DateTime EndDate { get; set; } public DateTime EndDate { get; set; }
/// <summary>
/// The starting balance for the backtest
/// </summary>
public decimal Balance { get; set; }
/// <summary>
/// Whether to only watch the backtest without executing trades
/// </summary>
public bool WatchOnly { get; set; } = false;
/// <summary> /// <summary>
/// Whether to save the backtest results /// Whether to save the backtest results
/// </summary> /// </summary>

View File

@@ -224,7 +224,6 @@ public class BotController : BaseController
Timeframe = request.Config.Timeframe, Timeframe = request.Config.Timeframe,
IsForWatchingOnly = request.Config.IsForWatchingOnly, IsForWatchingOnly = request.Config.IsForWatchingOnly,
BotTradingBalance = request.Config.BotTradingBalance, BotTradingBalance = request.Config.BotTradingBalance,
BotType = request.Config.BotType,
CooldownPeriod = request.Config.CooldownPeriod, CooldownPeriod = request.Config.CooldownPeriod,
MaxLossStreak = request.Config.MaxLossStreak, MaxLossStreak = request.Config.MaxLossStreak,
MaxPositionTimeHours = request.Config.MaxPositionTimeHours, MaxPositionTimeHours = request.Config.MaxPositionTimeHours,
@@ -236,7 +235,7 @@ public class BotController : BaseController
UseForDynamicStopLoss = request.Config.UseForDynamicStopLoss, UseForDynamicStopLoss = request.Config.UseForDynamicStopLoss,
// Set computed/default properties // Set computed/default properties
IsForBacktest = false, IsForBacktest = false,
FlipPosition = request.Config.BotType == BotType.FlippingBot, FlipPosition = request.Config.FlipPosition,
Name = request.Config.Name Name = request.Config.Name
}; };
@@ -256,12 +255,11 @@ public class BotController : BaseController
/// <summary> /// <summary>
/// Stops a bot specified by type and name. /// Stops a bot specified by type and name.
/// </summary> /// </summary>
/// <param name="botType">The type of the bot to stop.</param>
/// <param name="identifier">The identifier of the bot to stop.</param> /// <param name="identifier">The identifier of the bot to stop.</param>
/// <returns>A string indicating the result of the stop operation.</returns> /// <returns>A string indicating the result of the stop operation.</returns>
[HttpGet] [HttpGet]
[Route("Stop")] [Route("Stop")]
public async Task<ActionResult<string>> Stop(BotType botType, string identifier) public async Task<ActionResult<string>> Stop(string identifier)
{ {
try try
{ {
@@ -271,8 +269,8 @@ public class BotController : BaseController
return Forbid("You don't have permission to stop this bot"); return Forbid("You don't have permission to stop this bot");
} }
var result = await _mediator.Send(new StopBotCommand(botType, identifier)); var result = await _mediator.Send(new StopBotCommand(identifier));
_logger.LogInformation($"{botType} type with identifier {identifier} is now {result}"); _logger.LogInformation($"Bot identifier {identifier} is now {result}");
await NotifyBotSubscriberAsync(); await NotifyBotSubscriberAsync();
@@ -343,7 +341,7 @@ public class BotController : BaseController
foreach (var bot in userBots) foreach (var bot in userBots)
{ {
await _mediator.Send(new StopBotCommand(bot.Config.BotType, bot.Identifier)); await _mediator.Send(new StopBotCommand(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");
} }
@@ -422,7 +420,8 @@ 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.Config.BotType, bot.Identifier)); await _mediator.Send(
new StopBotCommand(bot.Identifier));
// Get the saved bot backup // Get the saved bot backup
var backup = _botService.GetBotBackup(bot.Identifier); var backup = _botService.GetBotBackup(bot.Identifier);
@@ -776,7 +775,6 @@ public class BotController : BaseController
Timeframe = request.Config.Timeframe, Timeframe = request.Config.Timeframe,
IsForWatchingOnly = request.Config.IsForWatchingOnly, IsForWatchingOnly = request.Config.IsForWatchingOnly,
BotTradingBalance = request.Config.BotTradingBalance, BotTradingBalance = request.Config.BotTradingBalance,
BotType = request.Config.BotType,
CooldownPeriod = request.Config.CooldownPeriod, CooldownPeriod = request.Config.CooldownPeriod,
MaxLossStreak = request.Config.MaxLossStreak, MaxLossStreak = request.Config.MaxLossStreak,
MaxPositionTimeHours = request.Config.MaxPositionTimeHours, MaxPositionTimeHours = request.Config.MaxPositionTimeHours,
@@ -788,7 +786,7 @@ public class BotController : BaseController
UseForDynamicStopLoss = request.Config.UseForDynamicStopLoss, UseForDynamicStopLoss = request.Config.UseForDynamicStopLoss,
// Set computed/default properties // Set computed/default properties
IsForBacktest = false, IsForBacktest = false,
FlipPosition = request.Config.BotType == BotType.FlippingBot, FlipPosition = request.Config.FlipPosition,
Name = request.Config.Name Name = request.Config.Name
}; };

View File

@@ -38,18 +38,14 @@ public class TradingBotConfigRequest
[Required] [Required]
public decimal BotTradingBalance { get; set; } public decimal BotTradingBalance { get; set; }
/// <summary>
/// The type of bot (SimpleBot, ScalpingBot, FlippingBot)
/// </summary>
[Required]
public BotType BotType { get; set; }
/// <summary> /// <summary>
/// The name/identifier for this bot /// The name/identifier for this bot
/// </summary> /// </summary>
[Required] [Required]
public string Name { get; set; } public string Name { get; set; }
[Required] public bool FlipPosition { get; set; }
/// <summary> /// <summary>
/// Cooldown period between trades (in candles) /// Cooldown period between trades (in candles)
/// </summary> /// </summary>

View File

@@ -272,7 +272,6 @@ public class StatisticService : IStatisticService
Timeframe = timeframe, Timeframe = timeframe,
IsForWatchingOnly = true, IsForWatchingOnly = true,
BotTradingBalance = 1000, BotTradingBalance = 1000,
BotType = BotType.ScalpingBot,
IsForBacktest = true, IsForBacktest = true,
CooldownPeriod = 1, CooldownPeriod = 1,
MaxLossStreak = 0, MaxLossStreak = 0,

View File

@@ -6,25 +6,19 @@ namespace Managing.Application.Abstractions
public interface IBotFactory public interface IBotFactory
{ {
IBot CreateSimpleBot(string botName, Workflow workflow); IBot CreateSimpleBot(string botName, Workflow workflow);
/// <summary> /// <summary>
/// Creates a trading bot using the unified TradingBot class /// Creates a trading bot using the unified TradingBot class
/// </summary> /// </summary>
/// <param name="config">The trading bot configuration</param> /// <param name="config">The trading bot configuration</param>
/// <returns>ITradingBot instance</returns> /// <returns>ITradingBot instance</returns>
ITradingBot CreateTradingBot(TradingBotConfig config); ITradingBot CreateTradingBot(TradingBotConfig config);
/// <summary> /// <summary>
/// Creates a trading bot for backtesting using the unified TradingBot class /// Creates a trading bot for backtesting using the unified TradingBot class
/// </summary> /// </summary>
/// <param name="config">The trading bot configuration</param> /// <param name="config">The trading bot configuration</param>
/// <returns>ITradingBot instance configured for backtesting</returns> /// <returns>ITradingBot instance configured for backtesting</returns>
ITradingBot CreateBacktestTradingBot(TradingBotConfig config); ITradingBot CreateBacktestTradingBot(TradingBotConfig config);
// Legacy methods - these will use TradingBot internally but maintain backward compatibility
ITradingBot CreateScalpingBot(TradingBotConfig config);
ITradingBot CreateBacktestScalpingBot(TradingBotConfig config);
ITradingBot CreateFlippingBot(TradingBotConfig config);
ITradingBot CreateBacktestFlippingBot(TradingBotConfig config);
} }
} }

View File

@@ -114,7 +114,7 @@ namespace Managing.Application.Backtesting
User user = null) User user = null)
{ {
// Set FlipPosition based on BotType // Set FlipPosition based on BotType
config.FlipPosition = config.BotType == BotType.FlippingBot; config.FlipPosition = config.FlipPosition;
var tradingBot = _botFactory.CreateBacktestTradingBot(config); var tradingBot = _botFactory.CreateBacktestTradingBot(config);
@@ -164,11 +164,11 @@ namespace Managing.Application.Backtesting
private List<Candle> GetCandles(Account account, Ticker ticker, Timeframe timeframe, private List<Candle> GetCandles(Account account, Ticker ticker, Timeframe timeframe,
DateTime startDate, DateTime endDate) DateTime startDate, DateTime endDate)
{ {
var candles = _exchangeService.GetCandlesInflux(account.Exchange, ticker, var candles = _exchangeService.GetCandlesInflux(TradingExchanges.Evm, ticker,
startDate, timeframe, endDate).Result; startDate, timeframe, endDate).Result;
if (candles == null || candles.Count == 0) if (candles == null || candles.Count == 0)
throw new Exception($"No candles for {ticker} on {account.Exchange}"); throw new Exception($"No candles for {ticker} on {timeframe} timeframe");
return candles; return candles;
} }

View File

@@ -3,7 +3,6 @@ using Managing.Application.Abstractions.Services;
using Managing.Domain.Bots; using Managing.Domain.Bots;
using Managing.Domain.Workflows; using Managing.Domain.Workflows;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
namespace Managing.Application.Bots.Base namespace Managing.Application.Bots.Base
{ {
@@ -61,64 +60,5 @@ namespace Managing.Application.Bots.Base
_botService, _botService,
config); config);
} }
// Legacy methods for backward compatibility - will be deprecated
ITradingBot IBotFactory.CreateScalpingBot(TradingBotConfig config)
{
config.BotType = BotType.ScalpingBot;
config.FlipPosition = false;
return new TradingBot(
_exchangeService,
_tradingBotLogger,
_tradingService,
_accountService,
_messengerService,
_botService,
config);
}
ITradingBot IBotFactory.CreateBacktestScalpingBot(TradingBotConfig config)
{
config.BotType = BotType.ScalpingBot;
config.IsForBacktest = true;
config.FlipPosition = false;
return new TradingBot(
_exchangeService,
_tradingBotLogger,
_tradingService,
_accountService,
_messengerService,
_botService,
config);
}
public ITradingBot CreateFlippingBot(TradingBotConfig config)
{
config.BotType = BotType.FlippingBot;
config.FlipPosition = true;
return new TradingBot(
_exchangeService,
_tradingBotLogger,
_tradingService,
_accountService,
_messengerService,
_botService,
config);
}
public ITradingBot CreateBacktestFlippingBot(TradingBotConfig config)
{
config.BotType = BotType.FlippingBot;
config.IsForBacktest = true;
config.FlipPosition = true;
return new TradingBot(
_exchangeService,
_tradingBotLogger,
_tradingService,
_accountService,
_messengerService,
_botService,
config);
}
} }
} }

View File

@@ -187,7 +187,7 @@ public class TradingBot : Bot, ITradingBot
Logger.LogInformation($"____________________{Name}____________________"); Logger.LogInformation($"____________________{Name}____________________");
Logger.LogInformation( Logger.LogInformation(
$"Time : {DateTime.Now} - Server time {DateTime.Now.ToUniversalTime()} - Last candle : {OptimizedCandles.Last().Date} - Bot : {Name} - Type {Config.BotType} - Ticker : {Config.Ticker}"); $"Time : {DateTime.Now} - Server time {DateTime.Now.ToUniversalTime()} - Last candle : {OptimizedCandles.Last().Date} - Bot : {Name} - Ticker : {Config.Ticker}");
} }
var previousLastCandle = OptimizedCandles.LastOrDefault(); var previousLastCandle = OptimizedCandles.LastOrDefault();
@@ -1450,7 +1450,6 @@ public class TradingBot : Bot, ITradingBot
} }
// Protect critical properties that shouldn't change for running bots // Protect critical properties that shouldn't change for running bots
var protectedBotType = Config.BotType;
var protectedIsForBacktest = Config.IsForBacktest; var protectedIsForBacktest = Config.IsForBacktest;
var protectedName = allowNameChange ? newConfig.Name : Config.Name; var protectedName = allowNameChange ? newConfig.Name : Config.Name;
@@ -1470,7 +1469,6 @@ public class TradingBot : Bot, ITradingBot
Config = newConfig; Config = newConfig;
// Restore protected properties // Restore protected properties
Config.BotType = protectedBotType;
Config.IsForBacktest = protectedIsForBacktest; Config.IsForBacktest = protectedIsForBacktest;
Config.Name = protectedName; Config.Name = protectedName;
@@ -1532,7 +1530,6 @@ public class TradingBot : Bot, ITradingBot
Timeframe = Config.Timeframe, Timeframe = Config.Timeframe,
IsForWatchingOnly = Config.IsForWatchingOnly, IsForWatchingOnly = Config.IsForWatchingOnly,
BotTradingBalance = Config.BotTradingBalance, BotTradingBalance = Config.BotTradingBalance,
BotType = Config.BotType,
IsForBacktest = Config.IsForBacktest, IsForBacktest = Config.IsForBacktest,
CooldownPeriod = Config.CooldownPeriod, CooldownPeriod = Config.CooldownPeriod,
MaxLossStreak = Config.MaxLossStreak, MaxLossStreak = Config.MaxLossStreak,

View File

@@ -1,17 +1,14 @@
using MediatR; using MediatR;
using static Managing.Common.Enums;
namespace Managing.Application.ManageBot.Commands namespace Managing.Application.ManageBot.Commands
{ {
public class StopBotCommand : IRequest<string> public class StopBotCommand : IRequest<string>
{ {
public string Name { get; } public string Identifier { get; }
public BotType BotType { get; }
public StopBotCommand(BotType botType, string name) public StopBotCommand(string identifier)
{ {
BotType = botType; Identifier = identifier;
Name = name;
} }
} }
} }

View File

@@ -70,37 +70,25 @@ namespace Managing.Application.ManageBot
Timeframe = request.Config.Timeframe, Timeframe = request.Config.Timeframe,
IsForWatchingOnly = request.Config.IsForWatchingOnly, IsForWatchingOnly = request.Config.IsForWatchingOnly,
BotTradingBalance = request.Config.BotTradingBalance, BotTradingBalance = request.Config.BotTradingBalance,
BotType = request.Config.BotType,
IsForBacktest = request.Config.IsForBacktest, IsForBacktest = request.Config.IsForBacktest,
CooldownPeriod = CooldownPeriod =
request.Config.CooldownPeriod > 0 ? request.Config.CooldownPeriod : 1, // Default to 1 if not set request.Config.CooldownPeriod > 0 ? request.Config.CooldownPeriod : 1, // Default to 1 if not set
MaxLossStreak = request.Config.MaxLossStreak, MaxLossStreak = request.Config.MaxLossStreak,
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.BotType == BotType.FlippingBot, // Set FlipPosition based on BotType FlipPosition = request.Config.FlipPosition, // Set FlipPosition
Name = request.Config.Name ?? request.Name, Name = request.Config.Name ?? request.Name,
CloseEarlyWhenProfitable = request.Config.CloseEarlyWhenProfitable CloseEarlyWhenProfitable = request.Config.CloseEarlyWhenProfitable
}; };
switch (configToUse.BotType) var tradingBot = _botFactory.CreateTradingBot(configToUse);
{ tradingBot.User = request.User;
case BotType.SimpleBot:
var bot = _botFactory.CreateSimpleBot(request.Name, null);
bot.User = request.User;
_botService.AddSimpleBotToCache(bot);
return bot.GetStatus();
case BotType.ScalpingBot: // Log the configuration being used
case BotType.FlippingBot: await LogBotConfigurationAsync(tradingBot, $"{configToUse.Name} created");
var tradingBot = _botFactory.CreateTradingBot(configToUse);
tradingBot.User = request.User;
// Log the configuration being used _botService.AddTradingBotToCache(tradingBot);
await LogBotConfigurationAsync(tradingBot, $"{configToUse.BotType} created"); return tradingBot.GetStatus();
_botService.AddTradingBotToCache(tradingBot);
return tradingBot.GetStatus();
}
return botStatus.ToString(); return botStatus.ToString();
} }
@@ -116,7 +104,6 @@ namespace Managing.Application.ManageBot
{ {
var config = bot.GetConfiguration(); var config = bot.GetConfiguration();
var logMessage = $"{context} - Bot: {config.Name}, " + var logMessage = $"{context} - Bot: {config.Name}, " +
$"Type: {config.BotType}, " +
$"Account: {config.AccountName}, " + $"Account: {config.AccountName}, " +
$"Ticker: {config.Ticker}, " + $"Ticker: {config.Ticker}, " +
$"Balance: {config.BotTradingBalance}, " + $"Balance: {config.BotTradingBalance}, " +

View File

@@ -15,7 +15,7 @@ namespace Managing.Application.ManageBot
public Task<string> Handle(StopBotCommand request, CancellationToken cancellationToken) public Task<string> Handle(StopBotCommand request, CancellationToken cancellationToken)
{ {
return _botService.StopBot(request.Name); return _botService.StopBot(request.Identifier);
} }
} }
} }

View File

@@ -82,7 +82,6 @@ public class Backtest
Timeframe = Config.Timeframe, Timeframe = Config.Timeframe,
IsForWatchingOnly = false, // Always start as active bot IsForWatchingOnly = false, // Always start as active bot
BotTradingBalance = initialTradingBalance, BotTradingBalance = initialTradingBalance,
BotType = Config.BotType,
IsForBacktest = false, // Always false for live bots IsForBacktest = false, // Always false for live bots
CooldownPeriod = Config.CooldownPeriod, CooldownPeriod = Config.CooldownPeriod,
MaxLossStreak = Config.MaxLossStreak, MaxLossStreak = Config.MaxLossStreak,
@@ -117,7 +116,6 @@ public class Backtest
Timeframe = Config.Timeframe, Timeframe = Config.Timeframe,
IsForWatchingOnly = Config.IsForWatchingOnly, IsForWatchingOnly = Config.IsForWatchingOnly,
BotTradingBalance = balance, BotTradingBalance = balance,
BotType = Config.BotType,
IsForBacktest = true, IsForBacktest = true,
CooldownPeriod = Config.CooldownPeriod, CooldownPeriod = Config.CooldownPeriod,
MaxLossStreak = Config.MaxLossStreak, MaxLossStreak = Config.MaxLossStreak,

View File

@@ -14,7 +14,6 @@ public class TradingBotConfig
[Required] public Timeframe Timeframe { get; set; } [Required] public Timeframe Timeframe { get; set; }
[Required] public bool IsForWatchingOnly { get; set; } [Required] public bool IsForWatchingOnly { get; set; }
[Required] public decimal BotTradingBalance { get; set; } [Required] public decimal BotTradingBalance { get; set; }
[Required] public BotType BotType { get; set; }
[Required] public bool IsForBacktest { get; set; } [Required] public bool IsForBacktest { get; set; }
[Required] public int CooldownPeriod { get; set; } [Required] public int CooldownPeriod { get; set; }
[Required] public int MaxLossStreak { get; set; } [Required] public int MaxLossStreak { get; set; }
@@ -68,17 +67,17 @@ public class TradingBotConfig
/// The actual Synth configuration is managed centrally in SynthPredictionService. /// The actual Synth configuration is managed centrally in SynthPredictionService.
/// </summary> /// </summary>
public bool UseSynthApi { get; set; } = false; public bool UseSynthApi { get; set; } = false;
/// <summary> /// <summary>
/// Whether to use Synth predictions for position sizing adjustments and risk assessment /// Whether to use Synth predictions for position sizing adjustments and risk assessment
/// </summary> /// </summary>
public bool UseForPositionSizing { get; set; } = true; public bool UseForPositionSizing { get; set; } = true;
/// <summary> /// <summary>
/// Whether to use Synth predictions for signal filtering /// Whether to use Synth predictions for signal filtering
/// </summary> /// </summary>
public bool UseForSignalFiltering { get; set; } = true; public bool UseForSignalFiltering { get; set; } = true;
/// <summary> /// <summary>
/// Whether to use Synth predictions for dynamic stop-loss/take-profit adjustments /// Whether to use Synth predictions for dynamic stop-loss/take-profit adjustments
/// </summary> /// </summary>

View File

@@ -1,7 +1,6 @@
using Exilion.TradingAtomics; using Exilion.TradingAtomics;
using Managing.Infrastructure.Databases.MongoDb.Attributes; using Managing.Infrastructure.Databases.MongoDb.Attributes;
using Managing.Infrastructure.Databases.MongoDb.Configurations; using Managing.Infrastructure.Databases.MongoDb.Configurations;
using static Managing.Common.Enums;
namespace Managing.Infrastructure.Databases.MongoDb.Collections namespace Managing.Infrastructure.Databases.MongoDb.Collections
{ {
@@ -12,6 +11,7 @@ namespace Managing.Infrastructure.Databases.MongoDb.Collections
public int WinRate { get; set; } public int WinRate { get; set; }
public decimal GrowthPercentage { get; set; } public decimal GrowthPercentage { get; set; }
public decimal HodlPercentage { get; set; } public decimal HodlPercentage { get; set; }
public TradingBotConfigDto Config { get; set; }
public List<PositionDto> Positions { get; set; } public List<PositionDto> Positions { get; set; }
public List<SignalDto> Signals { get; set; } public List<SignalDto> Signals { get; set; }
public List<CandleDto> Candles { get; set; } public List<CandleDto> Candles { get; set; }
@@ -22,17 +22,5 @@ namespace Managing.Infrastructure.Databases.MongoDb.Collections
public UserDto User { get; set; } public UserDto User { get; set; }
public PerformanceMetrics Statistics { get; set; } public PerformanceMetrics Statistics { get; set; }
public double Score { get; set; } public double Score { get; set; }
// TradingBotConfig properties
public string AccountName { get; set; }
public Ticker Ticker { get; set; }
public string ScenarioName { get; set; }
public Timeframe Timeframe { get; set; }
public bool IsForWatchingOnly { get; set; }
public decimal BotTradingBalance { get; set; }
public BotType BotType { get; set; }
public bool IsForBacktest { get; set; }
public int CooldownPeriod { get; set; }
public int MaxLossStreak { get; set; }
} }
} }

View File

@@ -4,6 +4,7 @@ using Managing.Domain.Backtests;
using Managing.Domain.Bots; using Managing.Domain.Bots;
using Managing.Domain.Candles; using Managing.Domain.Candles;
using Managing.Domain.MoneyManagements; using Managing.Domain.MoneyManagements;
using Managing.Domain.Risk;
using Managing.Domain.Scenarios; using Managing.Domain.Scenarios;
using Managing.Domain.Statistics; using Managing.Domain.Statistics;
using Managing.Domain.Strategies; using Managing.Domain.Strategies;
@@ -132,20 +133,7 @@ public static class MongoMappers
if (b == null) if (b == null)
return null; return null;
var config = new TradingBotConfig var config = Map(b.Config);
{
AccountName = b.AccountName,
Ticker = b.Ticker,
ScenarioName = b.ScenarioName,
Timeframe = b.Timeframe,
IsForWatchingOnly = b.IsForWatchingOnly,
BotTradingBalance = b.BotTradingBalance,
BotType = b.BotType,
IsForBacktest = b.IsForBacktest,
CooldownPeriod = b.CooldownPeriod,
MaxLossStreak = b.MaxLossStreak,
MoneyManagement = Map(b.MoneyManagement)
};
var bTest = new Backtest( var bTest = new Backtest(
config, config,
@@ -181,6 +169,7 @@ public static class MongoMappers
WinRate = result.WinRate, WinRate = result.WinRate,
GrowthPercentage = result.GrowthPercentage, GrowthPercentage = result.GrowthPercentage,
HodlPercentage = result.HodlPercentage, HodlPercentage = result.HodlPercentage,
Config = Map(result.Config),
Positions = Map(result.Positions), Positions = Map(result.Positions),
Signals = result.Signals.Select(s => Map(s)).ToList(), Signals = result.Signals.Select(s => Map(s)).ToList(),
Candles = result.Candles.Select(c => Map(c)).ToList(), Candles = result.Candles.Select(c => Map(c)).ToList(),
@@ -191,16 +180,6 @@ public static class MongoMappers
StartDate = result.StartDate, StartDate = result.StartDate,
EndDate = result.EndDate, EndDate = result.EndDate,
Score = result.Score, Score = result.Score,
AccountName = result.Config.AccountName,
Ticker = result.Config.Ticker,
ScenarioName = result.Config.ScenarioName,
Timeframe = result.Config.Timeframe,
IsForWatchingOnly = result.Config.IsForWatchingOnly,
BotTradingBalance = result.Config.BotTradingBalance,
BotType = result.Config.BotType,
IsForBacktest = result.Config.IsForBacktest,
CooldownPeriod = result.Config.CooldownPeriod,
MaxLossStreak = result.Config.MaxLossStreak
}; };
} }
@@ -933,4 +912,122 @@ public static class MongoMappers
} }
#endregion #endregion
#region TradingBotConfig
public static TradingBotConfigDto Map(TradingBotConfig config)
{
if (config == null)
return null;
return new TradingBotConfigDto
{
AccountName = config.AccountName,
MoneyManagement = Map(config.MoneyManagement),
Ticker = config.Ticker,
Timeframe = config.Timeframe,
IsForWatchingOnly = config.IsForWatchingOnly,
BotTradingBalance = config.BotTradingBalance,
IsForBacktest = config.IsForBacktest,
CooldownPeriod = config.CooldownPeriod,
MaxLossStreak = config.MaxLossStreak,
FlipPosition = config.FlipPosition,
Name = config.Name,
RiskManagement = Map(config.RiskManagement),
Scenario = Map(config.Scenario),
ScenarioName = config.ScenarioName,
MaxPositionTimeHours = config.MaxPositionTimeHours,
CloseEarlyWhenProfitable = config.CloseEarlyWhenProfitable,
FlipOnlyWhenInProfit = config.FlipOnlyWhenInProfit,
UseSynthApi = config.UseSynthApi,
UseForPositionSizing = config.UseForPositionSizing,
UseForSignalFiltering = config.UseForSignalFiltering,
UseForDynamicStopLoss = config.UseForDynamicStopLoss
};
}
public static TradingBotConfig Map(TradingBotConfigDto dto)
{
if (dto == null)
return null;
return new TradingBotConfig
{
AccountName = dto.AccountName,
MoneyManagement = Map(dto.MoneyManagement),
Ticker = dto.Ticker,
Timeframe = dto.Timeframe,
IsForWatchingOnly = dto.IsForWatchingOnly,
BotTradingBalance = dto.BotTradingBalance,
IsForBacktest = dto.IsForBacktest,
CooldownPeriod = dto.CooldownPeriod,
MaxLossStreak = dto.MaxLossStreak,
FlipPosition = dto.FlipPosition,
Name = dto.Name,
RiskManagement = Map(dto.RiskManagement),
Scenario = Map(dto.Scenario),
ScenarioName = dto.ScenarioName,
MaxPositionTimeHours = dto.MaxPositionTimeHours,
CloseEarlyWhenProfitable = dto.CloseEarlyWhenProfitable,
FlipOnlyWhenInProfit = dto.FlipOnlyWhenInProfit,
UseSynthApi = dto.UseSynthApi,
UseForPositionSizing = dto.UseForPositionSizing,
UseForSignalFiltering = dto.UseForSignalFiltering,
UseForDynamicStopLoss = dto.UseForDynamicStopLoss
};
}
#endregion
#region RiskManagement
public static RiskManagementDto Map(RiskManagement riskManagement)
{
if (riskManagement == null)
return null;
return new RiskManagementDto
{
AdverseProbabilityThreshold = riskManagement.AdverseProbabilityThreshold,
FavorableProbabilityThreshold = riskManagement.FavorableProbabilityThreshold,
RiskAversion = riskManagement.RiskAversion,
KellyMinimumThreshold = riskManagement.KellyMinimumThreshold,
KellyMaximumCap = riskManagement.KellyMaximumCap,
MaxLiquidationProbability = riskManagement.MaxLiquidationProbability,
SignalValidationTimeHorizonHours = riskManagement.SignalValidationTimeHorizonHours,
PositionMonitoringTimeHorizonHours = riskManagement.PositionMonitoringTimeHorizonHours,
PositionWarningThreshold = riskManagement.PositionWarningThreshold,
PositionAutoCloseThreshold = riskManagement.PositionAutoCloseThreshold,
KellyFractionalMultiplier = riskManagement.KellyFractionalMultiplier,
RiskTolerance = riskManagement.RiskTolerance,
UseExpectedUtility = riskManagement.UseExpectedUtility,
UseKellyCriterion = riskManagement.UseKellyCriterion
};
}
public static RiskManagement Map(RiskManagementDto dto)
{
if (dto == null)
return null;
return new RiskManagement
{
AdverseProbabilityThreshold = dto.AdverseProbabilityThreshold,
FavorableProbabilityThreshold = dto.FavorableProbabilityThreshold,
RiskAversion = dto.RiskAversion,
KellyMinimumThreshold = dto.KellyMinimumThreshold,
KellyMaximumCap = dto.KellyMaximumCap,
MaxLiquidationProbability = dto.MaxLiquidationProbability,
SignalValidationTimeHorizonHours = dto.SignalValidationTimeHorizonHours,
PositionMonitoringTimeHorizonHours = dto.PositionMonitoringTimeHorizonHours,
PositionWarningThreshold = dto.PositionWarningThreshold,
PositionAutoCloseThreshold = dto.PositionAutoCloseThreshold,
KellyFractionalMultiplier = dto.KellyFractionalMultiplier,
RiskTolerance = dto.RiskTolerance,
UseExpectedUtility = dto.UseExpectedUtility,
UseKellyCriterion = dto.UseKellyCriterion
};
}
#endregion
} }

View File

@@ -21,6 +21,7 @@ const Modal: React.FC<IModalProps> = ({
titleHeader={titleHeader} titleHeader={titleHeader}
onClose={onClose} onClose={onClose}
onSubmit={onSubmit} onSubmit={onSubmit}
showModal={showModal}
/> />
{children} {children}
</div> </div>

View File

@@ -10,9 +10,7 @@ import Logo from '../../../assets/img/logo.png'
import {Loader} from '../../atoms' import {Loader} from '../../atoms'
const navigation = [ const navigation = [
{ href: '/desk', name: 'Desk' },
{ href: '/bots', name: 'Bots' }, { href: '/bots', name: 'Bots' },
{ href: '/workflow', name: 'Workflows' },
{ href: '/scenarios', name: 'Scenarios' }, { href: '/scenarios', name: 'Scenarios' },
{ href: '/backtest', name: 'Backtest' }, { href: '/backtest', name: 'Backtest' },
{ href: '/tools', name: 'Tools' }, { href: '/tools', name: 'Tools' },

View File

@@ -1,6 +1,6 @@
import type { FC } from 'react' import type {FC} from 'react'
import type { ITabsProps } from '../../../global/type' import type {ITabsProps} from '../../../global/type.tsx'
/** /**
* Avalible Props * Avalible Props
@@ -19,7 +19,7 @@ const Tabs: FC<ITabsProps> = ({
addButton = false, addButton = false,
onAddButton, onAddButton,
}) => { }) => {
const Panel = tabs && tabs.find((tab) => tab.index === selectedTab) const Panel = tabs && tabs.find((tab: any) => tab.index === selectedTab)
return ( return (
<div <div
@@ -28,7 +28,7 @@ const Tabs: FC<ITabsProps> = ({
} }
> >
<div className="tabs" role="tablist" aria-orientation={orientation}> <div className="tabs" role="tablist" aria-orientation={orientation}>
{tabs.map((tab) => ( {tabs.map((tab: any) => (
<button <button
className={ className={
'mb-5 tab tab-bordered ' + 'mb-5 tab tab-bordered ' +

View File

@@ -21,377 +21,374 @@ import TradeChart from '../Trading/TradeChart/TradeChart'
import {BotNameModal} from '../index' import {BotNameModal} from '../index'
function baseBadgeClass(isOutlined = false) { function baseBadgeClass(isOutlined = false) {
let classes = 'text-xs badge ' let classes = 'text-xs badge '
if (isOutlined) { if (isOutlined) {
classes += 'badge-outline ' classes += 'badge-outline '
} }
return classes return classes
} }
function botStatusResult( function botStatusResult(
growthPercentage: number | undefined, growthPercentage: number | undefined,
hodlPercentage: number | undefined hodlPercentage: number | undefined
) { ) {
if (growthPercentage != undefined && hodlPercentage != undefined) { if (growthPercentage != undefined && hodlPercentage != undefined) {
const isWinning = growthPercentage > hodlPercentage const isWinning = growthPercentage > hodlPercentage
const classes = const classes =
baseBadgeClass() + (isWinning ? 'badge-success' : 'badge-content') baseBadgeClass() + (isWinning ? 'badge-success' : 'badge-content')
return <div className={classes}>{isWinning ? 'Winning' : 'Losing'}</div> return <div className={classes}>{isWinning ? 'Winning' : 'Losing'}</div>
} }
} }
// function that return the number of day between a date and today // function that return the number of day between a date and today
function daysBetween(date: Date) { function daysBetween(date: Date) {
const oneDay = 24 * 60 * 60 * 1000 // hours*minutes*seconds*milliseconds const oneDay = 24 * 60 * 60 * 1000 // hours*minutes*seconds*milliseconds
const firstDate = new Date(date) const firstDate = new Date(date)
const secondDate = new Date() const secondDate = new Date()
const diffDays = Math.round( const diffDays = Math.round(
Math.abs((firstDate.getTime() - secondDate.getTime()) / oneDay) Math.abs((firstDate.getTime() - secondDate.getTime()) / oneDay)
) )
return diffDays return diffDays
} }
const BacktestCards: React.FC<IBacktestCards> = ({ list, setBacktests }) => { const BacktestCards: React.FC<IBacktestCards> = ({list, setBacktests}) => {
console.log(list) console.log(list)
const { apiUrl } = useApiUrlStore() const {apiUrl} = useApiUrlStore()
const [showMoneyManagementModal, setShowMoneyManagementModal] = const [showMoneyManagementModal, setShowMoneyManagementModal] =
React.useState(false) React.useState(false)
const [selectedMoneyManagement, setSelectedMoneyManagement] = const [selectedMoneyManagement, setSelectedMoneyManagement] =
React.useState<MoneyManagement>() React.useState<MoneyManagement>()
const [showBotNameModal, setShowBotNameModal] = useState(false) const [showBotNameModal, setShowBotNameModal] = useState(false)
const [isForWatchOnly, setIsForWatchOnly] = useState(false) const [isForWatchOnly, setIsForWatchOnly] = useState(false)
const [currentBacktest, setCurrentBacktest] = useState<Backtest | null>(null) const [currentBacktest, setCurrentBacktest] = useState<Backtest | null>(null)
const [selectedMoneyManagementName, setSelectedMoneyManagementName] = useState<string>('') const [selectedMoneyManagementName, setSelectedMoneyManagementName] = useState<string>('')
// Fetch money managements // Fetch money managements
const { data: moneyManagements } = useQuery({ const {data: moneyManagements} = useQuery({
queryFn: async () => { queryFn: async () => {
const moneyManagementClient = new MoneyManagementClient({}, apiUrl) const moneyManagementClient = new MoneyManagementClient({}, apiUrl)
return await moneyManagementClient.moneyManagement_GetMoneyManagements() return await moneyManagementClient.moneyManagement_GetMoneyManagements()
}, },
queryKey: ['moneyManagements'], queryKey: ['moneyManagements'],
}) })
// Set the first money management as default when the data is loaded // Set the first money management as default when the data is loaded
useEffect(() => { useEffect(() => {
if (moneyManagements && moneyManagements.length > 0) { if (moneyManagements && moneyManagements.length > 0) {
setSelectedMoneyManagementName(moneyManagements[0].name) setSelectedMoneyManagementName(moneyManagements[0].name)
} }
}, [moneyManagements]) }, [moneyManagements])
async function runBot(botName: string, backtest: Backtest, isForWatchOnly: boolean, moneyManagementName: string, initialTradingBalance: number) { async function runBot(botName: string, backtest: Backtest, isForWatchOnly: boolean, moneyManagementName: string, initialTradingBalance: number) {
const t = new Toast('Bot is starting') const t = new Toast('Bot is starting')
const client = new BotClient({}, apiUrl) const client = new BotClient({}, apiUrl)
// Check if the money management name is "custom" or contains "custom" // Check if the money management name is "custom" or contains "custom"
const isCustomMoneyManagement = const isCustomMoneyManagement =
!moneyManagementName || !moneyManagementName ||
moneyManagementName.toLowerCase() === 'custom' || moneyManagementName.toLowerCase() === 'custom' ||
moneyManagementName.toLowerCase().includes('custom'); moneyManagementName.toLowerCase().includes('custom');
// Create TradingBotConfig from the backtest configuration // Create TradingBotConfig from the backtest configuration
const tradingBotConfig: TradingBotConfig = { const tradingBotConfig: TradingBotConfig = {
accountName: backtest.config.accountName, accountName: backtest.config.accountName,
ticker: backtest.config.ticker, ticker: backtest.config.ticker,
scenarioName: backtest.config.scenarioName, scenarioName: backtest.config.scenarioName,
timeframe: backtest.config.timeframe, timeframe: backtest.config.timeframe,
botType: backtest.config.botType, isForWatchingOnly: isForWatchOnly,
isForWatchingOnly: isForWatchOnly, isForBacktest: false, // This is for running a live bot
isForBacktest: false, // This is for running a live bot cooldownPeriod: backtest.config.cooldownPeriod,
cooldownPeriod: backtest.config.cooldownPeriod, maxLossStreak: backtest.config.maxLossStreak,
maxLossStreak: backtest.config.maxLossStreak, maxPositionTimeHours: backtest.config.maxPositionTimeHours,
maxPositionTimeHours: backtest.config.maxPositionTimeHours, flipOnlyWhenInProfit: backtest.config.flipOnlyWhenInProfit,
flipOnlyWhenInProfit: backtest.config.flipOnlyWhenInProfit, flipPosition: backtest.config.flipPosition,
flipPosition: backtest.config.flipPosition, name: botName,
name: botName, botTradingBalance: initialTradingBalance,
botTradingBalance: initialTradingBalance, // Use the optimized or original money management from backtest if it's custom
// Use the optimized or original money management from backtest if it's custom moneyManagement: isCustomMoneyManagement ?
moneyManagement: isCustomMoneyManagement ? (backtest.optimizedMoneyManagement || backtest.config.moneyManagement || {
(backtest.optimizedMoneyManagement || backtest.config.moneyManagement || { name: 'default',
name: 'default', leverage: 1,
leverage: 1, stopLoss: 0.01,
stopLoss: 0.01, takeProfit: 0.02,
takeProfit: 0.02, timeframe: backtest.config.timeframe
timeframe: backtest.config.timeframe }) :
}) : backtest.config.moneyManagement, // Always provide a valid MoneyManagement object
backtest.config.moneyManagement, // Always provide a valid MoneyManagement object closeEarlyWhenProfitable: backtest.config.closeEarlyWhenProfitable || false
closeEarlyWhenProfitable: backtest.config.closeEarlyWhenProfitable || false };
};
const request: StartBotRequest = { const request: StartBotRequest = {
config: tradingBotConfig as unknown as TradingBotConfigRequest, config: tradingBotConfig as unknown as TradingBotConfigRequest,
}
await client
.bot_Start(request)
.then((botStatus: string) => {
t.update('info', 'Bot status: ' + botStatus)
})
.catch((err) => {
t.update('error', 'Error: ' + err)
})
} }
await client const handleOpenBotNameModal = (backtest: Backtest, isForWatchOnly: boolean) => {
.bot_Start(request) setCurrentBacktest(backtest)
.then((botStatus: string) => { setIsForWatchOnly(isForWatchOnly)
t.update('info', 'Bot status: ' + botStatus) setShowBotNameModal(true)
})
.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 runOptimizedBacktest(backtest: Backtest) {
const t = new Toast('Optimized backtest is running')
const client = new BacktestClient({}, apiUrl)
// Calculate dates for the API call
const startDate = backtest.candles[0].date
const endDate = backtest.candles[backtest.candles.length - 1].date
// Create optimized backtest config
const optimizedConfig: TradingBotConfig = {
...backtest.config,
name: `${backtest.config.ticker}-${backtest.config.scenarioName}-Optimized`,
moneyManagement: backtest.optimizedMoneyManagement || backtest.config.moneyManagement
} }
const request: RunBacktestRequest = { const handleCloseBotNameModal = () => {
config: optimizedConfig as unknown as TradingBotConfigRequest, setShowBotNameModal(false)
startDate: startDate,
endDate: endDate,
balance: backtest.walletBalances[0].value,
watchOnly: false,
save: false,
} }
await client const handleSubmitBotName = (botName: string, backtest: Backtest, isForWatchOnly: boolean, moneyManagementName: string, initialTradingBalance: number) => {
.backtest_Run(request) runBot(botName, backtest, isForWatchOnly, moneyManagementName, initialTradingBalance)
.then((backtest: Backtest) => { setShowBotNameModal(false)
t.update('success', `${backtest.config.ticker} Backtest Succeeded`) }
setBacktests((arr: Backtest[]) => [...arr, backtest])
})
.catch((err) => {
t.update('error', 'Error :' + err)
})
}
function saveMoneyManagement(moneyManagement: MoneyManagement) { async function runOptimizedBacktest(backtest: Backtest) {
setSelectedMoneyManagement(moneyManagement) const t = new Toast('Optimized backtest is running')
setShowMoneyManagementModal(true) const client = new BacktestClient({}, apiUrl)
}
return ( // Calculate dates for the API call
<div className="flex flex-wrap m-4 -mx-4"> const startDate = backtest.candles[0].date
{list?.map((backtest: Backtest, index: number) => ( const endDate = backtest.candles[backtest.candles.length - 1].date
<div
key={index.toString()}
className="sm:w-1/2 md:w-1/2 xl:w-1/2 w-full p-2"
>
<div className="indicator">
<div className="indicator-item indicator-top">
<button className="btn btn-primary h-5 min-h-0 px-2 mr-5 rounded-full">
<TrashIcon width={15}></TrashIcon>
</button>
</div>
<div className="card bg-base-300 shadow-xl"> // Create optimized backtest config
<figure className="z-0"> const optimizedConfig: TradingBotConfig = {
{ ...backtest.config,
<TradeChart name: `${backtest.config.ticker}-${backtest.config.scenarioName}-Optimized`,
candles={backtest.candles} moneyManagement: backtest.optimizedMoneyManagement || backtest.config.moneyManagement
positions={backtest.positions} }
walletBalances={backtest.walletBalances}
signals={backtest.signals}
indicatorsValues={backtest.indicatorsValues}
width={720}
height={512}
></TradeChart>
}
</figure>
<div className="card-body"> const request: RunBacktestRequest = {
<h2 className="card-title text-sm"> config: optimizedConfig as unknown as TradingBotConfigRequest,
<div className="dropdown"> startDate: startDate,
<label endDate: endDate,
htmlFor={index.toString()} save: false,
tabIndex={index} }
className=""
> await client
<DotsVerticalIcon className="text-primary w-5 h-5" /> .backtest_Run(request)
</label> .then((backtest: Backtest) => {
<ul t.update('success', `${backtest.config.ticker} Backtest Succeeded`)
id={index.toString()} setBacktests((arr: Backtest[]) => [...arr, backtest])
className="dropdown-content menu bg-base-100 rounded-box w-52 p-2 shadow" })
> .catch((err) => {
<li> t.update('error', 'Error :' + err)
<button })
className="text-xs" }
onClick={() => handleOpenBotNameModal(backtest, false)}
> function saveMoneyManagement(moneyManagement: MoneyManagement) {
Run bot setSelectedMoneyManagement(moneyManagement)
</button> setShowMoneyManagementModal(true)
</li> }
<li>
<button return (
className="text-xs" <div className="flex flex-wrap m-4 -mx-4">
onClick={() => handleOpenBotNameModal(backtest, true)} {list?.map((backtest: Backtest, index: number) => (
> <div
Run watcher key={index.toString()}
</button> className="sm:w-1/2 md:w-1/2 xl:w-1/2 w-full p-2"
</li> >
<li> <div className="indicator">
<button <div className="indicator-item indicator-top">
className="text-xs" <button className="btn btn-primary h-5 min-h-0 px-2 mr-5 rounded-full">
onClick={() => <TrashIcon width={15}></TrashIcon>
saveMoneyManagement(backtest.config.moneyManagement) </button>
} </div>
>
Save money management <div className="card bg-base-300 shadow-xl">
</button> <figure className="z-0">
</li> {
<li> <TradeChart
<button candles={backtest.candles}
className="text-xs" positions={backtest.positions}
onClick={() => runOptimizedBacktest(backtest)} walletBalances={backtest.walletBalances}
> signals={backtest.signals}
Run optimized money management indicatorsValues={backtest.indicatorsValues}
</button> width={720}
</li> height={512}
</ul> ></TradeChart>
</div> }
{backtest.config.ticker} </figure>
{botStatusResult(
backtest.growthPercentage, <div className="card-body">
backtest.hodlPercentage <h2 className="card-title text-sm">
)} <div className="dropdown">
</h2> <label
<div className="columns-4 mb-2"> htmlFor={index.toString()}
<div> tabIndex={index}
<CardText className=""
title="Ticker" >
content={backtest.config.ticker} <DotsVerticalIcon className="text-primary w-5 h-5"/>
></CardText> </label>
<CardText <ul
title="Account" id={index.toString()}
content={backtest.config.accountName} className="dropdown-content menu bg-base-100 rounded-box w-52 p-2 shadow"
></CardText> >
<CardText <li>
title="Scenario" <button
content={backtest.config.scenarioName} className="text-xs"
></CardText> onClick={() => handleOpenBotNameModal(backtest, false)}
<CardText >
title="Timeframe" Run bot
content={backtest.config.timeframe?.toString()} </button>
></CardText> </li>
</div> <li>
</div> <button
<div className="columns-4 mb-2"> className="text-xs"
<CardText onClick={() => handleOpenBotNameModal(backtest, true)}
title="Duration" >
content={moment Run watcher
.duration( </button>
moment( </li>
backtest.candles[backtest.candles.length - 1].date <li>
).diff(backtest.candles[0].date) <button
) className="text-xs"
.humanize()} onClick={() =>
></CardText> saveMoneyManagement(backtest.config.moneyManagement)
{/* <CardSignal signals={backtest.signals}></CardSignal> */} }
<CardPosition >
positivePosition={true} Save money management
positions={backtest.positions.filter((p) => { </button>
const realized = p.profitAndLoss?.realized ?? 0 </li>
return realized > 0 ? p : null <li>
})} <button
></CardPosition> className="text-xs"
<CardPosition onClick={() => runOptimizedBacktest(backtest)}
positivePosition={false} >
positions={backtest.positions.filter((p) => { Run optimized money management
const realized = p.profitAndLoss?.realized ?? 0 </button>
return realized <= 0 ? p : null </li>
})} </ul>
></CardPosition> </div>
<CardPositionItem {backtest.config.ticker}
positions={backtest.positions} {botStatusResult(
></CardPositionItem> backtest.growthPercentage,
backtest.hodlPercentage
)}
</h2>
<div className="columns-4 mb-2">
<div>
<CardText
title="Ticker"
content={backtest.config.ticker}
></CardText>
<CardText
title="Account"
content={backtest.config.accountName}
></CardText>
<CardText
title="Scenario"
content={backtest.config.scenarioName ?? backtest.config.scenario?.name}
></CardText>
<CardText
title="Timeframe"
content={backtest.config.timeframe?.toString()}
></CardText>
</div>
</div>
<div className="columns-4 mb-2">
<CardText
title="Duration"
content={moment
.duration(
moment(
backtest.candles[backtest.candles.length - 1].date
).diff(backtest.candles[0].date)
)
.humanize()}
></CardText>
{/* <CardSignal signals={backtest.signals}></CardSignal> */}
<CardPosition
positivePosition={true}
positions={backtest.positions.filter((p) => {
const realized = p.profitAndLoss?.realized ?? 0
return realized > 0 ? p : null
})}
></CardPosition>
<CardPosition
positivePosition={false}
positions={backtest.positions.filter((p) => {
const realized = p.profitAndLoss?.realized ?? 0
return realized <= 0 ? p : null
})}
></CardPosition>
<CardPositionItem
positions={backtest.positions}
></CardPositionItem>
</div>
<div className="columns-4 mb-2">
<div>
<CardText
title="Max Drowdown"
content={
backtest.statistics.maxDrawdown?.toFixed(4).toString() +
'$'
}
></CardText>
<CardText
title="PNL"
content={backtest.finalPnl?.toFixed(4).toString() + '$'}
></CardText>
<CardText
title="Sharpe Ratio"
content={
(backtest.statistics.sharpeRatio
? backtest.statistics.sharpeRatio * 100
: 0
)
.toFixed(4)
.toString() + '%'
}
></CardText>
<CardText
title="%Hodl"
content={
backtest.hodlPercentage?.toFixed(2).toString() + '%'
}
></CardText>
</div>
</div>
<div className="card-actions justify-center pt-2 text-sm">
<div className={baseBadgeClass(true)}>
WR {backtest.winRate?.toFixed(2).toString()} %
</div>
<div className={baseBadgeClass(true)}>
PNL {backtest.growthPercentage?.toFixed(2).toString()} %
</div>
</div>
</div>
</div>
</div>
</div> </div>
))}
<div className="columns-4 mb-2"> <MoneyManagementModal
<div> showModal={showMoneyManagementModal}
<CardText moneyManagement={selectedMoneyManagement}
title="Max Drowdown" onClose={() => setShowMoneyManagementModal(false)}
content={ />
backtest.statistics.maxDrawdown?.toFixed(4).toString() +
'$' {showBotNameModal && currentBacktest && moneyManagements && (
} <BotNameModal
></CardText> showModal={showBotNameModal}
<CardText onClose={handleCloseBotNameModal}
title="PNL" backtest={currentBacktest}
content={backtest.finalPnl?.toFixed(4).toString() + '$'} isForWatchOnly={isForWatchOnly}
></CardText> onSubmitBotName={(botName, backtest, isForWatchOnly, moneyManagementName, initialTradingBalance) =>
<CardText handleSubmitBotName(botName, backtest, isForWatchOnly, moneyManagementName, initialTradingBalance)
title="Sharpe Ratio" }
content={ moneyManagements={moneyManagements}
(backtest.statistics.sharpeRatio selectedMoneyManagement={selectedMoneyManagementName}
? backtest.statistics.sharpeRatio * 100 setSelectedMoneyManagement={setSelectedMoneyManagementName}
: 0 />
) )}
.toFixed(4)
.toString() + '%'
}
></CardText>
<CardText
title="%Hodl"
content={
backtest.hodlPercentage?.toFixed(2).toString() + '%'
}
></CardText>
</div>
</div>
<div className="card-actions justify-center pt-2 text-sm">
<div className={baseBadgeClass(true)}>
WR {backtest.winRate?.toFixed(2).toString()} %
</div>
<div className={baseBadgeClass(true)}>
PNL {backtest.growthPercentage?.toFixed(2).toString()} %
</div>
</div>
</div>
</div>
</div>
</div> </div>
))} )
<MoneyManagementModal
showModal={showMoneyManagementModal}
moneyManagement={selectedMoneyManagement}
onClose={() => setShowMoneyManagementModal(false)}
/>
{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={selectedMoneyManagementName}
setSelectedMoneyManagement={setSelectedMoneyManagementName}
/>
)}
</div>
)
} }
export default BacktestCards export default BacktestCards

View File

@@ -351,16 +351,15 @@ const BacktestRowDetails: React.FC<IBacktestRowDetailsProps> = ({
content={getAverageTradesPerDay() + " trades/day"} content={getAverageTradesPerDay() + " trades/day"}
></CardText> ></CardText>
</div> </div>
<div> <div className="w-full">
<figure> <figure className="w-full">
<TradeChart <TradeChart
width={1400}
height={1100}
candles={candles} candles={candles}
positions={positions} positions={positions}
walletBalances={walletBalances} walletBalances={walletBalances}
indicatorsValues={indicatorsValues} indicatorsValues={indicatorsValues}
signals={signals} signals={signals}
height={1000}
></TradeChart> ></TradeChart>
</figure> </figure>
</div> </div>

View File

@@ -4,491 +4,491 @@ import React, {useEffect, useState} from 'react'
import useApiUrlStore from '../../../app/store/apiStore' import useApiUrlStore from '../../../app/store/apiStore'
import type {Backtest} from '../../../generated/ManagingApi' import type {Backtest} from '../../../generated/ManagingApi'
import {BacktestClient} from '../../../generated/ManagingApi' import {BacktestClient} from '../../../generated/ManagingApi'
import type {IBacktestCards} from '../../../global/type' import type {IBacktestCards} from '../../../global/type.tsx'
import {CardText, SelectColumnFilter, Table} from '../../mollecules' import {CardText, SelectColumnFilter, Table} from '../../mollecules'
import {UnifiedTradingModal} from '../index' import {UnifiedTradingModal} from '../index'
import Toast from '../../mollecules/Toast/Toast' import Toast from '../../mollecules/Toast/Toast'
import BacktestRowDetails from './backtestRowDetails' import BacktestRowDetails from './backtestRowDetails'
const BacktestTable: React.FC<IBacktestCards> = ({ list, isFetching, setBacktests }) => { const BacktestTable: React.FC<IBacktestCards> = ({list, isFetching, setBacktests}) => {
const [rows, setRows] = useState<Backtest[]>([]) const [rows, setRows] = useState<Backtest[]>([])
const { apiUrl } = useApiUrlStore() const {apiUrl} = useApiUrlStore()
const [optimizedMoneyManagement, setOptimizedMoneyManagement] = useState({ const [optimizedMoneyManagement, setOptimizedMoneyManagement] = useState({
stopLoss: 0, stopLoss: 0,
takeProfit: 0, takeProfit: 0,
}) })
const [positionTimingStats, setPositionTimingStats] = useState({ const [positionTimingStats, setPositionTimingStats] = useState({
averageOpenTime: 0, averageOpenTime: 0,
medianOpenTime: 0, medianOpenTime: 0,
losingPositionsAverageOpenTime: 0, losingPositionsAverageOpenTime: 0,
}) })
const [cooldownRecommendations, setCooldownRecommendations] = useState({ const [cooldownRecommendations, setCooldownRecommendations] = useState({
averageCooldown: 0, averageCooldown: 0,
medianCooldown: 0, medianCooldown: 0,
}) })
// Bot configuration modal state
const [showBotConfigModal, setShowBotConfigModal] = useState(false)
const [selectedBacktest, setSelectedBacktest] = useState<Backtest | null>(null)
// Backtest configuration modal state // Bot configuration modal state
const [showBacktestConfigModal, setShowBacktestConfigModal] = useState(false) const [showBotConfigModal, setShowBotConfigModal] = useState(false)
const [selectedBacktestForRerun, setSelectedBacktestForRerun] = useState<Backtest | null>(null) const [selectedBacktest, setSelectedBacktest] = useState<Backtest | null>(null)
const handleOpenBotConfigModal = (backtest: Backtest) => { // Backtest configuration modal state
setSelectedBacktest(backtest) const [showBacktestConfigModal, setShowBacktestConfigModal] = useState(false)
setShowBotConfigModal(true) const [selectedBacktestForRerun, setSelectedBacktestForRerun] = useState<Backtest | null>(null)
}
const handleCloseBotConfigModal = () => { const handleOpenBotConfigModal = (backtest: Backtest) => {
setShowBotConfigModal(false) setSelectedBacktest(backtest)
setSelectedBacktest(null) setShowBotConfigModal(true)
} }
const handleOpenBacktestConfigModal = (backtest: Backtest) => { const handleCloseBotConfigModal = () => {
setSelectedBacktestForRerun(backtest) setShowBotConfigModal(false)
setShowBacktestConfigModal(true) setSelectedBacktest(null)
} }
const handleCloseBacktestConfigModal = () => { const handleOpenBacktestConfigModal = (backtest: Backtest) => {
setShowBacktestConfigModal(false) setSelectedBacktestForRerun(backtest)
setSelectedBacktestForRerun(null) setShowBacktestConfigModal(true)
} }
async function deleteBacktest(id: string) { const handleCloseBacktestConfigModal = () => {
const t = new Toast('Deleting backtest') setShowBacktestConfigModal(false)
const client = new BacktestClient({}, apiUrl) setSelectedBacktestForRerun(null)
}
await client async function deleteBacktest(id: string) {
.backtest_DeleteBacktest(id) const t = new Toast('Deleting backtest')
.then(() => { const client = new BacktestClient({}, apiUrl)
t.update('success', 'Backtest deleted')
// Remove the deleted backtest from the list
if (list) {
const updatedList = list.filter(backtest => backtest.id !== id);
setBacktests(updatedList);
}
})
.catch((err) => {
t.update('error', err)
})
}
const getScoreColor = (score: number) => { await client
if (score >= 75) return '#08C25F'; // success .backtest_DeleteBacktest(id)
if (score >= 50) return '#B0DB43'; // info .then(() => {
if (score >= 25) return '#EB6F22'; // warning t.update('success', 'Backtest deleted')
return '#FF5340'; // error // Remove the deleted backtest from the list
}; if (list) {
const updatedList = list.filter(backtest => backtest.id !== id);
setBacktests(updatedList);
}
})
.catch((err) => {
t.update('error', err)
})
}
const columns = React.useMemo( const getScoreColor = (score: number) => {
() => [ if (score >= 75) return '#08C25F'; // success
{ if (score >= 50) return '#B0DB43'; // info
Header: 'Informations', if (score >= 25) return '#EB6F22'; // warning
columns: [ return '#FF5340'; // error
{ };
Cell: ({ row }: any) => (
// Use Cell to render an expander for each row. const columns = React.useMemo(
// We can use the getToggleRowExpandedProps prop-getter () => [
// to build the expander. {
<span {...row.getToggleRowExpandedProps()}> Header: 'Informations',
columns: [
{
Cell: ({row}: any) => (
// Use Cell to render an expander for each row.
// We can use the getToggleRowExpandedProps prop-getter
// to build the expander.
<span {...row.getToggleRowExpandedProps()}>
{row.isExpanded ? ( {row.isExpanded ? (
<ChevronDownIcon></ChevronDownIcon> <ChevronDownIcon></ChevronDownIcon>
) : ( ) : (
<ChevronRightIcon></ChevronRightIcon> <ChevronRightIcon></ChevronRightIcon>
)} )}
</span> </span>
), ),
// Make sure it has an ID // Make sure it has an ID
Header: ({ Header: ({
getToggleAllRowsExpandedProps, getToggleAllRowsExpandedProps,
isAllRowsExpanded, isAllRowsExpanded,
}: any) => ( }: any) => (
<span {...getToggleAllRowsExpandedProps()}> <span {...getToggleAllRowsExpandedProps()}>
{isAllRowsExpanded ? 'v' : '>'} {isAllRowsExpanded ? 'v' : '>'}
</span> </span>
), ),
// Build our expander column // Build our expander column
id: 'expander', id: 'expander',
}, },
{ {
Header: 'Score', Header: 'Score',
accessor: 'score', accessor: 'score',
Cell: ({ cell }: any) => ( Cell: ({cell}: any) => (
<span style={{ <span style={{
color: getScoreColor(cell.row.values.score), color: getScoreColor(cell.row.values.score),
fontWeight: 500, fontWeight: 500,
display: 'inline-block', display: 'inline-block',
width: '60px' width: '60px'
}}> }}>
{cell.row.values.score.toFixed(2)} {cell.row.values.score.toFixed(2)}
</span> </span>
), ),
disableFilters: true, disableFilters: true,
}, },
{ {
Filter: SelectColumnFilter, Filter: SelectColumnFilter,
Header: 'Ticker', Header: 'Ticker',
accessor: 'config.ticker', accessor: 'config.ticker',
disableSortBy: true, disableSortBy: true,
}, },
{ {
Filter: SelectColumnFilter, Filter: SelectColumnFilter,
Header: 'Timeframe', Header: 'Timeframe',
accessor: 'config.timeframe', accessor: 'config.timeframe',
disableSortBy: true, disableSortBy: true,
}, },
{ {
Filter: SelectColumnFilter, Filter: SelectColumnFilter,
Header: 'Scenario', Header: 'Scenario',
accessor: 'config.scenarioName', accessor: 'config.scenarioName',
disableSortBy: true, disableSortBy: true,
}, },
{ {
Filter: SelectColumnFilter, Filter: SelectColumnFilter,
Header: 'BotType', Header: 'BotType',
accessor: 'config.botType', accessor: 'config.botType',
disableSortBy: true, disableSortBy: true,
}, },
],
},
{
Header: 'Results',
columns: [
{
Cell: ({cell}: any) => (
<>{cell.row.values.finalPnl.toFixed(2)} $</>
),
Header: 'Pnl $',
accessor: 'finalPnl',
disableFilters: true,
sortType: 'basic',
},
{
Cell: ({cell}: any) => (
<>{cell.row.values.hodlPercentage.toFixed(2)} %</>
),
Header: 'Hodl %',
accessor: 'hodlPercentage',
disableFilters: true,
sortType: 'basic',
},
{
Cell: ({cell}: any) => <>{cell.row.values.winRate} %</>,
Header: 'Winrate',
accessor: 'winRate',
disableFilters: true,
},
{
Cell: ({cell}: any) => (
<>{cell.row.values.growthPercentage.toFixed(2)} %</>
),
Header: 'Pnl %',
accessor: 'growthPercentage',
disableFilters: true,
sortType: 'basic',
},
{
Cell: ({cell}: any) => (
<>
{(
cell.row.values.growthPercentage -
cell.row.values.hodlPercentage
).toFixed(2)}
</>
),
Header: 'H/P',
accessor: 'diff',
disableFilters: true,
sortType: 'basic',
},
],
},
{
Header: 'Action',
columns: [
{
Cell: ({cell}: any) => (
<>
<div className="tooltip" data-tip="Delete backtest">
<button
data-value={cell.row.values.name}
onClick={() => deleteBacktest(cell.row.values.id)}
>
<TrashIcon className="text-accent w-4"></TrashIcon>
</button>
</div>
</>
),
Header: '',
accessor: 'id',
disableFilters: true,
},
{
Cell: ({cell}: any) => (
<>
<div className="tooltip" data-tip="Re-run backtest with same config">
<button
data-value={cell.row.values.name}
onClick={() => handleOpenBacktestConfigModal(cell.row.original as Backtest)}
>
<CogIcon className="text-info w-4"></CogIcon>
</button>
</div>
</>
),
Header: '',
accessor: 'rerun',
disableFilters: true,
},
{
Cell: ({cell}: any) => (
<>
<div className="tooltip" data-tip="Create bot from backtest">
<button
data-value={cell.row.values.name}
onClick={() => handleOpenBotConfigModal(cell.row.original as Backtest)}
>
<PlayIcon className="text-primary w-4"></PlayIcon>
</button>
</div>
</>
),
Header: '',
accessor: 'runner',
disableFilters: true,
}
],
},
], ],
}, []
{ )
Header: 'Results',
columns: [
{
Cell: ({ cell }: any) => (
<>{cell.row.values.finalPnl.toFixed(2)} $</>
),
Header: 'Pnl $',
accessor: 'finalPnl',
disableFilters: true,
sortType: 'basic',
},
{
Cell: ({ cell }: any) => (
<>{cell.row.values.hodlPercentage.toFixed(2)} %</>
),
Header: 'Hodl %',
accessor: 'hodlPercentage',
disableFilters: true,
sortType: 'basic',
},
{
Cell: ({ cell }: any) => <>{cell.row.values.winRate} %</>,
Header: 'Winrate',
accessor: 'winRate',
disableFilters: true,
},
{
Cell: ({ cell }: any) => (
<>{cell.row.values.growthPercentage.toFixed(2)} %</>
),
Header: 'Pnl %',
accessor: 'growthPercentage',
disableFilters: true,
sortType: 'basic',
},
{
Cell: ({ cell }: any) => (
<>
{(
cell.row.values.growthPercentage -
cell.row.values.hodlPercentage
).toFixed(2)}
</>
),
Header: 'H/P',
accessor: 'diff',
disableFilters: true,
sortType: 'basic',
},
],
},
{
Header: 'Action',
columns: [
{
Cell: ({ cell }: any) => (
<>
<div className="tooltip" data-tip="Delete backtest">
<button
data-value={cell.row.values.name}
onClick={() => deleteBacktest(cell.row.values.id)}
>
<TrashIcon className="text-accent w-4"></TrashIcon>
</button>
</div>
</>
),
Header: '',
accessor: 'id',
disableFilters: true,
},
{
Cell: ({ cell }: any) => (
<>
<div className="tooltip" data-tip="Re-run backtest with same config">
<button
data-value={cell.row.values.name}
onClick={() => handleOpenBacktestConfigModal(cell.row.original as Backtest)}
>
<CogIcon className="text-info w-4"></CogIcon>
</button>
</div>
</>
),
Header: '',
accessor: 'rerun',
disableFilters: true,
},
{
Cell: ({ cell }: any) => (
<>
<div className="tooltip" data-tip="Create bot from backtest">
<button
data-value={cell.row.values.name}
onClick={() => handleOpenBotConfigModal(cell.row.original as Backtest)}
>
<PlayIcon className="text-primary w-4"></PlayIcon>
</button>
</div>
</>
),
Header: '',
accessor: 'runner',
disableFilters: true,
}
],
},
],
[]
)
useEffect(() => { useEffect(() => {
if (list) { if (list) {
setRows(list) setRows(list)
// Calculate average optimized money management
if (list.length > 0) {
const optimized = list.map((b) => b.optimizedMoneyManagement);
const stopLoss = optimized.reduce((acc, curr) => acc + (curr?.stopLoss ?? 0), 0);
const takeProfit = optimized.reduce((acc, curr) => acc + (curr?.takeProfit ?? 0), 0);
setOptimizedMoneyManagement({
stopLoss: stopLoss / optimized.length,
takeProfit: takeProfit / optimized.length,
});
// Calculate position timing statistics // Calculate average optimized money management
const allPositions = list.flatMap(backtest => backtest.positions); if (list.length > 0) {
const finishedPositions = allPositions.filter(p => p.status === 'Finished'); const optimized = list.map((b) => b.optimizedMoneyManagement);
const stopLoss = optimized.reduce((acc, curr) => acc + (curr?.stopLoss ?? 0), 0);
if (finishedPositions.length > 0) { const takeProfit = optimized.reduce((acc, curr) => acc + (curr?.takeProfit ?? 0), 0);
// Calculate position open times in hours
const openTimes = finishedPositions.map(position => {
const openTime = new Date(position.open.date);
// Find the closing trade (either stopLoss or takeProfit that was filled)
let closeTime = new Date();
if (position.stopLoss.status === 'Filled') {
closeTime = new Date(position.stopLoss.date);
} else if (position.takeProfit1.status === 'Filled') {
closeTime = new Date(position.takeProfit1.date);
} else if (position.takeProfit2?.status === 'Filled') {
closeTime = new Date(position.takeProfit2.date);
}
// Return time difference in hours
return (closeTime.getTime() - openTime.getTime()) / (1000 * 60 * 60);
});
// Calculate average setOptimizedMoneyManagement({
const averageOpenTime = openTimes.reduce((sum, time) => sum + time, 0) / openTimes.length; stopLoss: stopLoss / optimized.length,
takeProfit: takeProfit / optimized.length,
});
// Calculate median // Calculate position timing statistics
const sortedTimes = [...openTimes].sort((a, b) => a - b); const allPositions = list.flatMap(backtest => backtest.positions);
const medianOpenTime = sortedTimes.length % 2 === 0 const finishedPositions = allPositions.filter(p => p.status === 'Finished');
? (sortedTimes[sortedTimes.length / 2 - 1] + sortedTimes[sortedTimes.length / 2]) / 2
: sortedTimes[Math.floor(sortedTimes.length / 2)];
// Calculate average for losing positions if (finishedPositions.length > 0) {
const losingPositions = finishedPositions.filter(p => (p.profitAndLoss?.realized ?? 0) < 0); // Calculate position open times in hours
let losingPositionsAverageOpenTime = 0; const openTimes = finishedPositions.map(position => {
const openTime = new Date(position.open.date);
if (losingPositions.length > 0) { // Find the closing trade (either stopLoss or takeProfit that was filled)
const losingOpenTimes = losingPositions.map(position => { let closeTime = new Date();
const openTime = new Date(position.open.date);
let closeTime = new Date();
if (position.stopLoss.status === 'Filled') {
closeTime = new Date(position.stopLoss.date);
} else if (position.takeProfit1.status === 'Filled') {
closeTime = new Date(position.takeProfit1.date);
} else if (position.takeProfit2?.status === 'Filled') {
closeTime = new Date(position.takeProfit2.date);
}
return (closeTime.getTime() - openTime.getTime()) / (1000 * 60 * 60);
});
losingPositionsAverageOpenTime = losingOpenTimes.reduce((sum, time) => sum + time, 0) / losingOpenTimes.length;
}
setPositionTimingStats({ if (position.stopLoss.status === 'Filled') {
averageOpenTime, closeTime = new Date(position.stopLoss.date);
medianOpenTime, } else if (position.takeProfit1.status === 'Filled') {
losingPositionsAverageOpenTime, closeTime = new Date(position.takeProfit1.date);
}); } else if (position.takeProfit2?.status === 'Filled') {
} closeTime = new Date(position.takeProfit2.date);
}
// Calculate cooldown recommendations across all backtests // Return time difference in hours
const allCooldownValues: number[] = []; return (closeTime.getTime() - openTime.getTime()) / (1000 * 60 * 60);
});
list.forEach(backtest => {
if (backtest.positions.length < 2 || !backtest.candles || backtest.candles.length < 2) {
return;
}
// Determine candle timeframe in milliseconds // Calculate average
const candleTimeframeMs = new Date(backtest.candles[1].date).getTime() - new Date(backtest.candles[0].date).getTime(); const averageOpenTime = openTimes.reduce((sum, time) => sum + time, 0) / openTimes.length;
const sortedPositions = [...backtest.positions].sort((a, b) => { // Calculate median
const dateA = new Date(a.open.date).getTime(); const sortedTimes = [...openTimes].sort((a, b) => a - b);
const dateB = new Date(b.open.date).getTime(); const medianOpenTime = sortedTimes.length % 2 === 0
return dateA - dateB; ? (sortedTimes[sortedTimes.length / 2 - 1] + sortedTimes[sortedTimes.length / 2]) / 2
}); : sortedTimes[Math.floor(sortedTimes.length / 2)];
for (let i = 0; i < sortedPositions.length - 1; i++) { // Calculate average for losing positions
const currentPosition = sortedPositions[i]; const losingPositions = finishedPositions.filter(p => (p.profitAndLoss?.realized ?? 0) < 0);
const nextPosition = sortedPositions[i + 1]; let losingPositionsAverageOpenTime = 0;
const currentRealized = currentPosition.profitAndLoss?.realized ?? 0; if (losingPositions.length > 0) {
const nextRealized = nextPosition.profitAndLoss?.realized ?? 0; const losingOpenTimes = losingPositions.map(position => {
const openTime = new Date(position.open.date);
let closeTime = new Date();
// Check if current position is winning and next position is losing if (position.stopLoss.status === 'Filled') {
if (currentRealized > 0 && nextRealized <= 0) { closeTime = new Date(position.stopLoss.date);
// Calculate the close time of the current (winning) position } else if (position.takeProfit1.status === 'Filled') {
let currentCloseDate: Date | null = null; closeTime = new Date(position.takeProfit1.date);
if (currentPosition.profitAndLoss?.realized != null) { } else if (position.takeProfit2?.status === 'Filled') {
if (currentPosition.profitAndLoss.realized > 0) { closeTime = new Date(position.takeProfit2.date);
currentCloseDate = new Date(currentPosition.takeProfit1.date); }
} else {
currentCloseDate = new Date(currentPosition.stopLoss.date); return (closeTime.getTime() - openTime.getTime()) / (1000 * 60 * 60);
});
losingPositionsAverageOpenTime = losingOpenTimes.reduce((sum, time) => sum + time, 0) / losingOpenTimes.length;
}
setPositionTimingStats({
averageOpenTime,
medianOpenTime,
losingPositionsAverageOpenTime,
});
} }
}
if (currentCloseDate) { // Calculate cooldown recommendations across all backtests
const nextOpenDate = new Date(nextPosition.open.date); const allCooldownValues: number[] = [];
const gapInMs = nextOpenDate.getTime() - currentCloseDate.getTime();
list.forEach(backtest => {
if (gapInMs >= 0) { // Only consider positive gaps if (backtest.positions.length < 2 || !backtest.candles || backtest.candles.length < 2) {
// Convert milliseconds to number of candles return;
const gapInCandles = Math.floor(gapInMs / candleTimeframeMs); }
allCooldownValues.push(gapInCandles);
// Determine candle timeframe in milliseconds
const candleTimeframeMs = new Date(backtest.candles[1].date).getTime() - new Date(backtest.candles[0].date).getTime();
const sortedPositions = [...backtest.positions].sort((a, b) => {
const dateA = new Date(a.open.date).getTime();
const dateB = new Date(b.open.date).getTime();
return dateA - dateB;
});
for (let i = 0; i < sortedPositions.length - 1; i++) {
const currentPosition = sortedPositions[i];
const nextPosition = sortedPositions[i + 1];
const currentRealized = currentPosition.profitAndLoss?.realized ?? 0;
const nextRealized = nextPosition.profitAndLoss?.realized ?? 0;
// Check if current position is winning and next position is losing
if (currentRealized > 0 && nextRealized <= 0) {
// Calculate the close time of the current (winning) position
let currentCloseDate: Date | null = null;
if (currentPosition.profitAndLoss?.realized != null) {
if (currentPosition.profitAndLoss.realized > 0) {
currentCloseDate = new Date(currentPosition.takeProfit1.date);
} else {
currentCloseDate = new Date(currentPosition.stopLoss.date);
}
}
if (currentCloseDate) {
const nextOpenDate = new Date(nextPosition.open.date);
const gapInMs = nextOpenDate.getTime() - currentCloseDate.getTime();
if (gapInMs >= 0) { // Only consider positive gaps
// Convert milliseconds to number of candles
const gapInCandles = Math.floor(gapInMs / candleTimeframeMs);
allCooldownValues.push(gapInCandles);
}
}
}
}
});
if (allCooldownValues.length > 0) {
// Calculate average cooldown
const averageCooldown = allCooldownValues.reduce((sum, value) => sum + value, 0) / allCooldownValues.length;
// Calculate median cooldown
const sortedCooldowns = [...allCooldownValues].sort((a, b) => a - b);
const medianCooldown = sortedCooldowns.length % 2 === 0
? (sortedCooldowns[sortedCooldowns.length / 2 - 1] + sortedCooldowns[sortedCooldowns.length / 2]) / 2
: sortedCooldowns[Math.floor(sortedCooldowns.length / 2)];
setCooldownRecommendations({
averageCooldown: Math.ceil(averageCooldown),
medianCooldown: Math.ceil(medianCooldown),
});
} }
}
} }
}
});
if (allCooldownValues.length > 0) {
// Calculate average cooldown
const averageCooldown = allCooldownValues.reduce((sum, value) => sum + value, 0) / allCooldownValues.length;
// Calculate median cooldown
const sortedCooldowns = [...allCooldownValues].sort((a, b) => a - b);
const medianCooldown = sortedCooldowns.length % 2 === 0
? (sortedCooldowns[sortedCooldowns.length / 2 - 1] + sortedCooldowns[sortedCooldowns.length / 2]) / 2
: sortedCooldowns[Math.floor(sortedCooldowns.length / 2)];
setCooldownRecommendations({
averageCooldown: Math.ceil(averageCooldown),
medianCooldown: Math.ceil(medianCooldown),
});
} }
} }, [list])
}
}, [list])
return ( return (
<>
{isFetching ? (
<div className="flex justify-center">
<progress className="progress progress-primary w-56"></progress>
</div>
) : (
<> <>
{list && list.length > 0 && ( {isFetching ? (
<> <div className="flex justify-center">
<div className="mb-4"> <progress className="progress progress-primary w-56"></progress>
<CardText </div>
title="Average Optimized Money Management" ) : (
content={ <>
"SL: " + optimizedMoneyManagement.stopLoss.toFixed(2) + "% | TP: " + {list && list.length > 0 && (
optimizedMoneyManagement.takeProfit.toFixed(2) + "% | R/R: " + <>
(optimizedMoneyManagement.takeProfit / optimizedMoneyManagement.stopLoss || 0).toFixed(2) <div className="mb-4">
} <CardText
/> title="Average Optimized Money Management"
</div> content={
<div className="mb-4"> "SL: " + optimizedMoneyManagement.stopLoss.toFixed(2) + "% | TP: " +
<CardText optimizedMoneyManagement.takeProfit.toFixed(2) + "% | R/R: " +
title="Position Timing Statistics" (optimizedMoneyManagement.takeProfit / optimizedMoneyManagement.stopLoss || 0).toFixed(2)
content={ }
"Avg: " + positionTimingStats.averageOpenTime.toFixed(1) + "h | " + />
"Median: " + positionTimingStats.medianOpenTime.toFixed(1) + "h | " + </div>
"Losing Avg: " + positionTimingStats.losingPositionsAverageOpenTime.toFixed(1) + "h" <div className="mb-4">
} <CardText
/> title="Position Timing Statistics"
</div> content={
<div className="mb-4"> "Avg: " + positionTimingStats.averageOpenTime.toFixed(1) + "h | " +
<CardText "Median: " + positionTimingStats.medianOpenTime.toFixed(1) + "h | " +
title="Cooldown Recommendations" "Losing Avg: " + positionTimingStats.losingPositionsAverageOpenTime.toFixed(1) + "h"
content={ }
"Avg: " + cooldownRecommendations.averageCooldown + " candles | " + />
"Median: " + cooldownRecommendations.medianCooldown + " candles" </div>
} <div className="mb-4">
/> <CardText
</div> title="Cooldown Recommendations"
</> content={
)} "Avg: " + cooldownRecommendations.averageCooldown + " candles | " +
<Table "Median: " + cooldownRecommendations.medianCooldown + " candles"
columns={columns} }
data={rows} />
renderRowSubCompontent={({ row }: any) => ( </div>
<BacktestRowDetails </>
backtest={row.original} )}
/> <Table
)} columns={columns}
/> data={rows}
renderRowSubCompontent={({row}: any) => (
{/* Bot Configuration Modal */} <BacktestRowDetails
{selectedBacktest && ( backtest={row.original}
<UnifiedTradingModal />
showModal={showBotConfigModal} )}
mode="createBot" />
backtest={selectedBacktest}
closeModal={handleCloseBotConfigModal}
/>
)}
{/* Backtest Configuration Modal */} {/* Bot Configuration Modal */}
{selectedBacktestForRerun && ( {selectedBacktest && (
<UnifiedTradingModal <UnifiedTradingModal
showModal={showBacktestConfigModal} showModal={showBotConfigModal}
mode="backtest" mode="createBot"
backtest={selectedBacktestForRerun} backtest={selectedBacktest}
closeModal={handleCloseBacktestConfigModal} closeModal={handleCloseBotConfigModal}
setBacktests={setBacktests} />
showLoopSlider={true} )}
/>
)} {/* Backtest Configuration Modal */}
{selectedBacktestForRerun && (
<UnifiedTradingModal
showModal={showBacktestConfigModal}
mode="backtest"
backtest={selectedBacktestForRerun}
closeModal={handleCloseBacktestConfigModal}
setBacktests={setBacktests}
showLoopSlider={true}
/>
)}
</>
)}
</> </>
)} )
</>
)
} }
export default BacktestTable export default BacktestTable

View File

@@ -47,8 +47,8 @@ type ITradeChartProps = {
walletBalances?: KeyValuePairOfDateTimeAndDecimal[] | null walletBalances?: KeyValuePairOfDateTimeAndDecimal[] | null
indicatorsValues?: { [key in keyof typeof IndicatorType]?: IndicatorsResultBase; } | null; indicatorsValues?: { [key in keyof typeof IndicatorType]?: IndicatorsResultBase; } | null;
stream?: Candle | null stream?: Candle | null
width: number width?: number
height: number height?: number
} }
const TradeChart = ({ const TradeChart = ({
@@ -62,12 +62,88 @@ const TradeChart = ({
height, height,
}: ITradeChartProps) => { }: ITradeChartProps) => {
const chartRef = React.useRef<HTMLDivElement>(null) const chartRef = React.useRef<HTMLDivElement>(null)
const containerRef = React.useRef<HTMLDivElement>(null)
const chart = useRef<IChartApi>() const chart = useRef<IChartApi>()
const {themeProperty} = useTheme() const {themeProperty} = useTheme()
const theme = themeProperty() const theme = themeProperty()
const series1 = useRef<ISeriesApi<'Candlestick'>>() const series1 = useRef<ISeriesApi<'Candlestick'>>()
const [timeDiff, setTimeDiff] = useState<number>(0) const [timeDiff, setTimeDiff] = useState<number>(0)
const [candleCount, setCandleCount] = useState<number>(candles.length) const [candleCount, setCandleCount] = useState<number>(candles.length)
const [chartDimensions, setChartDimensions] = useState({ width: 0, height: 0 })
// Get responsive dimensions
const getResponsiveDimensions = () => {
if (!containerRef.current) return { width: width || 510, height: height || 300 }
const containerWidth = containerRef.current.offsetWidth
const containerHeight = containerRef.current.offsetHeight
// Use provided dimensions if available, otherwise calculate responsive ones
if (width && height) {
return { width, height }
}
// For responsive mode, calculate based on container
const calculatedWidth = containerWidth > 0 ? containerWidth : 510
// Use different aspect ratios for different screen sizes
let aspectRatio = 0.6 // Default ratio (height/width)
if (containerWidth < 768) { // Mobile
aspectRatio = 0.8 // Taller on mobile for better visibility
} else if (containerWidth < 1024) { // Tablet
aspectRatio = 0.65
}
const calculatedHeight = height || Math.max(250, calculatedWidth * aspectRatio)
return {
width: calculatedWidth,
height: calculatedHeight
}
}
// Resize observer to handle container size changes
useEffect(() => {
if (!containerRef.current) return
const resizeObserver = new ResizeObserver(() => {
const newDimensions = getResponsiveDimensions()
setChartDimensions(newDimensions)
if (chart.current) {
chart.current.applyOptions({
width: newDimensions.width,
height: newDimensions.height
})
}
})
resizeObserver.observe(containerRef.current)
return () => {
resizeObserver.disconnect()
}
}, [width, height])
// Handle window resize for additional responsiveness
useEffect(() => {
const handleResize = () => {
setTimeout(() => {
const newDimensions = getResponsiveDimensions()
setChartDimensions(newDimensions)
if (chart.current) {
chart.current.applyOptions({
width: newDimensions.width,
height: newDimensions.height
})
}
}, 100) // Small delay to ensure container has updated
}
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [width, height])
function buildLine( function buildLine(
color: string, color: string,
@@ -164,7 +240,10 @@ const TradeChart = ({
} }
useEffect(() => { useEffect(() => {
if (chartRef.current) { if (chartRef.current && containerRef.current) {
const initialDimensions = getResponsiveDimensions()
setChartDimensions(initialDimensions)
const lineColor = theme['base-100'] const lineColor = theme['base-100']
chart.current = createChart(chartRef.current, { chart.current = createChart(chartRef.current, {
crosshair: { crosshair: {
@@ -194,7 +273,7 @@ const TradeChart = ({
visible: false, visible: false,
}, },
}, },
height: height, height: initialDimensions.height,
layout: { layout: {
background: {color: theme['base-300']}, background: {color: theme['base-300']},
textColor: theme.accent, textColor: theme.accent,
@@ -213,7 +292,7 @@ const TradeChart = ({
secondsVisible: true, secondsVisible: true,
timeVisible: true, timeVisible: true,
}, },
width: width, width: initialDimensions.width,
}) })
prepareChart() prepareChart()
@@ -710,7 +789,15 @@ const TradeChart = ({
} }
} }
return <div ref={chartRef}/> return (
<div
ref={containerRef}
className="w-full h-full"
style={{ minHeight: height || 250 }}
>
<div ref={chartRef} className="w-full h-full" />
</div>
)
} }
export default TradeChart export default TradeChart

View File

@@ -6,25 +6,24 @@ import useApiUrlStore from '../../../app/store/apiStore'
import {useCustomMoneyManagement} from '../../../app/store/customMoneyManagement' import {useCustomMoneyManagement} from '../../../app/store/customMoneyManagement'
import {useCustomScenario} from '../../../app/store/customScenario' import {useCustomScenario} from '../../../app/store/customScenario'
import { import {
AccountClient, AccountClient,
BacktestClient, BacktestClient,
BotClient, BotClient,
BotType, DataClient,
DataClient, MoneyManagement,
MoneyManagement, MoneyManagementClient,
MoneyManagementClient, RiskManagement,
RiskManagement, RiskToleranceLevel,
RiskToleranceLevel, RunBacktestRequest,
RunBacktestRequest, Scenario,
Scenario, ScenarioClient,
ScenarioClient, ScenarioRequest,
ScenarioRequest, SignalType,
SignalType, StartBotRequest,
StartBotRequest, Ticker,
Ticker, Timeframe,
Timeframe, TradingBotConfigRequest,
TradingBotConfigRequest, UpdateBotConfigRequest,
UpdateBotConfigRequest,
} from '../../../generated/ManagingApi' } from '../../../generated/ManagingApi'
import type {IUnifiedTradingConfigInput, UnifiedTradingModalProps} from '../../../global/type' import type {IUnifiedTradingConfigInput, UnifiedTradingModalProps} from '../../../global/type'
import {Loader, Slider} from '../../atoms' import {Loader, Slider} from '../../atoms'
@@ -197,12 +196,11 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
setGlobalCustomScenario((backtest.config as any).scenario); // Also update global store for prefilling setGlobalCustomScenario((backtest.config as any).scenario); // Also update global store for prefilling
setSelectedScenario('custom'); setSelectedScenario('custom');
} else if (backtest.config.scenarioName) { } else if (backtest.config.scenarioName) {
setSelectedScenario(backtest.config.scenarioName); setSelectedScenario(backtest.config.scenarioName);
setShowCustomScenario(false); setShowCustomScenario(false);
} }
setValue('timeframe', backtest.config.timeframe); setValue('timeframe', backtest.config.timeframe);
setValue('botType', backtest.config.botType);
setValue('cooldownPeriod', backtest.config.cooldownPeriod); setValue('cooldownPeriod', backtest.config.cooldownPeriod);
setValue('maxLossStreak', backtest.config.maxLossStreak); setValue('maxLossStreak', backtest.config.maxLossStreak);
setValue('maxPositionTimeHours', backtest.config.maxPositionTimeHours); setValue('maxPositionTimeHours', backtest.config.maxPositionTimeHours);
@@ -245,7 +243,6 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
} }
setValue('timeframe', backtest.config.timeframe); setValue('timeframe', backtest.config.timeframe);
setValue('botType', backtest.config.botType);
setValue('cooldownPeriod', backtest.config.cooldownPeriod); setValue('cooldownPeriod', backtest.config.cooldownPeriod);
setValue('maxLossStreak', backtest.config.maxLossStreak); setValue('maxLossStreak', backtest.config.maxLossStreak);
setValue('maxPositionTimeHours', backtest.config.maxPositionTimeHours); setValue('maxPositionTimeHours', backtest.config.maxPositionTimeHours);
@@ -305,7 +302,6 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
} }
setValue('scenarioName', config.scenarioName || ''); setValue('scenarioName', config.scenarioName || '');
setValue('timeframe', config.timeframe); setValue('timeframe', config.timeframe);
setValue('botType', config.botType);
setValue('cooldownPeriod', config.cooldownPeriod); setValue('cooldownPeriod', config.cooldownPeriod);
setValue('maxLossStreak', config.maxLossStreak); setValue('maxLossStreak', config.maxLossStreak);
setValue('maxPositionTimeHours', config.maxPositionTimeHours); setValue('maxPositionTimeHours', config.maxPositionTimeHours);
@@ -443,13 +439,14 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
const onMoneyManagementChange = (e: React.ChangeEvent<HTMLSelectElement>) => { const onMoneyManagementChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
if (e.target.value === 'custom') { if (e.target.value === 'custom') {
setShowCustomMoneyManagement(true); setShowCustomMoneyManagement(true);
setSelectedMoneyManagement('custom'); // Set selected to 'custom'
setCustomMoneyManagement(undefined); setCustomMoneyManagement(undefined);
setGlobalCustomMoneyManagement(null); // Clear global store when creating new custom setGlobalCustomMoneyManagement(null); // Clear global store when creating new custom
} else { } else {
setShowCustomMoneyManagement(false); setShowCustomMoneyManagement(false);
setSelectedMoneyManagement(e.target.value); // Update selected money management
setCustomMoneyManagement(undefined); setCustomMoneyManagement(undefined);
setGlobalCustomMoneyManagement(null); // Clear global store when switching away from custom setGlobalCustomMoneyManagement(null); // Clear global store when switching away from custom
setSelectedMoneyManagement(e.target.value);
} }
}; };
@@ -544,7 +541,7 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
} }
console.log(form) console.log(form)
console.log(moneyManagement) console.log(customMoneyManagement)
if (!moneyManagement) { if (!moneyManagement) {
t.update('error', 'Money management is required'); t.update('error', 'Money management is required');
return; return;
@@ -557,8 +554,8 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
scenario: customScenario ? convertScenarioToRequest(customScenario) : undefined, scenario: customScenario ? convertScenarioToRequest(customScenario) : undefined,
scenarioName: customScenario ? undefined : form.scenarioName, scenarioName: customScenario ? undefined : form.scenarioName,
timeframe: form.timeframe, timeframe: form.timeframe,
botType: form.botType,
isForWatchingOnly: form.isForWatchingOnly || false, isForWatchingOnly: form.isForWatchingOnly || false,
flipPosition: false, // Default to false since we're only using isForWatchingOnly checkbox
cooldownPeriod: form.cooldownPeriod, cooldownPeriod: form.cooldownPeriod,
maxLossStreak: form.maxLossStreak, maxLossStreak: form.maxLossStreak,
maxPositionTimeHours: form.maxPositionTimeHours, maxPositionTimeHours: form.maxPositionTimeHours,
@@ -592,6 +589,7 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
closeModal(); closeModal();
} catch (error: any) { } catch (error: any) {
console.error(error);
t.update('error', `Error: ${error.message || error}`); t.update('error', `Error: ${error.message || error}`);
} }
}; };
@@ -607,7 +605,6 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
scenario: customScenario ? convertScenarioToRequest(customScenario) : undefined, scenario: customScenario ? convertScenarioToRequest(customScenario) : undefined,
scenarioName: customScenario ? undefined : form.scenarioName, scenarioName: customScenario ? undefined : form.scenarioName,
timeframe: form.timeframe, timeframe: form.timeframe,
botType: form.botType,
isForWatchingOnly: false, isForWatchingOnly: false,
cooldownPeriod: form.cooldownPeriod, cooldownPeriod: form.cooldownPeriod,
maxLossStreak: form.maxLossStreak, maxLossStreak: form.maxLossStreak,
@@ -622,14 +619,13 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
useForDynamicStopLoss: form.useForDynamicStopLoss ?? true, useForDynamicStopLoss: form.useForDynamicStopLoss ?? true,
moneyManagementName: showCustomMoneyManagement ? undefined : selectedMoneyManagement, moneyManagementName: showCustomMoneyManagement ? undefined : selectedMoneyManagement,
moneyManagement: customMoneyManagement, moneyManagement: customMoneyManagement,
flipPosition: form.isForWatchingOnly ?? false,
}; };
const request: RunBacktestRequest = { const request: RunBacktestRequest = {
config: tradingBotConfigRequest, config: tradingBotConfigRequest,
startDate: new Date(form.startDate), startDate: new Date(form.startDate),
endDate: new Date(form.endDate), endDate: new Date(form.endDate),
balance: form.balance,
watchOnly: false,
save: form.save || false, save: form.save || false,
}; };
@@ -724,45 +720,29 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
</FormInput> </FormInput>
</div> </div>
{/* Second Row: Money Management & Bot Type */} {/* Money Management */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <FormInput label="Money Management" htmlFor="moneyManagement">
<FormInput label="Money Management" htmlFor="moneyManagement"> <select
<select className="select select-bordered w-full"
className="select select-bordered w-full" value={selectedMoneyManagement || (showCustomMoneyManagement ? 'custom' : '')}
value={selectedMoneyManagement || (showCustomMoneyManagement ? 'custom' : '')} onChange={onMoneyManagementChange}
onChange={onMoneyManagementChange} >
> {moneyManagements.length === 0 ? (
{moneyManagements.length === 0 ? ( <option value="" disabled>No money management available - create a custom one below</option>
<option value="" disabled>No money management available - create a custom one below</option> ) : (
) : ( <>
<> {moneyManagements.map((item) => (
{moneyManagements.map((item) => ( <option key={item.name} value={item.name}>
<option key={item.name} value={item.name}> {item.name}
{item.name} </option>
</option> ))}
))} </>
</> )}
)} <option key="custom" value="custom">
<option key="custom" value="custom"> {moneyManagements.length === 0 ? 'Create Custom Money Management' : 'Custom'}
{moneyManagements.length === 0 ? 'Create Custom Money Management' : 'Custom'} </option>
</option> </select>
</select> </FormInput>
</FormInput>
<FormInput label="Bot Type" htmlFor="botType">
<select
className="select select-bordered w-full"
{...register('botType', { required: true })}
disabled={mode === 'updateBot'} // Can't change bot type for existing bots
>
{[BotType.ScalpingBot, BotType.FlippingBot].map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</select>
</FormInput>
</div>
{/* Custom Money Management */} {/* Custom Money Management */}
{showCustomMoneyManagement && ( {showCustomMoneyManagement && (

View File

@@ -504,12 +504,8 @@ export class BotClient extends AuthorizedApiBase {
return Promise.resolve<string>(null as any); return Promise.resolve<string>(null as any);
} }
bot_Stop(botType: BotType | undefined, identifier: string | null | undefined): Promise<string> { bot_Stop(identifier: string | null | undefined): Promise<string> {
let url_ = this.baseUrl + "/Bot/Stop?"; let url_ = this.baseUrl + "/Bot/Stop?";
if (botType === null)
throw new Error("The parameter 'botType' cannot be null.");
else if (botType !== undefined)
url_ += "botType=" + encodeURIComponent("" + botType) + "&";
if (identifier !== undefined && identifier !== null) if (identifier !== undefined && identifier !== null)
url_ += "identifier=" + encodeURIComponent("" + identifier) + "&"; url_ += "identifier=" + encodeURIComponent("" + identifier) + "&";
url_ = url_.replace(/[?&]$/, ""); url_ = url_.replace(/[?&]$/, "");
@@ -2868,7 +2864,6 @@ export interface TradingBotConfig {
timeframe: Timeframe; timeframe: Timeframe;
isForWatchingOnly: boolean; isForWatchingOnly: boolean;
botTradingBalance: number; botTradingBalance: number;
botType: BotType;
isForBacktest: boolean; isForBacktest: boolean;
cooldownPeriod: number; cooldownPeriod: number;
maxLossStreak: number; maxLossStreak: number;
@@ -3014,12 +3009,6 @@ export enum Ticker {
Unknown = "Unknown", Unknown = "Unknown",
} }
export enum BotType {
SimpleBot = "SimpleBot",
ScalpingBot = "ScalpingBot",
FlippingBot = "FlippingBot",
}
export interface RiskManagement { export interface RiskManagement {
adverseProbabilityThreshold: number; adverseProbabilityThreshold: number;
favorableProbabilityThreshold: number; favorableProbabilityThreshold: number;
@@ -3336,8 +3325,6 @@ export interface RunBacktestRequest {
config?: TradingBotConfigRequest | null; config?: TradingBotConfigRequest | null;
startDate?: Date; startDate?: Date;
endDate?: Date; endDate?: Date;
balance?: number;
watchOnly?: boolean;
save?: boolean; save?: boolean;
} }
@@ -3347,8 +3334,8 @@ export interface TradingBotConfigRequest {
timeframe: Timeframe; timeframe: Timeframe;
isForWatchingOnly: boolean; isForWatchingOnly: boolean;
botTradingBalance: number; botTradingBalance: number;
botType: BotType;
name: string; name: string;
flipPosition: boolean;
cooldownPeriod: number; cooldownPeriod: number;
maxLossStreak: number; maxLossStreak: number;
scenario?: ScenarioRequest | null; scenario?: ScenarioRequest | null;
@@ -3397,6 +3384,12 @@ export interface StartBotRequest {
config?: TradingBotConfigRequest | null; config?: TradingBotConfigRequest | null;
} }
export enum BotType {
SimpleBot = "SimpleBot",
ScalpingBot = "ScalpingBot",
FlippingBot = "FlippingBot",
}
export interface TradingBotResponse { export interface TradingBotResponse {
status: string; status: string;
signals: Signal[]; signals: Signal[];

View File

@@ -111,7 +111,6 @@ export interface TradingBotConfig {
timeframe: Timeframe; timeframe: Timeframe;
isForWatchingOnly: boolean; isForWatchingOnly: boolean;
botTradingBalance: number; botTradingBalance: number;
botType: BotType;
isForBacktest: boolean; isForBacktest: boolean;
cooldownPeriod: number; cooldownPeriod: number;
maxLossStreak: number; maxLossStreak: number;
@@ -257,12 +256,6 @@ export enum Ticker {
Unknown = "Unknown", Unknown = "Unknown",
} }
export enum BotType {
SimpleBot = "SimpleBot",
ScalpingBot = "ScalpingBot",
FlippingBot = "FlippingBot",
}
export interface RiskManagement { export interface RiskManagement {
adverseProbabilityThreshold: number; adverseProbabilityThreshold: number;
favorableProbabilityThreshold: number; favorableProbabilityThreshold: number;
@@ -579,8 +572,6 @@ export interface RunBacktestRequest {
config?: TradingBotConfigRequest | null; config?: TradingBotConfigRequest | null;
startDate?: Date; startDate?: Date;
endDate?: Date; endDate?: Date;
balance?: number;
watchOnly?: boolean;
save?: boolean; save?: boolean;
} }
@@ -590,8 +581,8 @@ export interface TradingBotConfigRequest {
timeframe: Timeframe; timeframe: Timeframe;
isForWatchingOnly: boolean; isForWatchingOnly: boolean;
botTradingBalance: number; botTradingBalance: number;
botType: BotType;
name: string; name: string;
flipPosition: boolean;
cooldownPeriod: number; cooldownPeriod: number;
maxLossStreak: number; maxLossStreak: number;
scenario?: ScenarioRequest | null; scenario?: ScenarioRequest | null;
@@ -640,6 +631,12 @@ export interface StartBotRequest {
config?: TradingBotConfigRequest | null; config?: TradingBotConfigRequest | null;
} }
export enum BotType {
SimpleBot = "SimpleBot",
ScalpingBot = "ScalpingBot",
FlippingBot = "FlippingBot",
}
export interface TradingBotResponse { export interface TradingBotResponse {
status: string; status: string;
signals: Signal[]; signals: Signal[];

View File

@@ -6,8 +6,7 @@ import {CardPosition, CardSignal, CardText, Toast,} from '../../components/molle
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 {TradeChart, UnifiedTradingModal} from '../../components/organism' import {TradeChart, UnifiedTradingModal} from '../../components/organism'
import type {BotType, MoneyManagement, Position, TradingBotResponse} from '../../generated/ManagingApi' import {BotClient, BotType, MoneyManagement, Position, TradingBotResponse} from '../../generated/ManagingApi'
import {BotClient} from '../../generated/ManagingApi'
import type {IBotList} from '../../global/type.tsx' import type {IBotList} from '../../global/type.tsx'
import MoneyManagementModal from '../settingsPage/moneymanagement/moneyManagementModal' import MoneyManagementModal from '../settingsPage/moneymanagement/moneyManagementModal'
@@ -162,7 +161,7 @@ const BotList: React.FC<IBotList> = ({ list }) => {
if (status == 'Up') { if (status == 'Up') {
client client
.bot_Stop(botType, identifier) .bot_Stop(identifier)
.then(() => { .then(() => {
t.update('success', 'Bot stopped') t.update('success', 'Bot stopped')
}) })
@@ -195,7 +194,7 @@ const BotList: React.FC<IBotList> = ({ list }) => {
} }
function getUpdateBotBadge(bot: TradingBotResponse) { function getUpdateBotBadge(bot: TradingBotResponse) {
const classes = baseBadgeClass() + ' bg-warning' const classes = baseBadgeClass() + ' bg-orange-500'
return ( return (
<button className={classes} onClick={() => openUpdateBotModal(bot)}> <button className={classes} onClick={() => openUpdateBotModal(bot)}>
<p className="text-primary-content flex"> <p className="text-primary-content flex">
@@ -246,14 +245,12 @@ const BotList: React.FC<IBotList> = ({ list }) => {
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"
> >
<div className={cardClasses(bot.status)}> <div className={cardClasses(bot.status)}>
<figure> <figure className="w-full">
{ {
<TradeChart <TradeChart
candles={bot.candles} candles={bot.candles}
positions={bot.positions} positions={bot.positions}
signals={bot.signals} signals={bot.signals}
width={510}
height={300}
></TradeChart> ></TradeChart>
} }
</figure> </figure>
@@ -274,7 +271,7 @@ const BotList: React.FC<IBotList> = ({ list }) => {
{/* Action Badges */} {/* Action Badges */}
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{getToggleBotStatusBadge(bot.status, bot.identifier, bot.config.botType)} {getToggleBotStatusBadge(bot.status, bot.identifier, bot.config.flipPosition ? BotType.FlippingBot : BotType.SimpleBot)}
{getUpdateBotBadge(bot)} {getUpdateBotBadge(bot)}
{getManualPositionBadge(bot.identifier)} {getManualPositionBadge(bot.identifier)}
{getDeleteBadge(bot.identifier)} {getDeleteBadge(bot.identifier)}
@@ -294,7 +291,7 @@ const BotList: React.FC<IBotList> = ({ list }) => {
</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.config.botType}></CardText> <CardText title="Type" content={bot.config.flipPosition ? 'Flipping' : 'Simple'}></CardText>
</div> </div>
<div className="columns-2"> <div className="columns-2">
<CardPosition <CardPosition