Push merge conflict
This commit is contained in:
@@ -155,11 +155,10 @@ The `TradingBotConfig` class defines all configuration parameters for trading bo
|
||||
| `Timeframe` | `Timeframe` | Candle timeframe for analysis |
|
||||
| `IsForWatchingOnly` | `bool` | If true, bot only sends signals without trading |
|
||||
| `BotTradingBalance` | `decimal` | Initial trading balance for the bot |
|
||||
| `BotType` | `BotType` | Type of trading bot behavior |
|
||||
| `IsForBacktest` | `bool` | Whether this config is for backtesting |
|
||||
| `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) |
|
||||
| `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 |
|
||||
| `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)
|
||||
- `CloseEarlyWhenProfitable` allows immediate closure when profitable instead of waiting full duration
|
||||
|
||||
**Profit-Controlled Flipping:**
|
||||
- `FlipOnlyWhenInProfit` ensures safer trading by only flipping profitable positions
|
||||
**Bot Behavior Control:**
|
||||
- `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
|
||||
|
||||
**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 |
|
||||
| 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 |
|
||||
|-------------|----------------------------------------------------------------------------------------|
|
||||
| SimpleBot | Basic bot implementation for simple trading strategies |
|
||||
| ScalpingBot | Opens positions and waits for cooldown period before opening new ones in same direction |
|
||||
| FlippingBot | Advanced bot that can flip positions when opposite signals are triggered |
|
||||
#### Watch-Only Mode
|
||||
- **`IsForWatchingOnly`**: When `true`, the bot analyzes market conditions and generates signals but does not execute actual trades
|
||||
- Perfect for strategy testing, signal validation, or paper trading
|
||||
- Useful for developing confidence in a strategy before committing real capital
|
||||
|
||||
#### Flipping Mode Configuration
|
||||
|
||||
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)
|
||||
#### Position Flipping
|
||||
- **`FlipPosition`**: When `true`, enables the bot to flip positions when opposite signals are triggered
|
||||
- **`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
|
||||
2. Monitors for opposite direction signals from the same scenario
|
||||
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
|
||||
5. Continues this cycle for the duration of the bot's operation
|
||||
|
||||
**ScalpingBot vs FlippingBot:**
|
||||
- **ScalpingBot**: Opens position → Waits for exit signal → Closes → Cooldown → Opens new position
|
||||
- **FlippingBot**: Opens position → Monitors for opposite signals → Flips immediately (no cooldown between flips)
|
||||
**When Flipping is Disabled (`FlipPosition = false`):**
|
||||
- Opens position → Waits for exit signal → Closes → Cooldown → Opens new position in same direction
|
||||
- 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`)
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ using Managing.Domain.Strategies;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Api.Controllers;
|
||||
|
||||
@@ -192,15 +191,14 @@ public class BacktestController : BaseController
|
||||
ScenarioName = request.Config.ScenarioName,
|
||||
Scenario = scenario, // Use the converted scenario object
|
||||
Timeframe = request.Config.Timeframe,
|
||||
IsForWatchingOnly = request.WatchOnly,
|
||||
BotTradingBalance = request.Balance,
|
||||
BotType = request.Config.BotType,
|
||||
IsForWatchingOnly = request.Config.IsForWatchingOnly,
|
||||
BotTradingBalance = request.Config.BotTradingBalance,
|
||||
IsForBacktest = true,
|
||||
CooldownPeriod = request.Config.CooldownPeriod,
|
||||
MaxLossStreak = request.Config.MaxLossStreak,
|
||||
MaxPositionTimeHours = request.Config.MaxPositionTimeHours,
|
||||
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 ??
|
||||
$"Backtest-{request.Config.ScenarioName ?? request.Config.Scenario?.Name ?? "Custom"}-{DateTime.UtcNow:yyyyMMdd-HHmmss}",
|
||||
CloseEarlyWhenProfitable = request.Config.CloseEarlyWhenProfitable,
|
||||
@@ -210,21 +208,12 @@ public class BacktestController : BaseController
|
||||
UseForDynamicStopLoss = request.Config.UseForDynamicStopLoss
|
||||
};
|
||||
|
||||
switch (request.Config.BotType)
|
||||
{
|
||||
case BotType.SimpleBot:
|
||||
// SimpleBot backtest not implemented yet
|
||||
break;
|
||||
case BotType.ScalpingBot:
|
||||
case BotType.FlippingBot:
|
||||
backtestResult = await _backtester.RunTradingBotBacktest(
|
||||
backtestConfig,
|
||||
request.StartDate,
|
||||
request.EndDate,
|
||||
user,
|
||||
request.Save);
|
||||
break;
|
||||
}
|
||||
backtestResult = await _backtester.RunTradingBotBacktest(
|
||||
backtestConfig,
|
||||
request.StartDate,
|
||||
request.EndDate,
|
||||
user,
|
||||
request.Save);
|
||||
|
||||
await NotifyBacktesingSubscriberAsync(backtestResult);
|
||||
|
||||
@@ -281,16 +270,6 @@ public class RunBacktestRequest
|
||||
/// </summary>
|
||||
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>
|
||||
/// Whether to save the backtest results
|
||||
/// </summary>
|
||||
|
||||
@@ -224,7 +224,6 @@ public class BotController : BaseController
|
||||
Timeframe = request.Config.Timeframe,
|
||||
IsForWatchingOnly = request.Config.IsForWatchingOnly,
|
||||
BotTradingBalance = request.Config.BotTradingBalance,
|
||||
BotType = request.Config.BotType,
|
||||
CooldownPeriod = request.Config.CooldownPeriod,
|
||||
MaxLossStreak = request.Config.MaxLossStreak,
|
||||
MaxPositionTimeHours = request.Config.MaxPositionTimeHours,
|
||||
@@ -236,7 +235,7 @@ public class BotController : BaseController
|
||||
UseForDynamicStopLoss = request.Config.UseForDynamicStopLoss,
|
||||
// Set computed/default properties
|
||||
IsForBacktest = false,
|
||||
FlipPosition = request.Config.BotType == BotType.FlippingBot,
|
||||
FlipPosition = request.Config.FlipPosition,
|
||||
Name = request.Config.Name
|
||||
};
|
||||
|
||||
@@ -256,12 +255,11 @@ public class BotController : BaseController
|
||||
/// <summary>
|
||||
/// Stops a bot specified by type and name.
|
||||
/// </summary>
|
||||
/// <param name="botType">The type 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>
|
||||
[HttpGet]
|
||||
[Route("Stop")]
|
||||
public async Task<ActionResult<string>> Stop(BotType botType, string identifier)
|
||||
public async Task<ActionResult<string>> Stop(string identifier)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -271,8 +269,8 @@ public class BotController : BaseController
|
||||
return Forbid("You don't have permission to stop this bot");
|
||||
}
|
||||
|
||||
var result = await _mediator.Send(new StopBotCommand(botType, identifier));
|
||||
_logger.LogInformation($"{botType} type with identifier {identifier} is now {result}");
|
||||
var result = await _mediator.Send(new StopBotCommand(identifier));
|
||||
_logger.LogInformation($"Bot identifier {identifier} is now {result}");
|
||||
|
||||
await NotifyBotSubscriberAsync();
|
||||
|
||||
@@ -343,7 +341,7 @@ public class BotController : BaseController
|
||||
|
||||
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",
|
||||
$"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
|
||||
// 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
|
||||
var backup = _botService.GetBotBackup(bot.Identifier);
|
||||
@@ -776,7 +775,6 @@ public class BotController : BaseController
|
||||
Timeframe = request.Config.Timeframe,
|
||||
IsForWatchingOnly = request.Config.IsForWatchingOnly,
|
||||
BotTradingBalance = request.Config.BotTradingBalance,
|
||||
BotType = request.Config.BotType,
|
||||
CooldownPeriod = request.Config.CooldownPeriod,
|
||||
MaxLossStreak = request.Config.MaxLossStreak,
|
||||
MaxPositionTimeHours = request.Config.MaxPositionTimeHours,
|
||||
@@ -788,7 +786,7 @@ public class BotController : BaseController
|
||||
UseForDynamicStopLoss = request.Config.UseForDynamicStopLoss,
|
||||
// Set computed/default properties
|
||||
IsForBacktest = false,
|
||||
FlipPosition = request.Config.BotType == BotType.FlippingBot,
|
||||
FlipPosition = request.Config.FlipPosition,
|
||||
Name = request.Config.Name
|
||||
};
|
||||
|
||||
|
||||
@@ -38,18 +38,14 @@ public class TradingBotConfigRequest
|
||||
[Required]
|
||||
public decimal BotTradingBalance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of bot (SimpleBot, ScalpingBot, FlippingBot)
|
||||
/// </summary>
|
||||
[Required]
|
||||
public BotType BotType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name/identifier for this bot
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string Name { get; set; }
|
||||
|
||||
[Required] public bool FlipPosition { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Cooldown period between trades (in candles)
|
||||
/// </summary>
|
||||
|
||||
@@ -272,7 +272,6 @@ public class StatisticService : IStatisticService
|
||||
Timeframe = timeframe,
|
||||
IsForWatchingOnly = true,
|
||||
BotTradingBalance = 1000,
|
||||
BotType = BotType.ScalpingBot,
|
||||
IsForBacktest = true,
|
||||
CooldownPeriod = 1,
|
||||
MaxLossStreak = 0,
|
||||
|
||||
@@ -6,25 +6,19 @@ namespace Managing.Application.Abstractions
|
||||
public interface IBotFactory
|
||||
{
|
||||
IBot CreateSimpleBot(string botName, Workflow workflow);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates a trading bot using the unified TradingBot class
|
||||
/// </summary>
|
||||
/// <param name="config">The trading bot configuration</param>
|
||||
/// <returns>ITradingBot instance</returns>
|
||||
ITradingBot CreateTradingBot(TradingBotConfig config);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates a trading bot for backtesting using the unified TradingBot class
|
||||
/// </summary>
|
||||
/// <param name="config">The trading bot configuration</param>
|
||||
/// <returns>ITradingBot instance configured for backtesting</returns>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -114,7 +114,7 @@ namespace Managing.Application.Backtesting
|
||||
User user = null)
|
||||
{
|
||||
// Set FlipPosition based on BotType
|
||||
config.FlipPosition = config.BotType == BotType.FlippingBot;
|
||||
config.FlipPosition = config.FlipPosition;
|
||||
|
||||
var tradingBot = _botFactory.CreateBacktestTradingBot(config);
|
||||
|
||||
@@ -164,11 +164,11 @@ namespace Managing.Application.Backtesting
|
||||
private List<Candle> GetCandles(Account account, Ticker ticker, Timeframe timeframe,
|
||||
DateTime startDate, DateTime endDate)
|
||||
{
|
||||
var candles = _exchangeService.GetCandlesInflux(account.Exchange, ticker,
|
||||
var candles = _exchangeService.GetCandlesInflux(TradingExchanges.Evm, ticker,
|
||||
startDate, timeframe, endDate).Result;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ using Managing.Application.Abstractions.Services;
|
||||
using Managing.Domain.Bots;
|
||||
using Managing.Domain.Workflows;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.Bots.Base
|
||||
{
|
||||
@@ -61,64 +60,5 @@ namespace Managing.Application.Bots.Base
|
||||
_botService,
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -187,7 +187,7 @@ public class TradingBot : Bot, ITradingBot
|
||||
|
||||
Logger.LogInformation($"____________________{Name}____________________");
|
||||
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();
|
||||
@@ -1450,7 +1450,6 @@ public class TradingBot : Bot, ITradingBot
|
||||
}
|
||||
|
||||
// Protect critical properties that shouldn't change for running bots
|
||||
var protectedBotType = Config.BotType;
|
||||
var protectedIsForBacktest = Config.IsForBacktest;
|
||||
var protectedName = allowNameChange ? newConfig.Name : Config.Name;
|
||||
|
||||
@@ -1470,7 +1469,6 @@ public class TradingBot : Bot, ITradingBot
|
||||
Config = newConfig;
|
||||
|
||||
// Restore protected properties
|
||||
Config.BotType = protectedBotType;
|
||||
Config.IsForBacktest = protectedIsForBacktest;
|
||||
Config.Name = protectedName;
|
||||
|
||||
@@ -1532,7 +1530,6 @@ public class TradingBot : Bot, ITradingBot
|
||||
Timeframe = Config.Timeframe,
|
||||
IsForWatchingOnly = Config.IsForWatchingOnly,
|
||||
BotTradingBalance = Config.BotTradingBalance,
|
||||
BotType = Config.BotType,
|
||||
IsForBacktest = Config.IsForBacktest,
|
||||
CooldownPeriod = Config.CooldownPeriod,
|
||||
MaxLossStreak = Config.MaxLossStreak,
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
using MediatR;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.ManageBot.Commands
|
||||
{
|
||||
public class StopBotCommand : IRequest<string>
|
||||
{
|
||||
public string Name { get; }
|
||||
public BotType BotType { get; }
|
||||
public string Identifier { get; }
|
||||
|
||||
public StopBotCommand(BotType botType, string name)
|
||||
public StopBotCommand(string identifier)
|
||||
{
|
||||
BotType = botType;
|
||||
Name = name;
|
||||
Identifier = identifier;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,37 +70,25 @@ namespace Managing.Application.ManageBot
|
||||
Timeframe = request.Config.Timeframe,
|
||||
IsForWatchingOnly = request.Config.IsForWatchingOnly,
|
||||
BotTradingBalance = request.Config.BotTradingBalance,
|
||||
BotType = request.Config.BotType,
|
||||
IsForBacktest = request.Config.IsForBacktest,
|
||||
CooldownPeriod =
|
||||
request.Config.CooldownPeriod > 0 ? request.Config.CooldownPeriod : 1, // Default to 1 if not set
|
||||
MaxLossStreak = request.Config.MaxLossStreak,
|
||||
MaxPositionTimeHours = request.Config.MaxPositionTimeHours, // Properly handle nullable value
|
||||
FlipOnlyWhenInProfit = request.Config.FlipOnlyWhenInProfit,
|
||||
FlipPosition = request.Config.BotType == BotType.FlippingBot, // Set FlipPosition based on BotType
|
||||
FlipPosition = request.Config.FlipPosition, // Set FlipPosition
|
||||
Name = request.Config.Name ?? request.Name,
|
||||
CloseEarlyWhenProfitable = request.Config.CloseEarlyWhenProfitable
|
||||
};
|
||||
|
||||
switch (configToUse.BotType)
|
||||
{
|
||||
case BotType.SimpleBot:
|
||||
var bot = _botFactory.CreateSimpleBot(request.Name, null);
|
||||
bot.User = request.User;
|
||||
_botService.AddSimpleBotToCache(bot);
|
||||
return bot.GetStatus();
|
||||
var tradingBot = _botFactory.CreateTradingBot(configToUse);
|
||||
tradingBot.User = request.User;
|
||||
|
||||
case BotType.ScalpingBot:
|
||||
case BotType.FlippingBot:
|
||||
var tradingBot = _botFactory.CreateTradingBot(configToUse);
|
||||
tradingBot.User = request.User;
|
||||
// Log the configuration being used
|
||||
await LogBotConfigurationAsync(tradingBot, $"{configToUse.Name} created");
|
||||
|
||||
// Log the configuration being used
|
||||
await LogBotConfigurationAsync(tradingBot, $"{configToUse.BotType} created");
|
||||
|
||||
_botService.AddTradingBotToCache(tradingBot);
|
||||
return tradingBot.GetStatus();
|
||||
}
|
||||
_botService.AddTradingBotToCache(tradingBot);
|
||||
return tradingBot.GetStatus();
|
||||
|
||||
return botStatus.ToString();
|
||||
}
|
||||
@@ -116,7 +104,6 @@ namespace Managing.Application.ManageBot
|
||||
{
|
||||
var config = bot.GetConfiguration();
|
||||
var logMessage = $"{context} - Bot: {config.Name}, " +
|
||||
$"Type: {config.BotType}, " +
|
||||
$"Account: {config.AccountName}, " +
|
||||
$"Ticker: {config.Ticker}, " +
|
||||
$"Balance: {config.BotTradingBalance}, " +
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace Managing.Application.ManageBot
|
||||
|
||||
public Task<string> Handle(StopBotCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
return _botService.StopBot(request.Name);
|
||||
return _botService.StopBot(request.Identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,7 +82,6 @@ public class Backtest
|
||||
Timeframe = Config.Timeframe,
|
||||
IsForWatchingOnly = false, // Always start as active bot
|
||||
BotTradingBalance = initialTradingBalance,
|
||||
BotType = Config.BotType,
|
||||
IsForBacktest = false, // Always false for live bots
|
||||
CooldownPeriod = Config.CooldownPeriod,
|
||||
MaxLossStreak = Config.MaxLossStreak,
|
||||
@@ -117,7 +116,6 @@ public class Backtest
|
||||
Timeframe = Config.Timeframe,
|
||||
IsForWatchingOnly = Config.IsForWatchingOnly,
|
||||
BotTradingBalance = balance,
|
||||
BotType = Config.BotType,
|
||||
IsForBacktest = true,
|
||||
CooldownPeriod = Config.CooldownPeriod,
|
||||
MaxLossStreak = Config.MaxLossStreak,
|
||||
|
||||
@@ -14,7 +14,6 @@ public class TradingBotConfig
|
||||
[Required] public Timeframe Timeframe { get; set; }
|
||||
[Required] public bool IsForWatchingOnly { get; set; }
|
||||
[Required] public decimal BotTradingBalance { get; set; }
|
||||
[Required] public BotType BotType { get; set; }
|
||||
[Required] public bool IsForBacktest { get; set; }
|
||||
[Required] public int CooldownPeriod { get; set; }
|
||||
[Required] public int MaxLossStreak { get; set; }
|
||||
@@ -68,17 +67,17 @@ public class TradingBotConfig
|
||||
/// The actual Synth configuration is managed centrally in SynthPredictionService.
|
||||
/// </summary>
|
||||
public bool UseSynthApi { get; set; } = false;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Whether to use Synth predictions for position sizing adjustments and risk assessment
|
||||
/// </summary>
|
||||
public bool UseForPositionSizing { get; set; } = true;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Whether to use Synth predictions for signal filtering
|
||||
/// </summary>
|
||||
public bool UseForSignalFiltering { get; set; } = true;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Whether to use Synth predictions for dynamic stop-loss/take-profit adjustments
|
||||
/// </summary>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using Exilion.TradingAtomics;
|
||||
using Managing.Infrastructure.Databases.MongoDb.Attributes;
|
||||
using Managing.Infrastructure.Databases.MongoDb.Configurations;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Infrastructure.Databases.MongoDb.Collections
|
||||
{
|
||||
@@ -12,6 +11,7 @@ namespace Managing.Infrastructure.Databases.MongoDb.Collections
|
||||
public int WinRate { get; set; }
|
||||
public decimal GrowthPercentage { get; set; }
|
||||
public decimal HodlPercentage { get; set; }
|
||||
public TradingBotConfigDto Config { get; set; }
|
||||
public List<PositionDto> Positions { get; set; }
|
||||
public List<SignalDto> Signals { get; set; }
|
||||
public List<CandleDto> Candles { get; set; }
|
||||
@@ -22,17 +22,5 @@ namespace Managing.Infrastructure.Databases.MongoDb.Collections
|
||||
public UserDto User { get; set; }
|
||||
public PerformanceMetrics Statistics { 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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using Managing.Domain.Backtests;
|
||||
using Managing.Domain.Bots;
|
||||
using Managing.Domain.Candles;
|
||||
using Managing.Domain.MoneyManagements;
|
||||
using Managing.Domain.Risk;
|
||||
using Managing.Domain.Scenarios;
|
||||
using Managing.Domain.Statistics;
|
||||
using Managing.Domain.Strategies;
|
||||
@@ -132,20 +133,7 @@ public static class MongoMappers
|
||||
if (b == null)
|
||||
return null;
|
||||
|
||||
var config = new TradingBotConfig
|
||||
{
|
||||
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 config = Map(b.Config);
|
||||
|
||||
var bTest = new Backtest(
|
||||
config,
|
||||
@@ -181,6 +169,7 @@ public static class MongoMappers
|
||||
WinRate = result.WinRate,
|
||||
GrowthPercentage = result.GrowthPercentage,
|
||||
HodlPercentage = result.HodlPercentage,
|
||||
Config = Map(result.Config),
|
||||
Positions = Map(result.Positions),
|
||||
Signals = result.Signals.Select(s => Map(s)).ToList(),
|
||||
Candles = result.Candles.Select(c => Map(c)).ToList(),
|
||||
@@ -191,16 +180,6 @@ public static class MongoMappers
|
||||
StartDate = result.StartDate,
|
||||
EndDate = result.EndDate,
|
||||
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
|
||||
|
||||
#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
|
||||
}
|
||||
@@ -21,6 +21,7 @@ const Modal: React.FC<IModalProps> = ({
|
||||
titleHeader={titleHeader}
|
||||
onClose={onClose}
|
||||
onSubmit={onSubmit}
|
||||
showModal={showModal}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -10,9 +10,7 @@ import Logo from '../../../assets/img/logo.png'
|
||||
import {Loader} from '../../atoms'
|
||||
|
||||
const navigation = [
|
||||
{ href: '/desk', name: 'Desk' },
|
||||
{ href: '/bots', name: 'Bots' },
|
||||
{ href: '/workflow', name: 'Workflows' },
|
||||
{ href: '/scenarios', name: 'Scenarios' },
|
||||
{ href: '/backtest', name: 'Backtest' },
|
||||
{ href: '/tools', name: 'Tools' },
|
||||
|
||||
@@ -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
|
||||
@@ -19,7 +19,7 @@ const Tabs: FC<ITabsProps> = ({
|
||||
addButton = false,
|
||||
onAddButton,
|
||||
}) => {
|
||||
const Panel = tabs && tabs.find((tab) => tab.index === selectedTab)
|
||||
const Panel = tabs && tabs.find((tab: any) => tab.index === selectedTab)
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -28,7 +28,7 @@ const Tabs: FC<ITabsProps> = ({
|
||||
}
|
||||
>
|
||||
<div className="tabs" role="tablist" aria-orientation={orientation}>
|
||||
{tabs.map((tab) => (
|
||||
{tabs.map((tab: any) => (
|
||||
<button
|
||||
className={
|
||||
'mb-5 tab tab-bordered ' +
|
||||
|
||||
@@ -21,377 +21,374 @@ import TradeChart from '../Trading/TradeChart/TradeChart'
|
||||
import {BotNameModal} from '../index'
|
||||
|
||||
function baseBadgeClass(isOutlined = false) {
|
||||
let classes = 'text-xs badge '
|
||||
let classes = 'text-xs badge '
|
||||
|
||||
if (isOutlined) {
|
||||
classes += 'badge-outline '
|
||||
}
|
||||
return classes
|
||||
if (isOutlined) {
|
||||
classes += 'badge-outline '
|
||||
}
|
||||
return classes
|
||||
}
|
||||
|
||||
function botStatusResult(
|
||||
growthPercentage: number | undefined,
|
||||
hodlPercentage: number | undefined
|
||||
growthPercentage: number | undefined,
|
||||
hodlPercentage: number | undefined
|
||||
) {
|
||||
if (growthPercentage != undefined && hodlPercentage != undefined) {
|
||||
const isWinning = growthPercentage > hodlPercentage
|
||||
const classes =
|
||||
baseBadgeClass() + (isWinning ? 'badge-success' : 'badge-content')
|
||||
return <div className={classes}>{isWinning ? 'Winning' : 'Losing'}</div>
|
||||
}
|
||||
if (growthPercentage != undefined && hodlPercentage != undefined) {
|
||||
const isWinning = growthPercentage > hodlPercentage
|
||||
const classes =
|
||||
baseBadgeClass() + (isWinning ? 'badge-success' : 'badge-content')
|
||||
return <div className={classes}>{isWinning ? 'Winning' : 'Losing'}</div>
|
||||
}
|
||||
}
|
||||
|
||||
// function that return the number of day between a date and today
|
||||
function daysBetween(date: Date) {
|
||||
const oneDay = 24 * 60 * 60 * 1000 // hours*minutes*seconds*milliseconds
|
||||
const firstDate = new Date(date)
|
||||
const secondDate = new Date()
|
||||
const diffDays = Math.round(
|
||||
Math.abs((firstDate.getTime() - secondDate.getTime()) / oneDay)
|
||||
)
|
||||
return diffDays
|
||||
const oneDay = 24 * 60 * 60 * 1000 // hours*minutes*seconds*milliseconds
|
||||
const firstDate = new Date(date)
|
||||
const secondDate = new Date()
|
||||
const diffDays = Math.round(
|
||||
Math.abs((firstDate.getTime() - secondDate.getTime()) / oneDay)
|
||||
)
|
||||
return diffDays
|
||||
}
|
||||
|
||||
const BacktestCards: React.FC<IBacktestCards> = ({ list, setBacktests }) => {
|
||||
console.log(list)
|
||||
const { apiUrl } = useApiUrlStore()
|
||||
const [showMoneyManagementModal, setShowMoneyManagementModal] =
|
||||
React.useState(false)
|
||||
const [selectedMoneyManagement, setSelectedMoneyManagement] =
|
||||
React.useState<MoneyManagement>()
|
||||
const [showBotNameModal, setShowBotNameModal] = useState(false)
|
||||
const [isForWatchOnly, setIsForWatchOnly] = useState(false)
|
||||
const [currentBacktest, setCurrentBacktest] = useState<Backtest | null>(null)
|
||||
const [selectedMoneyManagementName, setSelectedMoneyManagementName] = useState<string>('')
|
||||
const BacktestCards: React.FC<IBacktestCards> = ({list, setBacktests}) => {
|
||||
console.log(list)
|
||||
const {apiUrl} = useApiUrlStore()
|
||||
const [showMoneyManagementModal, setShowMoneyManagementModal] =
|
||||
React.useState(false)
|
||||
const [selectedMoneyManagement, setSelectedMoneyManagement] =
|
||||
React.useState<MoneyManagement>()
|
||||
const [showBotNameModal, setShowBotNameModal] = useState(false)
|
||||
const [isForWatchOnly, setIsForWatchOnly] = useState(false)
|
||||
const [currentBacktest, setCurrentBacktest] = useState<Backtest | null>(null)
|
||||
const [selectedMoneyManagementName, setSelectedMoneyManagementName] = useState<string>('')
|
||||
|
||||
// Fetch money managements
|
||||
const { data: moneyManagements } = useQuery({
|
||||
queryFn: async () => {
|
||||
const moneyManagementClient = new MoneyManagementClient({}, apiUrl)
|
||||
return await moneyManagementClient.moneyManagement_GetMoneyManagements()
|
||||
},
|
||||
queryKey: ['moneyManagements'],
|
||||
})
|
||||
// Fetch money managements
|
||||
const {data: moneyManagements} = useQuery({
|
||||
queryFn: async () => {
|
||||
const moneyManagementClient = new MoneyManagementClient({}, apiUrl)
|
||||
return await moneyManagementClient.moneyManagement_GetMoneyManagements()
|
||||
},
|
||||
queryKey: ['moneyManagements'],
|
||||
})
|
||||
|
||||
// Set the first money management as default when the data is loaded
|
||||
useEffect(() => {
|
||||
if (moneyManagements && moneyManagements.length > 0) {
|
||||
setSelectedMoneyManagementName(moneyManagements[0].name)
|
||||
}
|
||||
}, [moneyManagements])
|
||||
// Set the first money management as default when the data is loaded
|
||||
useEffect(() => {
|
||||
if (moneyManagements && moneyManagements.length > 0) {
|
||||
setSelectedMoneyManagementName(moneyManagements[0].name)
|
||||
}
|
||||
}, [moneyManagements])
|
||||
|
||||
async function runBot(botName: string, backtest: Backtest, isForWatchOnly: boolean, moneyManagementName: string, initialTradingBalance: number) {
|
||||
const t = new Toast('Bot is starting')
|
||||
const client = new BotClient({}, apiUrl)
|
||||
async function runBot(botName: string, backtest: Backtest, isForWatchOnly: boolean, moneyManagementName: string, initialTradingBalance: number) {
|
||||
const t = new Toast('Bot is starting')
|
||||
const client = new BotClient({}, apiUrl)
|
||||
|
||||
// Check if the money management name is "custom" or contains "custom"
|
||||
const isCustomMoneyManagement =
|
||||
!moneyManagementName ||
|
||||
moneyManagementName.toLowerCase() === 'custom' ||
|
||||
moneyManagementName.toLowerCase().includes('custom');
|
||||
// Check if the money management name is "custom" or contains "custom"
|
||||
const isCustomMoneyManagement =
|
||||
!moneyManagementName ||
|
||||
moneyManagementName.toLowerCase() === 'custom' ||
|
||||
moneyManagementName.toLowerCase().includes('custom');
|
||||
|
||||
// Create TradingBotConfig from the backtest configuration
|
||||
const tradingBotConfig: TradingBotConfig = {
|
||||
accountName: backtest.config.accountName,
|
||||
ticker: backtest.config.ticker,
|
||||
scenarioName: backtest.config.scenarioName,
|
||||
timeframe: backtest.config.timeframe,
|
||||
botType: backtest.config.botType,
|
||||
isForWatchingOnly: isForWatchOnly,
|
||||
isForBacktest: false, // This is for running a live bot
|
||||
cooldownPeriod: backtest.config.cooldownPeriod,
|
||||
maxLossStreak: backtest.config.maxLossStreak,
|
||||
maxPositionTimeHours: backtest.config.maxPositionTimeHours,
|
||||
flipOnlyWhenInProfit: backtest.config.flipOnlyWhenInProfit,
|
||||
flipPosition: backtest.config.flipPosition,
|
||||
name: botName,
|
||||
botTradingBalance: initialTradingBalance,
|
||||
// Use the optimized or original money management from backtest if it's custom
|
||||
moneyManagement: isCustomMoneyManagement ?
|
||||
(backtest.optimizedMoneyManagement || backtest.config.moneyManagement || {
|
||||
name: 'default',
|
||||
leverage: 1,
|
||||
stopLoss: 0.01,
|
||||
takeProfit: 0.02,
|
||||
timeframe: backtest.config.timeframe
|
||||
}) :
|
||||
backtest.config.moneyManagement, // Always provide a valid MoneyManagement object
|
||||
closeEarlyWhenProfitable: backtest.config.closeEarlyWhenProfitable || false
|
||||
};
|
||||
// Create TradingBotConfig from the backtest configuration
|
||||
const tradingBotConfig: TradingBotConfig = {
|
||||
accountName: backtest.config.accountName,
|
||||
ticker: backtest.config.ticker,
|
||||
scenarioName: backtest.config.scenarioName,
|
||||
timeframe: backtest.config.timeframe,
|
||||
isForWatchingOnly: isForWatchOnly,
|
||||
isForBacktest: false, // This is for running a live bot
|
||||
cooldownPeriod: backtest.config.cooldownPeriod,
|
||||
maxLossStreak: backtest.config.maxLossStreak,
|
||||
maxPositionTimeHours: backtest.config.maxPositionTimeHours,
|
||||
flipOnlyWhenInProfit: backtest.config.flipOnlyWhenInProfit,
|
||||
flipPosition: backtest.config.flipPosition,
|
||||
name: botName,
|
||||
botTradingBalance: initialTradingBalance,
|
||||
// Use the optimized or original money management from backtest if it's custom
|
||||
moneyManagement: isCustomMoneyManagement ?
|
||||
(backtest.optimizedMoneyManagement || backtest.config.moneyManagement || {
|
||||
name: 'default',
|
||||
leverage: 1,
|
||||
stopLoss: 0.01,
|
||||
takeProfit: 0.02,
|
||||
timeframe: backtest.config.timeframe
|
||||
}) :
|
||||
backtest.config.moneyManagement, // Always provide a valid MoneyManagement object
|
||||
closeEarlyWhenProfitable: backtest.config.closeEarlyWhenProfitable || false
|
||||
};
|
||||
|
||||
const request: StartBotRequest = {
|
||||
config: tradingBotConfig as unknown as TradingBotConfigRequest,
|
||||
const request: StartBotRequest = {
|
||||
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
|
||||
.bot_Start(request)
|
||||
.then((botStatus: string) => {
|
||||
t.update('info', 'Bot status: ' + botStatus)
|
||||
})
|
||||
.catch((err) => {
|
||||
t.update('error', 'Error: ' + err)
|
||||
})
|
||||
}
|
||||
|
||||
const handleOpenBotNameModal = (backtest: Backtest, isForWatchOnly: boolean) => {
|
||||
setCurrentBacktest(backtest)
|
||||
setIsForWatchOnly(isForWatchOnly)
|
||||
setShowBotNameModal(true)
|
||||
}
|
||||
|
||||
const handleCloseBotNameModal = () => {
|
||||
setShowBotNameModal(false)
|
||||
}
|
||||
|
||||
const handleSubmitBotName = (botName: string, backtest: Backtest, isForWatchOnly: boolean, moneyManagementName: string, initialTradingBalance: number) => {
|
||||
runBot(botName, backtest, isForWatchOnly, moneyManagementName, initialTradingBalance)
|
||||
setShowBotNameModal(false)
|
||||
}
|
||||
|
||||
async function 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 handleOpenBotNameModal = (backtest: Backtest, isForWatchOnly: boolean) => {
|
||||
setCurrentBacktest(backtest)
|
||||
setIsForWatchOnly(isForWatchOnly)
|
||||
setShowBotNameModal(true)
|
||||
}
|
||||
|
||||
const request: RunBacktestRequest = {
|
||||
config: optimizedConfig as unknown as TradingBotConfigRequest,
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
balance: backtest.walletBalances[0].value,
|
||||
watchOnly: false,
|
||||
save: false,
|
||||
const handleCloseBotNameModal = () => {
|
||||
setShowBotNameModal(false)
|
||||
}
|
||||
|
||||
await client
|
||||
.backtest_Run(request)
|
||||
.then((backtest: Backtest) => {
|
||||
t.update('success', `${backtest.config.ticker} Backtest Succeeded`)
|
||||
setBacktests((arr: Backtest[]) => [...arr, backtest])
|
||||
})
|
||||
.catch((err) => {
|
||||
t.update('error', 'Error :' + err)
|
||||
})
|
||||
}
|
||||
const handleSubmitBotName = (botName: string, backtest: Backtest, isForWatchOnly: boolean, moneyManagementName: string, initialTradingBalance: number) => {
|
||||
runBot(botName, backtest, isForWatchOnly, moneyManagementName, initialTradingBalance)
|
||||
setShowBotNameModal(false)
|
||||
}
|
||||
|
||||
function saveMoneyManagement(moneyManagement: MoneyManagement) {
|
||||
setSelectedMoneyManagement(moneyManagement)
|
||||
setShowMoneyManagementModal(true)
|
||||
}
|
||||
async function runOptimizedBacktest(backtest: Backtest) {
|
||||
const t = new Toast('Optimized backtest is running')
|
||||
const client = new BacktestClient({}, apiUrl)
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap m-4 -mx-4">
|
||||
{list?.map((backtest: Backtest, index: number) => (
|
||||
<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>
|
||||
// Calculate dates for the API call
|
||||
const startDate = backtest.candles[0].date
|
||||
const endDate = backtest.candles[backtest.candles.length - 1].date
|
||||
|
||||
<div className="card bg-base-300 shadow-xl">
|
||||
<figure className="z-0">
|
||||
{
|
||||
<TradeChart
|
||||
candles={backtest.candles}
|
||||
positions={backtest.positions}
|
||||
walletBalances={backtest.walletBalances}
|
||||
signals={backtest.signals}
|
||||
indicatorsValues={backtest.indicatorsValues}
|
||||
width={720}
|
||||
height={512}
|
||||
></TradeChart>
|
||||
}
|
||||
</figure>
|
||||
// Create optimized backtest config
|
||||
const optimizedConfig: TradingBotConfig = {
|
||||
...backtest.config,
|
||||
name: `${backtest.config.ticker}-${backtest.config.scenarioName}-Optimized`,
|
||||
moneyManagement: backtest.optimizedMoneyManagement || backtest.config.moneyManagement
|
||||
}
|
||||
|
||||
<div className="card-body">
|
||||
<h2 className="card-title text-sm">
|
||||
<div className="dropdown">
|
||||
<label
|
||||
htmlFor={index.toString()}
|
||||
tabIndex={index}
|
||||
className=""
|
||||
>
|
||||
<DotsVerticalIcon className="text-primary w-5 h-5" />
|
||||
</label>
|
||||
<ul
|
||||
id={index.toString()}
|
||||
className="dropdown-content menu bg-base-100 rounded-box w-52 p-2 shadow"
|
||||
>
|
||||
<li>
|
||||
<button
|
||||
className="text-xs"
|
||||
onClick={() => handleOpenBotNameModal(backtest, false)}
|
||||
>
|
||||
Run bot
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
className="text-xs"
|
||||
onClick={() => handleOpenBotNameModal(backtest, true)}
|
||||
>
|
||||
Run watcher
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
className="text-xs"
|
||||
onClick={() =>
|
||||
saveMoneyManagement(backtest.config.moneyManagement)
|
||||
}
|
||||
>
|
||||
Save money management
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
className="text-xs"
|
||||
onClick={() => runOptimizedBacktest(backtest)}
|
||||
>
|
||||
Run optimized money management
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{backtest.config.ticker}
|
||||
{botStatusResult(
|
||||
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}
|
||||
></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>
|
||||
const request: RunBacktestRequest = {
|
||||
config: optimizedConfig as unknown as TradingBotConfigRequest,
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
save: false,
|
||||
}
|
||||
|
||||
await client
|
||||
.backtest_Run(request)
|
||||
.then((backtest: Backtest) => {
|
||||
t.update('success', `${backtest.config.ticker} Backtest Succeeded`)
|
||||
setBacktests((arr: Backtest[]) => [...arr, backtest])
|
||||
})
|
||||
.catch((err) => {
|
||||
t.update('error', 'Error :' + err)
|
||||
})
|
||||
}
|
||||
|
||||
function saveMoneyManagement(moneyManagement: MoneyManagement) {
|
||||
setSelectedMoneyManagement(moneyManagement)
|
||||
setShowMoneyManagementModal(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap m-4 -mx-4">
|
||||
{list?.map((backtest: Backtest, index: number) => (
|
||||
<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">
|
||||
<figure className="z-0">
|
||||
{
|
||||
<TradeChart
|
||||
candles={backtest.candles}
|
||||
positions={backtest.positions}
|
||||
walletBalances={backtest.walletBalances}
|
||||
signals={backtest.signals}
|
||||
indicatorsValues={backtest.indicatorsValues}
|
||||
width={720}
|
||||
height={512}
|
||||
></TradeChart>
|
||||
}
|
||||
</figure>
|
||||
|
||||
<div className="card-body">
|
||||
<h2 className="card-title text-sm">
|
||||
<div className="dropdown">
|
||||
<label
|
||||
htmlFor={index.toString()}
|
||||
tabIndex={index}
|
||||
className=""
|
||||
>
|
||||
<DotsVerticalIcon className="text-primary w-5 h-5"/>
|
||||
</label>
|
||||
<ul
|
||||
id={index.toString()}
|
||||
className="dropdown-content menu bg-base-100 rounded-box w-52 p-2 shadow"
|
||||
>
|
||||
<li>
|
||||
<button
|
||||
className="text-xs"
|
||||
onClick={() => handleOpenBotNameModal(backtest, false)}
|
||||
>
|
||||
Run bot
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
className="text-xs"
|
||||
onClick={() => handleOpenBotNameModal(backtest, true)}
|
||||
>
|
||||
Run watcher
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
className="text-xs"
|
||||
onClick={() =>
|
||||
saveMoneyManagement(backtest.config.moneyManagement)
|
||||
}
|
||||
>
|
||||
Save money management
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
className="text-xs"
|
||||
onClick={() => runOptimizedBacktest(backtest)}
|
||||
>
|
||||
Run optimized money management
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{backtest.config.ticker}
|
||||
{botStatusResult(
|
||||
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 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>
|
||||
<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>
|
||||
))}
|
||||
|
||||
<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
|
||||
|
||||
@@ -351,16 +351,15 @@ const BacktestRowDetails: React.FC<IBacktestRowDetailsProps> = ({
|
||||
content={getAverageTradesPerDay() + " trades/day"}
|
||||
></CardText>
|
||||
</div>
|
||||
<div>
|
||||
<figure>
|
||||
<div className="w-full">
|
||||
<figure className="w-full">
|
||||
<TradeChart
|
||||
width={1400}
|
||||
height={1100}
|
||||
candles={candles}
|
||||
positions={positions}
|
||||
walletBalances={walletBalances}
|
||||
indicatorsValues={indicatorsValues}
|
||||
signals={signals}
|
||||
height={1000}
|
||||
></TradeChart>
|
||||
</figure>
|
||||
</div>
|
||||
|
||||
@@ -4,491 +4,491 @@ import React, {useEffect, useState} from 'react'
|
||||
import useApiUrlStore from '../../../app/store/apiStore'
|
||||
import type {Backtest} 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 {UnifiedTradingModal} from '../index'
|
||||
import Toast from '../../mollecules/Toast/Toast'
|
||||
|
||||
import BacktestRowDetails from './backtestRowDetails'
|
||||
|
||||
const BacktestTable: React.FC<IBacktestCards> = ({ list, isFetching, setBacktests }) => {
|
||||
const [rows, setRows] = useState<Backtest[]>([])
|
||||
const { apiUrl } = useApiUrlStore()
|
||||
const [optimizedMoneyManagement, setOptimizedMoneyManagement] = useState({
|
||||
stopLoss: 0,
|
||||
takeProfit: 0,
|
||||
})
|
||||
const [positionTimingStats, setPositionTimingStats] = useState({
|
||||
averageOpenTime: 0,
|
||||
medianOpenTime: 0,
|
||||
losingPositionsAverageOpenTime: 0,
|
||||
})
|
||||
const [cooldownRecommendations, setCooldownRecommendations] = useState({
|
||||
averageCooldown: 0,
|
||||
medianCooldown: 0,
|
||||
})
|
||||
|
||||
// Bot configuration modal state
|
||||
const [showBotConfigModal, setShowBotConfigModal] = useState(false)
|
||||
const [selectedBacktest, setSelectedBacktest] = useState<Backtest | null>(null)
|
||||
const BacktestTable: React.FC<IBacktestCards> = ({list, isFetching, setBacktests}) => {
|
||||
const [rows, setRows] = useState<Backtest[]>([])
|
||||
const {apiUrl} = useApiUrlStore()
|
||||
const [optimizedMoneyManagement, setOptimizedMoneyManagement] = useState({
|
||||
stopLoss: 0,
|
||||
takeProfit: 0,
|
||||
})
|
||||
const [positionTimingStats, setPositionTimingStats] = useState({
|
||||
averageOpenTime: 0,
|
||||
medianOpenTime: 0,
|
||||
losingPositionsAverageOpenTime: 0,
|
||||
})
|
||||
const [cooldownRecommendations, setCooldownRecommendations] = useState({
|
||||
averageCooldown: 0,
|
||||
medianCooldown: 0,
|
||||
})
|
||||
|
||||
// Backtest configuration modal state
|
||||
const [showBacktestConfigModal, setShowBacktestConfigModal] = useState(false)
|
||||
const [selectedBacktestForRerun, setSelectedBacktestForRerun] = useState<Backtest | null>(null)
|
||||
// Bot configuration modal state
|
||||
const [showBotConfigModal, setShowBotConfigModal] = useState(false)
|
||||
const [selectedBacktest, setSelectedBacktest] = useState<Backtest | null>(null)
|
||||
|
||||
const handleOpenBotConfigModal = (backtest: Backtest) => {
|
||||
setSelectedBacktest(backtest)
|
||||
setShowBotConfigModal(true)
|
||||
}
|
||||
// Backtest configuration modal state
|
||||
const [showBacktestConfigModal, setShowBacktestConfigModal] = useState(false)
|
||||
const [selectedBacktestForRerun, setSelectedBacktestForRerun] = useState<Backtest | null>(null)
|
||||
|
||||
const handleCloseBotConfigModal = () => {
|
||||
setShowBotConfigModal(false)
|
||||
setSelectedBacktest(null)
|
||||
}
|
||||
const handleOpenBotConfigModal = (backtest: Backtest) => {
|
||||
setSelectedBacktest(backtest)
|
||||
setShowBotConfigModal(true)
|
||||
}
|
||||
|
||||
const handleOpenBacktestConfigModal = (backtest: Backtest) => {
|
||||
setSelectedBacktestForRerun(backtest)
|
||||
setShowBacktestConfigModal(true)
|
||||
}
|
||||
const handleCloseBotConfigModal = () => {
|
||||
setShowBotConfigModal(false)
|
||||
setSelectedBacktest(null)
|
||||
}
|
||||
|
||||
const handleCloseBacktestConfigModal = () => {
|
||||
setShowBacktestConfigModal(false)
|
||||
setSelectedBacktestForRerun(null)
|
||||
}
|
||||
const handleOpenBacktestConfigModal = (backtest: Backtest) => {
|
||||
setSelectedBacktestForRerun(backtest)
|
||||
setShowBacktestConfigModal(true)
|
||||
}
|
||||
|
||||
async function deleteBacktest(id: string) {
|
||||
const t = new Toast('Deleting backtest')
|
||||
const client = new BacktestClient({}, apiUrl)
|
||||
const handleCloseBacktestConfigModal = () => {
|
||||
setShowBacktestConfigModal(false)
|
||||
setSelectedBacktestForRerun(null)
|
||||
}
|
||||
|
||||
await client
|
||||
.backtest_DeleteBacktest(id)
|
||||
.then(() => {
|
||||
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)
|
||||
})
|
||||
}
|
||||
async function deleteBacktest(id: string) {
|
||||
const t = new Toast('Deleting backtest')
|
||||
const client = new BacktestClient({}, apiUrl)
|
||||
|
||||
const getScoreColor = (score: number) => {
|
||||
if (score >= 75) return '#08C25F'; // success
|
||||
if (score >= 50) return '#B0DB43'; // info
|
||||
if (score >= 25) return '#EB6F22'; // warning
|
||||
return '#FF5340'; // error
|
||||
};
|
||||
await client
|
||||
.backtest_DeleteBacktest(id)
|
||||
.then(() => {
|
||||
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 columns = React.useMemo(
|
||||
() => [
|
||||
{
|
||||
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()}>
|
||||
const getScoreColor = (score: number) => {
|
||||
if (score >= 75) return '#08C25F'; // success
|
||||
if (score >= 50) return '#B0DB43'; // info
|
||||
if (score >= 25) return '#EB6F22'; // warning
|
||||
return '#FF5340'; // error
|
||||
};
|
||||
|
||||
const columns = React.useMemo(
|
||||
() => [
|
||||
{
|
||||
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 ? (
|
||||
<ChevronDownIcon></ChevronDownIcon>
|
||||
<ChevronDownIcon></ChevronDownIcon>
|
||||
) : (
|
||||
<ChevronRightIcon></ChevronRightIcon>
|
||||
<ChevronRightIcon></ChevronRightIcon>
|
||||
)}
|
||||
</span>
|
||||
),
|
||||
),
|
||||
|
||||
// Make sure it has an ID
|
||||
Header: ({
|
||||
getToggleAllRowsExpandedProps,
|
||||
isAllRowsExpanded,
|
||||
}: any) => (
|
||||
<span {...getToggleAllRowsExpandedProps()}>
|
||||
// Make sure it has an ID
|
||||
Header: ({
|
||||
getToggleAllRowsExpandedProps,
|
||||
isAllRowsExpanded,
|
||||
}: any) => (
|
||||
<span {...getToggleAllRowsExpandedProps()}>
|
||||
{isAllRowsExpanded ? 'v' : '>'}
|
||||
</span>
|
||||
),
|
||||
// Build our expander column
|
||||
id: 'expander',
|
||||
},
|
||||
{
|
||||
Header: 'Score',
|
||||
accessor: 'score',
|
||||
Cell: ({ cell }: any) => (
|
||||
<span style={{
|
||||
color: getScoreColor(cell.row.values.score),
|
||||
fontWeight: 500,
|
||||
display: 'inline-block',
|
||||
width: '60px'
|
||||
}}>
|
||||
),
|
||||
// Build our expander column
|
||||
id: 'expander',
|
||||
},
|
||||
{
|
||||
Header: 'Score',
|
||||
accessor: 'score',
|
||||
Cell: ({cell}: any) => (
|
||||
<span style={{
|
||||
color: getScoreColor(cell.row.values.score),
|
||||
fontWeight: 500,
|
||||
display: 'inline-block',
|
||||
width: '60px'
|
||||
}}>
|
||||
{cell.row.values.score.toFixed(2)}
|
||||
</span>
|
||||
),
|
||||
disableFilters: true,
|
||||
},
|
||||
{
|
||||
Filter: SelectColumnFilter,
|
||||
Header: 'Ticker',
|
||||
accessor: 'config.ticker',
|
||||
disableSortBy: true,
|
||||
},
|
||||
|
||||
{
|
||||
Filter: SelectColumnFilter,
|
||||
Header: 'Timeframe',
|
||||
accessor: 'config.timeframe',
|
||||
disableSortBy: true,
|
||||
},
|
||||
{
|
||||
Filter: SelectColumnFilter,
|
||||
Header: 'Scenario',
|
||||
accessor: 'config.scenarioName',
|
||||
disableSortBy: true,
|
||||
},
|
||||
{
|
||||
Filter: SelectColumnFilter,
|
||||
Header: 'BotType',
|
||||
accessor: 'config.botType',
|
||||
disableSortBy: true,
|
||||
},
|
||||
),
|
||||
disableFilters: true,
|
||||
},
|
||||
{
|
||||
Filter: SelectColumnFilter,
|
||||
Header: 'Ticker',
|
||||
accessor: 'config.ticker',
|
||||
disableSortBy: true,
|
||||
},
|
||||
|
||||
{
|
||||
Filter: SelectColumnFilter,
|
||||
Header: 'Timeframe',
|
||||
accessor: 'config.timeframe',
|
||||
disableSortBy: true,
|
||||
},
|
||||
{
|
||||
Filter: SelectColumnFilter,
|
||||
Header: 'Scenario',
|
||||
accessor: 'config.scenarioName',
|
||||
disableSortBy: true,
|
||||
},
|
||||
{
|
||||
Filter: SelectColumnFilter,
|
||||
Header: 'BotType',
|
||||
accessor: 'config.botType',
|
||||
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(() => {
|
||||
if (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,
|
||||
});
|
||||
useEffect(() => {
|
||||
if (list) {
|
||||
setRows(list)
|
||||
|
||||
// Calculate position timing statistics
|
||||
const allPositions = list.flatMap(backtest => backtest.positions);
|
||||
const finishedPositions = allPositions.filter(p => p.status === 'Finished');
|
||||
|
||||
if (finishedPositions.length > 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 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);
|
||||
|
||||
// Calculate average
|
||||
const averageOpenTime = openTimes.reduce((sum, time) => sum + time, 0) / openTimes.length;
|
||||
setOptimizedMoneyManagement({
|
||||
stopLoss: stopLoss / optimized.length,
|
||||
takeProfit: takeProfit / optimized.length,
|
||||
});
|
||||
|
||||
// Calculate median
|
||||
const sortedTimes = [...openTimes].sort((a, b) => a - b);
|
||||
const medianOpenTime = sortedTimes.length % 2 === 0
|
||||
? (sortedTimes[sortedTimes.length / 2 - 1] + sortedTimes[sortedTimes.length / 2]) / 2
|
||||
: sortedTimes[Math.floor(sortedTimes.length / 2)];
|
||||
// Calculate position timing statistics
|
||||
const allPositions = list.flatMap(backtest => backtest.positions);
|
||||
const finishedPositions = allPositions.filter(p => p.status === 'Finished');
|
||||
|
||||
// Calculate average for losing positions
|
||||
const losingPositions = finishedPositions.filter(p => (p.profitAndLoss?.realized ?? 0) < 0);
|
||||
let losingPositionsAverageOpenTime = 0;
|
||||
|
||||
if (losingPositions.length > 0) {
|
||||
const losingOpenTimes = losingPositions.map(position => {
|
||||
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;
|
||||
}
|
||||
if (finishedPositions.length > 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();
|
||||
|
||||
setPositionTimingStats({
|
||||
averageOpenTime,
|
||||
medianOpenTime,
|
||||
losingPositionsAverageOpenTime,
|
||||
});
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
// Calculate cooldown recommendations across all backtests
|
||||
const allCooldownValues: number[] = [];
|
||||
|
||||
list.forEach(backtest => {
|
||||
if (backtest.positions.length < 2 || !backtest.candles || backtest.candles.length < 2) {
|
||||
return;
|
||||
}
|
||||
// Return time difference in hours
|
||||
return (closeTime.getTime() - openTime.getTime()) / (1000 * 60 * 60);
|
||||
});
|
||||
|
||||
// Determine candle timeframe in milliseconds
|
||||
const candleTimeframeMs = new Date(backtest.candles[1].date).getTime() - new Date(backtest.candles[0].date).getTime();
|
||||
// Calculate average
|
||||
const averageOpenTime = openTimes.reduce((sum, time) => sum + time, 0) / openTimes.length;
|
||||
|
||||
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;
|
||||
});
|
||||
// Calculate median
|
||||
const sortedTimes = [...openTimes].sort((a, b) => a - b);
|
||||
const medianOpenTime = sortedTimes.length % 2 === 0
|
||||
? (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++) {
|
||||
const currentPosition = sortedPositions[i];
|
||||
const nextPosition = sortedPositions[i + 1];
|
||||
// Calculate average for losing positions
|
||||
const losingPositions = finishedPositions.filter(p => (p.profitAndLoss?.realized ?? 0) < 0);
|
||||
let losingPositionsAverageOpenTime = 0;
|
||||
|
||||
const currentRealized = currentPosition.profitAndLoss?.realized ?? 0;
|
||||
const nextRealized = nextPosition.profitAndLoss?.realized ?? 0;
|
||||
if (losingPositions.length > 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 (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 (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({
|
||||
averageOpenTime,
|
||||
medianOpenTime,
|
||||
losingPositionsAverageOpenTime,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
// Calculate cooldown recommendations across all backtests
|
||||
const allCooldownValues: number[] = [];
|
||||
|
||||
list.forEach(backtest => {
|
||||
if (backtest.positions.length < 2 || !backtest.candles || backtest.candles.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<>
|
||||
{isFetching ? (
|
||||
<div className="flex justify-center">
|
||||
<progress className="progress progress-primary w-56"></progress>
|
||||
</div>
|
||||
) : (
|
||||
return (
|
||||
<>
|
||||
{list && list.length > 0 && (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<CardText
|
||||
title="Average Optimized Money Management"
|
||||
content={
|
||||
"SL: " + optimizedMoneyManagement.stopLoss.toFixed(2) + "% | TP: " +
|
||||
optimizedMoneyManagement.takeProfit.toFixed(2) + "% | R/R: " +
|
||||
(optimizedMoneyManagement.takeProfit / optimizedMoneyManagement.stopLoss || 0).toFixed(2)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<CardText
|
||||
title="Position Timing Statistics"
|
||||
content={
|
||||
"Avg: " + positionTimingStats.averageOpenTime.toFixed(1) + "h | " +
|
||||
"Median: " + positionTimingStats.medianOpenTime.toFixed(1) + "h | " +
|
||||
"Losing Avg: " + positionTimingStats.losingPositionsAverageOpenTime.toFixed(1) + "h"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<CardText
|
||||
title="Cooldown Recommendations"
|
||||
content={
|
||||
"Avg: " + cooldownRecommendations.averageCooldown + " candles | " +
|
||||
"Median: " + cooldownRecommendations.medianCooldown + " candles"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<Table
|
||||
columns={columns}
|
||||
data={rows}
|
||||
renderRowSubCompontent={({ row }: any) => (
|
||||
<BacktestRowDetails
|
||||
backtest={row.original}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Bot Configuration Modal */}
|
||||
{selectedBacktest && (
|
||||
<UnifiedTradingModal
|
||||
showModal={showBotConfigModal}
|
||||
mode="createBot"
|
||||
backtest={selectedBacktest}
|
||||
closeModal={handleCloseBotConfigModal}
|
||||
/>
|
||||
)}
|
||||
{isFetching ? (
|
||||
<div className="flex justify-center">
|
||||
<progress className="progress progress-primary w-56"></progress>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{list && list.length > 0 && (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<CardText
|
||||
title="Average Optimized Money Management"
|
||||
content={
|
||||
"SL: " + optimizedMoneyManagement.stopLoss.toFixed(2) + "% | TP: " +
|
||||
optimizedMoneyManagement.takeProfit.toFixed(2) + "% | R/R: " +
|
||||
(optimizedMoneyManagement.takeProfit / optimizedMoneyManagement.stopLoss || 0).toFixed(2)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<CardText
|
||||
title="Position Timing Statistics"
|
||||
content={
|
||||
"Avg: " + positionTimingStats.averageOpenTime.toFixed(1) + "h | " +
|
||||
"Median: " + positionTimingStats.medianOpenTime.toFixed(1) + "h | " +
|
||||
"Losing Avg: " + positionTimingStats.losingPositionsAverageOpenTime.toFixed(1) + "h"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<CardText
|
||||
title="Cooldown Recommendations"
|
||||
content={
|
||||
"Avg: " + cooldownRecommendations.averageCooldown + " candles | " +
|
||||
"Median: " + cooldownRecommendations.medianCooldown + " candles"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<Table
|
||||
columns={columns}
|
||||
data={rows}
|
||||
renderRowSubCompontent={({row}: any) => (
|
||||
<BacktestRowDetails
|
||||
backtest={row.original}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Backtest Configuration Modal */}
|
||||
{selectedBacktestForRerun && (
|
||||
<UnifiedTradingModal
|
||||
showModal={showBacktestConfigModal}
|
||||
mode="backtest"
|
||||
backtest={selectedBacktestForRerun}
|
||||
closeModal={handleCloseBacktestConfigModal}
|
||||
setBacktests={setBacktests}
|
||||
showLoopSlider={true}
|
||||
/>
|
||||
)}
|
||||
{/* Bot Configuration Modal */}
|
||||
{selectedBacktest && (
|
||||
<UnifiedTradingModal
|
||||
showModal={showBotConfigModal}
|
||||
mode="createBot"
|
||||
backtest={selectedBacktest}
|
||||
closeModal={handleCloseBotConfigModal}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Backtest Configuration Modal */}
|
||||
{selectedBacktestForRerun && (
|
||||
<UnifiedTradingModal
|
||||
showModal={showBacktestConfigModal}
|
||||
mode="backtest"
|
||||
backtest={selectedBacktestForRerun}
|
||||
closeModal={handleCloseBacktestConfigModal}
|
||||
setBacktests={setBacktests}
|
||||
showLoopSlider={true}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export default BacktestTable
|
||||
|
||||
@@ -47,8 +47,8 @@ type ITradeChartProps = {
|
||||
walletBalances?: KeyValuePairOfDateTimeAndDecimal[] | null
|
||||
indicatorsValues?: { [key in keyof typeof IndicatorType]?: IndicatorsResultBase; } | null;
|
||||
stream?: Candle | null
|
||||
width: number
|
||||
height: number
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
|
||||
const TradeChart = ({
|
||||
@@ -62,12 +62,88 @@ const TradeChart = ({
|
||||
height,
|
||||
}: ITradeChartProps) => {
|
||||
const chartRef = React.useRef<HTMLDivElement>(null)
|
||||
const containerRef = React.useRef<HTMLDivElement>(null)
|
||||
const chart = useRef<IChartApi>()
|
||||
const {themeProperty} = useTheme()
|
||||
const theme = themeProperty()
|
||||
const series1 = useRef<ISeriesApi<'Candlestick'>>()
|
||||
const [timeDiff, setTimeDiff] = useState<number>(0)
|
||||
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(
|
||||
color: string,
|
||||
@@ -164,7 +240,10 @@ const TradeChart = ({
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (chartRef.current) {
|
||||
if (chartRef.current && containerRef.current) {
|
||||
const initialDimensions = getResponsiveDimensions()
|
||||
setChartDimensions(initialDimensions)
|
||||
|
||||
const lineColor = theme['base-100']
|
||||
chart.current = createChart(chartRef.current, {
|
||||
crosshair: {
|
||||
@@ -194,7 +273,7 @@ const TradeChart = ({
|
||||
visible: false,
|
||||
},
|
||||
},
|
||||
height: height,
|
||||
height: initialDimensions.height,
|
||||
layout: {
|
||||
background: {color: theme['base-300']},
|
||||
textColor: theme.accent,
|
||||
@@ -213,7 +292,7 @@ const TradeChart = ({
|
||||
secondsVisible: true,
|
||||
timeVisible: true,
|
||||
},
|
||||
width: width,
|
||||
width: initialDimensions.width,
|
||||
})
|
||||
|
||||
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
|
||||
|
||||
@@ -6,25 +6,24 @@ import useApiUrlStore from '../../../app/store/apiStore'
|
||||
import {useCustomMoneyManagement} from '../../../app/store/customMoneyManagement'
|
||||
import {useCustomScenario} from '../../../app/store/customScenario'
|
||||
import {
|
||||
AccountClient,
|
||||
BacktestClient,
|
||||
BotClient,
|
||||
BotType,
|
||||
DataClient,
|
||||
MoneyManagement,
|
||||
MoneyManagementClient,
|
||||
RiskManagement,
|
||||
RiskToleranceLevel,
|
||||
RunBacktestRequest,
|
||||
Scenario,
|
||||
ScenarioClient,
|
||||
ScenarioRequest,
|
||||
SignalType,
|
||||
StartBotRequest,
|
||||
Ticker,
|
||||
Timeframe,
|
||||
TradingBotConfigRequest,
|
||||
UpdateBotConfigRequest,
|
||||
AccountClient,
|
||||
BacktestClient,
|
||||
BotClient,
|
||||
DataClient,
|
||||
MoneyManagement,
|
||||
MoneyManagementClient,
|
||||
RiskManagement,
|
||||
RiskToleranceLevel,
|
||||
RunBacktestRequest,
|
||||
Scenario,
|
||||
ScenarioClient,
|
||||
ScenarioRequest,
|
||||
SignalType,
|
||||
StartBotRequest,
|
||||
Ticker,
|
||||
Timeframe,
|
||||
TradingBotConfigRequest,
|
||||
UpdateBotConfigRequest,
|
||||
} from '../../../generated/ManagingApi'
|
||||
import type {IUnifiedTradingConfigInput, UnifiedTradingModalProps} from '../../../global/type'
|
||||
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
|
||||
setSelectedScenario('custom');
|
||||
} else if (backtest.config.scenarioName) {
|
||||
setSelectedScenario(backtest.config.scenarioName);
|
||||
setShowCustomScenario(false);
|
||||
setSelectedScenario(backtest.config.scenarioName);
|
||||
setShowCustomScenario(false);
|
||||
}
|
||||
|
||||
setValue('timeframe', backtest.config.timeframe);
|
||||
setValue('botType', backtest.config.botType);
|
||||
setValue('cooldownPeriod', backtest.config.cooldownPeriod);
|
||||
setValue('maxLossStreak', backtest.config.maxLossStreak);
|
||||
setValue('maxPositionTimeHours', backtest.config.maxPositionTimeHours);
|
||||
@@ -245,7 +243,6 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
|
||||
}
|
||||
|
||||
setValue('timeframe', backtest.config.timeframe);
|
||||
setValue('botType', backtest.config.botType);
|
||||
setValue('cooldownPeriod', backtest.config.cooldownPeriod);
|
||||
setValue('maxLossStreak', backtest.config.maxLossStreak);
|
||||
setValue('maxPositionTimeHours', backtest.config.maxPositionTimeHours);
|
||||
@@ -305,7 +302,6 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
|
||||
}
|
||||
setValue('scenarioName', config.scenarioName || '');
|
||||
setValue('timeframe', config.timeframe);
|
||||
setValue('botType', config.botType);
|
||||
setValue('cooldownPeriod', config.cooldownPeriod);
|
||||
setValue('maxLossStreak', config.maxLossStreak);
|
||||
setValue('maxPositionTimeHours', config.maxPositionTimeHours);
|
||||
@@ -443,13 +439,14 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
|
||||
const onMoneyManagementChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
if (e.target.value === 'custom') {
|
||||
setShowCustomMoneyManagement(true);
|
||||
setSelectedMoneyManagement('custom'); // Set selected to 'custom'
|
||||
setCustomMoneyManagement(undefined);
|
||||
setGlobalCustomMoneyManagement(null); // Clear global store when creating new custom
|
||||
} else {
|
||||
setShowCustomMoneyManagement(false);
|
||||
setSelectedMoneyManagement(e.target.value); // Update selected money management
|
||||
setCustomMoneyManagement(undefined);
|
||||
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(moneyManagement)
|
||||
console.log(customMoneyManagement)
|
||||
if (!moneyManagement) {
|
||||
t.update('error', 'Money management is required');
|
||||
return;
|
||||
@@ -557,8 +554,8 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
|
||||
scenario: customScenario ? convertScenarioToRequest(customScenario) : undefined,
|
||||
scenarioName: customScenario ? undefined : form.scenarioName,
|
||||
timeframe: form.timeframe,
|
||||
botType: form.botType,
|
||||
isForWatchingOnly: form.isForWatchingOnly || false,
|
||||
flipPosition: false, // Default to false since we're only using isForWatchingOnly checkbox
|
||||
cooldownPeriod: form.cooldownPeriod,
|
||||
maxLossStreak: form.maxLossStreak,
|
||||
maxPositionTimeHours: form.maxPositionTimeHours,
|
||||
@@ -592,6 +589,7 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
|
||||
|
||||
closeModal();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
t.update('error', `Error: ${error.message || error}`);
|
||||
}
|
||||
};
|
||||
@@ -607,7 +605,6 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
|
||||
scenario: customScenario ? convertScenarioToRequest(customScenario) : undefined,
|
||||
scenarioName: customScenario ? undefined : form.scenarioName,
|
||||
timeframe: form.timeframe,
|
||||
botType: form.botType,
|
||||
isForWatchingOnly: false,
|
||||
cooldownPeriod: form.cooldownPeriod,
|
||||
maxLossStreak: form.maxLossStreak,
|
||||
@@ -622,14 +619,13 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
|
||||
useForDynamicStopLoss: form.useForDynamicStopLoss ?? true,
|
||||
moneyManagementName: showCustomMoneyManagement ? undefined : selectedMoneyManagement,
|
||||
moneyManagement: customMoneyManagement,
|
||||
flipPosition: form.isForWatchingOnly ?? false,
|
||||
};
|
||||
|
||||
const request: RunBacktestRequest = {
|
||||
config: tradingBotConfigRequest,
|
||||
startDate: new Date(form.startDate),
|
||||
endDate: new Date(form.endDate),
|
||||
balance: form.balance,
|
||||
watchOnly: false,
|
||||
save: form.save || false,
|
||||
};
|
||||
|
||||
@@ -724,45 +720,29 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
|
||||
</FormInput>
|
||||
</div>
|
||||
|
||||
{/* Second Row: Money Management & Bot Type */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<FormInput label="Money Management" htmlFor="moneyManagement">
|
||||
<select
|
||||
className="select select-bordered w-full"
|
||||
value={selectedMoneyManagement || (showCustomMoneyManagement ? 'custom' : '')}
|
||||
onChange={onMoneyManagementChange}
|
||||
>
|
||||
{moneyManagements.length === 0 ? (
|
||||
<option value="" disabled>No money management available - create a custom one below</option>
|
||||
) : (
|
||||
<>
|
||||
{moneyManagements.map((item) => (
|
||||
<option key={item.name} value={item.name}>
|
||||
{item.name}
|
||||
</option>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
<option key="custom" value="custom">
|
||||
{moneyManagements.length === 0 ? 'Create Custom Money Management' : 'Custom'}
|
||||
</option>
|
||||
</select>
|
||||
</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>
|
||||
{/* Money Management */}
|
||||
<FormInput label="Money Management" htmlFor="moneyManagement">
|
||||
<select
|
||||
className="select select-bordered w-full"
|
||||
value={selectedMoneyManagement || (showCustomMoneyManagement ? 'custom' : '')}
|
||||
onChange={onMoneyManagementChange}
|
||||
>
|
||||
{moneyManagements.length === 0 ? (
|
||||
<option value="" disabled>No money management available - create a custom one below</option>
|
||||
) : (
|
||||
<>
|
||||
{moneyManagements.map((item) => (
|
||||
<option key={item.name} value={item.name}>
|
||||
{item.name}
|
||||
</option>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
<option key="custom" value="custom">
|
||||
{moneyManagements.length === 0 ? 'Create Custom Money Management' : 'Custom'}
|
||||
</option>
|
||||
</select>
|
||||
</FormInput>
|
||||
|
||||
{/* Custom Money Management */}
|
||||
{showCustomMoneyManagement && (
|
||||
|
||||
@@ -504,12 +504,8 @@ export class BotClient extends AuthorizedApiBase {
|
||||
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?";
|
||||
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)
|
||||
url_ += "identifier=" + encodeURIComponent("" + identifier) + "&";
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
@@ -2868,7 +2864,6 @@ export interface TradingBotConfig {
|
||||
timeframe: Timeframe;
|
||||
isForWatchingOnly: boolean;
|
||||
botTradingBalance: number;
|
||||
botType: BotType;
|
||||
isForBacktest: boolean;
|
||||
cooldownPeriod: number;
|
||||
maxLossStreak: number;
|
||||
@@ -3014,12 +3009,6 @@ export enum Ticker {
|
||||
Unknown = "Unknown",
|
||||
}
|
||||
|
||||
export enum BotType {
|
||||
SimpleBot = "SimpleBot",
|
||||
ScalpingBot = "ScalpingBot",
|
||||
FlippingBot = "FlippingBot",
|
||||
}
|
||||
|
||||
export interface RiskManagement {
|
||||
adverseProbabilityThreshold: number;
|
||||
favorableProbabilityThreshold: number;
|
||||
@@ -3336,8 +3325,6 @@ export interface RunBacktestRequest {
|
||||
config?: TradingBotConfigRequest | null;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
balance?: number;
|
||||
watchOnly?: boolean;
|
||||
save?: boolean;
|
||||
}
|
||||
|
||||
@@ -3347,8 +3334,8 @@ export interface TradingBotConfigRequest {
|
||||
timeframe: Timeframe;
|
||||
isForWatchingOnly: boolean;
|
||||
botTradingBalance: number;
|
||||
botType: BotType;
|
||||
name: string;
|
||||
flipPosition: boolean;
|
||||
cooldownPeriod: number;
|
||||
maxLossStreak: number;
|
||||
scenario?: ScenarioRequest | null;
|
||||
@@ -3397,6 +3384,12 @@ export interface StartBotRequest {
|
||||
config?: TradingBotConfigRequest | null;
|
||||
}
|
||||
|
||||
export enum BotType {
|
||||
SimpleBot = "SimpleBot",
|
||||
ScalpingBot = "ScalpingBot",
|
||||
FlippingBot = "FlippingBot",
|
||||
}
|
||||
|
||||
export interface TradingBotResponse {
|
||||
status: string;
|
||||
signals: Signal[];
|
||||
|
||||
@@ -111,7 +111,6 @@ export interface TradingBotConfig {
|
||||
timeframe: Timeframe;
|
||||
isForWatchingOnly: boolean;
|
||||
botTradingBalance: number;
|
||||
botType: BotType;
|
||||
isForBacktest: boolean;
|
||||
cooldownPeriod: number;
|
||||
maxLossStreak: number;
|
||||
@@ -257,12 +256,6 @@ export enum Ticker {
|
||||
Unknown = "Unknown",
|
||||
}
|
||||
|
||||
export enum BotType {
|
||||
SimpleBot = "SimpleBot",
|
||||
ScalpingBot = "ScalpingBot",
|
||||
FlippingBot = "FlippingBot",
|
||||
}
|
||||
|
||||
export interface RiskManagement {
|
||||
adverseProbabilityThreshold: number;
|
||||
favorableProbabilityThreshold: number;
|
||||
@@ -579,8 +572,6 @@ export interface RunBacktestRequest {
|
||||
config?: TradingBotConfigRequest | null;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
balance?: number;
|
||||
watchOnly?: boolean;
|
||||
save?: boolean;
|
||||
}
|
||||
|
||||
@@ -590,8 +581,8 @@ export interface TradingBotConfigRequest {
|
||||
timeframe: Timeframe;
|
||||
isForWatchingOnly: boolean;
|
||||
botTradingBalance: number;
|
||||
botType: BotType;
|
||||
name: string;
|
||||
flipPosition: boolean;
|
||||
cooldownPeriod: number;
|
||||
maxLossStreak: number;
|
||||
scenario?: ScenarioRequest | null;
|
||||
@@ -640,6 +631,12 @@ export interface StartBotRequest {
|
||||
config?: TradingBotConfigRequest | null;
|
||||
}
|
||||
|
||||
export enum BotType {
|
||||
SimpleBot = "SimpleBot",
|
||||
ScalpingBot = "ScalpingBot",
|
||||
FlippingBot = "FlippingBot",
|
||||
}
|
||||
|
||||
export interface TradingBotResponse {
|
||||
status: string;
|
||||
signals: Signal[];
|
||||
|
||||
@@ -6,8 +6,7 @@ import {CardPosition, CardSignal, CardText, Toast,} from '../../components/molle
|
||||
import ManualPositionModal from '../../components/mollecules/ManualPositionModal'
|
||||
import TradesModal from '../../components/mollecules/TradesModal/TradesModal'
|
||||
import {TradeChart, UnifiedTradingModal} from '../../components/organism'
|
||||
import type {BotType, MoneyManagement, Position, TradingBotResponse} from '../../generated/ManagingApi'
|
||||
import {BotClient} from '../../generated/ManagingApi'
|
||||
import {BotClient, BotType, MoneyManagement, Position, TradingBotResponse} from '../../generated/ManagingApi'
|
||||
import type {IBotList} from '../../global/type.tsx'
|
||||
import MoneyManagementModal from '../settingsPage/moneymanagement/moneyManagementModal'
|
||||
|
||||
@@ -162,7 +161,7 @@ const BotList: React.FC<IBotList> = ({ list }) => {
|
||||
|
||||
if (status == 'Up') {
|
||||
client
|
||||
.bot_Stop(botType, identifier)
|
||||
.bot_Stop(identifier)
|
||||
.then(() => {
|
||||
t.update('success', 'Bot stopped')
|
||||
})
|
||||
@@ -195,7 +194,7 @@ const BotList: React.FC<IBotList> = ({ list }) => {
|
||||
}
|
||||
|
||||
function getUpdateBotBadge(bot: TradingBotResponse) {
|
||||
const classes = baseBadgeClass() + ' bg-warning'
|
||||
const classes = baseBadgeClass() + ' bg-orange-500'
|
||||
return (
|
||||
<button className={classes} onClick={() => openUpdateBotModal(bot)}>
|
||||
<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"
|
||||
>
|
||||
<div className={cardClasses(bot.status)}>
|
||||
<figure>
|
||||
<figure className="w-full">
|
||||
{
|
||||
<TradeChart
|
||||
candles={bot.candles}
|
||||
positions={bot.positions}
|
||||
signals={bot.signals}
|
||||
width={510}
|
||||
height={300}
|
||||
></TradeChart>
|
||||
}
|
||||
</figure>
|
||||
@@ -274,7 +271,7 @@ const BotList: React.FC<IBotList> = ({ list }) => {
|
||||
|
||||
{/* Action Badges */}
|
||||
<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)}
|
||||
{getManualPositionBadge(bot.identifier)}
|
||||
{getDeleteBadge(bot.identifier)}
|
||||
@@ -294,7 +291,7 @@ const BotList: React.FC<IBotList> = ({ list }) => {
|
||||
</div>
|
||||
<div className="columns-2">
|
||||
<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 className="columns-2">
|
||||
<CardPosition
|
||||
|
||||
Reference in New Issue
Block a user