Update bot config on front and back

This commit is contained in:
2025-06-04 15:42:21 +07:00
parent f41af96406
commit 756cd5fb11
14 changed files with 422 additions and 369 deletions

View File

@@ -178,39 +178,21 @@ public class BacktestController : BaseController
break;
case BotType.ScalpingBot:
backtestResult = await _backtester.RunScalpingBotBacktest(
account,
moneyManagement,
request.Config.Ticker,
scenario,
request.Config.Timeframe,
request.Balance,
backtestConfig,
request.StartDate,
request.EndDate,
user,
request.WatchOnly,
request.Save,
cooldownPeriod: request.Config.CooldownPeriod,
maxLossStreak: request.Config.MaxLossStreak,
maxPositionTimeHours: request.Config.MaxPositionTimeHours,
flipOnlyWhenInProfit: request.Config.FlipOnlyWhenInProfit);
null);
break;
case BotType.FlippingBot:
backtestResult = await _backtester.RunFlippingBotBacktest(
account,
moneyManagement,
request.Config.Ticker,
scenario,
request.Config.Timeframe,
request.Balance,
backtestConfig,
request.StartDate,
request.EndDate,
user,
request.WatchOnly,
request.Save,
cooldownPeriod: request.Config.CooldownPeriod,
maxLossStreak: request.Config.MaxLossStreak,
maxPositionTimeHours: request.Config.MaxPositionTimeHours,
flipOnlyWhenInProfit: request.Config.FlipOnlyWhenInProfit);
null);
break;
}

View File

@@ -105,52 +105,64 @@ public class BotController : BaseController
{
try
{
// Check if user owns the account
if (!await UserOwnsBotAccount(request.Identifier, request.AccountName))
if (request.Config == null)
{
return Forbid("You don't have permission to start this bot");
return BadRequest("Bot configuration is required");
}
// Check if user owns the account specified in the request
if (!await UserOwnsBotAccount(null, request.Config.AccountName))
{
return Forbid("You don't have permission to start a bot with this account");
}
// Trigger error if money management is not provided
if (string.IsNullOrEmpty(request.MoneyManagementName))
if (string.IsNullOrEmpty(request.MoneyManagementName) && request.Config.MoneyManagement == null)
{
return BadRequest("Money management name is required");
return BadRequest("Money management name or money management object is required");
}
var user = await GetUser();
var moneyManagement = await _moneyManagementService.GetMoneyMangement(user, request.MoneyManagementName);
if (moneyManagement == null)
// Get money management if name is provided
MoneyManagement moneyManagement = request.Config.MoneyManagement;
if (!string.IsNullOrEmpty(request.MoneyManagementName))
{
return BadRequest("Money management not found");
moneyManagement = await _moneyManagementService.GetMoneyMangement(user, request.MoneyManagementName);
if (moneyManagement == null)
{
return BadRequest("Money management not found");
}
}
// Validate initialTradingBalance
if (request.InitialTradingBalance <= Constants.GMX.Config.MinimumPositionAmount)
if (request.Config.BotTradingBalance <= Constants.GMX.Config.MinimumPositionAmount)
{
return BadRequest(
$"Initial trading balance must be greater than {Constants.GMX.Config.MinimumPositionAmount}");
}
// Update the config with final money management
var config = new TradingBotConfig
{
AccountName = request.AccountName,
AccountName = request.Config.AccountName,
MoneyManagement = moneyManagement,
Ticker = request.Ticker,
ScenarioName = request.Scenario,
Timeframe = request.Timeframe,
IsForWatchingOnly = request.IsForWatchOnly,
BotTradingBalance = request.InitialTradingBalance,
BotType = request.BotType,
CooldownPeriod = request.CooldownPeriod,
MaxLossStreak = request.MaxLossStreak,
MaxPositionTimeHours = request.MaxPositionTimeHours,
FlipOnlyWhenInProfit = request.FlipOnlyWhenInProfit,
Ticker = request.Config.Ticker,
ScenarioName = request.Config.ScenarioName,
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,
FlipOnlyWhenInProfit = request.Config.FlipOnlyWhenInProfit,
IsForBacktest = false,
FlipPosition = request.BotType == BotType.FlippingBot,
Name = request.Name
FlipPosition = request.Config.BotType == BotType.FlippingBot,
Name = request.Config.Name
};
var result = await _mediator.Send(new StartBotCommand(config, request.Name, user));
var result = await _mediator.Send(new StartBotCommand(config, request.Config.Name, user));
await NotifyBotSubscriberAsync();
return Ok(result);
@@ -684,77 +696,14 @@ public class ClosePositionRequest
public class StartBotRequest
{
/// <summary>
/// The type of bot to start
/// The trading bot configuration
/// </summary>
public BotType BotType { get; set; }
public TradingBotConfig Config { get; set; }
/// <summary>
/// The identifier of the bot
/// Optional money management name (if not included in Config.MoneyManagement)
/// </summary>
public string Identifier { get; set; }
/// <summary>
/// The ticker to trade
/// </summary>
public Ticker Ticker { get; set; }
/// <summary>
/// The scenario to use
/// </summary>
public string Scenario { get; set; }
/// <summary>
/// The timeframe to use
/// </summary>
public Timeframe Timeframe { get; set; }
/// <summary>
/// The account name to use
/// </summary>
public string AccountName { get; set; }
/// <summary>
/// The money management name to use
/// </summary>
public string MoneyManagementName { get; set; }
/// <summary>
/// Whether the bot is for watching only
/// </summary>
public bool IsForWatchOnly { get; set; }
/// <summary>
/// The initial trading balance
/// </summary>
public decimal InitialTradingBalance { get; set; }
/// <summary>
/// The cooldown period in candles between positions
/// </summary>
public int CooldownPeriod { get; set; }
/// <summary>
/// The maximum number of consecutive losses before stopping
/// </summary>
public int MaxLossStreak { get; set; }
/// <summary>
/// The name of the bot
/// </summary>
public string Name { get; set; }
/// <summary>
/// Maximum time in hours that a position can remain open before being automatically closed.
/// Supports fractional values (e.g., 2.5 for 2 hours and 30 minutes).
/// If null, time-based position closure is disabled.
/// </summary>
public decimal? MaxPositionTimeHours { get; set; } = null;
/// <summary>
/// If true, positions will only be flipped when the current position is in profit.
/// If false, positions will be flipped regardless of profit status.
/// </summary>
public bool FlipOnlyWhenInProfit { get; set; } = true;
public string? MoneyManagementName { get; set; }
}
/// <summary>

View File

@@ -1,79 +1,40 @@
using Managing.Domain.Accounts;
using Managing.Domain.Backtests;
using Managing.Domain.Backtests;
using Managing.Domain.Bots;
using Managing.Domain.Candles;
using Managing.Domain.MoneyManagements;
using Managing.Domain.Scenarios;
using Managing.Domain.Users;
using static Managing.Common.Enums;
namespace Managing.Application.Abstractions.Services
{
public interface IBacktester
{
Task<Backtest> RunScalpingBotBacktest(
Account account,
MoneyManagement moneyManagement,
Ticker ticker,
Scenario scenario,
Timeframe timeframe,
decimal balance,
TradingBotConfig config,
DateTime startDate,
DateTime endDate,
User user = null,
bool isForWatchingOnly = false,
bool save = false,
List<Candle>? initialCandles = null,
int cooldownPeriod = 1,
int maxLossStreak = 0,
decimal? maxPositionTimeHours = null,
bool flipOnlyWhenInProfit = true);
List<Candle>? initialCandles = null);
Task<Backtest> RunFlippingBotBacktest(
Account account,
MoneyManagement moneyManagement,
Ticker ticker,
Scenario scenario,
Timeframe timeframe,
decimal balance,
TradingBotConfig config,
DateTime startDate,
DateTime endDate,
User user = null,
bool isForWatchingOnly = false,
bool save = false,
List<Candle>? initialCandles = null,
int cooldownPeriod = 1,
int maxLossStreak = 0,
decimal? maxPositionTimeHours = null,
bool flipOnlyWhenInProfit = true);
List<Candle>? initialCandles = null);
bool DeleteBacktest(string id);
bool DeleteBacktests();
Task<Backtest> RunScalpingBotBacktest(
Account account,
MoneyManagement moneyManagement,
Scenario scenario,
Timeframe timeframe,
TradingBotConfig config,
List<Candle> candles,
decimal balance,
User user = null,
int cooldownPeriod = 1,
int maxLossStreak = 0,
decimal? maxPositionTimeHours = null,
bool flipOnlyWhenInProfit = true);
User user = null);
Task<Backtest> RunFlippingBotBacktest(
Account account,
MoneyManagement moneyManagement,
Scenario scenario,
Timeframe timeframe,
TradingBotConfig config,
List<Candle> candles,
decimal balance,
User user = null,
int cooldownPeriod = 1,
int maxLossStreak = 0,
decimal? maxPositionTimeHours = null,
bool flipOnlyWhenInProfit = true);
User user = null);
// User-specific operations
Task<IEnumerable<Backtest>> GetBacktestsByUser(User user);

View File

@@ -6,6 +6,7 @@ using Managing.Application.Abstractions.Services;
using Managing.Application.Backtesting;
using Managing.Application.Bots.Base;
using Managing.Core;
using Managing.Domain.Bots;
using Managing.Domain.Candles;
using Managing.Domain.MoneyManagements;
using Managing.Domain.Scenarios;
@@ -42,7 +43,7 @@ namespace Managing.Application.Tests
_tradingService.Object,
botService);
_backtester = new Backtester(_exchangeService, _botFactory, backtestRepository, backtestLogger,
scenarioService);
scenarioService, _accountService.Object);
_elapsedTimes = new List<double>();
}
@@ -58,10 +59,25 @@ namespace Managing.Application.Tests
var localCandles =
FileHelpers.ReadJson<List<Candle>>($"{ticker.ToString()}-{timeframe.ToString()}-candles.json");
var config = new TradingBotConfig
{
AccountName = _account.Name,
MoneyManagement = MoneyManagement,
Ticker = ticker,
ScenarioName = scenario.Name,
Timeframe = timeframe,
IsForWatchingOnly = false,
BotTradingBalance = 1000,
BotType = BotType.FlippingBot,
IsForBacktest = true,
CooldownPeriod = 1,
MaxLossStreak = 0,
FlipPosition = true,
Name = "Test"
};
// Act
var backtestResult = await _backtester.RunFlippingBotBacktest(_account, MoneyManagement, ticker, scenario,
timeframe, 1000, new DateTime().AddDays(-3), DateTime.UtcNow,
initialCandles: localCandles.TakeLast(500).ToList());
var backtestResult = await _backtester.RunFlippingBotBacktest(config, localCandles.TakeLast(500).ToList());
var json = JsonConvert.SerializeObject(backtestResult, Formatting.None);
File.WriteAllText($"{ticker.ToString()}-{timeframe.ToString()}-{Guid.NewGuid()}.json", json);
@@ -90,9 +106,25 @@ namespace Managing.Application.Tests
var strategy = ScenarioHelpers.BuildStrategy(StrategyType.RsiDivergence, "RsiDiv", period: 5);
scenario.AddStrategy(strategy);
var config = new TradingBotConfig
{
AccountName = _account.Name,
MoneyManagement = MoneyManagement,
Ticker = ticker,
ScenarioName = scenario.Name,
Timeframe = timeframe,
IsForWatchingOnly = false,
BotTradingBalance = 1000,
BotType = BotType.ScalpingBot,
IsForBacktest = true,
CooldownPeriod = 1,
MaxLossStreak = 0,
FlipPosition = false,
Name = "Test"
};
// Act
var backtestResult = await _backtester.RunScalpingBotBacktest(_account, MoneyManagement, ticker, scenario,
timeframe, 1000, DateTime.UtcNow.AddDays(-6), DateTime.UtcNow, null);
var backtestResult = await _backtester.RunScalpingBotBacktest(config, DateTime.UtcNow.AddDays(-6), DateTime.UtcNow, null, false, null);
//WriteCsvReport(backtestResult.GetStringReport());
// Assert
@@ -120,9 +152,25 @@ namespace Managing.Application.Tests
TakeProfit = 0.02m
};
var config = new TradingBotConfig
{
AccountName = _account.Name,
MoneyManagement = moneyManagement,
Ticker = ticker,
ScenarioName = scenario.Name,
Timeframe = timeframe,
IsForWatchingOnly = false,
BotTradingBalance = 1000,
BotType = BotType.ScalpingBot,
IsForBacktest = true,
CooldownPeriod = 1,
MaxLossStreak = 0,
FlipPosition = false,
Name = "Test"
};
// Act
var backtestResult = await _backtester.RunScalpingBotBacktest(_account, moneyManagement, ticker, scenario,
timeframe, 1000, DateTime.UtcNow.AddDays(-6), DateTime.UtcNow, null);
var backtestResult = await _backtester.RunScalpingBotBacktest(config, DateTime.UtcNow.AddDays(-6), DateTime.UtcNow, null, false, null);
WriteCsvReport(backtestResult.GetStringReport());
// Assert
@@ -191,10 +239,38 @@ namespace Managing.Application.Tests
var backtestResult = botType switch
{
BotType.SimpleBot => throw new NotImplementedException(),
BotType.ScalpingBot => _backtester.RunScalpingBotBacktest(_account, moneyManagement,
scenario, timeframe, candles, 1000, null).Result,
BotType.FlippingBot => _backtester.RunFlippingBotBacktest(_account, moneyManagement,
scenario, timeframe, candles, 1000, null).Result,
BotType.ScalpingBot => _backtester.RunScalpingBotBacktest(new TradingBotConfig
{
AccountName = _account.Name,
MoneyManagement = moneyManagement,
Ticker = ticker,
ScenarioName = scenario.Name,
Timeframe = timeframe,
IsForWatchingOnly = false,
BotTradingBalance = 1000,
BotType = BotType.ScalpingBot,
IsForBacktest = true,
CooldownPeriod = 1,
MaxLossStreak = 0,
FlipPosition = false,
Name = "Test"
}, candles, null).Result,
BotType.FlippingBot => _backtester.RunFlippingBotBacktest(new TradingBotConfig
{
AccountName = _account.Name,
MoneyManagement = moneyManagement,
Ticker = ticker,
ScenarioName = scenario.Name,
Timeframe = timeframe,
IsForWatchingOnly = false,
BotTradingBalance = 1000,
BotType = BotType.FlippingBot,
IsForBacktest = true,
CooldownPeriod = 1,
MaxLossStreak = 0,
FlipPosition = true,
Name = "Test"
}, candles, null).Result,
_ => throw new NotImplementedException(),
};
timer.Stop();
@@ -299,10 +375,38 @@ namespace Managing.Application.Tests
var backtestResult = botType switch
{
BotType.SimpleBot => throw new NotImplementedException(),
BotType.ScalpingBot => _backtester.RunScalpingBotBacktest(_account, moneyManagement,
scenario, timeframe, candles, 1000, null).Result,
BotType.FlippingBot => _backtester.RunFlippingBotBacktest(_account, moneyManagement,
scenario, timeframe, candles, 1000, null).Result,
BotType.ScalpingBot => _backtester.RunScalpingBotBacktest(new TradingBotConfig
{
AccountName = _account.Name,
MoneyManagement = moneyManagement,
Ticker = ticker,
ScenarioName = scenario.Name,
Timeframe = timeframe,
IsForWatchingOnly = false,
BotTradingBalance = 1000,
BotType = BotType.ScalpingBot,
IsForBacktest = true,
CooldownPeriod = 1,
MaxLossStreak = 0,
FlipPosition = false,
Name = "Test"
}, candles, null).Result,
BotType.FlippingBot => _backtester.RunFlippingBotBacktest(new TradingBotConfig
{
AccountName = _account.Name,
MoneyManagement = moneyManagement,
Ticker = ticker,
ScenarioName = scenario.Name,
Timeframe = timeframe,
IsForWatchingOnly = false,
BotTradingBalance = 1000,
BotType = BotType.FlippingBot,
IsForBacktest = true,
CooldownPeriod = 1,
MaxLossStreak = 0,
FlipPosition = true,
Name = "Test"
}, candles, null).Result,
_ => throw new NotImplementedException(),
};

View File

@@ -1,6 +1,7 @@
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using Managing.Domain.Accounts;
using Managing.Domain.Bots;
using Managing.Domain.MoneyManagements;
using Managing.Domain.Scenarios;
using Managing.Domain.Shared.Helpers;
@@ -262,16 +263,30 @@ public class StatisticService : IStatisticService
TakeProfit = 0.02m
};
var config = new TradingBotConfig
{
AccountName = account.Name,
MoneyManagement = moneyManagement,
Ticker = ticker,
ScenarioName = scenario.Name,
Timeframe = timeframe,
IsForWatchingOnly = true,
BotTradingBalance = 1000,
BotType = BotType.ScalpingBot,
IsForBacktest = true,
CooldownPeriod = 1,
MaxLossStreak = 0,
FlipPosition = false,
Name = "StatisticsBacktest"
};
var backtest = await _backtester.RunScalpingBotBacktest(
account,
moneyManagement,
ticker,
scenario,
timeframe,
1000,
config,
DateTime.Now.AddDays(-7),
DateTime.Now,
isForWatchingOnly: true);
null,
false,
null);
return backtest.Signals;
}

View File

@@ -1,13 +1,11 @@
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using Managing.Core;
using Managing.Core.FixedSizedQueue;
using Managing.Domain.Accounts;
using Managing.Domain.Backtests;
using Managing.Domain.Bots;
using Managing.Domain.Candles;
using Managing.Domain.MoneyManagements;
using Managing.Domain.Scenarios;
using Managing.Domain.Shared.Helpers;
using Managing.Domain.Strategies;
@@ -26,18 +24,22 @@ namespace Managing.Application.Backtesting
private readonly IExchangeService _exchangeService;
private readonly IBotFactory _botFactory;
private readonly IScenarioService _scenarioService;
private readonly IAccountService _accountService;
public Backtester(
IExchangeService exchangeService,
IBotFactory botFactory,
IBacktestRepository backtestRepository,
ILogger<Backtester> logger, IScenarioService scenarioService)
ILogger<Backtester> logger,
IScenarioService scenarioService,
IAccountService accountService)
{
_exchangeService = exchangeService;
_botFactory = botFactory;
_backtestRepository = backtestRepository;
_logger = logger;
_scenarioService = scenarioService;
_accountService = accountService;
}
public Backtest RunSimpleBotBacktest(Workflow workflow, bool save = false)
@@ -54,45 +56,19 @@ namespace Managing.Application.Backtesting
}
public async Task<Backtest> RunScalpingBotBacktest(
Account account,
MoneyManagement moneyManagement,
Ticker ticker,
Scenario scenario,
Timeframe timeframe,
decimal balance,
TradingBotConfig config,
DateTime startDate,
DateTime endDate,
User user = null,
bool isForWatchingOnly = false,
bool save = false,
List<Candle>? initialCandles = null,
int cooldownPeriod = 1,
int maxLossStreak = 0,
decimal? maxPositionTimeHours = null,
bool flipOnlyWhenInProfit = true)
List<Candle>? initialCandles = null)
{
var config = new TradingBotConfig
{
AccountName = account.Name,
MoneyManagement = moneyManagement,
Ticker = ticker,
ScenarioName = scenario.Name,
Timeframe = timeframe,
IsForWatchingOnly = isForWatchingOnly,
BotTradingBalance = balance,
BotType = BotType.ScalpingBot,
IsForBacktest = true,
CooldownPeriod = cooldownPeriod,
MaxLossStreak = maxLossStreak,
MaxPositionTimeHours = maxPositionTimeHours,
FlipOnlyWhenInProfit = flipOnlyWhenInProfit
};
var account = await GetAccountFromConfig(config);
var scalpingBot = _botFactory.CreateBacktestScalpingBot(config);
scalpingBot.LoadScenario(scenario.Name);
scalpingBot.LoadScenario(config.ScenarioName);
scalpingBot.User = user;
await scalpingBot.LoadAccount();
var candles = initialCandles ?? GetCandles(account, ticker, timeframe, startDate, endDate);
var candles = initialCandles ?? GetCandles(account, config.Ticker, config.Timeframe, startDate, endDate);
var result = GetBacktestingResult(config, scalpingBot, candles);
if (user != null)
@@ -113,46 +89,20 @@ namespace Managing.Application.Backtesting
}
public async Task<Backtest> RunFlippingBotBacktest(
Account account,
MoneyManagement moneyManagement,
Ticker ticker,
Scenario scenario,
Timeframe timeframe,
decimal balance,
TradingBotConfig config,
DateTime startDate,
DateTime endDate,
User user = null,
bool isForWatchingOnly = false,
bool save = false,
List<Candle>? initialCandles = null,
int cooldownPeriod = 1,
int maxLossStreak = 0,
decimal? maxPositionTimeHours = null,
bool flipOnlyWhenInProfit = true)
List<Candle>? initialCandles = null)
{
var config = new TradingBotConfig
{
AccountName = account.Name,
MoneyManagement = moneyManagement,
Ticker = ticker,
ScenarioName = scenario.Name,
Timeframe = timeframe,
IsForWatchingOnly = isForWatchingOnly,
BotTradingBalance = balance,
BotType = BotType.FlippingBot,
IsForBacktest = true,
CooldownPeriod = cooldownPeriod,
MaxLossStreak = maxLossStreak,
MaxPositionTimeHours = maxPositionTimeHours,
FlipOnlyWhenInProfit = flipOnlyWhenInProfit
};
var account = await GetAccountFromConfig(config);
var flippingBot = _botFactory.CreateBacktestFlippingBot(config);
flippingBot.LoadScenario(scenario.Name);
flippingBot.LoadScenario(config.ScenarioName);
flippingBot.User = user;
await flippingBot.LoadAccount();
var candles = initialCandles ?? GetCandles(account, ticker, timeframe, startDate, endDate);
var candles = initialCandles ?? GetCandles(account, config.Ticker, config.Timeframe, startDate, endDate);
var result = GetBacktestingResult(config, flippingBot, candles);
if (user != null)
@@ -173,38 +123,13 @@ namespace Managing.Application.Backtesting
}
public async Task<Backtest> RunScalpingBotBacktest(
Account account,
MoneyManagement moneyManagement,
Scenario scenario,
Timeframe timeframe,
TradingBotConfig config,
List<Candle> candles,
decimal balance,
User user = null,
int cooldownPeriod = 1,
int maxLossStreak = 0,
decimal? maxPositionTimeHours = null,
bool flipOnlyWhenInProfit = true)
User user = null)
{
var ticker = MiscExtensions.ParseEnum<Ticker>(candles.FirstOrDefault().Ticker);
var config = new TradingBotConfig
{
AccountName = account.Name,
MoneyManagement = moneyManagement,
Ticker = ticker,
ScenarioName = scenario.Name,
Timeframe = timeframe,
IsForWatchingOnly = false,
BotTradingBalance = balance,
BotType = BotType.ScalpingBot,
IsForBacktest = true,
CooldownPeriod = cooldownPeriod,
MaxLossStreak = maxLossStreak,
MaxPositionTimeHours = maxPositionTimeHours,
FlipOnlyWhenInProfit = flipOnlyWhenInProfit
};
var account = await GetAccountFromConfig(config);
var bot = _botFactory.CreateBacktestScalpingBot(config);
bot.LoadScenario(scenario.Name);
bot.LoadScenario(config.ScenarioName);
bot.User = user;
await bot.LoadAccount();
@@ -219,38 +144,13 @@ namespace Managing.Application.Backtesting
}
public async Task<Backtest> RunFlippingBotBacktest(
Account account,
MoneyManagement moneyManagement,
Scenario scenario,
Timeframe timeframe,
TradingBotConfig config,
List<Candle> candles,
decimal balance,
User user = null,
int cooldownPeriod = 1,
int maxLossStreak = 0,
decimal? maxPositionTimeHours = null,
bool flipOnlyWhenInProfit = true)
User user = null)
{
var ticker = MiscExtensions.ParseEnum<Ticker>(candles.FirstOrDefault().Ticker);
var config = new TradingBotConfig
{
AccountName = account.Name,
MoneyManagement = moneyManagement,
Ticker = ticker,
ScenarioName = scenario.Name,
Timeframe = timeframe,
IsForWatchingOnly = false,
BotTradingBalance = balance,
BotType = BotType.FlippingBot,
IsForBacktest = true,
CooldownPeriod = cooldownPeriod,
MaxLossStreak = maxLossStreak,
MaxPositionTimeHours = maxPositionTimeHours,
FlipOnlyWhenInProfit = flipOnlyWhenInProfit
};
var account = await GetAccountFromConfig(config);
var bot = _botFactory.CreateBacktestFlippingBot(config);
bot.LoadScenario(scenario.Name);
bot.LoadScenario(config.ScenarioName);
bot.User = user;
await bot.LoadAccount();
@@ -264,6 +164,23 @@ namespace Managing.Application.Backtesting
return result;
}
private async Task<Account> GetAccountFromConfig(TradingBotConfig config)
{
// Use the account service to get the actual account
var account = await _accountService.GetAccount(config.AccountName, false, false);
if (account != null)
{
return account;
}
// Fallback: create a basic account structure if not found
return new Account
{
Name = config.AccountName,
Exchange = TradingExchanges.GmxV2 // Default exchange, should be configurable
};
}
private List<Candle> GetCandles(Account account, Ticker ticker, Timeframe timeframe,
DateTime startDate, DateTime endDate)
{

View File

@@ -468,28 +468,55 @@ public class TradingBot : Bot, ITradingBot
var currentTime = Config.IsForBacktest ? lastCandle.Date : DateTime.UtcNow;
// Check if position has exceeded maximum time limit (only if MaxPositionTimeHours is set)
if (Config.MaxPositionTimeHours.HasValue && HasPositionExceededTimeLimit(positionForSignal, currentTime))
// Check time-based position management (only if MaxPositionTimeHours is set)
if (Config.MaxPositionTimeHours.HasValue)
{
// Check if position is in profit or at breakeven before closing
var isPositionInProfit = await IsPositionInProfit(positionForSignal, lastCandle.Close);
var isAtBreakeven = Math.Abs(lastCandle.Close - positionForSignal.Open.Price) < 0.01m; // Small tolerance for breakeven
var hasExceededTimeLimit = HasPositionExceededTimeLimit(positionForSignal, currentTime);
if (isPositionInProfit || isAtBreakeven)
// Calculate current unrealized PNL for logging
var currentPnl = CalculateUnrealizedPnl(positionForSignal, lastCandle.Close);
var pnlPercentage = Math.Round((currentPnl / (positionForSignal.Open.Price * positionForSignal.Open.Quantity)) * 100, 2);
// Early closure logic when CloseEarlyWhenProfitable is enabled
if (Config.CloseEarlyWhenProfitable && (isPositionInProfit || isAtBreakeven))
{
await LogInformation(
$"Closing position due to time limit - Position opened at {positionForSignal.Open.Date}, " +
$"current time {currentTime}, max time limit {Config.MaxPositionTimeHours} hours. " +
$"Position is {(isPositionInProfit ? "in profit" : "at breakeven")} (entry: {positionForSignal.Open.Price}, current: {lastCandle.Close})");
$"Closing position early due to profitability - Position opened at {positionForSignal.Open.Date}, " +
$"current time {currentTime}. Position is {(isPositionInProfit ? "in profit" : "at breakeven")} " +
$"(entry: {positionForSignal.Open.Price}, current: {lastCandle.Close}). " +
$"Current PNL: ${currentPnl:F2} ({pnlPercentage:F2}%). " +
$"CloseEarlyWhenProfitable is enabled.");
await CloseTrade(signal, positionForSignal, positionForSignal.Open, lastCandle.Close, true);
return;
}
else
// Time limit exceeded logic
if (hasExceededTimeLimit)
{
await LogInformation(
$"Position has exceeded time limit ({Config.MaxPositionTimeHours} hours) but is at a loss " +
$"(entry: {positionForSignal.Open.Price}, current: {lastCandle.Close}). " +
$"Waiting for profit or breakeven before closing.");
if (Config.CloseEarlyWhenProfitable || isPositionInProfit || isAtBreakeven)
{
// Close when time limit is reached if:
// 1. CloseEarlyWhenProfitable is enabled (safety net), OR
// 2. Position is in profit/breakeven (when CloseEarlyWhenProfitable is disabled)
await LogInformation(
$"Closing position due to time limit - Position opened at {positionForSignal.Open.Date}, " +
$"current time {currentTime}, max time limit {Config.MaxPositionTimeHours} hours. " +
$"Position is {(isPositionInProfit ? "in profit" : isAtBreakeven ? "at breakeven" : "at a loss")} " +
$"(entry: {positionForSignal.Open.Price}, current: {lastCandle.Close}). " +
$"Current PNL: ${currentPnl:F2} ({pnlPercentage:F2}%)");
await CloseTrade(signal, positionForSignal, positionForSignal.Open, lastCandle.Close, true);
return;
}
else
{
await LogInformation(
$"Position has exceeded time limit ({Config.MaxPositionTimeHours} hours) but is at a loss " +
$"(entry: {positionForSignal.Open.Price}, current: {lastCandle.Close}). " +
$"Current PNL: ${currentPnl:F2} ({pnlPercentage:F2}%). " +
$"CloseEarlyWhenProfitable is disabled - waiting for profit or breakeven before closing.");
}
}
}
@@ -1269,6 +1296,28 @@ public class TradingBot : Bot, ITradingBot
return timeOpen >= maxTimeAllowed;
}
/// <summary>
/// Calculates the current unrealized PNL for a position
/// </summary>
/// <param name="position">The position to calculate PNL for</param>
/// <param name="currentPrice">The current market price</param>
/// <returns>The current unrealized PNL</returns>
private decimal CalculateUnrealizedPnl(Position position, decimal currentPrice)
{
if (position.OriginDirection == TradeDirection.Long)
{
return currentPrice - position.Open.Price;
}
else if (position.OriginDirection == TradeDirection.Short)
{
return position.Open.Price - currentPrice;
}
else
{
throw new ArgumentException("Invalid position direction");
}
}
/// <summary>
/// Updates the trading bot configuration with new settings.
/// This method validates the new configuration and applies it to the running bot.

View File

@@ -26,6 +26,14 @@ public class TradingBotConfig
/// </summary>
public decimal? MaxPositionTimeHours { get; set; }
/// <summary>
/// When MaxPositionTimeHours is set and this is true, the position will be closed as soon as
/// it reaches breakeven or profit, rather than waiting for the full time duration.
/// If false, the position will only be closed when MaxPositionTimeHours is reached.
/// Default is false to maintain existing behavior.
/// </summary>
public bool CloseEarlyWhenProfitable { get; set; } = false;
/// <summary>
/// If true, positions will only be flipped when the current position is in profit.
/// If false, positions will be flipped regardless of profit status.

View File

@@ -5,12 +5,11 @@ import {useQuery} from '@tanstack/react-query'
import useApiUrlStore from '../../../app/store/apiStore'
import type {
Backtest,
MoneyManagement,
RunBacktestRequest,
StartBotRequest,
Ticker,
TradingBotConfig
Backtest,
MoneyManagement,
RunBacktestRequest,
StartBotRequest,
TradingBotConfig
} from '../../../generated/ManagingApi'
import {BacktestClient, BotClient, MoneyManagementClient} from '../../../generated/ManagingApi'
import type {IBacktestCards} from '../../../global/type'
@@ -90,23 +89,39 @@ const BacktestCards: React.FC<IBacktestCards> = ({ list, setBacktests }) => {
moneyManagementName.toLowerCase() === 'custom' ||
moneyManagementName.toLowerCase().includes('custom');
const request: StartBotRequest = {
// Create TradingBotConfig from the backtest configuration
const tradingBotConfig: TradingBotConfig = {
accountName: backtest.config.accountName,
name: botName,
botType: backtest.config.botType,
isForWatchOnly: isForWatchOnly,
// Only use the money management name if it's not a custom money management, otherwise use optimized
moneyManagementName: isCustomMoneyManagement ?
(backtest.optimizedMoneyManagement?.name || backtest.config.moneyManagement?.name) :
moneyManagementName,
scenario: backtest.config.scenarioName,
ticker: backtest.config.ticker as Ticker,
ticker: backtest.config.ticker,
scenarioName: backtest.config.scenarioName,
timeframe: backtest.config.timeframe,
initialTradingBalance: initialTradingBalance,
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
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,
// Only use the money management name if it's not a custom money management
moneyManagementName: isCustomMoneyManagement ? undefined : moneyManagementName
}
await client

View File

@@ -47,7 +47,9 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
cooldownPeriod: 10, // Default cooldown period of 10 minutes
maxLossStreak: 0, // Default max loss streak of 0 (no limit)
maxPositionTimeHours: null, // Default to null (disabled)
flipOnlyWhenInProfit: true // Default to true
flipOnlyWhenInProfit: true, // Default to true
balance: 10000, // Default balance
closeEarlyWhenProfitable: false // Default to false
}
});
const [selectedAccount, setSelectedAccount] = useState<string>('')
@@ -123,14 +125,15 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
flipOnlyWhenInProfit: form.flipOnlyWhenInProfit ?? true,
flipPosition: form.botType === BotType.FlippingBot, // Set based on bot type
name: `Backtest-${scenarioName}-${ticker}-${new Date().toISOString()}`,
botTradingBalance: 0, // Will be set in the request
botTradingBalance: form.balance,
moneyManagement: customMoneyManagement || moneyManagements?.find(m => m.name === selectedMoneyManagement) || moneyManagements?.[0] || {
name: 'placeholder',
leverage: 1,
stopLoss: 0.01,
takeProfit: 0.02,
timeframe: form.timeframe
}
},
closeEarlyWhenProfitable: form.closeEarlyWhenProfitable ?? false
};
// Create the RunBacktestRequest
@@ -138,7 +141,7 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
config: tradingBotConfig,
startDate: new Date(form.startDate),
endDate: new Date(form.endDate),
balance: balance,
balance: form.balance,
watchOnly: false,
save: form.save || false,
moneyManagementName: customMoneyManagement ? undefined : selectedMoneyManagement,
@@ -335,8 +338,8 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
} else if (selectedMoneyManagement) {
mm = moneyManagements.find((m) => m.name === selectedMoneyManagement);
}
// Use actual initial balance and a minimum threshold
const initialBalance = balance;
// Use form balance if available, otherwise fall back to state balance
const initialBalance = balance; // This state is kept in sync with form
const minBalance = 10; // You can make this configurable if needed
if (mm && mm.leverage && mm.stopLoss && initialBalance > minBalance) {
const perLoss = mm.leverage * mm.stopLoss;
@@ -400,8 +403,11 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
<input
type="number"
className="input input-bordered w-full"
value={balance}
onChange={(e) => setBalance(Number(e.target.value))}
{...register('balance', { valueAsNumber: true })}
onChange={(e) => {
setValue('balance', Number(e.target.value));
setBalance(Number(e.target.value)); // Keep state in sync for UI calculations
}}
/>
</FormInput>
@@ -443,7 +449,7 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
</FormInput>
</div>
{/* Sixth Row: Flip Only When In Profit & Save */}
{/* Sixth Row: Flip Only When In Profit & Close Early When Profitable */}
<div className="grid grid-cols-2 gap-4">
<FormInput label="Flip Only When In Profit" htmlFor="flipOnlyWhenInProfit">
<input
@@ -456,6 +462,20 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
</div>
</FormInput>
<FormInput label="Close Early When Profitable" htmlFor="closeEarlyWhenProfitable">
<input
type="checkbox"
className="toggle toggle-primary"
{...register('closeEarlyWhenProfitable')}
/>
<div className="text-xs text-gray-500 mt-1">
If enabled, positions will close early when they become profitable
</div>
</FormInput>
</div>
{/* Seventh Row: Save */}
<div className="grid grid-cols-1 gap-4">
<FormInput label="Save" htmlFor="save">
<input
type="checkbox"
@@ -465,7 +485,7 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
</FormInput>
</div>
{/* Seventh Row: Start Date & End Date */}
{/* Eighth Row: Start Date & End Date */}
<div className="grid grid-cols-2 gap-4">
<FormInput label="Start Date" htmlFor="startDate">
<input

View File

@@ -226,6 +226,25 @@ const BacktestRowDetails: React.FC<IBacktestRowDetailsProps> = ({
const cooldownRecommendations = getCooldownRecommendations();
// Calculate average trades per day
const getAverageTradesPerDay = () => {
if (positions.length === 0) return "0.00";
// Get all trade dates and sort them
const tradeDates = positions.map(position => new Date(position.open.date)).sort((a, b) => a.getTime() - b.getTime());
if (tradeDates.length < 2) return positions.length.toString();
// Calculate the date range in days
const firstTradeDate = tradeDates[0];
const lastTradeDate = tradeDates[tradeDates.length - 1];
const diffInMs = lastTradeDate.getTime() - firstTradeDate.getTime();
const diffInDays = Math.max(1, diffInMs / (1000 * 60 * 60 * 24)); // Ensure at least 1 day
const averageTradesPerDay = positions.length / diffInDays;
return averageTradesPerDay.toFixed(2);
};
return (
<>
<div className="grid grid-flow-row">
@@ -325,6 +344,10 @@ const BacktestRowDetails: React.FC<IBacktestRowDetailsProps> = ({
title="Median Cooldown"
content={cooldownRecommendations.median + " candles"}
></CardText>
<CardText
title="Avg Trades Per Day"
content={getAverageTradesPerDay() + " trades/day"}
></CardText>
</div>
<div>
<figure>

View File

@@ -3,7 +3,7 @@ import React, {useEffect, useState} from 'react'
import {useQuery} from '@tanstack/react-query'
import useApiUrlStore from '../../../app/store/apiStore'
import type {Backtest, StartBotRequest, Ticker,} from '../../../generated/ManagingApi'
import type {Backtest, StartBotRequest, Ticker, TradingBotConfig} from '../../../generated/ManagingApi'
import {BacktestClient, BotClient, MoneyManagementClient} from '../../../generated/ManagingApi'
import type {IBacktestCards} from '../../../global/type'
import {CardText, SelectColumnFilter, Table, Toast} from '../../mollecules'
@@ -58,19 +58,39 @@ const BacktestTable: React.FC<IBacktestCards> = ({ list, isFetching, setBacktest
moneyManagementName.toLowerCase() === 'custom' ||
moneyManagementName.toLowerCase().includes('custom');
const request: StartBotRequest = {
// Create TradingBotConfig from the backtest configuration
const tradingBotConfig: TradingBotConfig = {
accountName: backtest.config.accountName,
botType: backtest.config.botType,
isForWatchOnly: isForWatchOnly,
// Only use the money management name if it's not a custom money management
moneyManagementName: isCustomMoneyManagement ? '' : moneyManagementName,
scenario: backtest.config.scenarioName,
ticker: backtest.config.ticker as Ticker,
ticker: backtest.config.ticker,
scenarioName: backtest.config.scenarioName,
timeframe: backtest.config.timeframe,
initialTradingBalance: initialTradingBalance,
botType: backtest.config.botType,
isForWatchingOnly: isForWatchOnly,
isForBacktest: false, // This is for running a live bot
cooldownPeriod: backtest.config.cooldownPeriod,
maxLossStreak: backtest.config.maxLossStreak,
maxPositionTimeHours: backtest.config.maxPositionTimeHours,
flipOnlyWhenInProfit: backtest.config.flipOnlyWhenInProfit,
flipPosition: backtest.config.flipPosition,
name: botName,
botTradingBalance: initialTradingBalance,
// Use the money management from backtest if it's custom, otherwise leave null and use moneyManagementName
moneyManagement: isCustomMoneyManagement ?
(backtest.config.moneyManagement || {
name: 'default',
leverage: 1,
stopLoss: 0.01,
takeProfit: 0.02,
timeframe: backtest.config.timeframe
}) :
backtest.config.moneyManagement, // Always provide a valid MoneyManagement object
closeEarlyWhenProfitable: backtest.config.closeEarlyWhenProfitable || false
};
const request: StartBotRequest = {
config: tradingBotConfig,
// Only use the money management name if it's not a custom money management
moneyManagementName: isCustomMoneyManagement ? undefined : moneyManagementName
}
await client

View File

@@ -2699,6 +2699,7 @@ export interface TradingBotConfig {
flipPosition: boolean;
name: string;
maxPositionTimeHours?: number | null;
closeEarlyWhenProfitable?: boolean;
flipOnlyWhenInProfit: boolean;
}
@@ -3113,20 +3114,8 @@ export interface RunBacktestRequest {
}
export interface StartBotRequest {
botType?: BotType;
identifier?: string | null;
ticker?: Ticker;
scenario?: string | null;
timeframe?: Timeframe;
accountName?: string | null;
config?: TradingBotConfig | null;
moneyManagementName?: string | null;
isForWatchOnly?: boolean;
initialTradingBalance?: number;
cooldownPeriod?: number;
maxLossStreak?: number;
name?: string | null;
maxPositionTimeHours?: number | null;
flipOnlyWhenInProfit?: boolean;
}
export interface TradingBot {

View File

@@ -116,6 +116,7 @@ export type IBacktestsFormInput = {
maxLossStreak: number
maxPositionTimeHours?: number | null
flipOnlyWhenInProfit?: boolean
closeEarlyWhenProfitable?: boolean
}
export type IBacktestCards = {