Fix backtest

This commit is contained in:
2025-04-29 01:17:58 +07:00
parent 204bd87e6a
commit f91b12fbcc
14 changed files with 113 additions and 92 deletions

View File

@@ -1,6 +1,7 @@
using Managing.Common; using Managing.Common;
using Managing.Domain.Bots; using Managing.Domain.Bots;
using Managing.Domain.MoneyManagements; using Managing.Domain.MoneyManagements;
using Managing.Domain.Users;
using Managing.Domain.Workflows; using Managing.Domain.Workflows;
namespace Managing.Application.Abstractions; namespace Managing.Application.Abstractions;
@@ -8,7 +9,7 @@ namespace Managing.Application.Abstractions;
public interface IBotService public interface IBotService
{ {
void SaveOrUpdateBotBackup(BotBackup botBackup); void SaveOrUpdateBotBackup(BotBackup botBackup);
void SaveOrUpdateBotBackup(string name, Enums.BotType botType, string data); void SaveOrUpdateBotBackup(User user, string identifier, Enums.BotType botType, string data);
void AddSimpleBotToCache(IBot bot); void AddSimpleBotToCache(IBot bot);
void AddTradingBotToCache(ITradingBot bot); void AddTradingBotToCache(ITradingBot bot);
List<ITradingBot> GetActiveBots(); List<ITradingBot> GetActiveBots();

View File

@@ -1,5 +1,4 @@
using Managing.Core.FixedSizedQueue; using Managing.Core.FixedSizedQueue;
using Managing.Domain.Accounts;
using Managing.Domain.Bots; using Managing.Domain.Bots;
using Managing.Domain.Candles; using Managing.Domain.Candles;
using Managing.Domain.MoneyManagements; using Managing.Domain.MoneyManagements;
@@ -28,6 +27,7 @@ namespace Managing.Application.Abstractions
Dictionary<DateTime, decimal> WalletBalances { get; set; } Dictionary<DateTime, decimal> WalletBalances { get; set; }
Dictionary<StrategyType, StrategiesResultBase> StrategiesValues { get; set; } Dictionary<StrategyType, StrategiesResultBase> StrategiesValues { get; set; }
User User { get; set; } User User { get; set; }
string Identifier { get; set; }
Task Run(); Task Run();
Task ToggleIsForWatchOnly(); Task ToggleIsForWatchOnly();

View File

@@ -45,7 +45,7 @@ namespace Managing.Application.Bots
public override void SaveBackup() public override void SaveBackup()
{ {
var data = JsonConvert.SerializeObject(_workflow); var data = JsonConvert.SerializeObject(_workflow);
_botService.SaveOrUpdateBotBackup(Name, BotType.SimpleBot, data); _botService.SaveOrUpdateBotBackup(User, Identifier, BotType.SimpleBot, data);
} }
public override void LoadBackup(BotBackup backup) public override void LoadBackup(BotBackup backup)

View File

@@ -146,7 +146,7 @@ public class TradingBot : Bot, ITradingBot
public async Task LoadAccount() public async Task LoadAccount()
{ {
var account = await AccountService.GetAccount(AccountName, false, true); var account = await AccountService.GetAccount(AccountName, false, false);
if (account == null) if (account == null)
{ {
Logger.LogWarning($"No account found for this {AccountName}"); Logger.LogWarning($"No account found for this {AccountName}");
@@ -192,7 +192,7 @@ public class TradingBot : Bot, ITradingBot
if (balance < Constants.GMX.Config.MinimumPositionAmount) if (balance < Constants.GMX.Config.MinimumPositionAmount)
{ {
await LogWarning( await LogWarning(
$"Balance on broker is below {Constants.GMX.Config.MinimumPositionAmount} USD (actual: {balance}). Stopping bot and saving backup."); $"Balance on broker is below {Constants.GMX.Config.MinimumPositionAmount} USD (actual: {balance}). Stopping bot {Identifier} and saving backup.");
SaveBackup(); SaveBackup();
Stop(); Stop();
return; return;
@@ -587,8 +587,8 @@ public class TradingBot : Bot, ITradingBot
lastPrice, lastPrice,
signalIdentifier: signal.Identifier); signalIdentifier: signal.Identifier);
var position = await new OpenPositionCommandHandler(ExchangeService, AccountService, TradingService) var position = (new OpenPositionCommandHandler(ExchangeService, AccountService, TradingService)
.Handle(command); .Handle(command)).Result;
if (position != null) if (position != null)
{ {
@@ -665,12 +665,11 @@ public class TradingBot : Bot, ITradingBot
} }
else else
{ {
var command = new ClosePositionCommand(position, lastPrice); var command = new ClosePositionCommand(position, lastPrice, isForBacktest: IsForBacktest);
try try
{ {
var closedPosition = var closedPosition = (new ClosePositionCommandHandler(ExchangeService, AccountService, TradingService)
(new ClosePositionCommandHandler(ExchangeService, AccountService, TradingService) .Handle(command)).Result;
.Handle(command)).Result;
if (closedPosition.Status == (PositionStatus.Finished | PositionStatus.Flipped)) if (closedPosition.Status == (PositionStatus.Finished | PositionStatus.Flipped))
{ {
@@ -909,7 +908,7 @@ public class TradingBot : Bot, ITradingBot
BotTradingBalance = BotTradingBalance, BotTradingBalance = BotTradingBalance,
StartupTime = StartupTime, StartupTime = StartupTime,
}; };
BotService.SaveOrUpdateBotBackup(Name, BotType, JsonConvert.SerializeObject(data)); BotService.SaveOrUpdateBotBackup(User, Identifier, BotType, JsonConvert.SerializeObject(data));
} }
public override void LoadBackup(BotBackup backup) public override void LoadBackup(BotBackup backup)
@@ -925,6 +924,8 @@ public class TradingBot : Bot, ITradingBot
AccountName = data.AccountName; AccountName = data.AccountName;
IsForWatchingOnly = data.IsForWatchingOnly; IsForWatchingOnly = data.IsForWatchingOnly;
BotTradingBalance = data.BotTradingBalance; BotTradingBalance = data.BotTradingBalance;
Identifier = backup.Identifier;
User = backup.User;
// Restore the startup time if it was previously saved // Restore the startup time if it was previously saved
if (data.StartupTime != DateTime.MinValue) if (data.StartupTime != DateTime.MinValue)

View File

@@ -6,6 +6,7 @@ using Managing.Application.Bots;
using Managing.Common; using Managing.Common;
using Managing.Domain.Bots; using Managing.Domain.Bots;
using Managing.Domain.MoneyManagements; using Managing.Domain.MoneyManagements;
using Managing.Domain.Users;
using Managing.Domain.Workflows; using Managing.Domain.Workflows;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
@@ -43,14 +44,14 @@ namespace Managing.Application.ManageBot
await _botRepository.InsertBotAsync(botBackup); await _botRepository.InsertBotAsync(botBackup);
} }
public BotBackup GetBotBackup(string name) public BotBackup GetBotBackup(string identifier)
{ {
return _botRepository.GetBots().FirstOrDefault(b => b.Name == name); return _botRepository.GetBots().FirstOrDefault(b => b.Identifier == identifier);
} }
public void SaveOrUpdateBotBackup(string name, Enums.BotType botType, string data) public void SaveOrUpdateBotBackup(User user, string identifier, Enums.BotType botType, string data)
{ {
var backup = GetBotBackup(name); var backup = GetBotBackup(identifier);
if (backup != null) if (backup != null)
{ {
@@ -61,7 +62,8 @@ namespace Managing.Application.ManageBot
{ {
var botBackup = new BotBackup var botBackup = new BotBackup
{ {
Name = name, User = user,
Identifier = identifier,
BotType = botType, BotType = botType,
Data = data Data = data
}; };
@@ -88,13 +90,13 @@ namespace Managing.Application.ManageBot
{ {
var botTask = var botTask =
new BotTaskWrapper(Task.Run(() => bot.Start()), bot.GetType(), bot); // Pass bot as the instance new BotTaskWrapper(Task.Run(() => bot.Start()), bot.GetType(), bot); // Pass bot as the instance
_botTasks.AddOrUpdate(bot.GetName(), botTask, (key, existingVal) => botTask); _botTasks.AddOrUpdate(bot.Identifier, botTask, (key, existingVal) => botTask);
} }
public void AddTradingBotToCache(ITradingBot bot) public void AddTradingBotToCache(ITradingBot bot)
{ {
var botTask = new BotTaskWrapper(Task.Run(() => bot.Start()), bot.GetType(), bot); var botTask = new BotTaskWrapper(Task.Run(() => bot.Start()), bot.GetType(), bot);
_botTasks.AddOrUpdate(bot.GetName(), botTask, (key, existingVal) => botTask); _botTasks.AddOrUpdate(bot.Identifier, botTask, (key, existingVal) => botTask);
} }
public List<ITradingBot> GetActiveBots() public List<ITradingBot> GetActiveBots()
@@ -120,11 +122,11 @@ namespace Managing.Application.ManageBot
switch (backupBot.BotType) switch (backupBot.BotType)
{ {
case Enums.BotType.SimpleBot: // case Enums.BotType.SimpleBot:
bot = CreateSimpleBot(backupBot.Name, // bot = CreateSimpleBot(backupBot.Name,
null); // Assuming null is an acceptable parameter for workflow // null); // Assuming null is an acceptable parameter for workflow
botTask = Task.Run(() => ((IBot)bot).Start()); // botTask = Task.Run(() => ((IBot)bot).Start());
break; // break;
case Enums.BotType.ScalpingBot: case Enums.BotType.ScalpingBot:
var scalpingBotData = JsonConvert.DeserializeObject<TradingBotBackup>(backupBot.Data); var scalpingBotData = JsonConvert.DeserializeObject<TradingBotBackup>(backupBot.Data);
var scalpingMoneyManagement = var scalpingMoneyManagement =
@@ -132,7 +134,7 @@ namespace Managing.Application.ManageBot
bot = CreateScalpingBot( bot = CreateScalpingBot(
scalpingBotData.AccountName, scalpingBotData.AccountName,
scalpingMoneyManagement, scalpingMoneyManagement,
backupBot.Name, scalpingBotData.Name,
scalpingBotData.Ticker, scalpingBotData.Ticker,
scalpingBotData.ScenarioName, scalpingBotData.ScenarioName,
scalpingBotData.Timeframe, scalpingBotData.Timeframe,
@@ -147,7 +149,7 @@ namespace Managing.Application.ManageBot
bot = CreateFlippingBot( bot = CreateFlippingBot(
flippingBotData.AccountName, flippingBotData.AccountName,
flippingMoneyManagement, flippingMoneyManagement,
backupBot.Name, flippingBotData.Name,
flippingBotData.Ticker, flippingBotData.Ticker,
flippingBotData.ScenarioName, flippingBotData.ScenarioName,
flippingBotData.Timeframe, flippingBotData.Timeframe,
@@ -160,7 +162,7 @@ namespace Managing.Application.ManageBot
if (bot != null && botTask != null) if (bot != null && botTask != null)
{ {
var botWrapper = new BotTaskWrapper(botTask, bot.GetType(), bot); var botWrapper = new BotTaskWrapper(botTask, bot.GetType(), bot);
_botTasks.AddOrUpdate(backupBot.Name, botWrapper, (key, existingVal) => botWrapper); _botTasks.AddOrUpdate(backupBot.Identifier, botWrapper, (key, existingVal) => botWrapper);
} }
} }

View File

@@ -1,8 +1,8 @@
using MediatR; using Managing.Application.Abstractions;
using static Managing.Common.Enums;
using Managing.Application.Abstractions;
using Managing.Core; using Managing.Core;
using MediatR;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
namespace Managing.Application.ManageBot; namespace Managing.Application.ManageBot;
@@ -19,67 +19,71 @@ public class LoadBackupBotCommandHandler : IRequestHandler<LoadBackupBotCommand,
} }
public Task<string> Handle(LoadBackupBotCommand request, CancellationToken cancellationToken) public Task<string> Handle(LoadBackupBotCommand request, CancellationToken cancellationToken)
{
var backupBots = _botService.GetSavedBots().ToList();
_logger.LogInformation("Loading {Count} backup bots.", backupBots.Count);
var result = new Dictionary<string, BotStatus>();
bool anyBackupStarted = false;
bool anyBotActive = false;
foreach (var backupBot in backupBots)
{ {
try var backupBots = _botService.GetSavedBots().ToList();
_logger.LogInformation("Loading {Count} backup bots.", backupBots.Count);
var result = new Dictionary<string, BotStatus>();
bool anyBackupStarted = false;
bool anyBotActive = false;
foreach (var backupBot in backupBots)
{ {
var activeBot = _botService.GetActiveBots().FirstOrDefault(b => b.GetName() == backupBot.Name); try
if (activeBot == null)
{ {
_logger.LogInformation("No active instance found for bot {BotName}. Starting backup...", backupBot.Name); var activeBot = _botService.GetActiveBots().FirstOrDefault(b => b.Identifier == backupBot.Identifier);
_botService.StartBotFromBackup(backupBot);
activeBot = _botService.GetActiveBots().FirstOrDefault(b => b.GetName() == backupBot.Name); if (activeBot == null)
if (activeBot != null)
{ {
result[activeBot.GetName()] = BotStatus.Backup; _logger.LogInformation("No active instance found for bot {Identifier}. Starting backup...",
anyBackupStarted = true; backupBot.Identifier);
_logger.LogInformation("Backup bot {BotName} started successfully.", backupBot.Name); _botService.StartBotFromBackup(backupBot);
activeBot = _botService.GetActiveBots().FirstOrDefault(b => b.Identifier == backupBot.Identifier);
if (activeBot != null)
{
result[activeBot.Identifier] = BotStatus.Backup;
anyBackupStarted = true;
_logger.LogInformation("Backup bot {Identifier} started successfully.", backupBot.Identifier);
}
else
{
result[backupBot.Identifier] = BotStatus.Down;
_logger.LogWarning("Backup bot {Identifier} failed to start.", backupBot.Identifier);
}
} }
else else
{ {
result[backupBot.Name] = BotStatus.Down; var status = MiscExtensions.ParseEnum<BotStatus>(activeBot.GetStatus());
_logger.LogWarning("Backup bot {BotName} failed to start.", backupBot.Name); result[activeBot.Identifier] = status;
anyBotActive = true;
_logger.LogInformation("Bot {Identifier} is already active with status {Status}.",
activeBot.Identifier,
status);
} }
} }
else catch (Exception ex)
{ {
var status = MiscExtensions.ParseEnum<BotStatus>(activeBot.GetStatus()); _logger.LogError(ex, "Error loading bot {Identifier}. Deleting its backup.", backupBot.Identifier);
result[activeBot.GetName()] = status; _botService.DeleteBotBackup(backupBot.Identifier);
anyBotActive = true; result[backupBot.Identifier] = BotStatus.Down;
_logger.LogInformation("Bot {BotName} is already active with status {Status}.", activeBot.GetName(), status);
} }
} }
catch (Exception ex)
{ var summary = string.Join(", ", result.Select(b => $"{b.Key}: {b.Value}"));
_logger.LogError(ex, "Error loading bot {BotName}. Deleting its backup.", backupBot.Name); _logger.LogInformation("Bot loading completed. Summary: {Summary}", summary);
_botService.DeleteBotBackup(backupBot.Name);
result[backupBot.Name] = BotStatus.Down; // Determine final status
} BotStatus finalStatus = anyBackupStarted
? BotStatus.Backup
: anyBotActive
? BotStatus.Up
: BotStatus.Down;
_logger.LogInformation("Final aggregate bot status: {FinalStatus}", finalStatus);
return Task.FromResult(finalStatus.ToString());
} }
var summary = string.Join(", ", result.Select(b => $"{b.Key}: {b.Value}"));
_logger.LogInformation("Bot loading completed. Summary: {Summary}", summary);
// Determine final status
BotStatus finalStatus = anyBackupStarted
? BotStatus.Backup
: anyBotActive ? BotStatus.Up : BotStatus.Down;
_logger.LogInformation("Final aggregate bot status: {FinalStatus}", finalStatus);
return Task.FromResult(finalStatus.ToString());
}
} }
public class LoadBackupBotCommand : IRequest<string> public class LoadBackupBotCommand : IRequest<string>

View File

@@ -27,14 +27,14 @@ public class ClosePositionCommandHandler(
return request.Position; return request.Position;
} }
var isForPaperTrading = request.Position.Initiator == PositionInitiator.PaperTrading; var isForPaperTrading = request.IsForBacktest;
var lastPrice = request.Position.Initiator == PositionInitiator.PaperTrading var lastPrice = request.Position.Initiator == PositionInitiator.PaperTrading
? request.ExecutionPrice.GetValueOrDefault() ? request.ExecutionPrice.GetValueOrDefault()
: exchangeService.GetPrice(account, request.Position.Ticker, DateTime.UtcNow); : exchangeService.GetPrice(account, request.Position.Ticker, DateTime.UtcNow);
// Check if position still open // Check if position still open
if (!isForPaperTrading) if (!request.IsForBacktest)
{ {
var p = (await exchangeService.GetBrokerPositions(account)) var p = (await exchangeService.GetBrokerPositions(account))
.FirstOrDefault(x => x.Ticker == request.Position.Ticker); .FirstOrDefault(x => x.Ticker == request.Position.Ticker);
@@ -65,17 +65,19 @@ public class ClosePositionCommandHandler(
request.Position.ProfitAndLoss = request.Position.ProfitAndLoss =
TradingBox.GetProfitAndLoss(request.Position, closedPosition.Quantity, lastPrice, TradingBox.GetProfitAndLoss(request.Position, closedPosition.Quantity, lastPrice,
request.Position.Open.Leverage); request.Position.Open.Leverage);
tradingService.UpdatePosition(request.Position);
if (!request.IsForBacktest)
tradingService.UpdatePosition(request.Position);
} }
return request.Position; return request.Position;
} }
catch (Exception ex) catch (Exception ex)
{ {
// Log the error - regardless of the error type logger?.LogError(ex, "Error closing position: {Message} \n Stacktrace : {StackTrace}", ex.Message,
logger?.LogError(ex, "Error closing position: {Message}", ex.Message); ex.StackTrace);
throw new Exception(ex.Message); throw;
} }
} }
} }

View File

@@ -5,13 +5,15 @@ namespace Managing.Application.Trading.Commands
{ {
public class ClosePositionCommand : IRequest<Position> public class ClosePositionCommand : IRequest<Position>
{ {
public ClosePositionCommand(Position position, decimal? executionPrice = null) public ClosePositionCommand(Position position, decimal? executionPrice = null, bool isForBacktest = false)
{ {
Position = position; Position = position;
ExecutionPrice = executionPrice; ExecutionPrice = executionPrice;
IsForBacktest = isForBacktest;
} }
public Position Position { get; } public Position Position { get; }
public decimal? ExecutionPrice { get; set; } public decimal? ExecutionPrice { get; set; }
public bool IsForBacktest { get; set; }
} }
} }

View File

@@ -1,10 +1,12 @@
using Managing.Domain.Users;
using static Managing.Common.Enums; using static Managing.Common.Enums;
namespace Managing.Domain.Bots; namespace Managing.Domain.Bots;
public class BotBackup public class BotBackup
{ {
public string Name { get; set; }
public BotType BotType { get; set; } public BotType BotType { get; set; }
public string Identifier { get; set; }
public User User { get; set; }
public string Data { get; set; } public string Data { get; set; }
} }

View File

@@ -8,12 +8,15 @@
void Restart(); void Restart();
string GetStatus(); string GetStatus();
string GetName(); string GetName();
/// <summary> /// <summary>
/// Gets the total runtime of the bot since it was started /// Gets the total runtime of the bot since it was started
/// </summary> /// </summary>
/// <returns>TimeSpan representing the runtime, or TimeSpan.Zero if the bot is not running</returns> /// <returns>TimeSpan representing the runtime, or TimeSpan.Zero if the bot is not running</returns>
TimeSpan GetRuntime(); TimeSpan GetRuntime();
string Identifier { get; set; }
void SaveBackup(); void SaveBackup();
void LoadBackup(BotBackup backup); void LoadBackup(BotBackup backup);
} }
} }

View File

@@ -28,7 +28,7 @@ public class BotRepository : IBotRepository
public async Task UpdateBackupBot(BotBackup bot) public async Task UpdateBackupBot(BotBackup bot)
{ {
var b = await _botRepository.FindOneAsync(b => b.Name == bot.Name); var b = await _botRepository.FindOneAsync(b => b.Identifier == bot.Identifier);
var dto = MongoMappers.Map(bot); var dto = MongoMappers.Map(bot);
dto.Id = b.Id; dto.Id = b.Id;
_botRepository.Update(dto); _botRepository.Update(dto);

View File

@@ -10,4 +10,6 @@ public class BotDto : Document
public string Name { get; set; } public string Name { get; set; }
public string Data { get; set; } public string Data { get; set; }
public BotType BotType { get; set; } public BotType BotType { get; set; }
public string Identifier { get; set; }
public UserDto User { get; set; }
} }

View File

@@ -720,7 +720,8 @@ public static class MongoMappers
return new BotDto return new BotDto
{ {
Name = bot.Name, User = Map(bot.User),
Identifier = bot.Identifier,
BotType = bot.BotType, BotType = bot.BotType,
Data = bot.Data, Data = bot.Data,
}; };
@@ -732,7 +733,8 @@ public static class MongoMappers
return new BotBackup return new BotBackup
{ {
Name = b.Name, User = Map(b.User),
Identifier = b.Identifier,
BotType = b.BotType, BotType = b.BotType,
Data = b.Data Data = b.Data
}; };

View File

@@ -197,7 +197,7 @@ public class TradingRepository : ITradingRepository
public void UpdatePosition(Position position) public void UpdatePosition(Position position)
{ {
var p = _positionRepository.FindOne(p => p.Open.ExchangeOrderId == position.Open.ExchangeOrderId); var p = _positionRepository.FindOne(p => p.Identifier.Equals(position.Identifier));
var dto = MongoMappers.Map(position); var dto = MongoMappers.Map(position);
dto.Id = p.Id; dto.Id = p.Id;
_positionRepository.Update(dto); _positionRepository.Update(dto);