Trading bot grain (#33)
* Trading bot Grain * Fix a bit more of the trading bot * Advance on the tradingbot grain * Fix build * Fix db script * Fix user login * Fix a bit backtest * Fix cooldown and backtest * start fixing bot start * Fix startup * Setup local db * Fix build and update candles and scenario * Add bot registry * Add reminder * Updateing the grains * fix bootstraping * Save stats on tick * Save bot data every tick * Fix serialization * fix save bot stats * Fix get candles * use dict instead of list for position * Switch hashset to dict * Fix a bit * Fix bot launch and bot view * add migrations * Remove the tolist * Add agent grain * Save agent summary * clean * Add save bot * Update get bots * Add get bots * Fix stop/restart * fix Update config * Update scanner table on new backtest saved * Fix backtestRowDetails.tsx * Fix agentIndex * Update agentIndex * Fix more things * Update user cache * Fix * Fix account load/start/restart/run
This commit is contained in:
@@ -1,52 +0,0 @@
|
||||
using Managing.Application.Abstractions.Repositories;
|
||||
using Managing.Domain.Bots;
|
||||
using Managing.Domain.Users;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.ManageBot
|
||||
{
|
||||
public interface IBackupBotService
|
||||
{
|
||||
Task<BotBackup> GetBotBackup(string identifier);
|
||||
Task SaveOrUpdateBotBackup(User user, string identifier, BotStatus status, TradingBotBackup data);
|
||||
}
|
||||
|
||||
public class BackupBotService : IBackupBotService
|
||||
{
|
||||
private readonly IBotRepository _botRepository;
|
||||
|
||||
public BackupBotService(IBotRepository botRepository)
|
||||
{
|
||||
_botRepository = botRepository;
|
||||
}
|
||||
|
||||
public async Task<BotBackup> GetBotBackup(string identifier)
|
||||
{
|
||||
return await _botRepository.GetBotByIdentifierAsync(identifier);
|
||||
}
|
||||
|
||||
public async Task SaveOrUpdateBotBackup(User user, string identifier, BotStatus status, TradingBotBackup data)
|
||||
{
|
||||
var backup = await GetBotBackup(identifier);
|
||||
|
||||
if (backup != null)
|
||||
{
|
||||
backup.LastStatus = status;
|
||||
backup.Data = data;
|
||||
await _botRepository.UpdateBackupBot(backup);
|
||||
}
|
||||
else
|
||||
{
|
||||
var botBackup = new BotBackup
|
||||
{
|
||||
LastStatus = status,
|
||||
User = user,
|
||||
Identifier = identifier,
|
||||
Data = data
|
||||
};
|
||||
|
||||
await _botRepository.InsertBotAsync(botBackup);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Managing.Application.Abstractions;
|
||||
using Managing.Application.Abstractions.Grains;
|
||||
using Managing.Application.Abstractions.Repositories;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.Bots;
|
||||
using Managing.Core;
|
||||
using Managing.Domain.Bots;
|
||||
using Managing.Domain.Scenarios;
|
||||
using Managing.Domain.Users;
|
||||
using Managing.Domain.Workflows;
|
||||
using Managing.Domain.Shared.Helpers;
|
||||
using Managing.Domain.Trades;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static Managing.Common.Enums;
|
||||
@@ -16,301 +17,136 @@ namespace Managing.Application.ManageBot
|
||||
public class BotService : IBotService
|
||||
{
|
||||
private readonly IBotRepository _botRepository;
|
||||
private readonly IExchangeService _exchangeService;
|
||||
private readonly IMessengerService _messengerService;
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly ILogger<TradingBotBase> _tradingBotLogger;
|
||||
private readonly ITradingService _tradingService;
|
||||
private readonly IMoneyManagementService _moneyManagementService;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IBackupBotService _backupBotService;
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly IGrainFactory _grainFactory;
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
|
||||
private ConcurrentDictionary<string, BotTaskWrapper> _botTasks =
|
||||
new ConcurrentDictionary<string, BotTaskWrapper>();
|
||||
|
||||
public BotService(IBotRepository botRepository, IExchangeService exchangeService,
|
||||
IMessengerService messengerService, IAccountService accountService, ILogger<TradingBotBase> tradingBotLogger,
|
||||
ITradingService tradingService, IMoneyManagementService moneyManagementService, IUserService userService,
|
||||
IBackupBotService backupBotService, IServiceScopeFactory scopeFactory, IGrainFactory grainFactory)
|
||||
public BotService(IBotRepository botRepository,
|
||||
IMessengerService messengerService, ILogger<TradingBotBase> tradingBotLogger,
|
||||
ITradingService tradingService, IGrainFactory grainFactory, IServiceScopeFactory scopeFactory)
|
||||
{
|
||||
_botRepository = botRepository;
|
||||
_exchangeService = exchangeService;
|
||||
_messengerService = messengerService;
|
||||
_accountService = accountService;
|
||||
_tradingBotLogger = tradingBotLogger;
|
||||
_tradingService = tradingService;
|
||||
_moneyManagementService = moneyManagementService;
|
||||
_userService = userService;
|
||||
_backupBotService = backupBotService;
|
||||
_scopeFactory = scopeFactory;
|
||||
_grainFactory = grainFactory;
|
||||
_scopeFactory = scopeFactory;
|
||||
}
|
||||
|
||||
public class BotTaskWrapper
|
||||
{
|
||||
public Task Task { get; private set; }
|
||||
public Type BotType { get; private set; }
|
||||
public object BotInstance { get; private set; }
|
||||
|
||||
public BotTaskWrapper(Task task, Type botType, object botInstance)
|
||||
{
|
||||
Task = task;
|
||||
BotType = botType;
|
||||
BotInstance = botInstance;
|
||||
}
|
||||
}
|
||||
|
||||
public void AddSimpleBotToCache(IBot bot)
|
||||
{
|
||||
var botTask = new BotTaskWrapper(Task.Run(() => bot.Start()), bot.GetType(), bot);
|
||||
_botTasks.AddOrUpdate(bot.Identifier, botTask, (key, existingVal) => botTask);
|
||||
}
|
||||
|
||||
public void AddTradingBotToCache(ITradingBot bot)
|
||||
{
|
||||
var botTask = new BotTaskWrapper(Task.Run(() => bot.Start()), bot.GetType(), bot);
|
||||
_botTasks.AddOrUpdate(bot.Identifier, botTask, (key, existingVal) => botTask);
|
||||
}
|
||||
|
||||
private async Task InitBot(ITradingBot bot, BotBackup backupBot)
|
||||
{
|
||||
try
|
||||
{
|
||||
var user = await _userService.GetUser(backupBot.User.Name);
|
||||
bot.User = user;
|
||||
|
||||
// Load backup data into the bot
|
||||
bot.LoadBackup(backupBot);
|
||||
|
||||
// Only start the bot if the backup status is Up
|
||||
if (backupBot.LastStatus == BotStatus.Up)
|
||||
{
|
||||
// Start the bot asynchronously without waiting for completion
|
||||
_ = Task.Run(() => bot.Start());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Keep the bot in Down status if it was originally Down
|
||||
bot.Stop();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_tradingBotLogger.LogError(ex, "Error initializing bot {Identifier} from backup", backupBot.Identifier);
|
||||
// Ensure the bot is stopped if initialization fails
|
||||
bot.Stop();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public List<ITradingBot> GetActiveBots()
|
||||
{
|
||||
var bots = _botTasks.Values
|
||||
.Where(wrapper => typeof(ITradingBot).IsAssignableFrom(wrapper.BotType))
|
||||
.Select(wrapper => wrapper.BotInstance as ITradingBot)
|
||||
.Where(bot => bot != null)
|
||||
.ToList();
|
||||
|
||||
return bots;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<BotBackup>> GetSavedBotsAsync()
|
||||
public async Task<IEnumerable<Bot>> GetBotsAsync()
|
||||
{
|
||||
return await _botRepository.GetBotsAsync();
|
||||
}
|
||||
|
||||
public async Task StartBotFromBackup(BotBackup backupBot)
|
||||
public async Task<IEnumerable<Bot>> GetBotsByStatusAsync(BotStatus status)
|
||||
{
|
||||
object bot = null;
|
||||
Task botTask = null;
|
||||
return await _botRepository.GetBotsByStatusAsync(status);
|
||||
}
|
||||
|
||||
var scalpingBotData = backupBot.Data;
|
||||
|
||||
// Get the config directly from the backup
|
||||
var scalpingConfig = scalpingBotData.Config;
|
||||
|
||||
// Ensure the money management is properly loaded from database if needed
|
||||
if (scalpingConfig.MoneyManagement != null &&
|
||||
!string.IsNullOrEmpty(scalpingConfig.MoneyManagement.Name))
|
||||
public async Task<BotStatus> StopBot(Guid identifier)
|
||||
{
|
||||
try
|
||||
{
|
||||
var moneyManagement = _moneyManagementService
|
||||
.GetMoneyMangement(scalpingConfig.MoneyManagement.Name).Result;
|
||||
if (moneyManagement != null)
|
||||
{
|
||||
scalpingConfig.MoneyManagement = moneyManagement;
|
||||
}
|
||||
var grain = _grainFactory.GetGrain<ILiveTradingBotGrain>(identifier);
|
||||
await grain.StopAsync();
|
||||
return BotStatus.Down;
|
||||
}
|
||||
|
||||
// Ensure the scenario is properly loaded from database if needed
|
||||
if (scalpingConfig.Scenario == null && !string.IsNullOrEmpty(scalpingConfig.ScenarioName))
|
||||
catch (Exception e)
|
||||
{
|
||||
var scenario = await _tradingService.GetScenarioByNameAsync(scalpingConfig.ScenarioName);
|
||||
if (scenario != null)
|
||||
_tradingBotLogger.LogError(e, "Error stopping bot {Identifier}", identifier);
|
||||
return BotStatus.Down;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteBot(Guid identifier)
|
||||
{
|
||||
var grain = _grainFactory.GetGrain<ILiveTradingBotGrain>(identifier);
|
||||
|
||||
try
|
||||
{
|
||||
var config = await grain.GetConfiguration();
|
||||
var account = await grain.GetAccount();
|
||||
await grain.StopAsync();
|
||||
await _botRepository.DeleteBot(identifier);
|
||||
await grain.DeleteAsync();
|
||||
|
||||
var deleteMessage = $"🗑️ **Bot Deleted**\n\n" +
|
||||
$"🎯 **Agent:** {account.User.AgentName}\n" +
|
||||
$"🤖 **Bot Name:** {config.Name}\n" +
|
||||
$"⏰ **Deleted At:** {DateTime.UtcNow:MMM dd, yyyy • HH:mm:ss} UTC\n\n" +
|
||||
$"⚠️ **Bot has been permanently deleted and all data removed.**";
|
||||
|
||||
await _messengerService.SendTradeMessage(deleteMessage, false, account.User);
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_tradingBotLogger.LogError(e, "Error deleting bot {Identifier}", identifier);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<BotStatus> RestartBot(Guid identifier)
|
||||
{
|
||||
try
|
||||
{
|
||||
var registryGrain = _grainFactory.GetGrain<ILiveBotRegistryGrain>(0);
|
||||
var previousStatus = await registryGrain.GetBotStatus(identifier);
|
||||
|
||||
// If bot is already up, return the status directly
|
||||
if (previousStatus == BotStatus.Up)
|
||||
{
|
||||
scalpingConfig.Scenario = LightScenario.FromScenario(scenario);
|
||||
return BotStatus.Up;
|
||||
}
|
||||
|
||||
var botGrain = _grainFactory.GetGrain<ILiveTradingBotGrain>(identifier);
|
||||
if (previousStatus == BotStatus.None)
|
||||
{
|
||||
// First time startup
|
||||
await botGrain.StartAsync();
|
||||
var grainState = await botGrain.GetBotDataAsync();
|
||||
var account = await botGrain.GetAccount();
|
||||
var startupMessage = $"🚀 **Bot Started**\n\n" +
|
||||
$"🎯 **Agent:** {account.User.AgentName}\n" +
|
||||
$"🤖 **Bot Name:** {grainState.Config.Name}\n" +
|
||||
$"⏰ **Started At:** {DateTime.UtcNow:MMM dd, yyyy • HH:mm:ss} UTC\n" +
|
||||
$"🕐 **Startup Time:** {grainState.StartupTime:MMM dd, yyyy • HH:mm:ss} UTC\n\n" +
|
||||
$"✅ **Bot has been successfully started and is now active.**";
|
||||
|
||||
await _messengerService.SendTradeMessage(startupMessage, false, account.User);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"Scenario '{scalpingConfig.ScenarioName}' not found in database when loading backup");
|
||||
}
|
||||
}
|
||||
|
||||
if (scalpingConfig.Scenario == null)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Scenario object must be provided or ScenarioName must be valid when loading backup");
|
||||
}
|
||||
|
||||
// Ensure critical properties are set correctly for restored bots
|
||||
scalpingConfig.IsForBacktest = false;
|
||||
|
||||
// IMPORTANT: Save the backup to database BEFORE creating the Orleans grain
|
||||
// This ensures the backup exists when the grain tries to serialize it
|
||||
await SaveOrUpdateBotBackup(backupBot.User, backupBot.Identifier, backupBot.LastStatus, backupBot.Data);
|
||||
|
||||
bot = await CreateTradingBot(scalpingConfig);
|
||||
botTask = Task.Run(() => InitBot((ITradingBot)bot, backupBot));
|
||||
|
||||
if (bot != null && botTask != null)
|
||||
{
|
||||
var botWrapper = new BotTaskWrapper(botTask, bot.GetType(), bot);
|
||||
_botTasks.AddOrUpdate(backupBot.Identifier, botWrapper, (key, existingVal) => botWrapper);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<BotBackup> GetBotBackup(string identifier)
|
||||
{
|
||||
return await _botRepository.GetBotByIdentifierAsync(identifier);
|
||||
}
|
||||
|
||||
public async Task SaveOrUpdateBotBackup(User user, string identifier, BotStatus status, TradingBotBackup data)
|
||||
{
|
||||
var backup = await GetBotBackup(identifier);
|
||||
|
||||
if (backup != null)
|
||||
{
|
||||
backup.LastStatus = status;
|
||||
backup.Data = data;
|
||||
await _botRepository.UpdateBackupBot(backup);
|
||||
}
|
||||
else
|
||||
{
|
||||
var botBackup = new BotBackup
|
||||
{
|
||||
LastStatus = status,
|
||||
User = user,
|
||||
Identifier = identifier,
|
||||
Data = data
|
||||
};
|
||||
|
||||
await _botRepository.InsertBotAsync(botBackup);
|
||||
}
|
||||
}
|
||||
|
||||
public IBot CreateSimpleBot(string botName, Workflow workflow)
|
||||
{
|
||||
return new SimpleBot(botName, _tradingBotLogger, workflow, this, _backupBotService);
|
||||
}
|
||||
|
||||
public async Task<string> StopBot(string identifier)
|
||||
{
|
||||
if (_botTasks.TryGetValue(identifier, out var botWrapper))
|
||||
{
|
||||
if (botWrapper.BotInstance is IBot bot)
|
||||
{
|
||||
await Task.Run(() =>
|
||||
bot.Stop());
|
||||
|
||||
var stopMessage = $"🛑 **Bot Stopped**\n\n" +
|
||||
$"🎯 **Agent:** {bot.User.AgentName}\n" +
|
||||
$"🤖 **Bot Name:** {bot.Name}\n" +
|
||||
$"⏰ **Stopped At:** {DateTime.UtcNow:MMM dd, yyyy • HH:mm:ss} UTC\n\n" +
|
||||
$"✅ **Bot has been safely stopped and is no longer active.**";
|
||||
|
||||
await _messengerService.SendTradeMessage(stopMessage, false, bot.User);
|
||||
return bot.GetStatus();
|
||||
}
|
||||
}
|
||||
|
||||
return BotStatus.Down.ToString();
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteBot(string identifier)
|
||||
{
|
||||
if (_botTasks.TryRemove(identifier, out var botWrapper))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (botWrapper.BotInstance is IBot bot)
|
||||
{
|
||||
await Task.Run(() =>
|
||||
bot.Stop());
|
||||
|
||||
var deleteMessage = $"🗑️ **Bot Deleted**\n\n" +
|
||||
$"🎯 **Agent:** {bot.User.AgentName}\n" +
|
||||
$"🤖 **Bot Name:** {bot.Name}\n" +
|
||||
$"⏰ **Deleted At:** {DateTime.UtcNow:MMM dd, yyyy • HH:mm:ss} UTC\n\n" +
|
||||
$"⚠️ **Bot has been permanently deleted and all data removed.**";
|
||||
|
||||
await _messengerService.SendTradeMessage(deleteMessage, false, bot.User);
|
||||
}
|
||||
|
||||
await _botRepository.DeleteBotBackup(identifier);
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task<string> RestartBot(string identifier)
|
||||
{
|
||||
if (_botTasks.TryGetValue(identifier, out var botWrapper))
|
||||
{
|
||||
if (botWrapper.BotInstance is IBot bot)
|
||||
{
|
||||
// Stop the bot first to ensure clean state
|
||||
bot.Stop();
|
||||
|
||||
// Small delay to ensure stop is complete
|
||||
await Task.Delay(100);
|
||||
|
||||
// Restart the bot (this will update StartupTime)
|
||||
bot.Restart();
|
||||
|
||||
// Start the bot asynchronously without waiting for completion
|
||||
_ = Task.Run(() => bot.Start());
|
||||
|
||||
// Restart (bot was previously down)
|
||||
await botGrain.RestartAsync();
|
||||
var grainState = await botGrain.GetBotDataAsync();
|
||||
var account = await botGrain.GetAccount();
|
||||
var restartMessage = $"🔄 **Bot Restarted**\n\n" +
|
||||
$"🎯 **Agent:** {bot.User.AgentName}\n" +
|
||||
$"🤖 **Bot Name:** {bot.Name}\n" +
|
||||
$"🎯 **Agent:** {account.User.AgentName}\n" +
|
||||
$"🤖 **Bot Name:** {grainState.Config.Name}\n" +
|
||||
$"⏰ **Restarted At:** {DateTime.UtcNow:MMM dd, yyyy • HH:mm:ss} UTC\n" +
|
||||
$"🕐 **New Startup Time:** {bot.StartupTime:MMM dd, yyyy • HH:mm:ss} UTC\n\n" +
|
||||
$"🕐 **New Startup Time:** {grainState.StartupTime:MMM dd, yyyy • HH:mm:ss} UTC\n\n" +
|
||||
$"🚀 **Bot has been successfully restarted and is now active.**";
|
||||
|
||||
await _messengerService.SendTradeMessage(restartMessage, false, bot.User);
|
||||
return bot.GetStatus();
|
||||
await _messengerService.SendTradeMessage(restartMessage, false, account.User);
|
||||
}
|
||||
}
|
||||
|
||||
return BotStatus.Down.ToString();
|
||||
return BotStatus.Up;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_tradingBotLogger.LogError(e, "Error restarting bot {Identifier}", identifier);
|
||||
return BotStatus.Down;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ToggleIsForWatchingOnly(string identifier)
|
||||
private async Task<Bot> GetBot(Guid identifier)
|
||||
{
|
||||
if (_botTasks.TryGetValue(identifier, out var botTaskWrapper) &&
|
||||
botTaskWrapper.BotInstance is ITradingBot tradingBot)
|
||||
{
|
||||
await tradingBot.ToggleIsForWatchOnly();
|
||||
}
|
||||
var bot = await _botRepository.GetBotByIdentifierAsync(identifier);
|
||||
return bot;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -319,128 +155,198 @@ namespace Managing.Application.ManageBot
|
||||
/// <param name="identifier">The bot identifier</param>
|
||||
/// <param name="newConfig">The new configuration to apply</param>
|
||||
/// <returns>True if the configuration was successfully updated, false otherwise</returns>
|
||||
public async Task<bool> UpdateBotConfiguration(string identifier, TradingBotConfig newConfig)
|
||||
public async Task<bool> UpdateBotConfiguration(Guid identifier, TradingBotConfig newConfig)
|
||||
{
|
||||
if (_botTasks.TryGetValue(identifier, out var botTaskWrapper) &&
|
||||
botTaskWrapper.BotInstance is TradingBotBase tradingBot)
|
||||
var grain = _grainFactory.GetGrain<ILiveTradingBotGrain>(identifier);
|
||||
// Ensure the scenario is properly loaded from database if needed
|
||||
if (newConfig.Scenario == null && !string.IsNullOrEmpty(newConfig.ScenarioName))
|
||||
{
|
||||
// Ensure the scenario is properly loaded from database if needed
|
||||
if (newConfig.Scenario == null && !string.IsNullOrEmpty(newConfig.ScenarioName))
|
||||
var scenario = await _tradingService.GetScenarioByNameAsync(newConfig.ScenarioName);
|
||||
if (scenario != null)
|
||||
{
|
||||
var scenario = await _tradingService.GetScenarioByNameAsync(newConfig.ScenarioName);
|
||||
if (scenario != null)
|
||||
{
|
||||
newConfig.Scenario = LightScenario.FromScenario(scenario);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"Scenario '{newConfig.ScenarioName}' not found in database when updating configuration");
|
||||
}
|
||||
newConfig.Scenario = LightScenario.FromScenario(scenario);
|
||||
}
|
||||
|
||||
if (newConfig.Scenario == null)
|
||||
else
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Scenario object must be provided or ScenarioName must be valid when updating configuration");
|
||||
}
|
||||
|
||||
// Check if the bot name is changing
|
||||
if (newConfig.Name != identifier && !string.IsNullOrEmpty(newConfig.Name))
|
||||
{
|
||||
// Check if new name already exists
|
||||
if (_botTasks.ContainsKey(newConfig.Name))
|
||||
{
|
||||
return false; // New name already in use
|
||||
}
|
||||
|
||||
// Update the bot configuration first
|
||||
var updateResult = await tradingBot.UpdateConfiguration(newConfig, allowNameChange: true);
|
||||
|
||||
if (updateResult)
|
||||
{
|
||||
// Update the dictionary key
|
||||
if (_botTasks.TryRemove(identifier, out var removedWrapper))
|
||||
{
|
||||
_botTasks.TryAdd(newConfig.Name, removedWrapper);
|
||||
|
||||
// Update the backup with the new identifier
|
||||
if (!newConfig.IsForBacktest)
|
||||
{
|
||||
// Delete old backup
|
||||
await _botRepository.DeleteBotBackup(identifier);
|
||||
// Save new backup will be handled by the bot's SaveBackup method
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updateResult;
|
||||
}
|
||||
else
|
||||
{
|
||||
// No name change, just update configuration
|
||||
return await tradingBot.UpdateConfiguration(newConfig);
|
||||
$"Scenario '{newConfig.ScenarioName}' not found in database when updating configuration");
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
if (newConfig.Scenario == null)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Scenario object must be provided or ScenarioName must be valid when updating configuration");
|
||||
}
|
||||
|
||||
return await grain.UpdateConfiguration(newConfig);
|
||||
}
|
||||
|
||||
public async Task<ITradingBot> CreateTradingBot(TradingBotConfig config)
|
||||
public async Task<TradingBotConfig> GetBotConfig(Guid identifier)
|
||||
{
|
||||
// Ensure the scenario is properly loaded from database if needed
|
||||
if (config.Scenario == null && !string.IsNullOrEmpty(config.ScenarioName))
|
||||
{
|
||||
var scenario = await _tradingService.GetScenarioByNameAsync(config.ScenarioName);
|
||||
if (scenario != null)
|
||||
{
|
||||
config.Scenario = LightScenario.FromScenario(scenario);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException($"Scenario '{config.ScenarioName}' not found in database");
|
||||
}
|
||||
}
|
||||
|
||||
if (config.Scenario == null)
|
||||
{
|
||||
throw new ArgumentException("Scenario object must be provided or ScenarioName must be valid");
|
||||
}
|
||||
|
||||
// For now, use TradingBot for both live trading and backtesting
|
||||
// TODO: Implement Orleans grain for live trading when ready
|
||||
if (!config.IsForBacktest)
|
||||
{
|
||||
// Ensure critical properties are set correctly for live trading
|
||||
config.IsForBacktest = false;
|
||||
}
|
||||
|
||||
return new TradingBotBase(_tradingBotLogger, _scopeFactory, config);
|
||||
var grain = _grainFactory.GetGrain<ILiveTradingBotGrain>(identifier);
|
||||
return await grain.GetConfiguration();
|
||||
}
|
||||
|
||||
public async Task<ITradingBot> CreateBacktestTradingBot(TradingBotConfig config)
|
||||
public async Task<IEnumerable<string>> GetActiveBotsNamesAsync()
|
||||
{
|
||||
// Ensure the scenario is properly loaded from database if needed
|
||||
if (config.Scenario == null && !string.IsNullOrEmpty(config.ScenarioName))
|
||||
var bots = await _botRepository.GetBotsByStatusAsync(BotStatus.Up);
|
||||
return bots.Select(b => b.Name);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Bot>> GetBotsByUser(int id)
|
||||
{
|
||||
return await _botRepository.GetBotsByUserIdAsync(id);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Bot>> GetBotsByIdsAsync(IEnumerable<Guid> botIds)
|
||||
{
|
||||
return await _botRepository.GetBotsByIdsAsync(botIds);
|
||||
}
|
||||
|
||||
public async Task<Bot> GetBotByName(string name)
|
||||
{
|
||||
return await _botRepository.GetBotByNameAsync(name);
|
||||
}
|
||||
|
||||
public async Task<Bot> GetBotByIdentifier(Guid identifier)
|
||||
{
|
||||
return await _botRepository.GetBotByIdentifierAsync(identifier);
|
||||
}
|
||||
|
||||
public async Task<Position> OpenPositionManuallyAsync(Guid identifier, TradeDirection direction)
|
||||
{
|
||||
var grain = _grainFactory.GetGrain<ILiveTradingBotGrain>(identifier);
|
||||
return await grain.OpenPositionManuallyAsync(direction);
|
||||
}
|
||||
|
||||
public async Task<Position> ClosePositionAsync(Guid identifier, Guid positionId)
|
||||
{
|
||||
var grain = _grainFactory.GetGrain<ILiveTradingBotGrain>(identifier);
|
||||
return await grain.ClosePositionAsync(positionId);
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateBotStatisticsAsync(Guid identifier)
|
||||
{
|
||||
try
|
||||
{
|
||||
var scenario = await _tradingService.GetScenarioByNameAsync(config.ScenarioName);
|
||||
if (scenario != null)
|
||||
var grain = _grainFactory.GetGrain<ILiveTradingBotGrain>(identifier);
|
||||
var botData = await grain.GetBotDataAsync();
|
||||
|
||||
// Get the current bot from database
|
||||
var existingBot = await _botRepository.GetBotByIdentifierAsync(identifier);
|
||||
if (existingBot == null)
|
||||
{
|
||||
config.Scenario = LightScenario.FromScenario(scenario);
|
||||
_tradingBotLogger.LogWarning("Bot {Identifier} not found in database for statistics update",
|
||||
identifier);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate statistics using TradingBox helpers
|
||||
var (tradeWins, tradeLosses) = TradingBox.GetWinLossCount(botData.Positions);
|
||||
var pnl = botData.ProfitAndLoss;
|
||||
var fees = botData.Positions.Values.Sum(p =>
|
||||
{
|
||||
if (p.Open.Price > 0 && p.Open.Quantity > 0)
|
||||
{
|
||||
var positionSizeUsd = (p.Open.Price * p.Open.Quantity) * p.Open.Leverage;
|
||||
var uiFeeRate = 0.001m; // 0.1%
|
||||
var uiFeeOpen = positionSizeUsd * uiFeeRate;
|
||||
var networkFeeForOpening = 0.50m;
|
||||
return uiFeeOpen + networkFeeForOpening;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
var volume = TradingBox.GetTotalVolumeTraded(botData.Positions);
|
||||
|
||||
// Calculate ROI based on total investment
|
||||
var totalInvestment = botData.Positions.Values
|
||||
.Where(p => p.IsFinished())
|
||||
.Sum(p => p.Open.Quantity * p.Open.Price);
|
||||
var roi = totalInvestment > 0 ? (pnl / totalInvestment) * 100 : 0;
|
||||
|
||||
// Update bot statistics
|
||||
existingBot.TradeWins = tradeWins;
|
||||
existingBot.TradeLosses = tradeLosses;
|
||||
existingBot.Pnl = pnl;
|
||||
existingBot.Roi = roi;
|
||||
existingBot.Volume = volume;
|
||||
existingBot.Fees = fees;
|
||||
|
||||
// Use the new SaveBotStatisticsAsync method
|
||||
return await SaveBotStatisticsAsync(existingBot);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_tradingBotLogger.LogError(e, "Error updating bot statistics for {Identifier}", identifier);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> SaveBotStatisticsAsync(Bot bot)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (bot == null)
|
||||
{
|
||||
_tradingBotLogger.LogWarning("Cannot save bot statistics: bot object is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if bot already exists in database
|
||||
var existingBot = await _botRepository.GetBotByIdentifierAsync(bot.Identifier);
|
||||
|
||||
if (existingBot != null)
|
||||
{
|
||||
// Update existing bot
|
||||
await _botRepository.UpdateBot(bot);
|
||||
_tradingBotLogger.LogDebug(
|
||||
"Updated bot statistics for bot {BotId}: Wins={Wins}, Losses={Losses}, PnL={PnL}, ROI={ROI}%, Volume={Volume}, Fees={Fees}",
|
||||
bot.Identifier, bot.TradeWins, bot.TradeLosses, bot.Pnl, bot.Roi, bot.Volume, bot.Fees);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException($"Scenario '{config.ScenarioName}' not found in database");
|
||||
// Insert new bot
|
||||
await _botRepository.InsertBotAsync(bot);
|
||||
_tradingBotLogger.LogInformation(
|
||||
"Created new bot statistics for bot {BotId}: Wins={Wins}, Losses={Losses}, PnL={PnL}, ROI={ROI}%, Volume={Volume}, Fees={Fees}",
|
||||
bot.Identifier, bot.TradeWins, bot.TradeLosses, bot.Pnl, bot.Roi, bot.Volume, bot.Fees);
|
||||
}
|
||||
}
|
||||
|
||||
if (config.Scenario == null)
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new ArgumentException("Scenario object must be provided or ScenarioName must be valid");
|
||||
_tradingBotLogger.LogError(e, "Error saving bot statistics for bot {BotId}", bot?.Identifier);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
config.IsForBacktest = true;
|
||||
return new TradingBotBase(_tradingBotLogger, _scopeFactory, config);
|
||||
public async Task<(IEnumerable<Bot> Bots, int TotalCount)> GetBotsPaginatedAsync(
|
||||
int pageNumber,
|
||||
int pageSize,
|
||||
BotStatus? status = null,
|
||||
string? name = null,
|
||||
string? ticker = null,
|
||||
string? agentName = null,
|
||||
string sortBy = "CreateDate",
|
||||
string sortDirection = "Desc")
|
||||
{
|
||||
return await ServiceScopeHelpers.WithScopedService<IBotRepository, (IEnumerable<Bot> Bots, int TotalCount)>(
|
||||
_scopeFactory,
|
||||
async repo =>
|
||||
{
|
||||
return await repo.GetBotsPaginatedAsync(
|
||||
pageNumber,
|
||||
pageSize,
|
||||
status,
|
||||
name,
|
||||
ticker,
|
||||
agentName,
|
||||
sortBy,
|
||||
sortDirection);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,10 @@ namespace Managing.Application.ManageBot.Commands;
|
||||
|
||||
public class DeleteBotCommand : IRequest<bool>
|
||||
{
|
||||
public string Name { get; }
|
||||
public Guid Identifier { get; }
|
||||
|
||||
public DeleteBotCommand(string name)
|
||||
public DeleteBotCommand(Guid identifier)
|
||||
{
|
||||
Name = name;
|
||||
Identifier = identifier;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
using Managing.Application.Abstractions;
|
||||
using Managing.Domain.Bots;
|
||||
using MediatR;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.ManageBot.Commands
|
||||
{
|
||||
public class GetActiveBotsCommand : IRequest<List<ITradingBot>>
|
||||
public class GetBotsByStatusCommand : IRequest<IEnumerable<Bot>>
|
||||
{
|
||||
public GetActiveBotsCommand()
|
||||
public BotStatus Status { get; }
|
||||
|
||||
public GetBotsByStatusCommand(BotStatus status)
|
||||
{
|
||||
Status = status;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using Managing.Domain.Statistics;
|
||||
using MediatR;
|
||||
|
||||
namespace Managing.Application.ManageBot.Commands
|
||||
{
|
||||
/// <summary>
|
||||
/// Command to retrieve all agent summaries with complete data
|
||||
/// </summary>
|
||||
public class GetAllAgentSummariesCommand : IRequest<IEnumerable<AgentSummary>>
|
||||
{
|
||||
/// <summary>
|
||||
/// Optional time filter to apply (24H, 3D, 1W, 1M, 1Y, Total)
|
||||
/// </summary>
|
||||
public string TimeFilter { get; }
|
||||
|
||||
public GetAllAgentSummariesCommand(string timeFilter = "Total")
|
||||
{
|
||||
TimeFilter = timeFilter;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using Managing.Application.Abstractions;
|
||||
using Managing.Domain.Bots;
|
||||
using Managing.Domain.Users;
|
||||
using MediatR;
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Managing.Application.ManageBot.Commands
|
||||
/// <summary>
|
||||
/// Command to retrieve all active agents and their strategies
|
||||
/// </summary>
|
||||
public class GetAllAgentsCommand : IRequest<Dictionary<User, List<ITradingBot>>>
|
||||
public class GetAllAgentsCommand : IRequest<Dictionary<User, List<Bot>>>
|
||||
{
|
||||
/// <summary>
|
||||
/// Optional time filter to apply (24H, 3D, 1W, 1M, 1Y, Total)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
using Managing.Domain.Bots;
|
||||
using MediatR;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.ManageBot.Commands
|
||||
{
|
||||
public class GetBotsByUserAndStatusCommand : IRequest<IEnumerable<Bot>>
|
||||
{
|
||||
public int UserId { get; }
|
||||
public BotStatus Status { get; }
|
||||
|
||||
public GetBotsByUserAndStatusCommand(int userId, BotStatus status)
|
||||
{
|
||||
UserId = userId;
|
||||
Status = status;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using Managing.Domain.Statistics;
|
||||
using MediatR;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.ManageBot.Commands
|
||||
{
|
||||
/// <summary>
|
||||
/// Command to retrieve paginated agent summaries with sorting and filtering
|
||||
/// </summary>
|
||||
public class GetPaginatedAgentSummariesCommand : IRequest<(IEnumerable<AgentSummary> Results, int TotalCount)>
|
||||
{
|
||||
/// <summary>
|
||||
/// Page number (1-based)
|
||||
/// </summary>
|
||||
public int Page { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of items per page
|
||||
/// </summary>
|
||||
public int PageSize { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Field to sort by
|
||||
/// </summary>
|
||||
public SortableFields SortBy { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Sort order (asc or desc)
|
||||
/// </summary>
|
||||
public string SortOrder { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional list of agent names to filter by
|
||||
/// </summary>
|
||||
public IEnumerable<string>? AgentNames { get; }
|
||||
|
||||
public GetPaginatedAgentSummariesCommand(
|
||||
int page = 1,
|
||||
int pageSize = 10,
|
||||
SortableFields sortBy = SortableFields.TotalPnL,
|
||||
string sortOrder = "desc",
|
||||
IEnumerable<string>? agentNames = null)
|
||||
{
|
||||
Page = page;
|
||||
PageSize = pageSize;
|
||||
SortBy = sortBy;
|
||||
SortOrder = sortOrder;
|
||||
AgentNames = agentNames;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using Managing.Application.Abstractions;
|
||||
using Managing.Domain.Bots;
|
||||
using MediatR;
|
||||
|
||||
namespace Managing.Application.ManageBot.Commands
|
||||
@@ -6,13 +6,13 @@ namespace Managing.Application.ManageBot.Commands
|
||||
/// <summary>
|
||||
/// Command to retrieve all strategies owned by a specific user
|
||||
/// </summary>
|
||||
public class GetUserStrategiesCommand : IRequest<List<ITradingBot>>
|
||||
public class GetUserStrategiesCommand : IRequest<List<Bot>>
|
||||
{
|
||||
public string UserName { get; }
|
||||
public string AgentName { get; }
|
||||
|
||||
public GetUserStrategiesCommand(string userName)
|
||||
public GetUserStrategiesCommand(string agentName)
|
||||
{
|
||||
UserName = userName;
|
||||
AgentName = agentName;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using Managing.Application.Abstractions;
|
||||
using Managing.Domain.Bots;
|
||||
using MediatR;
|
||||
|
||||
namespace Managing.Application.ManageBot.Commands
|
||||
@@ -6,7 +6,7 @@ namespace Managing.Application.ManageBot.Commands
|
||||
/// <summary>
|
||||
/// Command to retrieve a specific strategy owned by a user
|
||||
/// </summary>
|
||||
public class GetUserStrategyCommand : IRequest<ITradingBot>
|
||||
public class GetUserStrategyCommand : IRequest<Bot>
|
||||
{
|
||||
/// <summary>
|
||||
/// The username of the agent/user that owns the strategy
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
using Managing.Domain.Trades;
|
||||
using MediatR;
|
||||
|
||||
namespace Managing.Application.ManageBot.Commands;
|
||||
|
||||
public class ManualPositionCommand : IRequest<Position>
|
||||
{
|
||||
public Guid PositionId { get; set; }
|
||||
public Guid Identifier { get; set; }
|
||||
|
||||
public ManualPositionCommand(Guid identifier, Guid positionId)
|
||||
{
|
||||
Identifier = identifier;
|
||||
PositionId = positionId;
|
||||
}
|
||||
}
|
||||
@@ -3,15 +3,13 @@ using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.ManageBot.Commands
|
||||
{
|
||||
public class RestartBotCommand : IRequest<string>
|
||||
public class RestartBotCommand : IRequest<BotStatus>
|
||||
{
|
||||
public string Name { get; }
|
||||
public BotType BotType { get; }
|
||||
public Guid Identifier { get; }
|
||||
|
||||
public RestartBotCommand(BotType botType, string name)
|
||||
public RestartBotCommand(Guid identifier)
|
||||
{
|
||||
BotType = botType;
|
||||
Name = name;
|
||||
Identifier = identifier;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
using Managing.Domain.Bots;
|
||||
using Managing.Domain.Users;
|
||||
using MediatR;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.ManageBot.Commands
|
||||
{
|
||||
public class StartBotCommand : IRequest<string>
|
||||
public class StartBotCommand : IRequest<BotStatus>
|
||||
{
|
||||
public string Name { get; }
|
||||
public TradingBotConfig Config { get; }
|
||||
public User User { get; }
|
||||
public User User { get; internal set; }
|
||||
public bool CreateOnly { get; }
|
||||
|
||||
public StartBotCommand(TradingBotConfig config, string name, User user)
|
||||
public StartBotCommand(TradingBotConfig config, User user, bool createOnly = false)
|
||||
{
|
||||
Config = config;
|
||||
Name = name;
|
||||
User = user;
|
||||
CreateOnly = createOnly;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
using MediatR;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.ManageBot.Commands
|
||||
{
|
||||
public class StopBotCommand : IRequest<string>
|
||||
public class StopBotCommand : IRequest<BotStatus>
|
||||
{
|
||||
public string Identifier { get; }
|
||||
public Guid Identifier { get; }
|
||||
|
||||
public StopBotCommand(string identifier)
|
||||
public StopBotCommand(Guid identifier)
|
||||
{
|
||||
Identifier = identifier;
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
using MediatR;
|
||||
|
||||
namespace Managing.Application.ManageBot.Commands
|
||||
{
|
||||
public class ToggleIsForWatchingCommand : IRequest<string>
|
||||
{
|
||||
public string Name { get; }
|
||||
|
||||
public ToggleIsForWatchingCommand(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,12 +6,12 @@ namespace Managing.Application.ManageBot.Commands
|
||||
/// <summary>
|
||||
/// Command to update the configuration of a running trading bot
|
||||
/// </summary>
|
||||
public class UpdateBotConfigCommand : IRequest<string>
|
||||
public class UpdateBotConfigCommand : IRequest<bool>
|
||||
{
|
||||
public string Identifier { get; }
|
||||
public Guid Identifier { get; }
|
||||
public TradingBotConfig NewConfig { get; }
|
||||
|
||||
public UpdateBotConfigCommand(string identifier, TradingBotConfig newConfig)
|
||||
public UpdateBotConfigCommand(Guid identifier, TradingBotConfig newConfig)
|
||||
{
|
||||
Identifier = identifier;
|
||||
NewConfig = newConfig;
|
||||
|
||||
@@ -18,6 +18,6 @@ public class DeleteBotCommandHandler : IRequestHandler<DeleteBotCommand, bool>
|
||||
|
||||
public Task<bool> Handle(DeleteBotCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
return _botService.DeleteBot(request.Name);
|
||||
return _botService.DeleteBot(request.Identifier);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
using Managing.Application.Abstractions;
|
||||
using Managing.Application.ManageBot.Commands;
|
||||
using Managing.Domain.Bots;
|
||||
using MediatR;
|
||||
|
||||
namespace Managing.Application.ManageBot
|
||||
{
|
||||
public class GetActiveBotsCommandHandler(IBotService botService)
|
||||
: IRequestHandler<GetActiveBotsCommand, List<ITradingBot>>
|
||||
public class GetBotsByStatusCommandHandler(IBotService botService)
|
||||
: IRequestHandler<GetBotsByStatusCommand, IEnumerable<Bot>>
|
||||
{
|
||||
public Task<List<ITradingBot>> Handle(GetActiveBotsCommand request, CancellationToken cancellationToken)
|
||||
public async Task<IEnumerable<Bot>> Handle(GetBotsByStatusCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(botService.GetActiveBots());
|
||||
return await botService.GetBotsByStatusAsync(request.Status);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,11 +20,11 @@ namespace Managing.Application.ManageBot
|
||||
_accountService = accountService;
|
||||
}
|
||||
|
||||
public Task<List<AgentStatusResponse>> Handle(GetAgentStatusesCommand request,
|
||||
public async Task<List<AgentStatusResponse>> Handle(GetAgentStatusesCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = new List<AgentStatusResponse>();
|
||||
var allActiveBots = _botService.GetActiveBots();
|
||||
var allActiveBots = await _botService.GetBotsByStatusAsync(BotStatus.Up);
|
||||
|
||||
// Group bots by user and determine status
|
||||
var agentGroups = allActiveBots
|
||||
@@ -38,7 +38,9 @@ namespace Managing.Application.ManageBot
|
||||
var bots = agentGroup.ToList();
|
||||
|
||||
// Determine agent status: Online if at least one strategy is running, Offline otherwise
|
||||
var agentStatus = bots.Any(bot => bot.GetStatus() == BotStatus.Up.ToString()) ? AgentStatus.Online : AgentStatus.Offline;
|
||||
var agentStatus = bots.Any(bot => bot.Status == BotStatus.Up)
|
||||
? AgentStatus.Online
|
||||
: AgentStatus.Offline;
|
||||
|
||||
result.Add(new AgentStatusResponse
|
||||
{
|
||||
@@ -47,7 +49,7 @@ namespace Managing.Application.ManageBot
|
||||
});
|
||||
}
|
||||
|
||||
return Task.FromResult(result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.ManageBot.Commands;
|
||||
using Managing.Domain.Statistics;
|
||||
using MediatR;
|
||||
|
||||
namespace Managing.Application.ManageBot
|
||||
{
|
||||
/// <summary>
|
||||
/// Handler for retrieving all agent summaries with complete data
|
||||
/// </summary>
|
||||
public class GetAllAgentSummariesCommandHandler : IRequestHandler<GetAllAgentSummariesCommand,
|
||||
IEnumerable<AgentSummary>>
|
||||
{
|
||||
private readonly IStatisticService _statisticService;
|
||||
|
||||
public GetAllAgentSummariesCommandHandler(IStatisticService statisticService)
|
||||
{
|
||||
_statisticService = statisticService;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<AgentSummary>> Handle(GetAllAgentSummariesCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Get all agent summaries from the database
|
||||
var allAgentSummaries = await _statisticService.GetAllAgentSummaries();
|
||||
|
||||
if (request.TimeFilter != "Total")
|
||||
{
|
||||
var cutoffDate = GetCutoffDate(request.TimeFilter);
|
||||
allAgentSummaries = allAgentSummaries.Where(a =>
|
||||
a.UpdatedAt >= cutoffDate ||
|
||||
(a.Runtime.HasValue && a.Runtime.Value >= cutoffDate));
|
||||
}
|
||||
|
||||
return allAgentSummaries;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the cutoff date based on the time filter
|
||||
/// </summary>
|
||||
private DateTime GetCutoffDate(string timeFilter)
|
||||
{
|
||||
return timeFilter switch
|
||||
{
|
||||
"24H" => DateTime.UtcNow.AddHours(-24),
|
||||
"3D" => DateTime.UtcNow.AddDays(-3),
|
||||
"1W" => DateTime.UtcNow.AddDays(-7),
|
||||
"1M" => DateTime.UtcNow.AddMonths(-1),
|
||||
"1Y" => DateTime.UtcNow.AddYears(-1),
|
||||
_ => DateTime.MinValue // Default to include all data
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
using Managing.Application.Abstractions;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.ManageBot.Commands;
|
||||
using Managing.Common;
|
||||
using Managing.Domain.Users;
|
||||
using MediatR;
|
||||
|
||||
namespace Managing.Application.ManageBot
|
||||
{
|
||||
/// <summary>
|
||||
/// Handler for retrieving all agents and their strategies
|
||||
/// </summary>
|
||||
public class GetAllAgentsCommandHandler : IRequestHandler<GetAllAgentsCommand, Dictionary<User, List<ITradingBot>>>
|
||||
{
|
||||
private readonly IBotService _botService;
|
||||
private readonly IAccountService _accountService;
|
||||
|
||||
public GetAllAgentsCommandHandler(IBotService botService, IAccountService accountService)
|
||||
{
|
||||
_botService = botService;
|
||||
_accountService = accountService;
|
||||
}
|
||||
|
||||
public Task<Dictionary<User, List<ITradingBot>>> Handle(GetAllAgentsCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = new Dictionary<User, List<ITradingBot>>();
|
||||
var allActiveBots = _botService.GetActiveBots();
|
||||
|
||||
// Group bots by user
|
||||
foreach (var bot in allActiveBots)
|
||||
{
|
||||
if (bot.User == null)
|
||||
{
|
||||
// Skip bots without a user (this shouldn't happen, but just to be safe)
|
||||
continue;
|
||||
}
|
||||
|
||||
// Apply time filtering if needed (except for "Total")
|
||||
if (request.TimeFilter != "Total")
|
||||
{
|
||||
// Check if this bot had activity within the specified time range
|
||||
if (!BotHasActivityInTimeRange(bot, request.TimeFilter))
|
||||
{
|
||||
continue; // Skip this bot if it doesn't have activity in the time range
|
||||
}
|
||||
}
|
||||
|
||||
// Add the bot to the user's list
|
||||
if (!result.ContainsKey(bot.User))
|
||||
{
|
||||
result[bot.User] = new List<ITradingBot>();
|
||||
}
|
||||
|
||||
result[bot.User].Add(bot);
|
||||
}
|
||||
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a bot has had trading activity within the specified time range
|
||||
/// </summary>
|
||||
private bool BotHasActivityInTimeRange(ITradingBot bot, string timeFilter)
|
||||
{
|
||||
// Convert time filter to a DateTime
|
||||
DateTime cutoffDate = DateTime.UtcNow;
|
||||
|
||||
switch (timeFilter)
|
||||
{
|
||||
case "24H":
|
||||
cutoffDate = DateTime.UtcNow.AddHours(-24);
|
||||
break;
|
||||
case "3D":
|
||||
cutoffDate = DateTime.UtcNow.AddDays(-3);
|
||||
break;
|
||||
case "1W":
|
||||
cutoffDate = DateTime.UtcNow.AddDays(-7);
|
||||
break;
|
||||
case "1M":
|
||||
cutoffDate = DateTime.UtcNow.AddMonths(-1);
|
||||
break;
|
||||
case "1Y":
|
||||
cutoffDate = DateTime.UtcNow.AddYears(-1);
|
||||
break;
|
||||
default:
|
||||
// Default to "Total" (no filtering)
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if there are any positions with activity after the cutoff date
|
||||
return bot.Positions.Any(p =>
|
||||
p.Date >= cutoffDate ||
|
||||
(p.Open.Date >= cutoffDate) ||
|
||||
(p.StopLoss.Status == Enums.TradeStatus.Filled && p.StopLoss.Date >= cutoffDate) ||
|
||||
(p.TakeProfit1.Status == Enums.TradeStatus.Filled && p.TakeProfit1.Date >= cutoffDate) ||
|
||||
(p.TakeProfit2 != null && p.TakeProfit2.Status == Enums.TradeStatus.Filled &&
|
||||
p.TakeProfit2.Date >= cutoffDate));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Managing.Application.Abstractions;
|
||||
using Managing.Application.ManageBot.Commands;
|
||||
using Managing.Domain.Bots;
|
||||
using MediatR;
|
||||
|
||||
namespace Managing.Application.ManageBot
|
||||
{
|
||||
public class GetBotsByUserAndStatusCommandHandler(IBotService botService)
|
||||
: IRequestHandler<GetBotsByUserAndStatusCommand, IEnumerable<Bot>>
|
||||
{
|
||||
public async Task<IEnumerable<Bot>> Handle(GetBotsByUserAndStatusCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// Get all bots for the user
|
||||
var userBots = await botService.GetBotsByUser(request.UserId);
|
||||
|
||||
// Filter by status
|
||||
return userBots.Where(bot => bot.Status == request.Status);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,52 +1,25 @@
|
||||
using Managing.Application.Abstractions;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.ManageBot.Commands;
|
||||
using MediatR;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.ManageBot
|
||||
{
|
||||
/// <summary>
|
||||
/// Handler for retrieving only online agent names
|
||||
/// </summary>
|
||||
public class GetOnlineAgentNamesCommandHandler : IRequestHandler<GetOnlineAgentNamesCommand, List<string>>
|
||||
public class GetOnlineAgentNamesCommandHandler : IRequestHandler<GetOnlineAgentNamesCommand, IEnumerable<string>>
|
||||
{
|
||||
private readonly IBotService _botService;
|
||||
private readonly IAccountService _accountService;
|
||||
|
||||
public GetOnlineAgentNamesCommandHandler(IBotService botService, IAccountService accountService)
|
||||
public GetOnlineAgentNamesCommandHandler(IBotService botService)
|
||||
{
|
||||
_botService = botService;
|
||||
_accountService = accountService;
|
||||
}
|
||||
|
||||
public Task<List<string>> Handle(GetOnlineAgentNamesCommand request,
|
||||
public async Task<IEnumerable<string>> Handle(GetOnlineAgentNamesCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var onlineAgentNames = new List<string>();
|
||||
var allActiveBots = _botService.GetActiveBots();
|
||||
|
||||
// Group bots by user and determine status
|
||||
var agentGroups = allActiveBots
|
||||
.Where(bot => bot.User != null)
|
||||
.GroupBy(bot => bot.User)
|
||||
.ToList();
|
||||
|
||||
foreach (var agentGroup in agentGroups)
|
||||
{
|
||||
var user = agentGroup.Key;
|
||||
var bots = agentGroup.ToList();
|
||||
|
||||
// Only include agents that have at least one strategy running (Online status)
|
||||
var isOnline = bots.Any(bot => bot.GetStatus() == BotStatus.Up.ToString());
|
||||
|
||||
if (isOnline)
|
||||
{
|
||||
onlineAgentNames.Add(user.AgentName);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult(onlineAgentNames);
|
||||
return await _botService.GetActiveBotsNamesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Managing.Application.Abstractions.Repositories;
|
||||
using Managing.Application.ManageBot.Commands;
|
||||
using Managing.Domain.Statistics;
|
||||
using MediatR;
|
||||
|
||||
namespace Managing.Application.ManageBot
|
||||
{
|
||||
/// <summary>
|
||||
/// Handler for retrieving paginated agent summaries with sorting and filtering
|
||||
/// </summary>
|
||||
public class GetPaginatedAgentSummariesCommandHandler : IRequestHandler<GetPaginatedAgentSummariesCommand,
|
||||
(IEnumerable<AgentSummary> Results, int TotalCount)>
|
||||
{
|
||||
private readonly IAgentSummaryRepository _agentSummaryRepository;
|
||||
|
||||
public GetPaginatedAgentSummariesCommandHandler(IAgentSummaryRepository agentSummaryRepository)
|
||||
{
|
||||
_agentSummaryRepository = agentSummaryRepository;
|
||||
}
|
||||
|
||||
public async Task<(IEnumerable<AgentSummary> Results, int TotalCount)> Handle(
|
||||
GetPaginatedAgentSummariesCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return await _agentSummaryRepository.GetPaginatedAsync(
|
||||
request.Page,
|
||||
request.PageSize,
|
||||
request.SortBy,
|
||||
request.SortOrder,
|
||||
request.AgentNames);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,26 @@
|
||||
using Managing.Application.Abstractions;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.ManageBot.Commands;
|
||||
using Managing.Domain.Bots;
|
||||
using MediatR;
|
||||
|
||||
namespace Managing.Application.ManageBot
|
||||
{
|
||||
public class GetUserStrategiesCommandHandler : IRequestHandler<GetUserStrategiesCommand, List<ITradingBot>>
|
||||
public class GetUserStrategiesCommandHandler : IRequestHandler<GetUserStrategiesCommand, IEnumerable<Bot>>
|
||||
{
|
||||
private readonly IBotService _botService;
|
||||
private readonly IUserService _userService;
|
||||
|
||||
public GetUserStrategiesCommandHandler(IBotService botService)
|
||||
public GetUserStrategiesCommandHandler(IBotService botService, IUserService userService)
|
||||
{
|
||||
_botService = botService;
|
||||
_userService = userService;
|
||||
}
|
||||
|
||||
public Task<List<ITradingBot>> Handle(GetUserStrategiesCommand request, CancellationToken cancellationToken)
|
||||
public async Task<IEnumerable<Bot>> Handle(GetUserStrategiesCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var allActiveBots = _botService.GetActiveBots();
|
||||
var userBots = allActiveBots
|
||||
.Where(bot => bot.User != null && bot.User.AgentName == request.UserName)
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult(userBots);
|
||||
var user = await _userService.GetUserByAgentName(request.AgentName);
|
||||
return await _botService.GetBotsByUser(user.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using Managing.Application.Abstractions;
|
||||
using Managing.Application.ManageBot.Commands;
|
||||
using Managing.Domain.Bots;
|
||||
using MediatR;
|
||||
|
||||
namespace Managing.Application.ManageBot
|
||||
@@ -7,7 +8,7 @@ namespace Managing.Application.ManageBot
|
||||
/// <summary>
|
||||
/// Handler for retrieving a specific strategy owned by a user
|
||||
/// </summary>
|
||||
public class GetUserStrategyCommandHandler : IRequestHandler<GetUserStrategyCommand, ITradingBot>
|
||||
public class GetUserStrategyCommandHandler : IRequestHandler<GetUserStrategyCommand, Bot>
|
||||
{
|
||||
private readonly IBotService _botService;
|
||||
|
||||
@@ -16,17 +17,14 @@ namespace Managing.Application.ManageBot
|
||||
_botService = botService;
|
||||
}
|
||||
|
||||
public Task<ITradingBot> Handle(GetUserStrategyCommand request, CancellationToken cancellationToken)
|
||||
public async Task<Bot> Handle(GetUserStrategyCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var allActiveBots = _botService.GetActiveBots();
|
||||
|
||||
// Find the specific strategy that matches both user and strategy name
|
||||
var strategy = allActiveBots
|
||||
.FirstOrDefault(bot =>
|
||||
bot.User.AgentName == request.AgentName &&
|
||||
bot.Identifier == request.StrategyName);
|
||||
|
||||
return Task.FromResult(strategy);
|
||||
var strategy = await _botService.GetBotByName(request.StrategyName);
|
||||
if (strategy == null)
|
||||
{
|
||||
throw new Exception($"Strategy with name {request.StrategyName} not found");
|
||||
}
|
||||
return strategy;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
using Managing.Application.Abstractions;
|
||||
using Managing.Core;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.ManageBot;
|
||||
|
||||
public class LoadBackupBotCommandHandler : IRequestHandler<LoadBackupBotCommand, string>
|
||||
{
|
||||
private readonly IBotService _botService;
|
||||
private readonly ILogger<LoadBackupBotCommandHandler> _logger;
|
||||
|
||||
public LoadBackupBotCommandHandler(
|
||||
ILogger<LoadBackupBotCommandHandler> logger, IBotService botService)
|
||||
{
|
||||
_logger = logger;
|
||||
_botService = botService;
|
||||
}
|
||||
|
||||
public async Task<string> Handle(LoadBackupBotCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var backupBots = (await _botService.GetSavedBotsAsync()).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 activeBot = _botService.GetActiveBots().FirstOrDefault(b => b.Identifier == backupBot.Identifier);
|
||||
|
||||
if (activeBot == null)
|
||||
{
|
||||
_logger.LogInformation("No active instance found for bot {Identifier}. Starting backup...",
|
||||
backupBot.Identifier);
|
||||
|
||||
// Start the bot from backup
|
||||
_botService.StartBotFromBackup(backupBot);
|
||||
|
||||
// Wait a short time to allow the bot to initialize
|
||||
await Task.Delay(1000, cancellationToken);
|
||||
|
||||
// Try to get the active bot multiple times to ensure it's properly started
|
||||
int attempts = 0;
|
||||
const int maxAttempts = 2;
|
||||
|
||||
while (attempts < maxAttempts)
|
||||
{
|
||||
activeBot = _botService.GetActiveBots()
|
||||
.FirstOrDefault(b => b.Identifier == backupBot.Identifier);
|
||||
if (activeBot != null)
|
||||
{
|
||||
// Check if the bot was originally Down
|
||||
if (backupBot.LastStatus == BotStatus.Down)
|
||||
{
|
||||
result[activeBot.Identifier] = BotStatus.Down;
|
||||
_logger.LogInformation(
|
||||
"Backup bot {Identifier} loaded but kept in Down status as it was originally Down.",
|
||||
backupBot.Identifier);
|
||||
}
|
||||
else
|
||||
{
|
||||
result[activeBot.Identifier] = BotStatus.Up;
|
||||
anyBackupStarted = true;
|
||||
_logger.LogInformation("Backup bot {Identifier} started successfully.",
|
||||
backupBot.Identifier);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
attempts++;
|
||||
if (attempts < maxAttempts)
|
||||
{
|
||||
await Task.Delay(1000, cancellationToken); // Wait another second before next attempt
|
||||
}
|
||||
}
|
||||
|
||||
if (activeBot == null)
|
||||
{
|
||||
result[backupBot.Identifier] = BotStatus.Down;
|
||||
_logger.LogWarning("Backup bot {Identifier} failed to start after {MaxAttempts} attempts.",
|
||||
backupBot.Identifier, maxAttempts);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var status = MiscExtensions.ParseEnum<BotStatus>(activeBot.GetStatus());
|
||||
result[activeBot.Identifier] = status;
|
||||
anyBotActive = true;
|
||||
_logger.LogInformation("Bot {Identifier} is already active with status {Status}.",
|
||||
activeBot.Identifier,
|
||||
status);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error loading bot {Identifier}. Deleting its backup.", backupBot.Identifier);
|
||||
result[backupBot.Identifier] = BotStatus.Down;
|
||||
}
|
||||
}
|
||||
|
||||
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 finalStatus.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public class LoadBackupBotCommand : IRequest<string>
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using Managing.Application.Abstractions;
|
||||
using Managing.Application.ManageBot.Commands;
|
||||
using Managing.Domain.Trades;
|
||||
using MediatR;
|
||||
|
||||
namespace Managing.Application.ManageBot;
|
||||
|
||||
public class ManualPositionCommandHandler : IRequestHandler<ManualPositionCommand, Position>
|
||||
{
|
||||
private readonly IBotService _botService;
|
||||
|
||||
public ManualPositionCommandHandler(IBotService botService)
|
||||
{
|
||||
_botService = botService;
|
||||
}
|
||||
|
||||
public async Task<Position> Handle(ManualPositionCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var bot = await _botService.GetBotByIdentifier(request.Identifier);
|
||||
if (bot == null)
|
||||
{
|
||||
throw new Exception($"Bot with identifier {request.Identifier} not found");
|
||||
}
|
||||
|
||||
return await _botService.ClosePositionAsync(request.Identifier, request.PositionId);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
using Managing.Application.Abstractions;
|
||||
using Managing.Application.ManageBot.Commands;
|
||||
using MediatR;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.ManageBot
|
||||
{
|
||||
public class RestartBotCommandHandler : IRequestHandler<RestartBotCommand, string>
|
||||
public class RestartBotCommandHandler : IRequestHandler<RestartBotCommand, BotStatus>
|
||||
{
|
||||
private readonly IBotService _botService;
|
||||
|
||||
@@ -13,9 +14,9 @@ namespace Managing.Application.ManageBot
|
||||
_botService = botService;
|
||||
}
|
||||
|
||||
public Task<string> Handle(RestartBotCommand request, CancellationToken cancellationToken)
|
||||
public async Task<BotStatus> Handle(RestartBotCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
return _botService.RestartBot(request.Name);
|
||||
return await _botService.RestartBot(request.Identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,42 +1,38 @@
|
||||
using Managing.Application.Abstractions;
|
||||
using Managing.Application.Abstractions.Grains;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.ManageBot.Commands;
|
||||
using Managing.Common;
|
||||
using Managing.Domain.Bots;
|
||||
using MediatR;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.ManageBot
|
||||
{
|
||||
public class StartBotCommandHandler : IRequestHandler<StartBotCommand, string>
|
||||
public class StartBotCommandHandler : IRequestHandler<StartBotCommand, BotStatus>
|
||||
{
|
||||
private readonly IBotFactory _botFactory;
|
||||
private readonly IBotService _botService;
|
||||
private readonly IMoneyManagementService _moneyManagementService;
|
||||
private readonly IExchangeService _exchangeService;
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly IGrainFactory _grainFactory;
|
||||
|
||||
public StartBotCommandHandler(IBotFactory botFactory, IBotService botService,
|
||||
IMoneyManagementService moneyManagementService, IExchangeService exchangeService,
|
||||
IAccountService accountService)
|
||||
public StartBotCommandHandler(
|
||||
IAccountService accountService, IGrainFactory grainFactory)
|
||||
{
|
||||
_botFactory = botFactory;
|
||||
_botService = botService;
|
||||
_moneyManagementService = moneyManagementService;
|
||||
_exchangeService = exchangeService;
|
||||
_accountService = accountService;
|
||||
_grainFactory = grainFactory;
|
||||
}
|
||||
|
||||
public async Task<string> Handle(StartBotCommand request, CancellationToken cancellationToken)
|
||||
public async Task<BotStatus> Handle(StartBotCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
BotStatus botStatus = BotStatus.Down;
|
||||
|
||||
// Validate the configuration
|
||||
if (request.Config == null)
|
||||
{
|
||||
throw new ArgumentException("Bot configuration is required");
|
||||
}
|
||||
|
||||
if (request.Config.Scenario == null || !request.Config.Scenario.Indicators.Any())
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Scenario or indicators not loaded properly in constructor. This indicates a configuration error.");
|
||||
}
|
||||
|
||||
if (request.Config.BotTradingBalance <= Constants.GMX.Config.MinimumPositionAmount)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
@@ -59,68 +55,23 @@ namespace Managing.Application.ManageBot
|
||||
throw new Exception($"Account {request.Config.AccountName} has no USDC balance or not enough balance");
|
||||
}
|
||||
|
||||
// Ensure essential configuration values are properly set
|
||||
var configToUse = new TradingBotConfig
|
||||
{
|
||||
AccountName = request.Config.AccountName,
|
||||
MoneyManagement = request.Config.MoneyManagement,
|
||||
Ticker = request.Config.Ticker,
|
||||
ScenarioName = request.Config.ScenarioName,
|
||||
Scenario = request.Config.Scenario,
|
||||
Timeframe = request.Config.Timeframe,
|
||||
IsForWatchingOnly = request.Config.IsForWatchingOnly,
|
||||
BotTradingBalance = request.Config.BotTradingBalance,
|
||||
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.FlipPosition, // Set FlipPosition
|
||||
Name = request.Config.Name ?? request.Name,
|
||||
CloseEarlyWhenProfitable = request.Config.CloseEarlyWhenProfitable
|
||||
};
|
||||
|
||||
var tradingBot = await _botFactory.CreateTradingBot(configToUse);
|
||||
tradingBot.User = request.User;
|
||||
|
||||
// Log the configuration being used
|
||||
LogBotConfigurationAsync(tradingBot, $"{configToUse.Name} created");
|
||||
|
||||
_botService.AddTradingBotToCache(tradingBot);
|
||||
return tradingBot.GetStatus();
|
||||
|
||||
return botStatus.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs the bot configuration for debugging and audit purposes
|
||||
/// </summary>
|
||||
/// <param name="bot">The trading bot instance</param>
|
||||
/// <param name="context">Context information for the log</param>
|
||||
private void LogBotConfigurationAsync(ITradingBot bot, string context)
|
||||
{
|
||||
try
|
||||
{
|
||||
var config = bot.GetConfiguration();
|
||||
var logMessage = $"{context} - Bot: {config.Name}, " +
|
||||
$"Account: {config.AccountName}, " +
|
||||
$"Ticker: {config.Ticker}, " +
|
||||
$"Balance: {config.BotTradingBalance}, " +
|
||||
$"MaxTime: {config.MaxPositionTimeHours?.ToString() ?? "Disabled"}, " +
|
||||
$"FlipOnlyProfit: {config.FlipOnlyWhenInProfit}, " +
|
||||
$"FlipPosition: {config.FlipPosition}, " +
|
||||
$"Cooldown: {config.CooldownPeriod}, " +
|
||||
$"MaxLoss: {config.MaxLossStreak}";
|
||||
|
||||
// Log through the bot's logger (this will use the bot's logging mechanism)
|
||||
// For now, we'll just add a comment that this could be enhanced with actual logging
|
||||
// Console.WriteLine(logMessage); // Could be replaced with proper logging
|
||||
var botGrain = _grainFactory.GetGrain<ILiveTradingBotGrain>(Guid.NewGuid());
|
||||
await botGrain.CreateAsync(request.Config, request.User);
|
||||
|
||||
// Only start the bot if createOnly is false
|
||||
if (!request.CreateOnly)
|
||||
{
|
||||
await botGrain.StartAsync();
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Ignore logging errors to not affect bot creation
|
||||
throw new Exception($"Failed to start bot: {ex.Message}, {ex.StackTrace}");
|
||||
}
|
||||
|
||||
return request.CreateOnly ? BotStatus.None : BotStatus.Up;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
using Managing.Application.Abstractions;
|
||||
using Managing.Application.ManageBot.Commands;
|
||||
using MediatR;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.ManageBot
|
||||
{
|
||||
public class StopBotCommandHandler : IRequestHandler<StopBotCommand, string>
|
||||
public class StopBotCommandHandler : IRequestHandler<StopBotCommand, BotStatus>
|
||||
{
|
||||
private readonly IBotService _botService;
|
||||
|
||||
@@ -13,9 +14,9 @@ namespace Managing.Application.ManageBot
|
||||
_botService = botService;
|
||||
}
|
||||
|
||||
public Task<string> Handle(StopBotCommand request, CancellationToken cancellationToken)
|
||||
public async Task<BotStatus> Handle(StopBotCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
return _botService.StopBot(request.Identifier);
|
||||
return await _botService.StopBot(request.Identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using Managing.Application.Abstractions;
|
||||
using Managing.Application.ManageBot.Commands;
|
||||
using MediatR;
|
||||
|
||||
namespace Managing.Application.ManageBot
|
||||
{
|
||||
public class ToggleIsForWatchingCommandHandler : IRequestHandler<ToggleIsForWatchingCommand, string>
|
||||
{
|
||||
private readonly IBotService _botService;
|
||||
|
||||
public ToggleIsForWatchingCommandHandler(IBotService botService)
|
||||
{
|
||||
_botService = botService;
|
||||
}
|
||||
|
||||
public Task<string> Handle(ToggleIsForWatchingCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
_botService.ToggleIsForWatchingOnly(request.Name);
|
||||
var bot = _botService.GetActiveBots().FirstOrDefault(b => b.Name == request.Name);
|
||||
return Task.FromResult(bot?.Config.IsForWatchingOnly.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ namespace Managing.Application.ManageBot
|
||||
/// <summary>
|
||||
/// Handler for updating trading bot configurations
|
||||
/// </summary>
|
||||
public class UpdateBotConfigCommandHandler : IRequestHandler<UpdateBotConfigCommand, string>
|
||||
public class UpdateBotConfigCommandHandler : IRequestHandler<UpdateBotConfigCommand, bool>
|
||||
{
|
||||
private readonly IBotService _botService;
|
||||
|
||||
@@ -16,44 +16,27 @@ namespace Managing.Application.ManageBot
|
||||
_botService = botService;
|
||||
}
|
||||
|
||||
public async Task<string> Handle(UpdateBotConfigCommand request, CancellationToken cancellationToken)
|
||||
public async Task<bool> Handle(UpdateBotConfigCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(request.Identifier))
|
||||
{
|
||||
throw new ArgumentException("Bot identifier is required");
|
||||
}
|
||||
|
||||
if (request.NewConfig == null)
|
||||
{
|
||||
throw new ArgumentException("New configuration is required");
|
||||
}
|
||||
|
||||
// Get the bot from active bots
|
||||
var activeBots = _botService.GetActiveBots();
|
||||
var bot = activeBots.FirstOrDefault(b => b.Identifier == request.Identifier);
|
||||
var bot = await _botService.GetBotByIdentifier(request.Identifier);
|
||||
|
||||
if (bot == null)
|
||||
{
|
||||
return $"Bot with identifier {request.Identifier} not found or is not running";
|
||||
throw new Exception($"Bot with identifier {request.Identifier} not found");
|
||||
}
|
||||
|
||||
// Update the bot configuration
|
||||
var updateResult = await bot.UpdateConfiguration(request.NewConfig);
|
||||
|
||||
if (updateResult)
|
||||
{
|
||||
return $"Bot configuration updated successfully for {request.Identifier}";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"Failed to update bot configuration for {request.Identifier}";
|
||||
}
|
||||
return await _botService.UpdateBotConfiguration(request.Identifier, request.NewConfig);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"Error updating bot configuration: {ex.Message}";
|
||||
throw new Exception($"Error updating bot configuration: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user