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:
Oda
2025-08-04 23:07:06 +02:00
committed by GitHub
parent cd378587aa
commit 082ae8714b
215 changed files with 9562 additions and 14028 deletions

View File

@@ -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);
});
}
}
}