From 082ae8714bb4e946d848f476f2b3c9c371bbf194 Mon Sep 17 00:00:00 2001
From: Oda <102867384+CryptoOda@users.noreply.github.com>
Date: Mon, 4 Aug 2025 23:07:06 +0200
Subject: [PATCH] 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
---
.cursor/rules/fullstack.mdc | 1 +
scripts/safe-migrate.sh | 315 +++-
src/Managing.Api.Workers/appsettings.Oda.json | 4 +
.../appsettings.Production.json | 4 +
.../Authorization/JwtMiddleware.cs | 15 +-
.../Controllers/BacktestController.cs | 3 +-
src/Managing.Api/Controllers/BotController.cs | 785 ++++-----
.../Controllers/DataController.cs | 363 +----
.../Controllers/ScenarioController.cs | 28 +-
.../Controllers/TradingController.cs | 2 +-
.../Controllers/UserController.cs | 46 +-
.../Requests/GetBotsPaginatedRequest.cs | 55 +
.../Models/Requests/UpdateBotConfigRequest.cs | 2 +-
.../Models/Responses/AgentSummaryViewModel.cs | 19 -
.../CandlesWithIndicatorsResponse.cs | 7 +-
.../Responses/PaginatedAgentIndexResponse.cs | 4 +-
.../Models/Responses/PaginatedResponse.cs | 43 +
.../Models/Responses/TradingBotResponse.cs | 25 +-
.../Responses/UserStrategyDetailsViewModel.cs | 10 +-
src/Managing.Api/Program.cs | 10 +-
src/Managing.Api/appsettings.Oda.json | 3 +-
src/Managing.Api/appsettings.Production.json | 3 +-
src/Managing.Api/appsettings.Sandbox.json | 3 +-
.../Grains/BotRegistryEntry.cs | 55 +
.../Grains/BotRegistryState.cs | 36 +
.../Grains/IBacktestTradingBotGrain.cs | 20 +-
.../Grains/ILiveBotRegistryGrain.cs | 51 +
.../Grains/ILiveTradingBotGrain.cs | 43 +
.../Grains/ITradingBotGrain.cs | 94 --
.../Models/TradingBotResponse.cs | 11 +-
.../Repositories/IAgentSummaryRepository.cs | 30 +
.../Repositories/IBotRepository.cs | 38 +-
.../Repositories/ICandleRepository.cs | 18 +-
.../Repositories/ITradingRepository.cs | 14 +-
.../Services/IBacktester.cs | 2 +-
.../Services/IExchangeService.cs | 14 +-
.../Services/IStatisticService.cs | 2 +
.../Services/ISynthPredictionService.cs | 6 +-
.../Services/ITradingService.cs | 21 +-
.../Services/IUserService.cs | 5 +-
src/Managing.Application.Tests/BotsTests.cs | 104 +-
...ndicatorTests.cs => IndicatorBaseTests.cs} | 68 +-
.../PositionTests.cs | 6 +-
.../ProfitAndLossTests.cs | 6 +-
.../BundleBacktestWorker.cs | 30 +-
.../StatisticService.cs | 29 +-
.../Abstractions/Grains/IAgentGrain.cs | 27 +
.../Grains/IScenarioRunnerGrain.cs | 22 +
.../Abstractions/IBotFactory.cs | 21 -
.../Abstractions/IBotService.cs | 65 +-
.../Abstractions/IScenarioService.cs | 18 +-
.../Abstractions/ITradingBot.cs | 23 +-
.../Backtesting/Backtester.cs | 10 +-
.../Bots/Base/BotFactory.cs | 49 -
.../Bots/Grains/AgentGrain.cs | 163 ++
.../Bots/Grains/BacktestTradingBotGrain.cs | 155 +-
.../Bots/Grains/LiveBotRegistryGrain.cs | 179 ++
.../Bots/Grains/LiveTradingBotGrain.cs | 747 +++++----
.../Bots/Models/AgentGrainState.cs | 8 +
src/Managing.Application/Bots/SimpleBot.cs | 58 -
.../Bots/TradingBotBase.cs | 780 +++------
.../Bots/TradingBotGrainState.cs | 22 +-
src/Managing.Application/GeneticService.cs | 59 +-
.../ManageBot/BackupBotService.cs | 52 -
.../ManageBot/BotService.cs | 624 +++----
.../ManageBot/Commands/DeleteBotCommand.cs | 6 +-
.../Commands/GetActiveBotsCommand.cs | 10 +-
.../Commands/GetAllAgentSummariesCommand.cs | 21 +
.../ManageBot/Commands/GetAllAgentsCommand.cs | 4 +-
.../Commands/GetBotsByUserAndStatusCommand.cs | 18 +
.../GetPaginatedAgentSummariesCommand.cs | 51 +
.../Commands/GetUserStrategiesCommand.cs | 10 +-
.../Commands/GetUserStrategyCommand.cs | 4 +-
.../Commands/ManualPositionCommand.cs | 16 +
.../ManageBot/Commands/RestartBotCommand.cs | 10 +-
.../ManageBot/Commands/StartBotCommand.cs | 11 +-
.../ManageBot/Commands/StopBotCommand.cs | 7 +-
.../Commands/ToggleIsForWatchingCommand.cs | 14 -
.../Commands/UpdateBotConfigCommand.cs | 6 +-
.../ManageBot/DeleteBotCommandHandler.cs | 2 +-
.../ManageBot/GetActiveBotsCommandHandler.cs | 9 +-
.../GetAgentStatusesCommandHandler.cs | 12 +-
.../GetAllAgentSummariesCommandHandler.cs | 54 +
.../ManageBot/GetAllAgentsCommandHandler.cs | 101 --
.../GetBotsByUserAndStatusCommandHandler.cs | 20 +
.../GetOnlineAgentNamesCommandHandler.cs | 35 +-
...etPaginatedAgentSummariesCommandHandler.cs | 33 +
.../GetUserStrategiesCommandHandler.cs | 18 +-
.../GetUserStrategyCommandHandler.cs | 20 +-
.../ManageBot/LoadBackupBotCommandHandler.cs | 125 --
.../ManageBot/ManualPositionCommandHandler.cs | 27 +
.../ManageBot/RestartBotCommandHandler.cs | 7 +-
.../ManageBot/StartBotCommandHandler.cs | 99 +-
.../ManageBot/StopBotCommandHandler.cs | 7 +-
.../ToggleIsForWatchingCommandHandler.cs | 23 -
.../UpdateBotConfigCommandHandler.cs | 29 +-
.../Managing.Application.csproj | 40 +-
.../Scenarios/ScenarioRunnerGrain.cs | 99 ++
.../Scenarios/ScenarioService.cs | 92 +-
.../Shared/SettingsService.cs | 18 +-
.../Synth/SynthPredictionService.cs | 3 +-
.../Trading/OpenPositionCommandHandler.cs | 2 +-
.../Trading/TradingService.cs | 48 +-
src/Managing.Application/Users/UserService.cs | 84 +-
.../Workers/BalanceTrackingWorker.cs | 30 +-
.../Workers/BotManagerWorker.cs | 22 -
src/Managing.Bootstrap/ApiBootstrap.cs | 93 +-
.../Managing.Bootstrap.csproj | 39 +-
src/Managing.Bootstrap/WorkersBootstrap.cs | 3 -
src/Managing.Common/Enums.cs | 17 +-
src/Managing.Domain/Backtests/Backtest.cs | 19 +-
src/Managing.Domain/Bots/Bot.cs | 129 +-
src/Managing.Domain/Bots/BotBackup.cs | 51 -
src/Managing.Domain/Bots/IBot.cs | 30 -
src/Managing.Domain/Bots/TradingBotBackup.cs | 7 +-
.../Candles/CandleExtensions.cs | 48 +-
.../Base/EmaBaseIndicatorBase.cs} | 4 +-
.../Base/IndicatorsResultBase.cs | 0
.../Context/StDevContext.cs | 15 +-
.../{Strategies => Indicators}/IIndicator.cs | 15 +-
.../Indicators/IndicatorBase.cs | 54 +
.../LightIndicator.cs | 36 +-
src/Managing.Domain/Indicators/LightSignal.cs | 71 +
.../Rules/CloseHigherThanThePreviousHigh.cs | 0
.../Rules/CloseLowerThanThePreviousHigh.cs | 0
.../Rules/RSIShouldBeBullish.cs | 0
.../{Strategies => Indicators}/Signal.cs | 1 +
.../Signals/ChandelierExitIndicatorBase.cs} | 25 +-
.../Signals/DualEmaCrossIndicatorBase.cs} | 22 +-
.../Signals/EmaCrossIndicator.cs | 16 +-
.../Signals/EmaCrossIndicatorBase.cs | 79 +
.../Signals/LaggingSTC.cs | 15 +-
.../Signals/MacdCrossIndicatorBase.cs} | 17 +-
.../RsiDivergenceConfirmIndicatorBase.cs} | 35 +-
.../Signals/RsiDivergenceIndicatorBase.cs} | 35 +-
.../Signals/StcIndicatorBase.cs} | 18 +-
.../Signals/SuperTrendCrossEma.cs | 19 +-
.../Signals/SuperTrendIndicatorBase.cs} | 21 +-
.../ThreeWhiteSoldiersIndicatorBase.cs} | 13 +-
.../Trends/EmaTrendIndicatorBase.cs} | 18 +-
.../Trends/StochRsiTrendIndicatorBase.cs} | 23 +-
.../Scenarios/LightScenario.cs | 11 +-
src/Managing.Domain/Scenarios/Scenario.cs | 22 +-
.../Scenarios/ScenarioHelpers.cs | 92 +-
.../Shared/Helpers/TradingBox.cs | 76 +-
.../Shared/Helpers/TradingHelpers.cs | 3 +-
.../Statistics/AgentSummary.cs | 47 +
src/Managing.Domain/Statistics/Spotlight.cs | 1 +
src/Managing.Domain/Strategies/Indicator.cs | 82 -
src/Managing.Domain/Strategies/LightSignal.cs | 76 -
src/Managing.Domain/Trades/Position.cs | 62 +-
src/Managing.Domain/Users/User.cs | 11 +-
.../InfluxDb/CandleRepository.cs | 22 +-
.../20250723194312_InitialCreate.Designer.cs | 1224 --------------
...1025_UpdateBotBackupDataToText.Designer.cs | 1224 --------------
...20250724141819_AddWorkerEntity.Designer.cs | 1259 ---------------
.../20250724141819_AddWorkerEntity.cs | 47 -
...0250724160015_AddSynthEntities.Designer.cs | 1349 ----------------
.../20250724160015_AddSynthEntities.cs | 85 -
...64138_UpdateScoreMessageToText.Designer.cs | 1349 ----------------
...20250724164138_UpdateScoreMessageToText.cs | 38 -
...014014_AddUserIdToBundleBacktestRequest.cs | 49 -
...nagementJsonFromBacktestEntity.Designer.cs | 1360 ----------------
...250725172635_AddUserIdToMoneyManagement.cs | 49 -
.../20250725173315_AddUserIdToBotBackup.cs | 49 -
.../20250725202808_RemoveFeeEntity.cs | 51 -
...ner.cs => 20250801100607_Init.Designer.cs} | 78 +-
...nitialCreate.cs => 20250801100607_Init.cs} | 330 ++--
...250801111224_UpdateUserEntity.Designer.cs} | 137 +-
....cs => 20250801111224_UpdateUserEntity.cs} | 26 +-
...0250803201734_AddTickerToBots.Designer.cs} | 121 +-
...y.cs => 20250803201734_AddTickerToBots.cs} | 20 +-
...0250803204725_UpdateBotTicker.Designer.cs} | 114 +-
.../20250803204725_UpdateBotTicker.cs | 22 +
...03231246_AddAgentSummaryEntity.Designer.cs | 1428 ++++++++++++++++
.../20250803231246_AddAgentSummaryEntity.cs | 71 +
..._AddMissingAgentSummaryColumns.Designer.cs | 1435 +++++++++++++++++
...804200654_AddMissingAgentSummaryColumns.cs | 42 +
.../ManagingDbContextModelSnapshot.cs | 145 +-
.../PostgreSql/AgentSummaryRepository.cs | 232 +++
.../PostgreSql/Entities/AgentSummaryEntity.cs | 20 +
.../PostgreSql/Entities/BotBackupEntity.cs | 36 -
.../PostgreSql/Entities/BotEntity.cs | 30 +
.../PostgreSql/Entities/PositionEntity.cs | 62 +-
.../PostgreSql/Entities/UserEntity.cs | 12 +-
.../PostgreSql/ManagingDbContext.cs | 133 +-
.../PostgreSql/PostgreSqlBotRepository.cs | 173 +-
.../PostgreSql/PostgreSqlMappers.cs | 140 +-
.../PostgreSql/PostgreSqlTradingRepository.cs | 117 +-
.../Abstractions/IExchangeProcessor.cs | 2 +-
.../ExchangeService.cs | 27 +-
.../Exchanges/BaseProcessor.cs | 2 +-
.../Exchanges/EvmProcessor.cs | 4 +-
.../Discord/DiscordService.cs | 2 +-
.../ExchangeServicesTests.cs | 4 +-
.../Services/Gmx/GmxV2Mappers.cs | 2 +-
.../src/app/store/customScenario.tsx | 6 +-
.../mollecules/CardText/CardText.tsx | 10 +-
.../components/mollecules/NavBar/NavBar.tsx | 7 +-
.../organism/ActiveBots/ActiveBots.tsx | 294 ++--
.../organism/Backtest/backtestModal.tsx | 11 -
.../organism/Backtest/backtestRowDetails.tsx | 77 +-
.../organism/Backtest/backtestTable.tsx | 7 +-
.../CustomScenario/CustomScenario.tsx | 12 +-
.../components/organism/Trading/Summary.tsx | 278 ++--
.../UnifiedTradingModal.tsx | 60 +-
.../src/generated/ManagingApi.ts | 677 ++++----
.../src/generated/ManagingApiTypes.ts | 284 ++--
src/Managing.WebApp/src/global/type.ts | 1 +
src/Managing.WebApp/src/hooks/useBots.tsx | 32 +
.../pages/backtestPage/backtestScanner.tsx | 78 +-
.../src/pages/botsPage/botList.tsx | 99 +-
.../src/pages/botsPage/bots.tsx | 111 +-
.../src/pages/dashboardPage/agentIndex.tsx | 165 +-
.../pages/settingsPage/UserInfoSettings.tsx | 6 +
215 files changed, 9562 insertions(+), 14028 deletions(-)
create mode 100644 src/Managing.Api/Models/Requests/GetBotsPaginatedRequest.cs
create mode 100644 src/Managing.Api/Models/Responses/PaginatedResponse.cs
create mode 100644 src/Managing.Application.Abstractions/Grains/BotRegistryEntry.cs
create mode 100644 src/Managing.Application.Abstractions/Grains/BotRegistryState.cs
create mode 100644 src/Managing.Application.Abstractions/Grains/ILiveBotRegistryGrain.cs
create mode 100644 src/Managing.Application.Abstractions/Grains/ILiveTradingBotGrain.cs
delete mode 100644 src/Managing.Application.Abstractions/Grains/ITradingBotGrain.cs
create mode 100644 src/Managing.Application.Abstractions/Repositories/IAgentSummaryRepository.cs
rename src/Managing.Application.Tests/{IndicatorTests.cs => IndicatorBaseTests.cs} (70%)
create mode 100644 src/Managing.Application/Abstractions/Grains/IAgentGrain.cs
create mode 100644 src/Managing.Application/Abstractions/Grains/IScenarioRunnerGrain.cs
delete mode 100644 src/Managing.Application/Abstractions/IBotFactory.cs
delete mode 100644 src/Managing.Application/Bots/Base/BotFactory.cs
create mode 100644 src/Managing.Application/Bots/Grains/AgentGrain.cs
create mode 100644 src/Managing.Application/Bots/Grains/LiveBotRegistryGrain.cs
create mode 100644 src/Managing.Application/Bots/Models/AgentGrainState.cs
delete mode 100644 src/Managing.Application/Bots/SimpleBot.cs
delete mode 100644 src/Managing.Application/ManageBot/BackupBotService.cs
create mode 100644 src/Managing.Application/ManageBot/Commands/GetAllAgentSummariesCommand.cs
create mode 100644 src/Managing.Application/ManageBot/Commands/GetBotsByUserAndStatusCommand.cs
create mode 100644 src/Managing.Application/ManageBot/Commands/GetPaginatedAgentSummariesCommand.cs
create mode 100644 src/Managing.Application/ManageBot/Commands/ManualPositionCommand.cs
delete mode 100644 src/Managing.Application/ManageBot/Commands/ToggleIsForWatchingCommand.cs
create mode 100644 src/Managing.Application/ManageBot/GetAllAgentSummariesCommandHandler.cs
delete mode 100644 src/Managing.Application/ManageBot/GetAllAgentsCommandHandler.cs
create mode 100644 src/Managing.Application/ManageBot/GetBotsByUserAndStatusCommandHandler.cs
create mode 100644 src/Managing.Application/ManageBot/GetPaginatedAgentSummariesCommandHandler.cs
delete mode 100644 src/Managing.Application/ManageBot/LoadBackupBotCommandHandler.cs
create mode 100644 src/Managing.Application/ManageBot/ManualPositionCommandHandler.cs
delete mode 100644 src/Managing.Application/ManageBot/ToggleIsForWatchingCommandHandler.cs
create mode 100644 src/Managing.Application/Scenarios/ScenarioRunnerGrain.cs
delete mode 100644 src/Managing.Application/Workers/BotManagerWorker.cs
delete mode 100644 src/Managing.Domain/Bots/BotBackup.cs
delete mode 100644 src/Managing.Domain/Bots/IBot.cs
rename src/Managing.Domain/{Strategies/Base/EmaBaseIndicator.cs => Indicators/Base/EmaBaseIndicatorBase.cs} (86%)
rename src/Managing.Domain/{Strategies => Indicators}/Base/IndicatorsResultBase.cs (100%)
rename src/Managing.Domain/{Strategies => Indicators}/Context/StDevContext.cs (89%)
rename src/Managing.Domain/{Strategies => Indicators}/IIndicator.cs (54%)
create mode 100644 src/Managing.Domain/Indicators/IndicatorBase.cs
rename src/Managing.Domain/{Strategies => Indicators}/LightIndicator.cs (64%)
create mode 100644 src/Managing.Domain/Indicators/LightSignal.cs
rename src/Managing.Domain/{Strategies => Indicators}/Rules/CloseHigherThanThePreviousHigh.cs (100%)
rename src/Managing.Domain/{Strategies => Indicators}/Rules/CloseLowerThanThePreviousHigh.cs (100%)
rename src/Managing.Domain/{Strategies => Indicators}/Rules/RSIShouldBeBullish.cs (100%)
rename src/Managing.Domain/{Strategies => Indicators}/Signal.cs (97%)
rename src/Managing.Domain/{Strategies/Signals/ChandelierExitIndicator.cs => Indicators/Signals/ChandelierExitIndicatorBase.cs} (81%)
rename src/Managing.Domain/{Strategies/Signals/DualEmaCrossIndicator.cs => Indicators/Signals/DualEmaCrossIndicatorBase.cs} (82%)
rename src/Managing.Domain/{Strategies => Indicators}/Signals/EmaCrossIndicator.cs (80%)
create mode 100644 src/Managing.Domain/Indicators/Signals/EmaCrossIndicatorBase.cs
rename src/Managing.Domain/{Strategies => Indicators}/Signals/LaggingSTC.cs (90%)
rename src/Managing.Domain/{Strategies/Signals/MacdCrossIndicator.cs => Indicators/Signals/MacdCrossIndicatorBase.cs} (85%)
rename src/Managing.Domain/{Strategies/Signals/RsiDivergenceConfirmIndicator.cs => Indicators/Signals/RsiDivergenceConfirmIndicatorBase.cs} (89%)
rename src/Managing.Domain/{Strategies/Signals/RsiDivergenceIndicator.cs => Indicators/Signals/RsiDivergenceIndicatorBase.cs} (87%)
rename src/Managing.Domain/{Strategies/Signals/StcIndicator.cs => Indicators/Signals/StcIndicatorBase.cs} (84%)
rename src/Managing.Domain/{Strategies => Indicators}/Signals/SuperTrendCrossEma.cs (92%)
rename src/Managing.Domain/{Strategies/Signals/SuperTrendIndicator.cs => Indicators/Signals/SuperTrendIndicatorBase.cs} (82%)
rename src/Managing.Domain/{Strategies/Signals/ThreeWhiteSoldiersIndicator.cs => Indicators/Signals/ThreeWhiteSoldiersIndicatorBase.cs} (79%)
rename src/Managing.Domain/{Strategies/Trends/EmaTrendIndicator.cs => Indicators/Trends/EmaTrendIndicatorBase.cs} (74%)
rename src/Managing.Domain/{Strategies/Trends/StochRsiTrendIndicator.cs => Indicators/Trends/StochRsiTrendIndicatorBase.cs} (82%)
create mode 100644 src/Managing.Domain/Statistics/AgentSummary.cs
delete mode 100644 src/Managing.Domain/Strategies/Indicator.cs
delete mode 100644 src/Managing.Domain/Strategies/LightSignal.cs
delete mode 100644 src/Managing.Infrastructure.Database/Migrations/20250723194312_InitialCreate.Designer.cs
delete mode 100644 src/Managing.Infrastructure.Database/Migrations/20250723221025_UpdateBotBackupDataToText.Designer.cs
delete mode 100644 src/Managing.Infrastructure.Database/Migrations/20250724141819_AddWorkerEntity.Designer.cs
delete mode 100644 src/Managing.Infrastructure.Database/Migrations/20250724141819_AddWorkerEntity.cs
delete mode 100644 src/Managing.Infrastructure.Database/Migrations/20250724160015_AddSynthEntities.Designer.cs
delete mode 100644 src/Managing.Infrastructure.Database/Migrations/20250724160015_AddSynthEntities.cs
delete mode 100644 src/Managing.Infrastructure.Database/Migrations/20250724164138_UpdateScoreMessageToText.Designer.cs
delete mode 100644 src/Managing.Infrastructure.Database/Migrations/20250724164138_UpdateScoreMessageToText.cs
delete mode 100644 src/Managing.Infrastructure.Database/Migrations/20250725014014_AddUserIdToBundleBacktestRequest.cs
delete mode 100644 src/Managing.Infrastructure.Database/Migrations/20250725092603_RemoveOptimizedMoneyManagementJsonFromBacktestEntity.Designer.cs
delete mode 100644 src/Managing.Infrastructure.Database/Migrations/20250725172635_AddUserIdToMoneyManagement.cs
delete mode 100644 src/Managing.Infrastructure.Database/Migrations/20250725173315_AddUserIdToBotBackup.cs
delete mode 100644 src/Managing.Infrastructure.Database/Migrations/20250725202808_RemoveFeeEntity.cs
rename src/Managing.Infrastructure.Database/Migrations/{20250725202808_RemoveFeeEntity.Designer.cs => 20250801100607_Init.Designer.cs} (96%)
rename src/Managing.Infrastructure.Database/Migrations/{20250723194312_InitialCreate.cs => 20250801100607_Init.cs} (86%)
rename src/Managing.Infrastructure.Database/Migrations/{20250725014014_AddUserIdToBundleBacktestRequest.Designer.cs => 20250801111224_UpdateUserEntity.Designer.cs} (95%)
rename src/Managing.Infrastructure.Database/Migrations/{20250723221025_UpdateBotBackupDataToText.cs => 20250801111224_UpdateUserEntity.cs} (50%)
rename src/Managing.Infrastructure.Database/Migrations/{20250725172635_AddUserIdToMoneyManagement.Designer.cs => 20250803201734_AddTickerToBots.Designer.cs} (95%)
rename src/Managing.Infrastructure.Database/Migrations/{20250725092603_RemoveOptimizedMoneyManagementJsonFromBacktestEntity.cs => 20250803201734_AddTickerToBots.cs} (55%)
rename src/Managing.Infrastructure.Database/Migrations/{20250725173315_AddUserIdToBotBackup.Designer.cs => 20250803204725_UpdateBotTicker.Designer.cs} (95%)
create mode 100644 src/Managing.Infrastructure.Database/Migrations/20250803204725_UpdateBotTicker.cs
create mode 100644 src/Managing.Infrastructure.Database/Migrations/20250803231246_AddAgentSummaryEntity.Designer.cs
create mode 100644 src/Managing.Infrastructure.Database/Migrations/20250803231246_AddAgentSummaryEntity.cs
create mode 100644 src/Managing.Infrastructure.Database/Migrations/20250804200654_AddMissingAgentSummaryColumns.Designer.cs
create mode 100644 src/Managing.Infrastructure.Database/Migrations/20250804200654_AddMissingAgentSummaryColumns.cs
create mode 100644 src/Managing.Infrastructure.Database/PostgreSql/AgentSummaryRepository.cs
create mode 100644 src/Managing.Infrastructure.Database/PostgreSql/Entities/AgentSummaryEntity.cs
delete mode 100644 src/Managing.Infrastructure.Database/PostgreSql/Entities/BotBackupEntity.cs
create mode 100644 src/Managing.Infrastructure.Database/PostgreSql/Entities/BotEntity.cs
create mode 100644 src/Managing.WebApp/src/hooks/useBots.tsx
diff --git a/.cursor/rules/fullstack.mdc b/.cursor/rules/fullstack.mdc
index c98b51d..080100a 100644
--- a/.cursor/rules/fullstack.mdc
+++ b/.cursor/rules/fullstack.mdc
@@ -94,4 +94,5 @@ Key Principles
- After finishing the editing, build the project
- you have to pass from controller -> application -> repository, do not inject repository inside controllers
- dont use command line to edit file, use agent mode capabilities to do it
+ - when dividing, make sure variable is not zero
diff --git a/scripts/safe-migrate.sh b/scripts/safe-migrate.sh
index 5edf38d..6847c72 100755
--- a/scripts/safe-migrate.sh
+++ b/scripts/safe-migrate.sh
@@ -18,7 +18,7 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
LOGS_DIR="$SCRIPT_DIR/$LOGS_DIR_NAME"
mkdir -p "$LOGS_DIR" || { echo "Failed to create logs directory: $LOGS_DIR"; exit 1; }
-LOG_FILE="./logs/migration_${ENVIRONMENT}_${TIMESTAMP}.log"
+LOG_FILE="$SCRIPT_DIR/logs/migration_${ENVIRONMENT}_${TIMESTAMP}.log"
# Colors for output
RED='\033[0;31m'
@@ -155,6 +155,12 @@ extract_connection_details() {
log "đ Extracted connection details: $DB_HOST:$DB_PORT/$DB_NAME (user: $DB_USER, password: $DB_PASSWORD)"
}
+# Helper function to get the first migration name
+get_first_migration() {
+ local first_migration=$(cd "$DB_PROJECT_PATH" && dotnet ef migrations list --no-build --startup-project "$API_PROJECT_PATH" | head -1 | awk '{print $1}')
+ echo "$first_migration"
+}
+
# Helper function to test PostgreSQL connectivity
test_postgres_connectivity() {
if ! command -v psql >/dev/null 2>&1; then
@@ -243,13 +249,6 @@ else
error "â Failed to build Managing.Infrastructure.Database project"
fi
-log "đ§ Building Managing.Api project..."
-if (cd "$API_PROJECT_PATH" && dotnet build); then
- log "â
Managing.Api project built successfully"
-else
- error "â Failed to build Managing.Api project"
-fi
-
# Step 1: Check Database Connection and Create if Needed
log "đ§ Step 1: Checking database connection and creating database if needed..."
@@ -417,13 +416,23 @@ else
error " This is critical. Please review the previous error messages and your connection string for '$ENVIRONMENT'."
fi
-# Step 2: Create Backup
-log "đĻ Step 2: Creating database backup using pg_dump..."
+# Step 2: Create database backup (only if database exists)
+log "đĻ Step 2: Checking if database backup is needed..."
-# Define the actual backup file path (absolute)
-BACKUP_FILE="$BACKUP_DIR/managing_${ENVIRONMENT}_backup_${TIMESTAMP}.sql"
-# Backup file display path (relative to script execution)
-BACKUP_FILE_DISPLAY="$BACKUP_DIR_NAME/$ENVIRONMENT/managing_${ENVIRONMENT}_backup_${TIMESTAMP}.sql"
+# Check if the target database exists
+DB_EXISTS=false
+if PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "postgres" -c "SELECT 1 FROM pg_database WHERE datname='$DB_NAME';" 2>/dev/null | grep -q "1 row"; then
+ DB_EXISTS=true
+ log "â
Target database '$DB_NAME' exists - proceeding with backup"
+else
+ log "âšī¸ Target database '$DB_NAME' does not exist - skipping backup"
+fi
+
+if [ "$DB_EXISTS" = "true" ]; then
+ # Define the actual backup file path (absolute)
+ BACKUP_FILE="$BACKUP_DIR/managing_${ENVIRONMENT}_backup_${TIMESTAMP}.sql"
+ # Backup file display path (relative to script execution)
+ BACKUP_FILE_DISPLAY="$BACKUP_DIR_NAME/$ENVIRONMENT/managing_${ENVIRONMENT}_backup_${TIMESTAMP}.sql"
# Create backup with retry logic
BACKUP_SUCCESS=false
@@ -439,11 +448,84 @@ for attempt in 1 2 3; do
else
# If pg_dump fails, fall back to EF Core migration script
warn "â ī¸ pg_dump failed, falling back to EF Core migration script..."
+
+ # Get the first migration name to generate complete script
+ FIRST_MIGRATION=$(get_first_migration)
+
+ if [ -n "$FIRST_MIGRATION" ]; then
+ log "đ Generating complete backup script from initial migration: $FIRST_MIGRATION"
+ if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --from "$FIRST_MIGRATION" --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE"); then
+ log "â
Complete EF Core Migration SQL Script generated: $BACKUP_FILE_DISPLAY"
+ BACKUP_SUCCESS=true
+ break
+ else
+ # Try fallback without specifying from migration
+ ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE") 2>&1 || true)
+ if [ $attempt -lt 3 ]; then
+ warn "â ī¸ Backup attempt $attempt failed. Retrying in 5 seconds..."
+ warn " EF CLI Output: $ERROR_OUTPUT"
+ sleep 5
+ else
+ error "â Database backup failed after 3 attempts."
+ error " EF CLI Output: $ERROR_OUTPUT"
+ error " Migration aborted for safety reasons."
+ fi
+ fi
+ else
+ # Fallback: generate script without specifying from migration
+ if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE"); then
+ log "â
EF Core Migration SQL Script generated: $BACKUP_FILE_DISPLAY"
+ BACKUP_SUCCESS=true
+ break
+ else
+ # Try fallback without specifying from migration
+ ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE") 2>&1 || true)
+ if [ $attempt -lt 3 ]; then
+ warn "â ī¸ Backup attempt $attempt failed. Retrying in 5 seconds..."
+ warn " EF CLI Output: $ERROR_OUTPUT"
+ sleep 5
+ else
+ error "â Database backup failed after 3 attempts."
+ error " EF CLI Output: $ERROR_OUTPUT"
+ error " Migration aborted for safety reasons."
+ fi
+ fi
+ fi
+ fi
+ else
+ # If pg_dump is not available, use EF Core migration script
+ warn "â ī¸ pg_dump not available, using EF Core migration script for backup..."
+
+ # Get the first migration name to generate complete script
+ FIRST_MIGRATION=$(get_first_migration)
+
+ if [ -n "$FIRST_MIGRATION" ]; then
+ log "đ Generating complete backup script from initial migration: $FIRST_MIGRATION"
+ if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --from "$FIRST_MIGRATION" --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE"); then
+ log "â
Complete EF Core Migration SQL Script generated: $BACKUP_FILE_DISPLAY"
+ BACKUP_SUCCESS=true
+ break
+ else
+ # Try fallback without specifying from migration
+ ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE") 2>&1 || true)
+ if [ $attempt -lt 3 ]; then
+ warn "â ī¸ Backup attempt $attempt failed. Retrying in 5 seconds..."
+ warn " EF CLI Output: $ERROR_OUTPUT"
+ sleep 5
+ else
+ error "â Database backup failed after 3 attempts."
+ error " EF CLI Output: $ERROR_OUTPUT"
+ error " Migration aborted for safety reasons."
+ fi
+ fi
+ else
+ # Fallback: generate script without specifying from migration
if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE"); then
log "â
EF Core Migration SQL Script generated: $BACKUP_FILE_DISPLAY"
BACKUP_SUCCESS=true
break
else
+ # Try fallback without specifying from migration
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE") 2>&1 || true)
if [ $attempt -lt 3 ]; then
warn "â ī¸ Backup attempt $attempt failed. Retrying in 5 seconds..."
@@ -456,33 +538,69 @@ for attempt in 1 2 3; do
fi
fi
fi
- else
- # If pg_dump is not available, use EF Core migration script
- warn "â ī¸ pg_dump not available, using EF Core migration script for backup..."
- if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE"); then
- log "â
EF Core Migration SQL Script generated: $BACKUP_FILE_DISPLAY"
- BACKUP_SUCCESS=true
- break
- else
- ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE") 2>&1 || true)
- if [ $attempt -lt 3 ]; then
- warn "â ī¸ Backup attempt $attempt failed. Retrying in 5 seconds..."
- warn " EF CLI Output: $ERROR_OUTPUT"
- sleep 5
- else
- error "â Database backup failed after 3 attempts."
- error " EF CLI Output: $ERROR_OUTPUT"
- error " Migration aborted for safety reasons."
- fi
- fi
fi
done
-# Check if backup was successful before proceeding
-if [ "$BACKUP_SUCCESS" != "true" ]; then
- error "â Database backup failed. Migration aborted for safety."
- error " Cannot proceed with migration without a valid backup."
- error " Please resolve backup issues and try again."
+ # Check if backup was successful before proceeding
+ if [ "$BACKUP_SUCCESS" != "true" ]; then
+ error "â Database backup failed. Migration aborted for safety."
+ error " Cannot proceed with migration without a valid backup."
+ error " Please resolve backup issues and try again."
+ fi
+fi
+
+# Step 2.5: Check for pending model changes and create migrations if needed
+log "đ Step 2.5: Checking for pending model changes..."
+
+# Check if there are any pending model changes that need migrations
+PENDING_CHANGES_OUTPUT=$( (cd "$DB_PROJECT_PATH" && dotnet ef migrations add --dry-run --startup-project "$API_PROJECT_PATH" --name "PendingChanges_${TIMESTAMP}") 2>&1 || true )
+
+if echo "$PENDING_CHANGES_OUTPUT" | grep -q "No pending model changes"; then
+ log "â
No pending model changes detected - existing migrations are up to date"
+else
+ log "â ī¸ Pending model changes detected that require new migrations"
+ echo ""
+ echo "=========================================="
+ echo "đ PENDING MODEL CHANGES DETECTED"
+ echo "=========================================="
+ echo "The following changes require new migrations:"
+ echo "$PENDING_CHANGES_OUTPUT"
+ echo ""
+ echo "Would you like to create a new migration now?"
+ echo "=========================================="
+ echo ""
+
+ read -p "đ§ Create new migration? (y/n): " create_migration
+
+ if [[ "$create_migration" =~ ^[Yy]$ ]]; then
+ log "đ Creating new migration..."
+
+ # Get migration name from user
+ read -p "đ Enter migration name (or press Enter for auto-generated name): " migration_name
+ if [ -z "$migration_name" ]; then
+ migration_name="Migration_${TIMESTAMP}"
+ fi
+
+ # Create the migration
+ if (cd "$DB_PROJECT_PATH" && dotnet ef migrations add "$migration_name" --startup-project "$API_PROJECT_PATH"); then
+ log "â
Migration '$migration_name' created successfully"
+
+ # Show the created migration file
+ LATEST_MIGRATION=$(find "$DB_PROJECT_PATH/Migrations" -name "*${migration_name}.cs" | head -1)
+ if [ -n "$LATEST_MIGRATION" ]; then
+ log "đ Migration file created: $(basename "$LATEST_MIGRATION")"
+ log " Location: $LATEST_MIGRATION"
+ fi
+ else
+ ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && dotnet ef migrations add "$migration_name" --startup-project "$API_PROJECT_PATH") 2>&1 || true )
+ error "â Failed to create migration '$migration_name'"
+ error " EF CLI Output: $ERROR_OUTPUT"
+ error " Please resolve the model issues and try again."
+ fi
+ else
+ log "â ī¸ Skipping migration creation. Proceeding with existing migrations only."
+ log " Note: If there are pending changes, the migration may fail."
+ fi
fi
# Step 3: Run Migration (This effectively is a retry if previous "update" failed, or a final apply)
@@ -507,8 +625,88 @@ fi
# Generate migration script first (Microsoft recommended approach)
MIGRATION_SCRIPT="$BACKUP_DIR/migration_${ENVIRONMENT}_${TIMESTAMP}.sql"
log "đ Step 3b: Generating migration script for pending migrations..."
-if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT"); then
- log "â
Migration script generated: $(basename "$MIGRATION_SCRIPT")"
+
+# Check if database is empty (no tables) to determine the best approach
+log "đ Checking if database has existing tables..."
+DB_HAS_TABLES=false
+if command -v psql >/dev/null 2>&1; then
+ TABLE_COUNT=$(PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public';" 2>/dev/null | tr -d ' ' || echo "0")
+ if [ "$TABLE_COUNT" -gt 0 ]; then
+ DB_HAS_TABLES=true
+ log "â
Database has $TABLE_COUNT existing tables - using idempotent script generation"
+ else
+ log "â ī¸ Database appears to be empty - using full migration script generation"
+ fi
+else
+ log "â ī¸ psql not available - assuming database has tables and using idempotent script generation"
+ DB_HAS_TABLES=true
+fi
+
+# Generate migration script based on database state
+if [ "$DB_HAS_TABLES" = "true" ]; then
+ # For databases with existing tables, we need to generate a complete script
+ # that includes all migrations from the beginning
+ log "đ Generating complete migration script from initial migration..."
+
+ # Get the first migration name to generate script from the beginning
+ FIRST_MIGRATION=$(get_first_migration)
+
+ if [ -n "$FIRST_MIGRATION" ]; then
+ log "đ Generating complete script for all migrations (idempotent)..."
+ if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT"); then
+ log "â
Complete migration script generated (all migrations, idempotent): $(basename "$MIGRATION_SCRIPT")"
+ else
+ ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT") 2>&1 || true )
+ error "â Failed to generate complete migration script."
+ error " EF CLI Output: $ERROR_OUTPUT"
+ error " Check the .NET project logs for detailed errors."
+ error " Backup script available at: $BACKUP_FILE_DISPLAY"
+ fi
+ else
+ # Fallback: generate script without specifying from migration
+ log "đ Fallback: Generating migration script without specifying from migration..."
+ if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT"); then
+ log "â
Migration script generated (idempotent): $(basename "$MIGRATION_SCRIPT")"
+ else
+ ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT") 2>&1 || true )
+ error "â Failed to generate idempotent migration script."
+ error " EF CLI Output: $ERROR_OUTPUT"
+ error " Check the .NET project logs for detailed errors."
+ error " Backup script available at: $BACKUP_FILE_DISPLAY"
+ fi
+ fi
+else
+ # Use full script generation for empty databases (generate script from the very beginning)
+ log "đ Generating full migration script for empty database..."
+
+ # Get the first migration name to generate script from the beginning
+ FIRST_MIGRATION=$(get_first_migration)
+
+ if [ -n "$FIRST_MIGRATION" ]; then
+ log "đ Generating complete script for all migrations..."
+ if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT"); then
+ log "â
Complete migration script generated (all migrations): $(basename "$MIGRATION_SCRIPT")"
+ else
+ ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT") 2>&1 || true )
+ error "â Failed to generate complete migration script."
+ error " EF CLI Output: $ERROR_OUTPUT"
+ error " Check the .NET project logs for detailed errors."
+ error " Backup script available at: $BACKUP_FILE_DISPLAY"
+ fi
+ else
+ # Fallback: generate script without specifying from migration
+ log "đ Fallback: Generating migration script without specifying from migration..."
+ if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT"); then
+ log "â
Migration script generated (fallback): $(basename "$MIGRATION_SCRIPT")"
+ else
+ ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT") 2>&1 || true )
+ error "â Failed to generate fallback migration script."
+ error " EF CLI Output: $ERROR_OUTPUT"
+ error " Check the .NET project logs for detailed errors."
+ error " Backup script available at: $BACKUP_FILE_DISPLAY"
+ fi
+ fi
+fi
# Show the migration script path to the user for review
echo ""
@@ -519,8 +717,26 @@ if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef mig
echo "Environment: $ENVIRONMENT"
echo "Database: $DB_HOST:$DB_PORT/$DB_NAME"
echo ""
+
+ # Show a preview of the migration script content
+ if [ -f "$MIGRATION_SCRIPT" ]; then
+ SCRIPT_SIZE=$(wc -l < "$MIGRATION_SCRIPT")
+ echo "đ Migration script contains $SCRIPT_SIZE lines"
+
+ # Show first 20 lines as preview
+ echo ""
+ echo "đ PREVIEW (first 20 lines):"
+ echo "----------------------------------------"
+ head -20 "$MIGRATION_SCRIPT" | sed 's/^/ /'
+ if [ "$SCRIPT_SIZE" -gt 20 ]; then
+ echo " ... (showing first 20 lines of $SCRIPT_SIZE total)"
+ fi
+ echo "----------------------------------------"
+ echo ""
+ fi
+
echo "â ī¸ IMPORTANT: Please review the migration script before proceeding!"
- echo " You can examine the script with: cat $MIGRATION_SCRIPT"
+ echo " You can examine the full script with: cat $MIGRATION_SCRIPT"
echo " Or open it in your editor to review the changes."
echo ""
echo "=========================================="
@@ -564,16 +780,15 @@ if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef mig
fi
fi
- # Clean up migration script after successful application
- rm -f "$MIGRATION_SCRIPT"
+ # Save a copy of the migration script for reference before cleaning up
+ MIGRATION_SCRIPT_COPY="$BACKUP_DIR/migration_${ENVIRONMENT}_${TIMESTAMP}_applied.sql"
+ if [ -f "$MIGRATION_SCRIPT" ]; then
+ cp "$MIGRATION_SCRIPT" "$MIGRATION_SCRIPT_COPY"
+ log "đ Migration script saved for reference: $(basename "$MIGRATION_SCRIPT_COPY")"
+ fi
-else
- ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT") 2>&1 || true )
- error "â Failed to generate migration script."
- error " EF CLI Output: $ERROR_OUTPUT"
- error " Check the .NET project logs for detailed errors."
- error " Backup script available at: $BACKUP_FILE_DISPLAY"
-fi
+ # Clean up temporary migration script after successful application
+ rm -f "$MIGRATION_SCRIPT"
# Step 4: Verify Migration
log "đ Step 4: Verifying migration status..."
diff --git a/src/Managing.Api.Workers/appsettings.Oda.json b/src/Managing.Api.Workers/appsettings.Oda.json
index 472643b..5b499a3 100644
--- a/src/Managing.Api.Workers/appsettings.Oda.json
+++ b/src/Managing.Api.Workers/appsettings.Oda.json
@@ -1,4 +1,8 @@
{
+ "PostgreSql": {
+ "ConnectionString": "Host=localhost;Port=5432;Database=managing;Username=postgres;Password=postgres",
+ "Orleans": "Host=localhost;Port=5432;Database=orleans;Username=postgres;Password=postgres"
+ },
"InfluxDb": {
"Url": "http://localhost:8086/",
"Organization": "managing-org",
diff --git a/src/Managing.Api.Workers/appsettings.Production.json b/src/Managing.Api.Workers/appsettings.Production.json
index 4f0fcb8..8d961ee 100644
--- a/src/Managing.Api.Workers/appsettings.Production.json
+++ b/src/Managing.Api.Workers/appsettings.Production.json
@@ -1,4 +1,8 @@
{
+ "PostgreSql": {
+ "ConnectionString": "Host=managing-postgre.apps.managing.live;Port=5432;Database=managing;Username=postgres;Password=29032b13a5bc4d37",
+ "Orleans": "Host=managing-postgre.apps.managing.live;Port=5432;Database=orleans;Username=postgres;Password=29032b13a5bc4d37"
+ },
"InfluxDb": {
"Url": "https://influx-db.apps.managing.live",
"Organization": "managing-org",
diff --git a/src/Managing.Api/Authorization/JwtMiddleware.cs b/src/Managing.Api/Authorization/JwtMiddleware.cs
index cc01bfd..18617e6 100644
--- a/src/Managing.Api/Authorization/JwtMiddleware.cs
+++ b/src/Managing.Api/Authorization/JwtMiddleware.cs
@@ -2,7 +2,6 @@
namespace Managing.Api.Authorization;
-
public class JwtMiddleware
{
private readonly RequestDelegate _next;
@@ -14,7 +13,21 @@ public class JwtMiddleware
public async Task Invoke(HttpContext context, IUserService userService, IJwtUtils jwtUtils)
{
+ if (context.Request.Path.StartsWithSegments("/User/create-token") ||
+ context.Request.Path.StartsWithSegments("/swagger") ||
+ context.Request.Path.StartsWithSegments("/health"))
+ {
+ await _next(context);
+ return;
+ }
+
var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
+
+ if (string.IsNullOrEmpty(token))
+ {
+ throw new UnauthorizedAccessException("Authorization token is missing");
+ }
+
var userId = jwtUtils.ValidateJwtToken(token);
if (userId != null)
{
diff --git a/src/Managing.Api/Controllers/BacktestController.cs b/src/Managing.Api/Controllers/BacktestController.cs
index 62dd49c..6ddbb92 100644
--- a/src/Managing.Api/Controllers/BacktestController.cs
+++ b/src/Managing.Api/Controllers/BacktestController.cs
@@ -340,7 +340,7 @@ public class BacktestController : BaseController
// Convert IndicatorRequest objects to Indicator domain objects
foreach (var indicatorRequest in request.Config.Scenario.Indicators)
{
- var indicator = new Indicator(indicatorRequest.Name, indicatorRequest.Type)
+ var indicator = new IndicatorBase(indicatorRequest.Name, indicatorRequest.Type)
{
SignalType = indicatorRequest.SignalType,
MinimumHistory = indicatorRequest.MinimumHistory,
@@ -706,7 +706,6 @@ public class BacktestController : BaseController
}
-
public MoneyManagement Map(MoneyManagementRequest moneyManagementRequest)
{
return new MoneyManagement
diff --git a/src/Managing.Api/Controllers/BotController.cs b/src/Managing.Api/Controllers/BotController.cs
index 6cfa921..105e210 100644
--- a/src/Managing.Api/Controllers/BotController.cs
+++ b/src/Managing.Api/Controllers/BotController.cs
@@ -5,12 +5,15 @@ using Managing.Application.Abstractions.Services;
using Managing.Application.Hubs;
using Managing.Application.ManageBot.Commands;
using Managing.Common;
+using Managing.Core;
+using Managing.Domain.Accounts;
using Managing.Domain.Backtests;
using Managing.Domain.Bots;
using Managing.Domain.MoneyManagements;
using Managing.Domain.Scenarios;
using Managing.Domain.Strategies;
using Managing.Domain.Trades;
+using Managing.Domain.Users;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@@ -36,6 +39,7 @@ public class BotController : BaseController
private readonly IBotService _botService;
private readonly IAccountService _accountService;
private readonly IMoneyManagementService _moneyManagementService;
+ private readonly IServiceScopeFactory _scopeFactory;
///
/// Initializes a new instance of the class.
@@ -44,9 +48,15 @@ public class BotController : BaseController
/// Mediator for handling commands and requests.
/// SignalR hub context for real-time communication.
/// Backtester for running backtests on bots.
+ ///
+ ///
+ ///
+ ///
+ ///
public BotController(ILogger logger, IMediator mediator, IHubContext hubContext,
IBacktester backtester, IBotService botService, IUserService userService,
- IAccountService accountService, IMoneyManagementService moneyManagementService) : base(userService)
+ IAccountService accountService, IMoneyManagementService moneyManagementService,
+ IServiceScopeFactory scopeFactory) : base(userService)
{
_logger = logger;
_mediator = mediator;
@@ -55,6 +65,7 @@ public class BotController : BaseController
_botService = botService;
_accountService = accountService;
_moneyManagementService = moneyManagementService;
+ _scopeFactory = scopeFactory;
}
///
@@ -63,7 +74,7 @@ public class BotController : BaseController
/// The identifier of the bot to check
/// Optional account name to check when creating a new bot
/// True if the user owns the account, False otherwise
- private async Task UserOwnsBotAccount(string identifier, string accountName = null)
+ private async Task UserOwnsBotAccount(Guid identifier, string accountName = null)
{
try
{
@@ -71,25 +82,30 @@ public class BotController : BaseController
if (user == null)
return false;
+
+ if (identifier != default)
+ {
+ // For existing bots, check if the user owns the bot's account
+ var bot = await _botService.GetBotByIdentifier(identifier);
+
+ if (bot != null)
+ {
+ return bot.User != null && bot.User.Name == user.Name;
+ }
+ }
+
+
// For new bot creation, check if the user owns the account provided in the request
if (!string.IsNullOrEmpty(accountName))
{
- var accountService = HttpContext.RequestServices.GetRequiredService();
- var account = await accountService.GetAccount(accountName, true, false);
+ var account = await ServiceScopeHelpers.WithScopedService(_scopeFactory,
+ async accountService => { return await accountService.GetAccount(accountName, true, false); });
+
// Compare the user names
return account != null && account.User != null && account.User.Name == user.Name;
}
- // For existing bots, check if the user owns the bot's account
- var activeBots = _botService.GetActiveBots();
- var bot = activeBots.FirstOrDefault(b => b.Identifier == identifier);
- if (bot == null)
- return true; // Bot doesn't exist yet, so no ownership conflict
-
- var botAccountService = HttpContext.RequestServices.GetRequiredService();
- var botAccount = await botAccountService.GetAccount(bot.Config.AccountName, true, false);
- // Compare the user names
- return botAccount != null && botAccount.User != null && botAccount.User.Name == user.Name;
+ return false;
}
catch (Exception ex)
{
@@ -109,138 +125,9 @@ public class BotController : BaseController
{
try
{
- if (request.Config == null)
- {
- return BadRequest("Bot configuration is required");
- }
+ var (config, user) = await ValidateAndPrepareBotRequest(request, "start");
- // Check if user owns the account specified in the request
- if (!await UserOwnsBotAccount(null, request.Config.AccountName))
- {
- return Forbid("You don't have permission to start a bot with this account");
- }
-
- // Validate that either money management name or object is provided
- if (string.IsNullOrEmpty(request.Config.MoneyManagementName) && request.Config.MoneyManagement == null)
- {
- return BadRequest("Either money management name or money management object is required");
- }
-
- var user = await GetUser();
-
- if (string.IsNullOrEmpty(user.AgentName))
- {
- return BadRequest(
- "Agent name is required to start a bot. Please configure your agent name in the user profile.");
- }
-
- // Get money management - either by name lookup or use provided object
- LightMoneyManagement moneyManagement;
- if (!string.IsNullOrEmpty(request.Config.MoneyManagementName))
- {
- moneyManagement =
- await _moneyManagementService.GetMoneyMangement(user, request.Config.MoneyManagementName);
- if (moneyManagement == null)
- {
- return BadRequest("Money management not found");
- }
- }
- else
- {
- moneyManagement = Map(request.Config.MoneyManagement);
- // Format percentage values if using custom money management
- moneyManagement?.FormatPercentage();
- }
-
- // Validate initialTradingBalance
- if (request.Config.BotTradingBalance <= Constants.GMX.Config.MinimumPositionAmount)
- {
- return BadRequest(
- $"Initial trading balance must be greater than {Constants.GMX.Config.MinimumPositionAmount}");
- }
-
- // Validate cooldown period
- if (request.Config.CooldownPeriod < 1)
- {
- return BadRequest("Cooldown period must be at least 1 candle");
- }
-
- // Validate max loss streak
- if (request.Config.MaxLossStreak < 0)
- {
- return BadRequest("Max loss streak cannot be negative");
- }
-
- // Validate max position time hours
- if (request.Config.MaxPositionTimeHours.HasValue && request.Config.MaxPositionTimeHours.Value <= 0)
- {
- return BadRequest("Max position time hours must be greater than 0 if specified");
- }
-
- // Validate CloseEarlyWhenProfitable consistency
- if (request.Config.CloseEarlyWhenProfitable && !request.Config.MaxPositionTimeHours.HasValue)
- {
- return BadRequest("CloseEarlyWhenProfitable can only be enabled when MaxPositionTimeHours is set");
- }
-
- // Handle scenario - either from ScenarioRequest or ScenarioName
- Scenario scenario = null;
- if (request.Config.Scenario != null)
- {
- // Convert ScenarioRequest to Scenario domain object
- scenario = new Scenario(request.Config.Scenario.Name, request.Config.Scenario.LoopbackPeriod)
- {
- User = user
- };
-
- // Convert IndicatorRequest objects to Indicator domain objects
- foreach (var indicatorRequest in request.Config.Scenario.Indicators)
- {
- var indicator = new Indicator(indicatorRequest.Name, indicatorRequest.Type)
- {
- SignalType = indicatorRequest.SignalType,
- MinimumHistory = indicatorRequest.MinimumHistory,
- Period = indicatorRequest.Period,
- FastPeriods = indicatorRequest.FastPeriods,
- SlowPeriods = indicatorRequest.SlowPeriods,
- SignalPeriods = indicatorRequest.SignalPeriods,
- Multiplier = indicatorRequest.Multiplier,
- SmoothPeriods = indicatorRequest.SmoothPeriods,
- StochPeriods = indicatorRequest.StochPeriods,
- CyclePeriods = indicatorRequest.CyclePeriods,
- User = user
- };
- scenario.AddIndicator(indicator);
- }
- }
-
- // Map the request to the full TradingBotConfig
- var config = new TradingBotConfig
- {
- AccountName = request.Config.AccountName,
- MoneyManagement = moneyManagement,
- Ticker = request.Config.Ticker,
- Scenario = LightScenario.FromScenario(scenario), // Convert to LightScenario for Orleans
- ScenarioName = request.Config.ScenarioName, // Fallback to scenario name if scenario object not provided
- Timeframe = request.Config.Timeframe,
- IsForWatchingOnly = request.Config.IsForWatchingOnly,
- BotTradingBalance = request.Config.BotTradingBalance,
- CooldownPeriod = request.Config.CooldownPeriod,
- MaxLossStreak = request.Config.MaxLossStreak,
- MaxPositionTimeHours = request.Config.MaxPositionTimeHours,
- FlipOnlyWhenInProfit = request.Config.FlipOnlyWhenInProfit,
- CloseEarlyWhenProfitable = request.Config.CloseEarlyWhenProfitable,
- UseSynthApi = request.Config.UseSynthApi,
- UseForPositionSizing = request.Config.UseForPositionSizing,
- UseForSignalFiltering = request.Config.UseForSignalFiltering,
- UseForDynamicStopLoss = request.Config.UseForDynamicStopLoss,
- // Set computed/default properties
- IsForBacktest = false,
- FlipPosition = request.Config.FlipPosition,
- Name = request.Config.Name
- };
-
- var result = await _mediator.Send(new StartBotCommand(config, request.Config.Name, user));
+ var result = await _mediator.Send(new StartBotCommand(config, user, false)); // createOnly = false
await NotifyBotSubscriberAsync();
return Ok(result);
@@ -252,6 +139,30 @@ public class BotController : BaseController
}
}
+ ///
+ /// Saves a bot configuration without starting it.
+ ///
+ /// The request containing bot configuration parameters.
+ /// A string indicating the result of the save operation.
+ [HttpPost]
+ [Route("Save")]
+ public async Task> Save(SaveBotRequest request)
+ {
+ try
+ {
+ var (config, user) = await ValidateAndPrepareBotRequest(request, "save");
+
+ var result = await _mediator.Send(new StartBotCommand(config, user, true)); // createOnly = true
+
+ return Ok(result);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error saving bot");
+ return StatusCode(500, $"Error saving bot: {ex.Message}");
+ }
+ }
+
///
/// Stops a bot specified by type and name.
@@ -260,7 +171,7 @@ public class BotController : BaseController
/// A string indicating the result of the stop operation.
[HttpGet]
[Route("Stop")]
- public async Task> Stop(string identifier)
+ public async Task> Stop(Guid identifier)
{
try
{
@@ -291,7 +202,7 @@ public class BotController : BaseController
/// A boolean indicating the result of the delete operation.
[HttpDelete]
[Route("Delete")]
- public async Task> Delete(string identifier)
+ public async Task> Delete(Guid identifier)
{
try
{
@@ -312,60 +223,15 @@ public class BotController : BaseController
}
}
- ///
- /// Stops all active bots.
- ///
- /// A string summarizing the results of the stop operations for all bots.
- [HttpPost("stop-all")]
- public async Task StopAll()
- {
- // This method should be restricted to only stop bots owned by the current user
- var user = await GetUser();
- if (user == null)
- return "No authenticated user found";
-
- try
- {
- var bots = await GetBotList();
- // Filter to only include bots owned by the current user
- var userBots = new List();
-
- foreach (var bot in bots)
- {
- var account = await _accountService.GetAccount(bot.Config.AccountName, true, false);
- // Compare the user names
- if (account != null && account.User != null && account.User.Name == user.Name)
- {
- userBots.Add(bot);
- }
- }
-
- foreach (var bot in userBots)
- {
- await _mediator.Send(new StopBotCommand(bot.Identifier));
- await _hubContext.Clients.All.SendAsync("SendNotification",
- $"Bot {bot.Identifier} paused by {user.Name}.", "Info");
- }
-
- await NotifyBotSubscriberAsync();
- return "All your bots have been stopped successfully!";
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error stopping all bots");
- return $"Error stopping bots: {ex.Message}";
- }
- }
///
/// Restarts a bot specified by type and name.
///
- /// The type of the bot to restart.
/// The identifier of the bot to restart.
/// A string indicating the result of the restart operation.
[HttpGet]
[Route("Restart")]
- public async Task> Restart(BotType botType, string identifier)
+ public async Task> Restart(Guid identifier)
{
try
{
@@ -375,10 +241,8 @@ public class BotController : BaseController
return Forbid("You don't have permission to restart this bot");
}
- var result = await _mediator.Send(new RestartBotCommand(botType, identifier));
- _logger.LogInformation($"{botType} type with identifier {identifier} is now {result}");
-
- await NotifyBotSubscriberAsync();
+ var result = await _mediator.Send(new RestartBotCommand(identifier));
+ _logger.LogInformation($"Bot with identifier {identifier} is now {result}");
return Ok(result);
}
@@ -389,92 +253,6 @@ public class BotController : BaseController
}
}
- ///
- /// Restarts all active bots.
- ///
- /// A string summarizing the results of the restart operations for all bots.
- [HttpPost("restart-all")]
- public async Task RestartAll()
- {
- var user = await GetUser();
- if (user == null)
- return "No authenticated user found";
-
- try
- {
- var bots = await GetBotList();
- // Filter to only include bots owned by the current user
- var userBots = new List();
- var accountService = HttpContext.RequestServices.GetRequiredService();
-
- foreach (var bot in bots)
- {
- var account = await accountService.GetAccount(bot.Config.AccountName, true, false);
- // Compare the user names
- if (account != null && account.User != null && account.User.Name == user.Name)
- {
- userBots.Add(bot);
- }
- }
-
- foreach (var bot in userBots)
- {
- // We can't directly restart a bot with just BotType and Name
- // Instead, stop the bot and then retrieve the backup to start it again
- await _mediator.Send(
- new StopBotCommand(bot.Identifier));
-
- // Get the saved bot backup
- var backup = await _botService.GetBotBackup(bot.Identifier);
- if (backup != null)
- {
- _botService.StartBotFromBackup(backup);
- await _hubContext.Clients.All.SendAsync("SendNotification",
- $"Bot {bot.Identifier} restarted by {user.Name}.", "Info");
- }
- }
-
- await NotifyBotSubscriberAsync();
- return "All your bots have been restarted successfully!";
- }
- catch (Exception e)
- {
- _logger.LogError(e, "Failed to restart all bots");
- return $"Error restarting bots: {e.Message}";
- }
- }
-
- ///
- /// Toggles the watching status of a bot specified by name.
- ///
- /// The identifier of the bot to toggle watching status.
- /// A string indicating the new watching status of the bot.
- [HttpGet]
- [Route("ToggleIsForWatching")]
- public async Task> ToggleIsForWatching(string identifier)
- {
- try
- {
- // Check if user owns the account
- if (!await UserOwnsBotAccount(identifier))
- {
- return Forbid("You don't have permission to modify this bot");
- }
-
- var result = await _mediator.Send(new ToggleIsForWatchingCommand(identifier));
- _logger.LogInformation($"Bot with identifier {identifier} is now {result}");
-
- await NotifyBotSubscriberAsync();
-
- return Ok(result);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error toggling bot watching status");
- return StatusCode(500, $"Error toggling bot watching status: {ex.Message}");
- }
- }
-
///
/// Retrieves a list of active bots.
///
@@ -482,33 +260,165 @@ public class BotController : BaseController
[HttpGet]
public async Task> GetActiveBots()
{
- return await GetBotList();
+ return await GetBotsByStatusAsync(BotStatus.Up);
}
///
- /// Retrieves a list of active bots by sending a command to the mediator.
+ /// Retrieves a list of bots by status.
///
- /// A list of trading bots.
- private async Task> GetBotList()
+ /// The status to filter bots by (None, Down, Up)
+ /// A list of trading bots with the specified status.
+ [HttpGet]
+ [Route("ByStatus/{status}")]
+ public async Task> GetBotsByStatus(BotStatus status)
+ {
+ return await GetBotsByStatusAsync(status);
+ }
+
+ ///
+ /// Retrieves a list of saved bots (status None) for the current user.
+ ///
+ /// A list of saved trading bots for the current user.
+ [HttpGet]
+ [Route("GetMySavedBots")]
+ public async Task> GetMySavedBots()
+ {
+ try
+ {
+ var user = await GetUser();
+ if (user == null)
+ {
+ return new List();
+ }
+
+ var result = await _mediator.Send(new GetBotsByUserAndStatusCommand(user.Id, BotStatus.None));
+ return MapBotsToTradingBotResponse(result);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error getting saved bots for user");
+ return new List();
+ }
+ }
+
+ ///
+ /// Retrieves a paginated list of bots with filtering and sorting capabilities.
+ ///
+ /// Page number (1-based). Default is 1.
+ /// Number of items per page. Default is 10, maximum is 100.
+ /// Filter by bot status. If null, returns bots of all statuses.
+ /// Filter by bot name (partial match, case-insensitive). If null, no name filtering is applied.
+ /// Filter by ticker (partial match, case-insensitive). If null, no ticker filtering is applied.
+ /// Filter by agent name (partial match, case-insensitive). If null, no agent name filtering is applied.
+ /// Sort field. Valid values: "Name", "Ticker", "Status", "CreateDate", "StartupTime", "Pnl", "WinRate", "AgentName". Default is "CreateDate".
+ /// Sort direction. Default is "Desc".
+ /// A paginated response containing trading bots
+ [HttpGet]
+ [Route("Paginated")]
+ public async Task> GetBotsPaginated(
+ int pageNumber = 1,
+ int pageSize = 10,
+ BotStatus? status = null,
+ string? name = null,
+ string? ticker = null,
+ string? agentName = null,
+ string sortBy = "CreateDate",
+ string sortDirection = "Desc")
+ {
+ try
+ {
+ // Validate parameters
+ if (pageNumber < 1)
+ {
+ pageNumber = 1;
+ }
+
+ if (pageSize < 1 || pageSize > 100)
+ {
+ pageSize = Math.Min(Math.Max(pageSize, 1), 100);
+ }
+
+ // Get paginated bots from service
+ var (bots, totalCount) = await _botService.GetBotsPaginatedAsync(
+ pageNumber,
+ pageSize,
+ status,
+ name,
+ ticker,
+ agentName,
+ sortBy,
+ sortDirection);
+
+ // Map to response objects
+ var tradingBotResponses = MapBotsToTradingBotResponse(bots);
+
+ // Calculate pagination metadata
+ var totalPages = (int)Math.Ceiling((double)totalCount / pageSize);
+ var hasPreviousPage = pageNumber > 1;
+ var hasNextPage = pageNumber < totalPages;
+
+ return new PaginatedResponse
+ {
+ Items = tradingBotResponses,
+ TotalCount = totalCount,
+ PageNumber = pageNumber,
+ PageSize = pageSize,
+ TotalPages = totalPages,
+ HasPreviousPage = hasPreviousPage,
+ HasNextPage = hasNextPage
+ };
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error getting paginated bots");
+ return new PaginatedResponse
+ {
+ Items = new List(),
+ TotalCount = 0,
+ PageNumber = pageNumber,
+ PageSize = pageSize,
+ TotalPages = 0,
+ HasPreviousPage = false,
+ HasNextPage = false
+ };
+ }
+ }
+
+ ///
+ /// Retrieves a list of bots by status by sending a command to the mediator.
+ ///
+ /// The status to filter bots by
+ /// A list of trading bots.
+ private async Task> GetBotsByStatusAsync(BotStatus status)
+ {
+ var result = await _mediator.Send(new GetBotsByStatusCommand(status));
+ return MapBotsToTradingBotResponse(result);
+ }
+
+ ///
+ /// Maps a collection of Bot entities to TradingBotResponse objects.
+ ///
+ /// The collection of bots to map
+ /// A list of TradingBotResponse objects
+ private static List MapBotsToTradingBotResponse(IEnumerable bots)
{
- var result = await _mediator.Send(new GetActiveBotsCommand());
var list = new List();
- foreach (var item in result)
+ foreach (var item in bots)
{
list.Add(new TradingBotResponse
{
- Status = item.GetStatus(),
- Signals = item.Signals.ToList(),
- Positions = item.Positions,
- Candles = item.Candles.DistinctBy(c => c.Date).ToList(),
- WinRate = item.GetWinRate(),
- ProfitAndLoss = item.GetProfitAndLoss(),
- Identifier = item.Identifier,
+ Status = item.Status.ToString(),
+ WinRate = (item.TradeWins + item.TradeLosses) != 0
+ ? item.TradeWins / (item.TradeWins + item.TradeLosses)
+ : 0,
+ ProfitAndLoss = item.Pnl,
+ Identifier = item.Identifier.ToString(),
AgentName = item.User.AgentName,
- Config = item.Config,
CreateDate = item.CreateDate,
- StartupTime = item.StartupTime
+ StartupTime = item.StartupTime,
+ Name = item.Name,
+ Ticker = item.Ticker,
});
}
@@ -520,7 +430,7 @@ public class BotController : BaseController
///
private async Task NotifyBotSubscriberAsync()
{
- var botsList = await GetBotList();
+ var botsList = await GetBotsByStatusAsync(BotStatus.Up);
await _hubContext.Clients.All.SendAsync("BotsSubscription", botsList);
}
@@ -541,22 +451,19 @@ public class BotController : BaseController
return Forbid("You don't have permission to open positions for this bot");
}
- var activeBots = _botService.GetActiveBots();
- var bot = activeBots.FirstOrDefault(b => b.Identifier == request.Identifier);
+ var bot = await _botService.GetBotByIdentifier(request.Identifier);
if (bot == null)
{
return NotFound($"Bot with identifier {request.Identifier} not found or is not a trading bot");
}
- if (bot.GetStatus() != BotStatus.Up.ToString())
+ if (bot.Status != BotStatus.Up)
{
return BadRequest($"Bot with identifier {request.Identifier} is not running");
}
- var position = await bot.OpenPositionManually(
- request.Direction
- );
+ var position = await _botService.OpenPositionManuallyAsync(request.Identifier, request.Direction);
await NotifyBotSubscriberAsync();
return Ok(position);
@@ -586,45 +493,13 @@ public class BotController : BaseController
return Forbid("You don't have permission to close positions for this bot");
}
- var activeBots = _botService.GetActiveBots();
- var bot = activeBots.FirstOrDefault(b => b.Identifier == request.Identifier);
-
- if (bot == null)
- {
- return NotFound($"Bot with identifier {request.Identifier} not found or is not a trading bot");
- }
-
- if (bot.GetStatus() != BotStatus.Up.ToString())
- {
- return BadRequest($"Bot with identifier {request.Identifier} is not running");
- }
-
- // Find the position to close
- var position = bot.Positions.FirstOrDefault(p => p.Identifier == request.PositionId);
+ var position = await _botService.ClosePositionAsync(request.Identifier, request.PositionId);
if (position == null)
{
return NotFound(
$"Position with ID {request.PositionId} not found for bot with identifier {request.Identifier}");
}
- // Find the signal associated with this position
- var signal = bot.Signals.FirstOrDefault(s => s.Identifier == position.SignalIdentifier);
- if (signal == null)
- {
- return NotFound($"Signal not found for position {request.PositionId}");
- }
-
- // Get current price
- var lastCandle = bot.OptimizedCandles.LastOrDefault();
- if (lastCandle == null)
- {
- return BadRequest("Cannot get current price to close position");
- }
-
- // Close the position at market price
- await bot.CloseTrade(signal, position, position.Open, lastCandle.Close, true);
-
- await NotifyBotSubscriberAsync();
return Ok(position);
}
catch (Exception ex)
@@ -634,6 +509,38 @@ public class BotController : BaseController
}
}
+ ///
+ /// Retrieves the configuration of an existing bot.
+ ///
+ /// The identifier of the bot to get configuration for
+ /// The bot configuration
+ [HttpGet]
+ [Route("GetConfig/{identifier}")]
+ public async Task> GetBotConfig(Guid identifier)
+ {
+ try
+ {
+ // Check if user owns the account
+ if (!await UserOwnsBotAccount(identifier))
+ {
+ return Forbid("You don't have permission to view this bot's configuration");
+ }
+
+ var config = await _botService.GetBotConfig(identifier);
+ if (config == null)
+ {
+ return NotFound($"Bot with identifier {identifier} not found");
+ }
+
+ return Ok(config);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error getting bot configuration for identifier {Identifier}", identifier);
+ return StatusCode(500, $"Error getting bot configuration: {ex.Message}");
+ }
+ }
+
///
/// Updates the configuration of an existing bot.
///
@@ -651,7 +558,7 @@ public class BotController : BaseController
return Unauthorized("User not found");
}
- if (string.IsNullOrEmpty(request.Identifier))
+ if (request.Identifier == Guid.Empty)
{
return BadRequest("Bot identifier is required");
}
@@ -668,37 +575,24 @@ public class BotController : BaseController
}
// Get the existing bot to ensure it exists and get current config
- var bots = _botService.GetActiveBots();
- var existingBot = bots.FirstOrDefault(b => b.Identifier == request.Identifier);
+ var existingBot = await _botService.GetBotByIdentifier(request.Identifier);
if (existingBot == null)
{
return NotFound($"Bot with identifier '{request.Identifier}' not found");
}
+ var config = await _botService.GetBotConfig(request.Identifier);
+
// If the account is being changed, verify the user owns the new account too
- if (existingBot.Config.AccountName != request.Config.AccountName)
+ if (config.AccountName != request.Config.AccountName)
{
- if (!await UserOwnsBotAccount(null, request.Config.AccountName))
+ if (!await UserOwnsBotAccount(request.Identifier, request.Config.AccountName))
{
return Forbid("You don't have permission to use this account");
}
}
- // If the bot name is being changed, check for conflicts
- var isNameChanging = !string.IsNullOrEmpty(request.Config.Name) &&
- request.Config.Name != request.Identifier;
-
- if (isNameChanging)
- {
- // Check if new name already exists
- var existingBotWithNewName = bots.FirstOrDefault(b => b.Identifier == request.Config.Name);
- if (existingBotWithNewName != null)
- {
- return BadRequest($"A bot with the name '{request.Config.Name}' already exists");
- }
- }
-
// Validate and get the money management
LightMoneyManagement moneyManagement = null;
if (!string.IsNullOrEmpty(request.MoneyManagementName))
@@ -736,7 +630,7 @@ public class BotController : BaseController
else
{
// Use existing bot's money management if no new one is provided
- moneyManagement = existingBot.Config.MoneyManagement;
+ moneyManagement = config.MoneyManagement;
}
// Validate CloseEarlyWhenProfitable requires MaxPositionTimeHours
@@ -758,7 +652,7 @@ public class BotController : BaseController
// Convert IndicatorRequest objects to Indicator domain objects
foreach (var indicatorRequest in request.Config.Scenario.Indicators)
{
- var indicator = new Indicator(indicatorRequest.Name, indicatorRequest.Type)
+ var indicator = new IndicatorBase(indicatorRequest.Name, indicatorRequest.Type)
{
SignalType = indicatorRequest.SignalType,
MinimumHistory = indicatorRequest.MinimumHistory,
@@ -802,27 +696,15 @@ public class BotController : BaseController
Name = request.Config.Name
};
- // Update the bot configuration using the enhanced method
var success = await _botService.UpdateBotConfiguration(request.Identifier, updatedConfig);
if (success)
{
- var finalBotName = isNameChanging ? request.Config.Name : request.Identifier;
-
- await _hubContext.Clients.All.SendAsync("SendNotification",
- $"Bot {finalBotName} configuration updated successfully by {user.Name}." +
- (isNameChanging ? $" (renamed from {request.Identifier})" : ""), "Info");
-
- await NotifyBotSubscriberAsync();
-
- return Ok(isNameChanging
- ? $"Bot configuration updated successfully and renamed to '{request.Config.Name}'"
- : "Bot configuration updated successfully");
+ return Ok("Bot configuration updated successfully");
}
else
{
- return BadRequest("Failed to update bot configuration. " +
- (isNameChanging ? "The new name might already be in use." : ""));
+ return BadRequest("Failed to update bot configuration");
}
}
catch (Exception ex)
@@ -832,6 +714,153 @@ public class BotController : BaseController
}
}
+ ///
+ /// Validates and prepares the bot request by performing all necessary validations and mapping.
+ ///
+ /// The bot request to validate and prepare
+ /// The operation being performed (start/save) for error messages
+ /// The prepared TradingBotConfig and User
+ /// Thrown when validation fails
+ /// Thrown when user doesn't have permission
+ private async Task<(TradingBotConfig, User)> ValidateAndPrepareBotRequest(StartBotRequest request,
+ string operation)
+ {
+ if (request.Config == null)
+ {
+ throw new ArgumentException("Bot configuration is required");
+ }
+
+ // Check if user owns the account specified in the request
+ if (!await UserOwnsBotAccount(Guid.Empty, request.Config.AccountName))
+ {
+ throw new UnauthorizedAccessException($"You don't have permission to {operation} a bot with this account");
+ }
+
+ // Validate that either money management name or object is provided
+ if (string.IsNullOrEmpty(request.Config.MoneyManagementName) && request.Config.MoneyManagement == null)
+ {
+ throw new ArgumentException("Either money management name or money management object is required");
+ }
+
+ var cachedUser = await GetUser();
+ var user = await _userService.GetUserByName(cachedUser.Name);
+
+ if (string.IsNullOrEmpty(user.AgentName))
+ {
+ throw new ArgumentException(
+ $"Agent name is required to {operation} a bot. Please configure your agent name in the user profile.");
+ }
+
+ // Get money management - either by name lookup or use provided object
+ LightMoneyManagement moneyManagement;
+ if (!string.IsNullOrEmpty(request.Config.MoneyManagementName))
+ {
+ moneyManagement =
+ await _moneyManagementService.GetMoneyMangement(user, request.Config.MoneyManagementName);
+ if (moneyManagement == null)
+ {
+ throw new ArgumentException("Money management not found");
+ }
+ }
+ else
+ {
+ moneyManagement = Map(request.Config.MoneyManagement);
+ // Format percentage values if using custom money management
+ moneyManagement?.FormatPercentage();
+ }
+
+ // Validate initialTradingBalance
+ if (request.Config.BotTradingBalance <= Constants.GMX.Config.MinimumPositionAmount)
+ {
+ throw new ArgumentException(
+ $"Initial trading balance must be greater than {Constants.GMX.Config.MinimumPositionAmount}");
+ }
+
+ // Validate cooldown period
+ if (request.Config.CooldownPeriod < 1)
+ {
+ throw new ArgumentException("Cooldown period must be at least 1 candle");
+ }
+
+ // Validate max loss streak
+ if (request.Config.MaxLossStreak < 0)
+ {
+ throw new ArgumentException("Max loss streak cannot be negative");
+ }
+
+ // Validate max position time hours
+ if (request.Config.MaxPositionTimeHours.HasValue && request.Config.MaxPositionTimeHours.Value <= 0)
+ {
+ throw new ArgumentException("Max position time hours must be greater than 0 if specified");
+ }
+
+ // Validate CloseEarlyWhenProfitable consistency
+ if (request.Config.CloseEarlyWhenProfitable && !request.Config.MaxPositionTimeHours.HasValue)
+ {
+ throw new ArgumentException(
+ "CloseEarlyWhenProfitable can only be enabled when MaxPositionTimeHours is set");
+ }
+
+ // Handle scenario - either from ScenarioRequest or ScenarioName
+ Scenario scenario = null;
+ if (request.Config.Scenario != null)
+ {
+ // Convert ScenarioRequest to Scenario domain object
+ scenario = new Scenario(request.Config.Scenario.Name, request.Config.Scenario.LoopbackPeriod)
+ {
+ User = user
+ };
+
+ // Convert IndicatorRequest objects to Indicator domain objects
+ foreach (var indicatorRequest in request.Config.Scenario.Indicators)
+ {
+ var indicator = new IndicatorBase(indicatorRequest.Name, indicatorRequest.Type)
+ {
+ SignalType = indicatorRequest.SignalType,
+ MinimumHistory = indicatorRequest.MinimumHistory,
+ Period = indicatorRequest.Period,
+ FastPeriods = indicatorRequest.FastPeriods,
+ SlowPeriods = indicatorRequest.SlowPeriods,
+ SignalPeriods = indicatorRequest.SignalPeriods,
+ Multiplier = indicatorRequest.Multiplier,
+ SmoothPeriods = indicatorRequest.SmoothPeriods,
+ StochPeriods = indicatorRequest.StochPeriods,
+ CyclePeriods = indicatorRequest.CyclePeriods,
+ User = user
+ };
+ scenario.AddIndicator(indicator);
+ }
+ }
+
+ // Map the request to the full TradingBotConfig
+ var config = new TradingBotConfig
+ {
+ AccountName = request.Config.AccountName,
+ MoneyManagement = moneyManagement,
+ Ticker = request.Config.Ticker,
+ Scenario = LightScenario.FromScenario(scenario), // Convert to LightScenario for Orleans
+ ScenarioName = request.Config.ScenarioName, // Fallback to scenario name if scenario object not provided
+ Timeframe = request.Config.Timeframe,
+ IsForWatchingOnly = request.Config.IsForWatchingOnly,
+ BotTradingBalance = request.Config.BotTradingBalance,
+ CooldownPeriod = request.Config.CooldownPeriod,
+ MaxLossStreak = request.Config.MaxLossStreak,
+ MaxPositionTimeHours = request.Config.MaxPositionTimeHours,
+ FlipOnlyWhenInProfit = request.Config.FlipOnlyWhenInProfit,
+ CloseEarlyWhenProfitable = request.Config.CloseEarlyWhenProfitable,
+ UseSynthApi = request.Config.UseSynthApi,
+ UseForPositionSizing = request.Config.UseForPositionSizing,
+ UseForSignalFiltering = request.Config.UseForSignalFiltering,
+ UseForDynamicStopLoss = request.Config.UseForDynamicStopLoss,
+ // Set computed/default properties
+ IsForBacktest = false,
+ FlipPosition = request.Config.FlipPosition,
+ Name = request.Config.Name
+ };
+
+ return (config, user);
+ }
+
public MoneyManagement Map(MoneyManagementRequest moneyManagementRequest)
{
return new MoneyManagement
@@ -853,7 +882,7 @@ public class OpenPositionManuallyRequest
///
/// The identifier of the bot
///
- public string Identifier { get; set; }
+ public Guid Identifier { get; set; }
///
/// The direction of the position
@@ -869,12 +898,12 @@ public class ClosePositionRequest
///
/// The identifier of the bot
///
- public string Identifier { get; set; }
+ public Guid Identifier { get; set; }
///
/// The ID of the position to close
///
- public string PositionId { get; set; }
+ public Guid PositionId { get; set; }
}
///
@@ -886,4 +915,8 @@ public class StartBotRequest
/// The trading bot configuration request with primary properties
///
public TradingBotConfigRequest Config { get; set; }
+}
+
+public class SaveBotRequest : StartBotRequest
+{
}
\ No newline at end of file
diff --git a/src/Managing.Api/Controllers/DataController.cs b/src/Managing.Api/Controllers/DataController.cs
index 7af5599..3146c82 100644
--- a/src/Managing.Api/Controllers/DataController.cs
+++ b/src/Managing.Api/Controllers/DataController.cs
@@ -1,6 +1,5 @@
īģŋusing Managing.Api.Models.Requests;
using Managing.Api.Models.Responses;
-using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Services;
using Managing.Application.Hubs;
using Managing.Application.ManageBot.Commands;
@@ -8,7 +7,6 @@ using Managing.Domain.Backtests;
using Managing.Domain.Bots;
using Managing.Domain.Candles;
using Managing.Domain.Scenarios;
-using Managing.Domain.Shared.Helpers;
using Managing.Domain.Statistics;
using Managing.Domain.Strategies;
using Managing.Domain.Strategies.Base;
@@ -244,7 +242,7 @@ public class DataController : ControllerBase
{
return Ok(new CandlesWithIndicatorsResponse
{
- Candles = new List(),
+ Candles = new HashSet(),
IndicatorsValues = new Dictionary()
});
}
@@ -290,8 +288,8 @@ public class DataController : ControllerBase
}
// Get active bots
- var activeBots = await _mediator.Send(new GetActiveBotsCommand());
- var currentCount = activeBots.Count;
+ var activeBots = await _mediator.Send(new GetBotsByStatusCommand(BotStatus.Up));
+ var currentCount = activeBots.Count();
// Get previous count from cache
var previousCount = _cacheService.GetValue(previousCountKey);
@@ -343,11 +341,11 @@ public class DataController : ControllerBase
}
// Get active bots
- var activeBots = await _mediator.Send(new GetActiveBotsCommand());
+ var activeBots = await _mediator.Send(new GetBotsByStatusCommand(BotStatus.Up));
// Calculate PnL for each bot once and store in a list of tuples
var botsWithPnL = activeBots
- .Select(bot => new { Bot = bot, PnL = bot.GetProfitAndLoss() })
+ .Select(bot => new { Bot = bot, PnL = bot.Pnl })
.OrderByDescending(item => item.PnL)
.Take(3)
.ToList();
@@ -441,55 +439,42 @@ public class DataController : ControllerBase
///
/// The trading bot to map
/// A view model with detailed strategy information
- private UserStrategyDetailsViewModel MapStrategyToViewModel(ITradingBot strategy)
+ private UserStrategyDetailsViewModel MapStrategyToViewModel(Bot strategy)
{
- // Get the runtime directly from the bot
- TimeSpan runtimeSpan = strategy.GetRuntime();
-
- // Get the startup time from the bot's internal property
- // If bot is not running, we use MinValue as a placeholder
- DateTime startupTime = DateTime.MinValue;
- if (strategy is Bot bot && bot.StartupTime != DateTime.MinValue)
- {
- startupTime = bot.StartupTime;
- }
-
// Calculate ROI percentage based on PnL relative to account value
- decimal pnl = strategy.GetProfitAndLoss();
+ decimal pnl = strategy.Pnl;
// If we had initial investment amount, we could calculate ROI like:
decimal initialInvestment = 1000; // Example placeholder, ideally should come from the account
decimal roi = pnl != 0 ? (pnl / initialInvestment) * 100 : 0;
// Calculate volume statistics
- decimal totalVolume = TradingBox.GetTotalVolumeTraded(strategy.Positions);
- decimal volumeLast24h = TradingBox.GetLast24HVolumeTraded(strategy.Positions);
+ decimal totalVolume = strategy.Volume;
+ decimal volumeLast24h = strategy.Volume;
// Calculate win/loss statistics
- (int wins, int losses) = TradingBox.GetWinLossCount(strategy.Positions);
+ (int wins, int losses) = (strategy.TradeWins, strategy.TradeLosses);
+ int winRate = wins + losses > 0 ? (wins * 100) / (wins + losses) : 0;
// Calculate ROI for last 24h
- decimal roiLast24h = TradingBox.GetLast24HROI(strategy.Positions);
+ decimal roiLast24h = strategy.Roi;
return new UserStrategyDetailsViewModel
{
Name = strategy.Name,
- ScenarioName = strategy.Config.ScenarioName,
- State = strategy.GetStatus() == BotStatus.Up.ToString() ? "RUNNING" :
- strategy.GetStatus() == BotStatus.Down.ToString() ? "STOPPED" : "UNUSED",
+ State = strategy.Status.ToString(),
PnL = pnl,
ROIPercentage = roi,
ROILast24H = roiLast24h,
- Runtime = startupTime,
- WinRate = strategy.GetWinRate(),
+ Runtime = strategy.StartupTime,
+ WinRate = winRate,
TotalVolumeTraded = totalVolume,
VolumeLast24H = volumeLast24h,
Wins = wins,
Losses = losses,
- Positions = strategy.Positions.OrderByDescending(p => p.Date)
- .ToList(), // Include sorted positions with most recent first
- Identifier = strategy.Identifier,
- WalletBalances = strategy.WalletBalances,
+ Positions = new Dictionary(),
+ Identifier = strategy.Identifier.ToString(),
+ WalletBalances = new Dictionary(),
};
}
@@ -544,18 +529,17 @@ public class DataController : ControllerBase
continue; // Skip agents with no strategies
}
- // Combine all positions from all strategies
- var allPositions = strategies.SelectMany(s => s.Positions).ToList();
+ // TODO: Add this calculation into repository for better performance
+
+ var globalPnL = strategies.Sum(s => s.Pnl);
+ var globalVolume = strategies.Sum(s => s.Volume);
+ var globalVolumeLast24h = strategies.Sum(s => s.Volume);
// Calculate agent metrics for platform totals
- decimal totalPnL = TradingBox.GetPnLInTimeRange(allPositions, timeFilter);
- decimal totalVolume = TradingBox.GetTotalVolumeTraded(allPositions);
- decimal volumeLast24h = TradingBox.GetLast24HVolumeTraded(allPositions);
-
// Add to platform totals
- totalPlatformPnL += totalPnL;
- totalPlatformVolume += totalVolume;
- totalPlatformVolumeLast24h += volumeLast24h;
+ totalPlatformPnL += globalPnL;
+ totalPlatformVolume += globalVolume;
+ totalPlatformVolumeLast24h += globalVolumeLast24h;
}
// Set the platform totals
@@ -569,128 +553,25 @@ public class DataController : ControllerBase
return Ok(summary);
}
- ///
- /// Retrieves a list of agent summaries for the agent index page
- ///
- /// Time filter to apply (24H, 3D, 1W, 1M, 1Y, Total)
- /// A list of agent summaries sorted by performance
- [HttpGet("GetAgentIndex")]
- public async Task> GetAgentIndex(string timeFilter = "Total")
- {
- // Validate time filter
- var validTimeFilters = new[] { "24H", "3D", "1W", "1M", "1Y", "Total" };
- if (!validTimeFilters.Contains(timeFilter))
- {
- timeFilter = "Total"; // Default to Total if invalid
- }
- string cacheKey = $"AgentIndex_{timeFilter}";
-
- // Check if the agent index is already cached
- var cachedIndex = _cacheService.GetValue(cacheKey);
-
- if (cachedIndex != null)
- {
- return Ok(cachedIndex);
- }
-
- // Get all agents and their strategies
- var agentsWithStrategies = await _mediator.Send(new GetAllAgentsCommand(timeFilter));
-
- // Create the agent index response
- var agentIndex = new AgentIndexViewModel
- {
- TimeFilter = timeFilter
- };
-
- // Create summaries for each agent
- foreach (var agent in agentsWithStrategies)
- {
- var user = agent.Key;
- var strategies = agent.Value;
-
- if (strategies.Count == 0)
- {
- continue; // Skip agents with no strategies
- }
-
- // Combine all positions from all strategies
- var allPositions = strategies.SelectMany(s => s.Positions).ToList();
-
- // Calculate agent metrics
- decimal totalPnL = TradingBox.GetPnLInTimeRange(allPositions, timeFilter);
- decimal pnlLast24h = TradingBox.GetPnLInTimeRange(allPositions, "24H");
-
- decimal totalROI = TradingBox.GetROIInTimeRange(allPositions, timeFilter);
- decimal roiLast24h = TradingBox.GetROIInTimeRange(allPositions, "24H");
-
- (int wins, int losses) = TradingBox.GetWinLossCountInTimeRange(allPositions, timeFilter);
-
- // Calculate trading volumes
- decimal totalVolume = TradingBox.GetTotalVolumeTraded(allPositions);
- decimal volumeLast24h = TradingBox.GetLast24HVolumeTraded(allPositions);
-
- // Calculate win rate
- int averageWinRate = 0;
- if (wins + losses > 0)
- {
- averageWinRate = (wins * 100) / (wins + losses);
- }
-
- // Add to agent summaries
- var agentSummary = new AgentSummaryViewModel
- {
- AgentName = user.AgentName,
- TotalPnL = totalPnL,
- PnLLast24h = pnlLast24h,
- TotalROI = totalROI,
- ROILast24h = roiLast24h,
- Wins = wins,
- Losses = losses,
- AverageWinRate = averageWinRate,
- ActiveStrategiesCount = strategies.Count,
- TotalVolume = totalVolume,
- VolumeLast24h = volumeLast24h
- };
-
- agentIndex.AgentSummaries.Add(agentSummary);
- }
-
- // Sort agent summaries by total PnL (highest first)
- agentIndex.AgentSummaries = agentIndex.AgentSummaries.OrderByDescending(a => a.TotalPnL).ToList();
-
- // Cache the results for 5 minutes
- _cacheService.SaveValue(cacheKey, agentIndex, TimeSpan.FromMinutes(5));
-
- return Ok(agentIndex);
- }
///
/// Retrieves a paginated list of agent summaries for the agent index page
///
- /// Time filter to apply (24H, 3D, 1W, 1M, 1Y, Total)
/// Page number (defaults to 1)
/// Number of items per page (defaults to 10, max 100)
- /// Field to sort by (TotalPnL, PnLLast24h, TotalROI, ROILast24h, Wins, Losses, AverageWinRate, ActiveStrategiesCount, TotalVolume, VolumeLast24h)
+ /// Field to sort by (TotalPnL, TotalROI, Wins, Losses, AgentName, CreatedAt, UpdatedAt)
/// Sort order - "asc" or "desc" (defaults to "desc")
/// Optional comma-separated list of agent names to filter by
/// A paginated list of agent summaries sorted by the specified field
[HttpGet("GetAgentIndexPaginated")]
public async Task> GetAgentIndexPaginated(
- string timeFilter = "Total",
int page = 1,
int pageSize = 10,
- string sortBy = "TotalPnL",
+ SortableFields sortBy = SortableFields.TotalPnL,
string sortOrder = "desc",
string? agentNames = null)
{
- // Validate time filter
- var validTimeFilters = new[] { "24H", "3D", "1W", "1M", "1Y", "Total" };
- if (!validTimeFilters.Contains(timeFilter))
- {
- timeFilter = "Total"; // Default to Total if invalid
- }
-
// Validate pagination parameters
if (page < 1)
{
@@ -708,177 +589,59 @@ public class DataController : ControllerBase
return BadRequest("Sort order must be 'asc' or 'desc'");
}
- // Validate sort by field
- var validSortFields = new[]
- {
- "TotalPnL", "PnLLast24h", "TotalROI", "ROILast24h", "Wins", "Losses", "AverageWinRate",
- "ActiveStrategiesCount", "TotalVolume", "VolumeLast24h"
- };
- if (!validSortFields.Contains(sortBy))
- {
- sortBy = "TotalPnL"; // Default to TotalPnL if invalid
- }
-
- // Create cache key that includes agent names filter
- var agentNamesForCache = !string.IsNullOrWhiteSpace(agentNames)
- ? string.Join("_", agentNames.Split(',', StringSplitOptions.RemoveEmptyEntries)
- .Select(name => name.Trim())
- .Where(name => !string.IsNullOrWhiteSpace(name))
- .OrderBy(name => name))
- : "all";
- string cacheKey = $"AgentIndex_{timeFilter}_{agentNamesForCache}";
-
- // Check if the agent index is already cached
- var cachedIndex = _cacheService.GetValue(cacheKey);
-
- List allAgentSummaries;
-
- if (cachedIndex != null)
- {
- allAgentSummaries = cachedIndex.AgentSummaries.ToList();
- }
- else
- {
- // Get all agents and their strategies
- var agentsWithStrategies = await _mediator.Send(new GetAllAgentsCommand(timeFilter));
-
- allAgentSummaries = new List();
-
- // Create summaries for each agent
- foreach (var agent in agentsWithStrategies)
- {
- var user = agent.Key;
- var strategies = agent.Value;
-
- if (strategies.Count == 0)
- {
- continue; // Skip agents with no strategies
- }
-
- // Combine all positions from all strategies
- var allPositions = strategies.SelectMany(s => s.Positions).ToList();
-
- // Calculate agent metrics
- decimal totalPnL = TradingBox.GetPnLInTimeRange(allPositions, timeFilter);
- decimal pnlLast24h = TradingBox.GetPnLInTimeRange(allPositions, "24H");
-
- decimal totalROI = TradingBox.GetROIInTimeRange(allPositions, timeFilter);
- decimal roiLast24h = TradingBox.GetROIInTimeRange(allPositions, "24H");
-
- (int wins, int losses) = TradingBox.GetWinLossCountInTimeRange(allPositions, timeFilter);
-
- // Calculate trading volumes
- decimal totalVolume = TradingBox.GetTotalVolumeTraded(allPositions);
- decimal volumeLast24h = TradingBox.GetLast24HVolumeTraded(allPositions);
-
- // Calculate win rate
- int averageWinRate = 0;
- if (wins + losses > 0)
- {
- averageWinRate = (wins * 100) / (wins + losses);
- }
-
- // Add to agent summaries
- var agentSummary = new AgentSummaryViewModel
- {
- AgentName = user.AgentName,
- TotalPnL = totalPnL,
- PnLLast24h = pnlLast24h,
- TotalROI = totalROI,
- ROILast24h = roiLast24h,
- Wins = wins,
- Losses = losses,
- AverageWinRate = averageWinRate,
- ActiveStrategiesCount = strategies.Count,
- TotalVolume = totalVolume,
- VolumeLast24h = volumeLast24h
- };
-
- allAgentSummaries.Add(agentSummary);
- }
-
- // Cache the results for 5 minutes
- var agentIndex = new AgentIndexViewModel
- {
- TimeFilter = timeFilter,
- AgentSummaries = allAgentSummaries
- };
- _cacheService.SaveValue(cacheKey, agentIndex, TimeSpan.FromMinutes(5));
- }
-
- // Apply agent name filtering if specified
+ // Parse agent names filter
+ IEnumerable? agentNamesList = null;
if (!string.IsNullOrWhiteSpace(agentNames))
{
- var agentNameList = agentNames.Split(',', StringSplitOptions.RemoveEmptyEntries)
+ agentNamesList = agentNames.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(name => name.Trim())
.Where(name => !string.IsNullOrWhiteSpace(name))
.ToList();
-
- if (agentNameList.Any())
- {
- allAgentSummaries = allAgentSummaries
- .Where(agent => agentNameList.Contains(agent.AgentName, StringComparer.OrdinalIgnoreCase))
- .ToList();
- }
}
- // Apply sorting
- var sortedSummaries = sortBy switch
+ // Get paginated results from database
+ var command = new GetPaginatedAgentSummariesCommand(page, pageSize, sortBy, sortOrder, agentNamesList);
+ var result = await _mediator.Send(command);
+ var agentSummaries = result.Results;
+ var totalCount = result.TotalCount;
+
+ // Map to view models
+ var agentSummaryViewModels = new List();
+ foreach (var agentSummary in agentSummaries)
{
- "TotalPnL" => sortOrder == "desc"
- ? allAgentSummaries.OrderByDescending(a => a.TotalPnL)
- : allAgentSummaries.OrderBy(a => a.TotalPnL),
- "PnLLast24h" => sortOrder == "desc"
- ? allAgentSummaries.OrderByDescending(a => a.PnLLast24h)
- : allAgentSummaries.OrderBy(a => a.PnLLast24h),
- "TotalROI" => sortOrder == "desc"
- ? allAgentSummaries.OrderByDescending(a => a.TotalROI)
- : allAgentSummaries.OrderBy(a => a.TotalROI),
- "ROILast24h" => sortOrder == "desc"
- ? allAgentSummaries.OrderByDescending(a => a.ROILast24h)
- : allAgentSummaries.OrderBy(a => a.ROILast24h),
- "Wins" => sortOrder == "desc"
- ? allAgentSummaries.OrderByDescending(a => a.Wins)
- : allAgentSummaries.OrderBy(a => a.Wins),
- "Losses" => sortOrder == "desc"
- ? allAgentSummaries.OrderByDescending(a => a.Losses)
- : allAgentSummaries.OrderBy(a => a.Losses),
- "AverageWinRate" => sortOrder == "desc"
- ? allAgentSummaries.OrderByDescending(a => a.AverageWinRate)
- : allAgentSummaries.OrderBy(a => a.AverageWinRate),
- "ActiveStrategiesCount" => sortOrder == "desc"
- ? allAgentSummaries.OrderByDescending(a => a.ActiveStrategiesCount)
- : allAgentSummaries.OrderBy(a => a.ActiveStrategiesCount),
- "TotalVolume" => sortOrder == "desc"
- ? allAgentSummaries.OrderByDescending(a => a.TotalVolume)
- : allAgentSummaries.OrderBy(a => a.TotalVolume),
- "VolumeLast24h" => sortOrder == "desc"
- ? allAgentSummaries.OrderByDescending(a => a.VolumeLast24h)
- : allAgentSummaries.OrderBy(a => a.VolumeLast24h),
- _ => sortOrder == "desc"
- ? allAgentSummaries.OrderByDescending(a => a.TotalPnL)
- : allAgentSummaries.OrderBy(a => a.TotalPnL)
- };
+ // Calculate win rate
+ int averageWinRate = 0;
+ if (agentSummary.Wins + agentSummary.Losses > 0)
+ {
+ averageWinRate = (agentSummary.Wins * 100) / (agentSummary.Wins + agentSummary.Losses);
+ }
+
+ // Map to view model
+ var agentSummaryViewModel = new AgentSummaryViewModel
+ {
+ AgentName = agentSummary.AgentName,
+ TotalPnL = agentSummary.TotalPnL,
+ TotalROI = agentSummary.TotalROI,
+ Wins = agentSummary.Wins,
+ Losses = agentSummary.Losses,
+ ActiveStrategiesCount = agentSummary.ActiveStrategiesCount,
+ TotalVolume = agentSummary.TotalVolume,
+ };
+
+ agentSummaryViewModels.Add(agentSummaryViewModel);
+ }
- var totalCount = allAgentSummaries.Count;
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
- // Apply pagination
- var paginatedSummaries = sortedSummaries
- .Skip((page - 1) * pageSize)
- .Take(pageSize)
- .ToList();
-
var response = new PaginatedAgentIndexResponse
{
- AgentSummaries = paginatedSummaries,
+ AgentSummaries = agentSummaryViewModels,
TotalCount = totalCount,
CurrentPage = page,
PageSize = pageSize,
TotalPages = totalPages,
HasNextPage = page < totalPages,
HasPreviousPage = page > 1,
- TimeFilter = timeFilter,
SortBy = sortBy,
SortOrder = sortOrder,
FilteredAgentNames = agentNames
@@ -970,7 +733,7 @@ public class DataController : ControllerBase
foreach (var indicatorRequest in scenarioRequest.Indicators)
{
- var indicator = new Indicator(indicatorRequest.Name, indicatorRequest.Type)
+ var indicator = new IndicatorBase(indicatorRequest.Name, indicatorRequest.Type)
{
SignalType = indicatorRequest.SignalType,
MinimumHistory = indicatorRequest.MinimumHistory,
diff --git a/src/Managing.Api/Controllers/ScenarioController.cs b/src/Managing.Api/Controllers/ScenarioController.cs
index 3db1642..7d26529 100644
--- a/src/Managing.Api/Controllers/ScenarioController.cs
+++ b/src/Managing.Api/Controllers/ScenarioController.cs
@@ -197,23 +197,23 @@ public class ScenarioController : BaseController
};
}
- private static IndicatorViewModel MapToIndicatorViewModel(Indicator indicator)
+ private static IndicatorViewModel MapToIndicatorViewModel(IndicatorBase indicatorBase)
{
return new IndicatorViewModel
{
- Name = indicator.Name,
- Type = indicator.Type,
- SignalType = indicator.SignalType,
- MinimumHistory = indicator.MinimumHistory,
- Period = indicator.Period,
- FastPeriods = indicator.FastPeriods,
- SlowPeriods = indicator.SlowPeriods,
- SignalPeriods = indicator.SignalPeriods,
- Multiplier = indicator.Multiplier,
- SmoothPeriods = indicator.SmoothPeriods,
- StochPeriods = indicator.StochPeriods,
- CyclePeriods = indicator.CyclePeriods,
- UserName = indicator.User?.Name
+ Name = indicatorBase.Name,
+ Type = indicatorBase.Type,
+ SignalType = indicatorBase.SignalType,
+ MinimumHistory = indicatorBase.MinimumHistory,
+ Period = indicatorBase.Period,
+ FastPeriods = indicatorBase.FastPeriods,
+ SlowPeriods = indicatorBase.SlowPeriods,
+ SignalPeriods = indicatorBase.SignalPeriods,
+ Multiplier = indicatorBase.Multiplier,
+ SmoothPeriods = indicatorBase.SmoothPeriods,
+ StochPeriods = indicatorBase.StochPeriods,
+ CyclePeriods = indicatorBase.CyclePeriods,
+ UserName = indicatorBase.User?.Name
};
}
}
\ No newline at end of file
diff --git a/src/Managing.Api/Controllers/TradingController.cs b/src/Managing.Api/Controllers/TradingController.cs
index 04ab52d..0a94597 100644
--- a/src/Managing.Api/Controllers/TradingController.cs
+++ b/src/Managing.Api/Controllers/TradingController.cs
@@ -85,7 +85,7 @@ public class TradingController : BaseController
/// The unique identifier of the position to close.
/// The closed position.
[HttpPost("ClosePosition")]
- public async Task> ClosePosition(string identifier)
+ public async Task> ClosePosition(Guid identifier)
{
var position = await _tradingService.GetPositionByIdentifierAsync(identifier);
var result = await _closeTradeCommandHandler.Handle(new ClosePositionCommand(position));
diff --git a/src/Managing.Api/Controllers/UserController.cs b/src/Managing.Api/Controllers/UserController.cs
index 8154b2d..ca55f71 100644
--- a/src/Managing.Api/Controllers/UserController.cs
+++ b/src/Managing.Api/Controllers/UserController.cs
@@ -26,7 +26,8 @@ public class UserController : BaseController
/// Service for user-related operations.
/// Utility for JWT token operations.
/// Service for webhook operations.
- public UserController(IConfiguration config, IUserService userService, IJwtUtils jwtUtils, IWebhookService webhookService)
+ public UserController(IConfiguration config, IUserService userService, IJwtUtils jwtUtils,
+ IWebhookService webhookService)
: base(userService)
{
_config = config;
@@ -40,7 +41,7 @@ public class UserController : BaseController
/// The login request containing user credentials.
/// A JWT token if authentication is successful; otherwise, an Unauthorized result.
[AllowAnonymous]
- [HttpPost]
+ [HttpPost("create-token")]
public async Task> CreateToken([FromBody] LoginRequest login)
{
var user = await _userService.Authenticate(login.Name, login.Address, login.Message, login.Signature);
@@ -52,16 +53,18 @@ public class UserController : BaseController
}
return Unauthorized();
- }
+ }
///
/// Gets the current user's information.
///
/// The current user's information.
+ [Authorize]
[HttpGet]
public async Task> GetCurrentUser()
{
var user = await base.GetUser();
+ user = await _userService.GetUserByName(user.Name);
return Ok(user);
}
@@ -70,6 +73,7 @@ public class UserController : BaseController
///
/// The new agent name to set.
/// The updated user with the new agent name.
+ [Authorize]
[HttpPut("agent-name")]
public async Task> UpdateAgentName([FromBody] string agentName)
{
@@ -83,6 +87,7 @@ public class UserController : BaseController
///
/// The new avatar URL to set.
/// The updated user with the new avatar URL.
+ [Authorize]
[HttpPut("avatar")]
public async Task> UpdateAvatarUrl([FromBody] string avatarUrl)
{
@@ -96,35 +101,37 @@ public class UserController : BaseController
///
/// The new Telegram channel to set.
/// The updated user with the new Telegram channel.
+ [Authorize]
[HttpPut("telegram-channel")]
public async Task> UpdateTelegramChannel([FromBody] string telegramChannel)
{
var user = await GetUser();
var updatedUser = await _userService.UpdateTelegramChannel(user, telegramChannel);
-
+
// Send welcome message to the newly configured telegram channel
if (!string.IsNullOrEmpty(telegramChannel))
{
try
{
var welcomeMessage = $"đ **Trading Bot - Welcome!**\n\n" +
- $"đ¯ **Agent:** {user.Name}\n" +
- $"đĄ **Channel ID:** {telegramChannel}\n" +
- $"â° **Setup Time:** {DateTime.UtcNow:MMM dd, yyyy âĸ HH:mm:ss} UTC\n\n" +
- $"đ **Notification Types:**\n" +
- $"âĸ đ Position Opens & Closes\n" +
- $"âĸ đ¤ Bot configuration changes\n\n" +
- $"đ **Welcome aboard!** Your trading notifications are now live.";
+ $"đ¯ **Agent:** {user.Name}\n" +
+ $"đĄ **Channel ID:** {telegramChannel}\n" +
+ $"â° **Setup Time:** {DateTime.UtcNow:MMM dd, yyyy âĸ HH:mm:ss} UTC\n\n" +
+ $"đ **Notification Types:**\n" +
+ $"âĸ đ Position Opens & Closes\n" +
+ $"âĸ đ¤ Bot configuration changes\n\n" +
+ $"đ **Welcome aboard!** Your trading notifications are now live.";
await _webhookService.SendMessage(welcomeMessage, telegramChannel);
}
catch (Exception ex)
{
// Log the error but don't fail the update operation
- Console.WriteLine($"Failed to send welcome message to telegram channel {telegramChannel}: {ex.Message}");
+ Console.WriteLine(
+ $"Failed to send welcome message to telegram channel {telegramChannel}: {ex.Message}");
}
}
-
+
return Ok(updatedUser);
}
@@ -132,11 +139,12 @@ public class UserController : BaseController
/// Tests the Telegram channel configuration by sending a test message.
///
/// A message indicating the test result.
+ [Authorize]
[HttpPost("telegram-channel/test")]
public async Task> TestTelegramChannel()
{
var user = await GetUser();
-
+
if (string.IsNullOrEmpty(user.TelegramChannel))
{
return BadRequest("No Telegram channel configured for this user. Please set a Telegram channel first.");
@@ -144,7 +152,7 @@ public class UserController : BaseController
try
{
- var testMessage = $"đ **Trading Bot - Channel Test**\n\n" +
+ var testMessage = $"đ **Trading Bot - Channel Test**\n\n" +
$"đ¯ **Agent:** {user.Name}\n" +
$"đĄ **Channel ID:** {user.TelegramChannel}\n" +
$"â° **Test Time:** {DateTime.UtcNow:MMM dd, yyyy âĸ HH:mm:ss} UTC\n\n" +
@@ -154,13 +162,13 @@ public class UserController : BaseController
$"đ **Ready to trade!** Your notifications are now active.";
await _webhookService.SendMessage(testMessage, user.TelegramChannel);
-
- return Ok($"Test message sent successfully to Telegram channel {user.TelegramChannel}. Please check your Telegram to verify delivery.");
+
+ return Ok(
+ $"Test message sent successfully to Telegram channel {user.TelegramChannel}. Please check your Telegram to verify delivery.");
}
catch (Exception ex)
{
return StatusCode(500, $"Failed to send test message: {ex.Message}");
}
}
-}
-
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/src/Managing.Api/Models/Requests/GetBotsPaginatedRequest.cs b/src/Managing.Api/Models/Requests/GetBotsPaginatedRequest.cs
new file mode 100644
index 0000000..976a062
--- /dev/null
+++ b/src/Managing.Api/Models/Requests/GetBotsPaginatedRequest.cs
@@ -0,0 +1,55 @@
+using static Managing.Common.Enums;
+
+namespace Managing.Api.Models.Requests;
+
+///
+/// Request model for getting paginated bots with filtering and sorting
+///
+public class GetBotsPaginatedRequest
+{
+ ///
+ /// Page number (1-based). Default is 1.
+ ///
+ public int PageNumber { get; set; } = 1;
+
+ ///
+ /// Number of items per page. Default is 10, maximum is 100.
+ ///
+ public int PageSize { get; set; } = 10;
+
+ ///
+ /// Filter by bot status. If null, returns bots of all statuses.
+ ///
+ public BotStatus? Status { get; set; }
+
+ ///
+ /// Filter by user ID. If null, returns bots for all users.
+ ///
+ public int? UserId { get; set; }
+
+ ///
+ /// Filter by bot name (partial match, case-insensitive). If null, no name filtering is applied.
+ ///
+ public string? Name { get; set; }
+
+ ///
+ /// Filter by ticker (partial match, case-insensitive). If null, no ticker filtering is applied.
+ ///
+ public string? Ticker { get; set; }
+
+ ///
+ /// Filter by agent name (partial match, case-insensitive). If null, no agent name filtering is applied.
+ ///
+ public string? AgentName { get; set; }
+
+ ///
+ /// Sort field. Valid values: "Name", "Ticker", "Status", "CreateDate", "StartupTime", "Pnl", "WinRate", "AgentName".
+ /// Default is "CreateDate".
+ ///
+ public string SortBy { get; set; } = "CreateDate";
+
+ ///
+ /// Sort direction. Default is "Desc" (descending).
+ ///
+ public string SortDirection { get; set; } = "Desc";
+}
\ No newline at end of file
diff --git a/src/Managing.Api/Models/Requests/UpdateBotConfigRequest.cs b/src/Managing.Api/Models/Requests/UpdateBotConfigRequest.cs
index 76c817b..148a320 100644
--- a/src/Managing.Api/Models/Requests/UpdateBotConfigRequest.cs
+++ b/src/Managing.Api/Models/Requests/UpdateBotConfigRequest.cs
@@ -13,7 +13,7 @@ public class UpdateBotConfigRequest
/// The unique identifier of the bot to update
///
[Required]
- public string Identifier { get; set; }
+ public Guid Identifier { get; set; }
///
/// The new trading bot configuration request
diff --git a/src/Managing.Api/Models/Responses/AgentSummaryViewModel.cs b/src/Managing.Api/Models/Responses/AgentSummaryViewModel.cs
index 9e36f42..e0ac7c0 100644
--- a/src/Managing.Api/Models/Responses/AgentSummaryViewModel.cs
+++ b/src/Managing.Api/Models/Responses/AgentSummaryViewModel.cs
@@ -15,21 +15,11 @@ namespace Managing.Api.Models.Responses
///
public decimal TotalPnL { get; set; }
- ///
- /// Profit and loss in the last 24 hours in USD
- ///
- public decimal PnLLast24h { get; set; }
-
///
/// Total return on investment as a percentage
///
public decimal TotalROI { get; set; }
- ///
- /// Return on investment in the last 24 hours as a percentage
- ///
- public decimal ROILast24h { get; set; }
-
///
/// Number of winning trades
///
@@ -40,10 +30,6 @@ namespace Managing.Api.Models.Responses
///
public int Losses { get; set; }
- ///
- /// Average win rate as a percentage
- ///
- public int AverageWinRate { get; set; }
///
/// Number of active strategies for this agent
@@ -54,11 +40,6 @@ namespace Managing.Api.Models.Responses
/// Total volume traded by this agent in USD
///
public decimal TotalVolume { get; set; }
-
- ///
- /// Volume traded in the last 24 hours in USD
- ///
- public decimal VolumeLast24h { get; set; }
}
///
diff --git a/src/Managing.Api/Models/Responses/CandlesWithIndicatorsResponse.cs b/src/Managing.Api/Models/Responses/CandlesWithIndicatorsResponse.cs
index 9d57f16..b2860e8 100644
--- a/src/Managing.Api/Models/Responses/CandlesWithIndicatorsResponse.cs
+++ b/src/Managing.Api/Models/Responses/CandlesWithIndicatorsResponse.cs
@@ -12,10 +12,11 @@ public class CandlesWithIndicatorsResponse
///
/// The list of candles.
///
- public List Candles { get; set; } = new List();
+ public HashSet Candles { get; set; } = new HashSet();
///
/// The calculated indicators values.
///
- public Dictionary IndicatorsValues { get; set; } = new Dictionary();
-}
\ No newline at end of file
+ public Dictionary IndicatorsValues { get; set; } =
+ new Dictionary();
+}
\ No newline at end of file
diff --git a/src/Managing.Api/Models/Responses/PaginatedAgentIndexResponse.cs b/src/Managing.Api/Models/Responses/PaginatedAgentIndexResponse.cs
index 54978d9..b556e98 100644
--- a/src/Managing.Api/Models/Responses/PaginatedAgentIndexResponse.cs
+++ b/src/Managing.Api/Models/Responses/PaginatedAgentIndexResponse.cs
@@ -1,3 +1,5 @@
+using static Managing.Common.Enums;
+
namespace Managing.Api.Models.Responses;
///
@@ -48,7 +50,7 @@ public class PaginatedAgentIndexResponse
///
/// Field used for sorting
///
- public string SortBy { get; set; } = "TotalPnL";
+ public SortableFields SortBy { get; set; } = SortableFields.TotalPnL;
///
/// Sort order (asc or desc)
diff --git a/src/Managing.Api/Models/Responses/PaginatedResponse.cs b/src/Managing.Api/Models/Responses/PaginatedResponse.cs
new file mode 100644
index 0000000..14aa41e
--- /dev/null
+++ b/src/Managing.Api/Models/Responses/PaginatedResponse.cs
@@ -0,0 +1,43 @@
+namespace Managing.Api.Models.Responses;
+
+///
+/// Generic pagination response model
+///
+/// The type of items in the response
+public class PaginatedResponse
+{
+ ///
+ /// The items for the current page
+ ///
+ public List Items { get; set; } = new();
+
+ ///
+ /// Total number of items across all pages
+ ///
+ public int TotalCount { get; set; }
+
+ ///
+ /// Current page number (1-based)
+ ///
+ public int PageNumber { get; set; }
+
+ ///
+ /// Number of items per page
+ ///
+ public int PageSize { get; set; }
+
+ ///
+ /// Total number of pages
+ ///
+ public int TotalPages { get; set; }
+
+ ///
+ /// Whether there is a previous page
+ ///
+ public bool HasPreviousPage { get; set; }
+
+ ///
+ /// Whether there is a next page
+ ///
+ public bool HasNextPage { get; set; }
+}
\ No newline at end of file
diff --git a/src/Managing.Api/Models/Responses/TradingBotResponse.cs b/src/Managing.Api/Models/Responses/TradingBotResponse.cs
index ce0989e..b0bdc27 100644
--- a/src/Managing.Api/Models/Responses/TradingBotResponse.cs
+++ b/src/Managing.Api/Models/Responses/TradingBotResponse.cs
@@ -1,7 +1,8 @@
īģŋusing System.ComponentModel.DataAnnotations;
-using Managing.Domain.Bots;
using Managing.Domain.Candles;
+using Managing.Domain.Indicators;
using Managing.Domain.Trades;
+using static Managing.Common.Enums;
namespace Managing.Api.Models.Responses
{
@@ -14,16 +15,16 @@ namespace Managing.Api.Models.Responses
public string Status { get; internal set; }
///
- /// List of signals generated by the bot
+ /// Dictionary of signals generated by the bot, keyed by signal identifier
///
[Required]
- public List Signals { get; internal set; }
+ public Dictionary Signals { get; internal set; }
///
- /// List of positions opened by the bot
+ /// Dictionary of positions opened by the bot, keyed by position identifier
///
[Required]
- public List Positions { get; internal set; }
+ public Dictionary Positions { get; internal set; }
///
/// Candles used by the bot for analysis
@@ -55,12 +56,6 @@ namespace Managing.Api.Models.Responses
[Required]
public string AgentName { get; set; }
- ///
- /// The full trading bot configuration
- ///
- [Required]
- public TradingBotConfig Config { get; internal set; }
-
///
/// The time when the bot was created
///
@@ -72,5 +67,13 @@ namespace Managing.Api.Models.Responses
///
[Required]
public DateTime StartupTime { get; internal set; }
+
+ [Required] public string Name { get; set; }
+
+ ///
+ /// The ticker/symbol being traded by this bot
+ ///
+ [Required]
+ public Ticker Ticker { get; set; }
}
}
\ No newline at end of file
diff --git a/src/Managing.Api/Models/Responses/UserStrategyDetailsViewModel.cs b/src/Managing.Api/Models/Responses/UserStrategyDetailsViewModel.cs
index dffbdbf..c2dcdd7 100644
--- a/src/Managing.Api/Models/Responses/UserStrategyDetailsViewModel.cs
+++ b/src/Managing.Api/Models/Responses/UserStrategyDetailsViewModel.cs
@@ -63,16 +63,12 @@ namespace Managing.Api.Models.Responses
public int Losses { get; set; }
///
- /// List of all positions executed by this strategy
+ /// Dictionary of all positions executed by this strategy, keyed by position identifier
///
- public List Positions { get; set; } = new List();
+ public Dictionary Positions { get; set; } = new Dictionary();
public string Identifier { get; set; }
- ///
- /// Name of the scenario used by this strategy
- ///
- public string ScenarioName { get; set; }
- public Dictionary WalletBalances { get; set; }
+ public Dictionary WalletBalances { get; set; } = new Dictionary();
}
}
\ No newline at end of file
diff --git a/src/Managing.Api/Program.cs b/src/Managing.Api/Program.cs
index fa83e38..0e323de 100644
--- a/src/Managing.Api/Program.cs
+++ b/src/Managing.Api/Program.cs
@@ -5,7 +5,6 @@ using Managing.Api.Authorization;
using Managing.Api.Filters;
using Managing.Api.HealthChecks;
using Managing.Application.Hubs;
-using Managing.Application.Workers;
using Managing.Bootstrap;
using Managing.Common;
using Managing.Core.Middleawares;
@@ -170,7 +169,7 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJw
ValidateIssuerSigningKey = true
};
});
-builder.Services.AddAuthorization();
+
builder.Services.AddCors(o => o.AddPolicy("CorsPolicy", builder =>
{
builder
@@ -233,12 +232,6 @@ builder.Services.AddSwaggerGen(options =>
});
builder.WebHost.SetupDiscordBot();
-if (builder.Configuration.GetValue("EnableBotManager", false))
-{
- builder.Services.AddHostedService();
-}
-
-// Workers are now registered in ApiBootstrap.cs
// App
var app = builder.Build();
@@ -288,5 +281,4 @@ app.UseEndpoints(endpoints =>
});
-
app.Run();
\ No newline at end of file
diff --git a/src/Managing.Api/appsettings.Oda.json b/src/Managing.Api/appsettings.Oda.json
index c338725..e8d65d8 100644
--- a/src/Managing.Api/appsettings.Oda.json
+++ b/src/Managing.Api/appsettings.Oda.json
@@ -5,7 +5,8 @@
"Token": "Fw2FPL2OwTzDHzSbR2Sd5xs0EKQYy00Q-hYKYAhr9cC1_q5YySONpxuf_Ck0PTjyUiF13xXmi__bu_pXH-H9zA=="
},
"PostgreSql": {
- "ConnectionString": "Host=localhost;Port=5432;Database=managing;Username=postgres;Password=postgres"
+ "ConnectionString": "Host=localhost;Port=5432;Database=managing;Username=postgres;Password=postgres",
+ "Orleans": "Host=localhost;Port=5432;Database=orleans;Username=postgres;Password=postgres"
},
"Privy": {
"AppId": "cm6f47n1l003jx7mjwaembhup",
diff --git a/src/Managing.Api/appsettings.Production.json b/src/Managing.Api/appsettings.Production.json
index 7419fd0..ed1a2ad 100644
--- a/src/Managing.Api/appsettings.Production.json
+++ b/src/Managing.Api/appsettings.Production.json
@@ -1,6 +1,7 @@
{
"PostgreSql": {
- "ConnectionString": "Host=apps.prod.live;Port=5432;Database=managing;Username=postgres;Password=postgres"
+ "ConnectionString": "Host=managing-postgre.apps.managing.live;Port=5432;Database=managing;Username=postgres;Password=29032b13a5bc4d37",
+ "Orleans": "Host=managing-postgre.apps.managing.live;Port=5432;Database=orleans;Username=postgres;Password=29032b13a5bc4d37"
},
"InfluxDb": {
"Url": "https://influx-db.apps.managing.live",
diff --git a/src/Managing.Api/appsettings.Sandbox.json b/src/Managing.Api/appsettings.Sandbox.json
index d2c082a..35aab81 100644
--- a/src/Managing.Api/appsettings.Sandbox.json
+++ b/src/Managing.Api/appsettings.Sandbox.json
@@ -1,6 +1,7 @@
{
"PostgreSql": {
- "ConnectionString": "Host=managing-postgre.apps.managing.live;Port=5432;Database=managing;Username=postgres;Password=29032b13a5bc4d37"
+ "ConnectionString": "Host=managing-postgre.apps.managing.live;Port=5432;Database=managing;Username=postgres;Password=29032b13a5bc4d37",
+ "Orleans": "Host=managing-postgre.apps.managing.live;Port=5432;Database=orleans;Username=postgres;Password=29032b13a5bc4d37"
},
"InfluxDb": {
"Url": "http://srv-captain--influx-db:8086/",
diff --git a/src/Managing.Application.Abstractions/Grains/BotRegistryEntry.cs b/src/Managing.Application.Abstractions/Grains/BotRegistryEntry.cs
new file mode 100644
index 0000000..9622b5e
--- /dev/null
+++ b/src/Managing.Application.Abstractions/Grains/BotRegistryEntry.cs
@@ -0,0 +1,55 @@
+using Orleans;
+using static Managing.Common.Enums;
+
+namespace Managing.Application.Abstractions.Grains;
+
+///
+/// A small serializable class to store bot metadata.
+/// This is a very lean object, perfect for fast storage and retrieval.
+///
+[GenerateSerializer]
+public class BotRegistryEntry
+{
+ ///
+ /// The unique identifier of the bot
+ ///
+ [Id(0)]
+ public Guid Identifier { get; set; }
+
+ ///
+ /// The unique identifier of the user who owns the bot
+ ///
+ [Id(1)]
+ public int UserId { get; set; }
+
+ ///
+ /// The current operational status of the bot
+ ///
+ [Id(2)]
+ public BotStatus Status { get; set; }
+
+ ///
+ /// When the bot was registered in the registry
+ ///
+ [Id(3)]
+ public DateTime RegisteredAt { get; set; } = DateTime.UtcNow;
+
+ ///
+ /// When the bot status was last updated
+ ///
+ [Id(4)]
+ public DateTime LastStatusUpdate { get; set; } = DateTime.UtcNow;
+
+ public BotRegistryEntry()
+ {
+ }
+
+ public BotRegistryEntry(Guid identifier, int userId, BotStatus status = BotStatus.None)
+ {
+ Identifier = identifier;
+ UserId = userId;
+ Status = status;
+ RegisteredAt = DateTime.UtcNow;
+ LastStatusUpdate = DateTime.UtcNow;
+ }
+}
\ No newline at end of file
diff --git a/src/Managing.Application.Abstractions/Grains/BotRegistryState.cs b/src/Managing.Application.Abstractions/Grains/BotRegistryState.cs
new file mode 100644
index 0000000..160cbaf
--- /dev/null
+++ b/src/Managing.Application.Abstractions/Grains/BotRegistryState.cs
@@ -0,0 +1,36 @@
+using Orleans;
+
+namespace Managing.Application.Abstractions.Grains;
+
+///
+/// Orleans grain state for BotRegistry.
+/// This class represents the persistent state of the bot registry grain.
+/// All properties must be serializable for Orleans state management.
+///
+[GenerateSerializer]
+public class BotRegistryState
+{
+ ///
+ /// Dictionary containing all registered bots. The key is the identifier.
+ ///
+ [Id(0)]
+ public Dictionary Bots { get; set; } = new();
+
+ ///
+ /// When the registry was last updated
+ ///
+ [Id(1)]
+ public DateTime LastUpdated { get; set; } = DateTime.UtcNow;
+
+ ///
+ /// Total number of bots currently registered
+ ///
+ [Id(2)]
+ public int TotalBotsCount { get; set; }
+
+ ///
+ /// Number of active bots (status = Up)
+ ///
+ [Id(3)]
+ public int ActiveBotsCount { get; set; }
+}
\ No newline at end of file
diff --git a/src/Managing.Application.Abstractions/Grains/IBacktestTradingBotGrain.cs b/src/Managing.Application.Abstractions/Grains/IBacktestTradingBotGrain.cs
index f89cb57..9bffc51 100644
--- a/src/Managing.Application.Abstractions/Grains/IBacktestTradingBotGrain.cs
+++ b/src/Managing.Application.Abstractions/Grains/IBacktestTradingBotGrain.cs
@@ -23,23 +23,5 @@ public interface IBacktestTradingBotGrain : IGrainWithGuidKey
/// The request ID to associate with this backtest
/// Additional metadata to associate with this backtest
/// The complete backtest result
- Task RunBacktestAsync(TradingBotConfig config, List candles, User user = null, bool save = false, bool withCandles = false, string requestId = null, object metadata = null);
-
- ///
- /// Gets the current backtest progress
- ///
- /// Backtest progress information
- Task GetBacktestProgressAsync();
+ Task RunBacktestAsync(TradingBotConfig config, HashSet candles, User user = null, bool save = false, bool withCandles = false, string requestId = null, object metadata = null);
}
-
-///
-/// Represents the progress of a backtest
-///
-public class BacktestProgress
-{
- public bool IsInitialized { get; set; }
- public int TotalCandles { get; set; }
- public int ProcessedCandles { get; set; }
- public double ProgressPercentage { get; set; }
- public bool IsComplete { get; set; }
-}
\ No newline at end of file
diff --git a/src/Managing.Application.Abstractions/Grains/ILiveBotRegistryGrain.cs b/src/Managing.Application.Abstractions/Grains/ILiveBotRegistryGrain.cs
new file mode 100644
index 0000000..06be513
--- /dev/null
+++ b/src/Managing.Application.Abstractions/Grains/ILiveBotRegistryGrain.cs
@@ -0,0 +1,51 @@
+using Orleans;
+using static Managing.Common.Enums;
+
+namespace Managing.Application.Abstractions.Grains;
+
+///
+/// Orleans grain interface for LiveBotRegistry operations.
+/// This interface defines the distributed, async operations available for the bot registry.
+/// The registry acts as a central, durable directory for all LiveTradingBot grains.
+///
+public interface ILiveBotRegistryGrain : IGrainWithIntegerKey
+{
+ ///
+ /// Registers a new bot with its user ID. This should be called by the LiveTradingBotGrain when it is first initialized.
+ /// The initial status will be BotStatus.Up.
+ ///
+ /// The unique identifier of the bot
+ /// The unique identifier of the user who owns the bot
+ /// A task that represents the asynchronous operation
+ Task RegisterBot(Guid identifier, int userId);
+
+ ///
+ /// Removes a bot from the registry. This should be a full removal, perhaps called when a user permanently deletes a bot.
+ ///
+ /// The unique identifier of the bot to unregister
+ /// A task that represents the asynchronous operation
+ Task UnregisterBot(Guid identifier);
+
+ ///
+ /// Returns a list of all bots in the registry. This is for a management dashboard to see all bots in the system.
+ ///
+ /// A list of all BotRegistryEntry objects in the registry
+ Task> GetAllBots();
+
+ ///
+ /// Returns a list of all bots associated with a specific user. This is the primary method for a user's watchlist.
+ ///
+ /// The unique identifier of the user
+ /// A list of BotRegistryEntry objects for the specified user
+ Task> GetBotsForUser(int userId);
+
+ ///
+ /// A dedicated method for updating only the bot's Status field (Up/Down).
+ /// This will be called by LiveTradingBot's StartAsync and StopAsync methods.
+ ///
+ /// The unique identifier of the bot
+ /// The new status to set for the bot
+ /// A task that represents the asynchronous operation
+ Task UpdateBotStatus(Guid identifier, BotStatus status);
+ Task GetBotStatus(Guid identifier);
+}
\ No newline at end of file
diff --git a/src/Managing.Application.Abstractions/Grains/ILiveTradingBotGrain.cs b/src/Managing.Application.Abstractions/Grains/ILiveTradingBotGrain.cs
new file mode 100644
index 0000000..2599042
--- /dev/null
+++ b/src/Managing.Application.Abstractions/Grains/ILiveTradingBotGrain.cs
@@ -0,0 +1,43 @@
+using Managing.Application.Abstractions.Models;
+using Managing.Domain.Accounts;
+using Managing.Domain.Bots;
+using Managing.Domain.Trades;
+using Managing.Domain.Users;
+using Orleans;
+using static Managing.Common.Enums;
+
+namespace Managing.Application.Abstractions.Grains;
+
+///
+/// Orleans grain interface for TradingBot operations.
+/// This interface defines the distributed, async operations available for trading bots.
+///
+public interface ILiveTradingBotGrain : IGrainWithGuidKey
+{
+ ///
+ /// Manually opens a position in the specified direction
+ ///
+ /// The direction of the trade (Long/Short)
+ /// The created Position object
+ Task OpenPositionManuallyAsync(TradeDirection direction);
+
+ ///
+ /// Gets comprehensive bot data including positions, signals, and performance metrics
+ ///
+ Task GetBotDataAsync();
+
+ Task CreateAsync(TradingBotConfig config, User user);
+ Task StartAsync();
+ Task StopAsync();
+
+ Task UpdateConfiguration(TradingBotConfig newConfig);
+ Task GetAccount();
+ Task GetConfiguration();
+ Task ClosePositionAsync(Guid positionId);
+ Task RestartAsync();
+
+ ///
+ /// Deletes the bot and cleans up all associated resources
+ ///
+ Task DeleteAsync();
+}
\ No newline at end of file
diff --git a/src/Managing.Application.Abstractions/Grains/ITradingBotGrain.cs b/src/Managing.Application.Abstractions/Grains/ITradingBotGrain.cs
deleted file mode 100644
index 16e802e..0000000
--- a/src/Managing.Application.Abstractions/Grains/ITradingBotGrain.cs
+++ /dev/null
@@ -1,94 +0,0 @@
-using Managing.Application.Abstractions.Models;
-using Managing.Domain.Bots;
-using Managing.Domain.Trades;
-using Orleans;
-using static Managing.Common.Enums;
-
-namespace Managing.Application.Abstractions.Grains;
-
-///
-/// Orleans grain interface for TradingBot operations.
-/// This interface defines the distributed, async operations available for trading bots.
-///
-public interface ITradingBotGrain : IGrainWithGuidKey
-{
- ///
- /// Starts the trading bot asynchronously
- ///
- Task StartAsync();
-
- ///
- /// Stops the trading bot asynchronously
- ///
- Task StopAsync();
-
- ///
- /// Gets the current status of the trading bot
- ///
- Task GetStatusAsync();
-
- ///
- /// Gets the current configuration of the trading bot
- ///
- Task GetConfigurationAsync();
-
- ///
- /// Updates the trading bot configuration
- ///
- /// The new configuration to apply
- /// True if the configuration was successfully updated
- Task UpdateConfigurationAsync(TradingBotConfig newConfig);
-
- ///
- /// Manually opens a position in the specified direction
- ///
- /// The direction of the trade (Long/Short)
- /// The created Position object
- Task OpenPositionManuallyAsync(TradeDirection direction);
-
- ///
- /// Toggles the bot between watch-only and trading mode
- ///
- Task ToggleIsForWatchOnlyAsync();
-
- ///
- /// Gets comprehensive bot data including positions, signals, and performance metrics
- ///
- Task GetBotDataAsync();
-
- ///
- /// Loads a bot backup into the grain state
- ///
- /// The bot backup to load
- Task LoadBackupAsync(BotBackup backup);
-
- ///
- /// Forces a backup save of the current bot state
- ///
- Task SaveBackupAsync();
-
- ///
- /// Gets the current profit and loss for the bot
- ///
- Task GetProfitAndLossAsync();
-
- ///
- /// Gets the current win rate percentage for the bot
- ///
- Task GetWinRateAsync();
-
- ///
- /// Gets the bot's execution count (number of Run cycles completed)
- ///
- Task GetExecutionCountAsync();
-
- ///
- /// Gets the bot's startup time
- ///
- Task GetStartupTimeAsync();
-
- ///
- /// Gets the bot's creation date
- ///
- Task GetCreateDateAsync();
-}
\ No newline at end of file
diff --git a/src/Managing.Application.Abstractions/Models/TradingBotResponse.cs b/src/Managing.Application.Abstractions/Models/TradingBotResponse.cs
index 6f3c92d..4b9307a 100644
--- a/src/Managing.Application.Abstractions/Models/TradingBotResponse.cs
+++ b/src/Managing.Application.Abstractions/Models/TradingBotResponse.cs
@@ -1,4 +1,5 @@
using Managing.Domain.Bots;
+using Managing.Domain.Indicators;
using Managing.Domain.Trades;
using Orleans;
using static Managing.Common.Enums;
@@ -16,7 +17,7 @@ public class TradingBotResponse
/// Bot identifier
///
[Id(0)]
- public string Identifier { get; set; } = string.Empty;
+ public Guid Identifier { get; set; } = Guid.Empty;
///
/// Bot display name
@@ -37,16 +38,16 @@ public class TradingBotResponse
public TradingBotConfig Config { get; set; }
///
- /// Trading positions
+ /// Trading positions dictionary, keyed by position identifier
///
[Id(4)]
- public List Positions { get; set; } = new();
+ public Dictionary Positions { get; set; } = new();
///
- /// Trading signals
+ /// Trading signals dictionary, keyed by signal identifier
///
[Id(5)]
- public List Signals { get; set; } = new();
+ public Dictionary Signals { get; set; } = new();
///
/// Wallet balance history
diff --git a/src/Managing.Application.Abstractions/Repositories/IAgentSummaryRepository.cs b/src/Managing.Application.Abstractions/Repositories/IAgentSummaryRepository.cs
new file mode 100644
index 0000000..9a13df0
--- /dev/null
+++ b/src/Managing.Application.Abstractions/Repositories/IAgentSummaryRepository.cs
@@ -0,0 +1,30 @@
+using Managing.Domain.Statistics;
+using static Managing.Common.Enums;
+
+namespace Managing.Application.Abstractions.Repositories;
+
+public interface IAgentSummaryRepository
+{
+ Task GetByUserIdAsync(int userId);
+ Task GetByAgentNameAsync(string agentName);
+ Task> GetAllAsync();
+ Task InsertAsync(AgentSummary agentSummary);
+ Task UpdateAsync(AgentSummary agentSummary);
+ Task SaveOrUpdateAsync(AgentSummary agentSummary);
+
+ ///
+ /// Gets paginated agent summaries with sorting and filtering
+ ///
+ /// Page number (1-based)
+ /// Number of items per page
+ /// Field to sort by
+ /// Sort order (asc or desc)
+ /// Optional list of agent names to filter by
+ /// Tuple containing the paginated results and total count
+ Task<(IEnumerable Results, int TotalCount)> GetPaginatedAsync(
+ int page,
+ int pageSize,
+ SortableFields sortBy,
+ string sortOrder,
+ IEnumerable? agentNames = null);
+}
\ No newline at end of file
diff --git a/src/Managing.Application.Abstractions/Repositories/IBotRepository.cs b/src/Managing.Application.Abstractions/Repositories/IBotRepository.cs
index 9459e6d..7e82ee8 100644
--- a/src/Managing.Application.Abstractions/Repositories/IBotRepository.cs
+++ b/src/Managing.Application.Abstractions/Repositories/IBotRepository.cs
@@ -1,12 +1,40 @@
using Managing.Domain.Bots;
+using static Managing.Common.Enums;
namespace Managing.Application.Abstractions.Repositories;
public interface IBotRepository
{
- Task InsertBotAsync(BotBackup bot);
- Task> GetBotsAsync();
- Task UpdateBackupBot(BotBackup bot);
- Task DeleteBotBackup(string botName);
- Task GetBotByIdentifierAsync(string identifier);
+ Task InsertBotAsync(Bot bot);
+ Task> GetBotsAsync();
+ Task UpdateBot(Bot bot);
+ Task DeleteBot(Guid identifier);
+ Task GetBotByIdentifierAsync(Guid identifier);
+ Task> GetBotsByIdsAsync(IEnumerable identifiers);
+ Task> GetBotsByUserIdAsync(int id);
+ Task> GetBotsByStatusAsync(BotStatus status);
+ Task GetBotByNameAsync(string name);
+
+ ///
+ /// Gets paginated bots with filtering and sorting
+ ///
+ /// Page number (1-based)
+ /// Number of items per page
+ /// Filter by status (optional)
+ /// Filter by user ID (optional)
+ /// Filter by name (partial match, case-insensitive)
+ /// Filter by ticker (partial match, case-insensitive)
+ /// Filter by agent name (partial match, case-insensitive)
+ /// Sort field
+ /// Sort direction ("Asc" or "Desc")
+ /// Tuple containing the bots for the current page and total count
+ Task<(IEnumerable 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");
}
\ No newline at end of file
diff --git a/src/Managing.Application.Abstractions/Repositories/ICandleRepository.cs b/src/Managing.Application.Abstractions/Repositories/ICandleRepository.cs
index 28e04bf..810bf5c 100644
--- a/src/Managing.Application.Abstractions/Repositories/ICandleRepository.cs
+++ b/src/Managing.Application.Abstractions/Repositories/ICandleRepository.cs
@@ -5,18 +5,20 @@ namespace Managing.Application.Abstractions.Repositories;
public interface ICandleRepository
{
- Task> GetCandles(
- Enums.TradingExchanges exchange,
- Enums.Ticker ticker,
- Enums.Timeframe timeframe,
- DateTime start);
-
- Task> GetCandles(
+ Task> GetCandles(
Enums.TradingExchanges exchange,
Enums.Ticker ticker,
Enums.Timeframe timeframe,
DateTime start,
- DateTime end);
+ int? limit = null);
+
+ Task> GetCandles(
+ Enums.TradingExchanges exchange,
+ Enums.Ticker ticker,
+ Enums.Timeframe timeframe,
+ DateTime start,
+ DateTime end,
+ int? limit = null);
Task> GetTickersAsync(
Enums.TradingExchanges exchange,
diff --git a/src/Managing.Application.Abstractions/Repositories/ITradingRepository.cs b/src/Managing.Application.Abstractions/Repositories/ITradingRepository.cs
index 011c0b7..c15a72c 100644
--- a/src/Managing.Application.Abstractions/Repositories/ITradingRepository.cs
+++ b/src/Managing.Application.Abstractions/Repositories/ITradingRepository.cs
@@ -13,18 +13,20 @@ public interface ITradingRepository
Task GetSignalByIdentifierAsync(string identifier, User user = null);
Task InsertPositionAsync(Position position);
Task UpdatePositionAsync(Position position);
- Task GetStrategyByNameAsync(string strategy);
+ Task GetStrategyByNameAsync(string strategy);
Task InsertScenarioAsync(Scenario scenario);
- Task InsertStrategyAsync(Indicator indicator);
+ Task InsertIndicatorAsync(IndicatorBase indicator);
Task> GetScenariosAsync();
- Task> GetStrategiesAsync();
- Task> GetIndicatorsAsync();
+ Task> GetStrategiesAsync();
+ Task> GetIndicatorsAsync();
Task DeleteScenarioAsync(string name);
Task DeleteIndicatorAsync(string name);
- Task GetPositionByIdentifierAsync(string identifier);
+ Task GetPositionByIdentifierAsync(Guid identifier);
Task> GetPositionsAsync(PositionInitiator positionInitiator);
Task> GetPositionsByStatusAsync(PositionStatus positionStatus);
Task UpdateScenarioAsync(Scenario scenario);
- Task UpdateStrategyAsync(Indicator indicator);
+ Task UpdateStrategyAsync(IndicatorBase indicatorBase);
+ Task GetStrategyByNameUserAsync(string name, User user);
+ Task GetScenarioByNameUserAsync(string scenarioName, User user);
}
\ No newline at end of file
diff --git a/src/Managing.Application.Abstractions/Services/IBacktester.cs b/src/Managing.Application.Abstractions/Services/IBacktester.cs
index 9490ad2..0173f1e 100644
--- a/src/Managing.Application.Abstractions/Services/IBacktester.cs
+++ b/src/Managing.Application.Abstractions/Services/IBacktester.cs
@@ -45,7 +45,7 @@ namespace Managing.Application.Abstractions.Services
/// The lightweight backtest results
Task RunTradingBotBacktest(
TradingBotConfig config,
- List candles,
+ HashSet candles,
User user = null,
bool withCandles = false,
string requestId = null,
diff --git a/src/Managing.Application.Abstractions/Services/IExchangeService.cs b/src/Managing.Application.Abstractions/Services/IExchangeService.cs
index 43fd508..e5d9c9e 100644
--- a/src/Managing.Application.Abstractions/Services/IExchangeService.cs
+++ b/src/Managing.Application.Abstractions/Services/IExchangeService.cs
@@ -45,16 +45,18 @@ public interface IExchangeService
Task> GetTrades(Account account, Ticker ticker);
Task CancelOrder(Account account, Ticker ticker);
decimal GetFee(Account account, bool isForPaperTrading = false);
- Candle GetCandle(Account account, Ticker ticker, DateTime date);
+ Task GetCandle(Account account, Ticker ticker, DateTime date);
Task GetQuantityInPosition(Account account, Ticker ticker);
- Task> GetCandlesInflux(TradingExchanges exchange, Ticker ticker, DateTime startDate,
- Timeframe timeframe);
+ Task> GetCandlesInflux(TradingExchanges exchange, Ticker ticker, DateTime startDate,
+ Timeframe timeframe, int? limit = null);
- Task> GetCandlesInflux(TradingExchanges exchange, Ticker ticker, DateTime startDate,
- Timeframe timeframe, DateTime endDate);
+ Task> GetCandlesInflux(TradingExchanges exchange, Ticker ticker, DateTime startDate,
+ Timeframe timeframe, DateTime endDate, int? limit = null);
+
+ Task GetBestPrice(Account account, Ticker ticker, decimal lastPrice, decimal quantity,
+ TradeDirection direction);
- Task GetBestPrice(Account account, Ticker ticker, decimal lastPrice, decimal quantity, TradeDirection direction);
Orderbook GetOrderbook(Account account, Ticker ticker);
Trade BuildEmptyTrade(Ticker ticker, decimal price, decimal quantity, TradeDirection direction, decimal? leverage,
diff --git a/src/Managing.Application.Abstractions/Services/IStatisticService.cs b/src/Managing.Application.Abstractions/Services/IStatisticService.cs
index 5b615a1..3884ead 100644
--- a/src/Managing.Application.Abstractions/Services/IStatisticService.cs
+++ b/src/Managing.Application.Abstractions/Services/IStatisticService.cs
@@ -28,4 +28,6 @@ public interface IStatisticService
Task UpdateTopVolumeTicker(Enums.TradingExchanges exchange, int top);
Task UpdateFundingRates();
Task> GetFundingRates();
+ Task SaveOrUpdateAgentSummary(AgentSummary agentSummary);
+ Task> GetAllAgentSummaries();
}
\ No newline at end of file
diff --git a/src/Managing.Application.Abstractions/Services/ISynthPredictionService.cs b/src/Managing.Application.Abstractions/Services/ISynthPredictionService.cs
index 6e0b096..9652857 100644
--- a/src/Managing.Application.Abstractions/Services/ISynthPredictionService.cs
+++ b/src/Managing.Application.Abstractions/Services/ISynthPredictionService.cs
@@ -1,4 +1,5 @@
using Managing.Domain.Bots;
+using Managing.Domain.Indicators;
using Managing.Domain.Synth.Models;
using static Managing.Common.Enums;
@@ -94,7 +95,7 @@ public interface ISynthPredictionService
/// Bot configuration with Synth settings
/// Risk assessment result
Task MonitorPositionRiskAsync(Ticker ticker, TradeDirection direction, decimal currentPrice,
- decimal liquidationPrice, string positionIdentifier, TradingBotConfig botConfig);
+ decimal liquidationPrice, Guid positionIdentifier, TradingBotConfig botConfig);
///
/// Estimates liquidation price based on money management settings
@@ -103,5 +104,6 @@ public interface ISynthPredictionService
/// Position direction
/// Money management settings
/// Estimated liquidation price
- decimal EstimateLiquidationPrice(decimal currentPrice, TradeDirection direction, LightMoneyManagement moneyManagement);
+ decimal EstimateLiquidationPrice(decimal currentPrice, TradeDirection direction,
+ LightMoneyManagement moneyManagement);
}
\ No newline at end of file
diff --git a/src/Managing.Application.Abstractions/Services/ITradingService.cs b/src/Managing.Application.Abstractions/Services/ITradingService.cs
index 2d0d846..2202446 100644
--- a/src/Managing.Application.Abstractions/Services/ITradingService.cs
+++ b/src/Managing.Application.Abstractions/Services/ITradingService.cs
@@ -1,12 +1,14 @@
īģŋusing Managing.Domain.Accounts;
using Managing.Domain.Bots;
using Managing.Domain.Candles;
+using Managing.Domain.Indicators;
using Managing.Domain.Scenarios;
using Managing.Domain.Statistics;
using Managing.Domain.Strategies;
using Managing.Domain.Strategies.Base;
using Managing.Domain.Synth.Models;
using Managing.Domain.Trades;
+using Managing.Domain.Users;
using Managing.Infrastructure.Evm.Models.Privy;
using static Managing.Common.Enums;
@@ -17,20 +19,20 @@ public interface ITradingService
Task GetScenarioByNameAsync(string scenario);
Task InsertPositionAsync(Position position);
Task UpdatePositionAsync(Position position);
- Task GetStrategyByNameAsync(string strategy);
+ Task GetIndicatorByNameAsync(string strategy);
Task InsertScenarioAsync(Scenario scenario);
- Task InsertStrategyAsync(Indicator indicator);
+ Task InsertIndicatorAsync(IndicatorBase indicatorBase);
Task> GetScenariosAsync();
- Task> GetStrategiesAsync();
+ Task> GetIndicatorsAsync();
Task DeleteScenarioAsync(string name);
- Task DeleteStrategyAsync(string name);
- Task GetPositionByIdentifierAsync(string identifier);
+ Task DeleteIndicatorAsync(string name);
+ Task GetPositionByIdentifierAsync(Guid identifier);
Task ManagePosition(Account account, Position position);
Task WatchTrader();
Task> GetTradersWatch();
Task UpdateScenarioAsync(Scenario scenario);
- Task UpdateStrategyAsync(Indicator indicator);
+ Task UpdateIndicatorAsync(IndicatorBase indicatorBase);
Task> GetBrokerPositions(Account account);
Task InitPrivyWallet(string publicAddress);
@@ -43,7 +45,7 @@ public interface ITradingService
TradingBotConfig botConfig, bool isBacktest);
Task MonitorSynthPositionRiskAsync(Ticker ticker, TradeDirection direction, decimal currentPrice,
- decimal liquidationPrice, string positionIdentifier, TradingBotConfig botConfig);
+ decimal liquidationPrice, Guid positionIdentifier, TradingBotConfig botConfig);
///
/// Calculates indicators values for a given scenario and candles.
@@ -53,5 +55,8 @@ public interface ITradingService
/// A dictionary of indicator types to their calculated values.
Dictionary CalculateIndicatorsValuesAsync(
Scenario scenario,
- List candles);
+ HashSet candles);
+
+ Task GetIndicatorByNameUserAsync(string name, User user);
+ Task GetScenarioByNameUserAsync(string scenarioName, User user);
}
\ No newline at end of file
diff --git a/src/Managing.Application.Abstractions/Services/IUserService.cs b/src/Managing.Application.Abstractions/Services/IUserService.cs
index a056352..999adf4 100644
--- a/src/Managing.Application.Abstractions/Services/IUserService.cs
+++ b/src/Managing.Application.Abstractions/Services/IUserService.cs
@@ -5,9 +5,10 @@ namespace Managing.Application.Abstractions.Services;
public interface IUserService
{
Task Authenticate(string name, string address, string message, string signature);
- Task GetUserByAddressAsync(string address);
+ Task GetUserByAddressAsync(string address, bool useCache = true);
Task UpdateAgentName(User user, string agentName);
Task UpdateAvatarUrl(User user, string avatarUrl);
Task UpdateTelegramChannel(User user, string telegramChannel);
- Task GetUser(string name);
+ Task GetUserByName(string name);
+ Task GetUserByAgentName(string agentName);
}
\ No newline at end of file
diff --git a/src/Managing.Application.Tests/BotsTests.cs b/src/Managing.Application.Tests/BotsTests.cs
index b8b84f6..ac4ed7f 100644
--- a/src/Managing.Application.Tests/BotsTests.cs
+++ b/src/Managing.Application.Tests/BotsTests.cs
@@ -4,14 +4,14 @@ using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using Managing.Application.Backtesting;
-using Managing.Application.Bots.Base;
using Managing.Application.Hubs;
-using Managing.Application.ManageBot;
using Managing.Core;
using Managing.Domain.Bots;
using Managing.Domain.Candles;
using Managing.Domain.MoneyManagements;
using Managing.Domain.Scenarios;
+using Managing.Domain.Strategies;
+using Managing.Domain.Strategies.Signals;
using Microsoft.AspNetCore.SignalR;
using Moq;
using Newtonsoft.Json;
@@ -22,7 +22,6 @@ namespace Managing.Application.Tests
{
public class BotsTests : BaseTests
{
- private readonly IBotFactory _botFactory;
private readonly IBacktester _backtester;
private readonly string _reportPath;
private string _analysePath;
@@ -37,19 +36,11 @@ namespace Managing.Application.Tests
var scenarioService = new Mock().Object;
var messengerService = new Mock().Object;
var kaigenService = new Mock().Object;
- var backupBotService = new Mock().Object;
var hubContext = new Mock>().Object;
var tradingBotLogger = TradingBaseTests.CreateTradingBotLogger();
var backtestLogger = TradingBaseTests.CreateBacktesterLogger();
var botService = new Mock().Object;
- _botFactory = new BotFactory(
- _exchangeService,
- tradingBotLogger,
- discordService,
- _accountService.Object,
- _tradingService.Object,
- botService, backupBotService);
- _backtester = new Backtester(_exchangeService, _botFactory, backtestRepository, backtestLogger,
+ _backtester = new Backtester(_exchangeService, backtestRepository, backtestLogger,
scenarioService, _accountService.Object, messengerService, kaigenService, hubContext, null);
_elapsedTimes = new List();
@@ -68,7 +59,6 @@ namespace Managing.Application.Tests
// Arrange
var scenario = new Scenario("FlippingScenario");
var strategy = ScenarioHelpers.BuildIndicator(IndicatorType.RsiDivergence, "RsiDiv", period: 14);
- scenario.AddIndicator(strategy);
var localCandles =
FileHelpers.ReadJson>($"{ticker.ToString()}-{timeframe.ToString()}-candles.json");
@@ -93,10 +83,11 @@ namespace Managing.Application.Tests
// Act
var backtestResult =
- await _backtester.RunTradingBotBacktest(config, localCandles.TakeLast(500).ToList(), null, false);
+ await _backtester.RunTradingBotBacktest(config, DateTime.UtcNow.AddDays(-6),
+ DateTime.UtcNow, null, false, false);
var json = JsonConvert.SerializeObject(backtestResult, Formatting.None);
- File.WriteAllText($"{ticker.ToString()}-{timeframe.ToString()}-{Guid.NewGuid()}.json", json);
+ File.WriteAllText($"{ticker}-{timeframe}-{Guid.NewGuid()}.json", json);
// WriteCsvReport(backtestResult.GetStringReport());
// Assert
@@ -119,8 +110,6 @@ namespace Managing.Application.Tests
{
// Arrange
var scenario = new Scenario("ScalpingScenario");
- var strategy = ScenarioHelpers.BuildIndicator(IndicatorType.RsiDivergence, "RsiDiv", period: 5);
- scenario.AddIndicator(strategy);
var config = new TradingBotConfig
{
@@ -158,10 +147,13 @@ namespace Managing.Application.Tests
int days)
{
// Arrange
- var scenario = new Scenario("ScalpingScenario");
- var strategy = ScenarioHelpers.BuildIndicator(IndicatorType.MacdCross, "RsiDiv", fastPeriods: 12,
- slowPeriods: 26, signalPeriods: 9);
- scenario.AddIndicator(strategy);
+ var scenario = new Scenario("ScalpingScenario")
+ {
+ Indicators = new List
+ {
+ new MacdCrossIndicatorBase("MacdCross", 12, 26, 9)
+ }
+ };
var moneyManagement = new MoneyManagement()
{
@@ -236,8 +228,10 @@ namespace Managing.Application.Tests
Parallel.For((long)periodRange[0], periodRange[1], options, i =>
{
var scenario = new Scenario("ScalpingScenario");
- var strategy = ScenarioHelpers.BuildIndicator(indicatorType, "RsiDiv", period: (int)i);
- scenario.AddIndicator(strategy);
+ scenario.Indicators = new List
+ {
+ new RsiDivergenceIndicatorBase("RsiDiv", (int)i)
+ };
// -0.5 to -5
for (decimal s = stopLossRange[0]; s < stopLossRange[1]; s += stopLossRange[2])
@@ -278,7 +272,8 @@ namespace Managing.Application.Tests
FlipOnlyWhenInProfit = true,
MaxPositionTimeHours = null,
CloseEarlyWhenProfitable = false
- }, candles, null, false).Result,
+ }, DateTime.UtcNow.AddDays(-6),
+ DateTime.UtcNow, null, false, false).Result,
BotType.FlippingBot => _backtester.RunTradingBotBacktest(new TradingBotConfig
{
AccountName = _account.Name,
@@ -296,7 +291,8 @@ namespace Managing.Application.Tests
FlipOnlyWhenInProfit = true,
MaxPositionTimeHours = null,
CloseEarlyWhenProfitable = false
- }, candles, null, false).Result,
+ }, DateTime.UtcNow.AddDays(-6),
+ DateTime.UtcNow, null, false, false).Result,
_ => throw new NotImplementedException(),
};
timer.Stop();
@@ -376,9 +372,10 @@ namespace Managing.Application.Tests
return;
var scenario = new Scenario("ScalpingScenario");
- var strategy = ScenarioHelpers.BuildIndicator(indicatorType, "RsiDiv", fastPeriods: 12,
- slowPeriods: 26, signalPeriods: 9);
- scenario.AddIndicator(strategy);
+ scenario.Indicators = new List
+ {
+ new MacdCrossIndicatorBase("MacdCross", 12, 26, 9)
+ };
// -0.5 to -5
for (decimal s = stopLossRange[0]; s < stopLossRange[1]; s += stopLossRange[2])
@@ -418,7 +415,8 @@ namespace Managing.Application.Tests
FlipOnlyWhenInProfit = true,
MaxPositionTimeHours = null,
CloseEarlyWhenProfitable = false
- }, candles, null).Result,
+ }, DateTime.UtcNow.AddDays(-6),
+ DateTime.UtcNow, null, false, false).Result,
BotType.FlippingBot => _backtester.RunTradingBotBacktest(new TradingBotConfig
{
AccountName = _account.Name,
@@ -436,7 +434,8 @@ namespace Managing.Application.Tests
FlipOnlyWhenInProfit = true,
MaxPositionTimeHours = null,
CloseEarlyWhenProfitable = false
- }, candles, null).Result,
+ }, DateTime.UtcNow.AddDays(-6),
+ DateTime.UtcNow, null, false, false).Result,
_ => throw new NotImplementedException(),
};
@@ -673,7 +672,8 @@ namespace Managing.Application.Tests
CloseEarlyWhenProfitable = false
};
- var backtestResult = _backtester.RunTradingBotBacktest(config, candles, null).Result;
+ var backtestResult = _backtester.RunTradingBotBacktest(config, DateTime.UtcNow.AddDays(-6),
+ DateTime.UtcNow, null, false, false).Result;
timer.Stop();
@@ -1042,8 +1042,13 @@ namespace Managing.Application.Tests
{
foreach (var parameterSet in strategyConfig.ParameterSets)
{
- var scenario = BuildScenario($"{strategyConfig.Name}_{parameterSet.Name}",
- new[] { (strategyConfig, parameterSet) });
+ var scenario = new Scenario($"{strategyConfig.Name}_{parameterSet.Name}")
+ {
+ Indicators = new List
+ {
+ new RsiDivergenceIndicatorBase("RsiDiv", (int)parameterSet.Period)
+ }
+ };
scenarios.Add(scenario);
}
}
@@ -1068,7 +1073,13 @@ namespace Managing.Application.Tests
{
var scenarioName = string.Join("_",
paramCombo.Select(p => $"{p.strategyConfig.Name}_{p.parameterSet.Name}"));
- var scenario = BuildScenario(scenarioName, paramCombo);
+ var scenario = new Scenario(scenarioName)
+ {
+ Indicators = new List
+ {
+ new RsiDivergenceIndicatorBase("RsiDiv", (int)paramCombo.First().parameterSet.Period)
+ }
+ };
scenario.LoopbackPeriod = 15;
scenarios.Add(scenario);
}
@@ -1077,31 +1088,6 @@ namespace Managing.Application.Tests
return scenarios;
}
- private Scenario BuildScenario(string scenarioName,
- IEnumerable<(StrategyConfiguration strategyConfig, ParameterSet parameterSet)> strategyParams)
- {
- var scenario = new Scenario(scenarioName);
-
- foreach (var (strategyConfig, parameterSet) in strategyParams)
- {
- var strategy = ScenarioHelpers.BuildIndicator(
- strategyConfig.Type,
- $"{strategyConfig.Name}_{parameterSet.Name}",
- period: parameterSet.Period,
- fastPeriods: parameterSet.FastPeriods,
- slowPeriods: parameterSet.SlowPeriods,
- signalPeriods: parameterSet.SignalPeriods,
- multiplier: parameterSet.Multiplier,
- stochPeriods: parameterSet.StochPeriods,
- smoothPeriods: parameterSet.SmoothPeriods,
- cyclePeriods: parameterSet.CyclePeriods);
-
- scenario.AddIndicator(strategy);
- }
-
- return scenario;
- }
-
private IEnumerable> GetCombinations(IEnumerable elements, int k)
{
return k == 0
diff --git a/src/Managing.Application.Tests/IndicatorTests.cs b/src/Managing.Application.Tests/IndicatorBaseTests.cs
similarity index 70%
rename from src/Managing.Application.Tests/IndicatorTests.cs
rename to src/Managing.Application.Tests/IndicatorBaseTests.cs
index a1c48db..541f6e8 100644
--- a/src/Managing.Application.Tests/IndicatorTests.cs
+++ b/src/Managing.Application.Tests/IndicatorBaseTests.cs
@@ -1,5 +1,7 @@
īģŋusing Managing.Application.Abstractions.Services;
using Managing.Domain.Accounts;
+using Managing.Domain.Candles;
+using Managing.Domain.Indicators;
using Managing.Domain.Strategies.Signals;
using Managing.Domain.Strategies.Trends;
using Xunit;
@@ -7,31 +9,30 @@ using static Managing.Common.Enums;
namespace Managing.Application.Tests
{
- public class IndicatorTests
+ public class IndicatorBaseTests
{
private readonly IExchangeService _exchangeService;
- public IndicatorTests()
+ public IndicatorBaseTests()
{
_exchangeService = TradingBaseTests.GetExchangeService();
}
[Theory]
[InlineData(TradingExchanges.Binance, Ticker.ADA, Timeframe.OneDay)]
- public void Should_Return_Signal_On_Rsi_BullishDivergence2(TradingExchanges exchange, Ticker ticker,
+ public async Task Should_Return_Signal_On_Rsi_BullishDivergence2(TradingExchanges exchange, Ticker ticker,
Timeframe timeframe)
{
var account = GetAccount(exchange);
// Arrange
- var rsiStrategy = new RsiDivergenceIndicator("unittest", 5);
- var candles = _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(-50), timeframe).Result;
+ var rsiStrategy = new RsiDivergenceIndicatorBase("unittest", 5);
+ var candles = await _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(-50), timeframe);
var resultSignal = new List();
// Act
foreach (var candle in candles)
{
- rsiStrategy.Candles.Enqueue(candle);
- var signals = rsiStrategy.Run();
+ var signals = rsiStrategy.Run(new HashSet { candle });
}
if (rsiStrategy.Signals != null && rsiStrategy.Signals.Count > 0)
@@ -52,20 +53,19 @@ namespace Managing.Application.Tests
[Theory]
[InlineData(TradingExchanges.Binance, Ticker.ADA, Timeframe.OneDay)]
- public void Shoud_Return_Signal_On_Rsi_BearishDivergence(TradingExchanges exchange, Ticker ticker,
+ public async Task Shoud_Return_Signal_On_Rsi_BearishDivergence(TradingExchanges exchange, Ticker ticker,
Timeframe timeframe)
{
// Arrange
var account = GetAccount(exchange);
- var rsiStrategy = new RsiDivergenceIndicator("unittest", 5);
- var candles = _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(-50), timeframe).Result;
+ var rsiStrategy = new RsiDivergenceIndicatorBase("unittest", 5);
+ var candles = await _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(-50), timeframe);
var resultSignal = new List();
// Act
foreach (var candle in candles)
{
- rsiStrategy.Candles.Enqueue(candle);
- var signals = rsiStrategy.Run();
+ var signals = rsiStrategy.Run(new HashSet { candle });
}
if (rsiStrategy.Signals != null && rsiStrategy.Signals.Count > 0)
@@ -84,15 +84,14 @@ namespace Managing.Application.Tests
{
// Arrange
var account = GetAccount(exchange);
- var rsiStrategy = new MacdCrossIndicator("unittest", 12, 26, 9);
+ var rsiStrategy = new MacdCrossIndicatorBase("unittest", 12, 26, 9);
var candles = await _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(days), timeframe);
var resultSignal = new List();
// Act
foreach (var candle in candles)
{
- rsiStrategy.Candles.Enqueue(candle);
- var signals = rsiStrategy.Run();
+ var signals = rsiStrategy.Run(new HashSet { candle });
}
if (rsiStrategy.Signals != null && rsiStrategy.Signals.Count > 0)
@@ -106,20 +105,20 @@ namespace Managing.Application.Tests
[Theory]
[InlineData(TradingExchanges.Ftx, Ticker.ADA, Timeframe.OneDay, -500)]
- public void Shoud_Return_Signal_On_SuperTrend(TradingExchanges exchange, Ticker ticker, Timeframe timeframe,
+ public async Task Shoud_Return_Signal_On_SuperTrend(TradingExchanges exchange, Ticker ticker,
+ Timeframe timeframe,
int days)
{
// Arrange
var account = GetAccount(exchange);
- var superTrendStrategy = new SuperTrendIndicator("unittest", 10, 3);
- var candles = _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(days), timeframe).Result;
+ var superTrendStrategy = new SuperTrendIndicatorBase("unittest", 10, 3);
+ var candles = await _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(days), timeframe);
var resultSignal = new List();
// Act
foreach (var candle in candles)
{
- superTrendStrategy.Candles.Enqueue(candle);
- var signals = superTrendStrategy.Run();
+ var signals = superTrendStrategy.Run(new HashSet { candle });
}
if (superTrendStrategy.Signals != null && superTrendStrategy.Signals.Count > 0)
@@ -133,21 +132,20 @@ namespace Managing.Application.Tests
[Theory]
[InlineData(TradingExchanges.Ftx, Ticker.ADA, Timeframe.OneDay, -500)]
- public void Shoud_Return_Signal_On_ChandelierExist(TradingExchanges exchange, Ticker ticker,
+ public async Task Shoud_Return_Signal_On_ChandelierExist(TradingExchanges exchange, Ticker ticker,
Timeframe timeframe, int days)
{
// Arrange
var account = GetAccount(exchange);
- var chandelierExitStrategy = new ChandelierExitIndicator("unittest", 22, 3);
- var candles = _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(days), timeframe, false)
- .Result;
+ var chandelierExitStrategy = new ChandelierExitIndicatorBase("unittest", 22, 3);
+ var candles =
+ await _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(days), timeframe, false);
var resultSignal = new List();
// Act
foreach (var candle in candles)
{
- chandelierExitStrategy.Candles.Enqueue(candle);
- var signals = chandelierExitStrategy.Run();
+ var signals = chandelierExitStrategy.Run(new HashSet { candle });
}
if (chandelierExitStrategy.Signals is { Count: > 0 })
@@ -161,20 +159,19 @@ namespace Managing.Application.Tests
[Theory]
[InlineData(TradingExchanges.Ftx, Ticker.ADA, Timeframe.OneDay, -500)]
- public void Shoud_Return_Signal_On_EmaTrend(TradingExchanges exchange, Ticker ticker, Timeframe timeframe,
+ public async Task Shoud_Return_Signal_On_EmaTrend(TradingExchanges exchange, Ticker ticker, Timeframe timeframe,
int days)
{
// Arrange
var account = GetAccount(exchange);
- var emaTrendSrategy = new EmaTrendIndicator("unittest", 200);
- var candles = _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(days), timeframe).Result;
+ var emaTrendSrategy = new EmaTrendIndicatorBase("unittest", 200);
+ var candles = await _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(days), timeframe);
var resultSignal = new List();
// Act
foreach (var candle in candles)
{
- emaTrendSrategy.Candles.Enqueue(candle);
- var signals = emaTrendSrategy.Run();
+ var signals = emaTrendSrategy.Run(new HashSet { candle });
}
if (emaTrendSrategy.Signals != null && emaTrendSrategy.Signals.Count > 0)
@@ -189,13 +186,13 @@ namespace Managing.Application.Tests
[Theory]
[InlineData(TradingExchanges.Evm, Ticker.BTC, Timeframe.FifteenMinutes, -50)]
- public void Shoud_Return_Signal_On_StochRsi(TradingExchanges exchange, Ticker ticker, Timeframe timeframe,
+ public async Task Shoud_Return_Signal_On_StochRsi(TradingExchanges exchange, Ticker ticker, Timeframe timeframe,
int days)
{
// Arrange
var account = GetAccount(exchange);
- var stochRsiStrategy = new StochRsiTrendIndicator("unittest", 14, 14, 3, 1);
- var candles = _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(days), timeframe).Result;
+ var stochRsiStrategy = new StochRsiTrendIndicatorBase("unittest", 14, 14, 3, 1);
+ var candles = await _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(days), timeframe);
var resultSignal = new List();
// var json = JsonConvert.SerializeObject(candles);
@@ -205,8 +202,7 @@ namespace Managing.Application.Tests
// Act
foreach (var candle in candles)
{
- stochRsiStrategy.Candles.Enqueue(candle);
- var signals = stochRsiStrategy.Run();
+ var signals = stochRsiStrategy.Run(new HashSet { candle });
}
if (stochRsiStrategy.Signals != null && stochRsiStrategy.Signals.Count > 0)
diff --git a/src/Managing.Application.Tests/PositionTests.cs b/src/Managing.Application.Tests/PositionTests.cs
index 1fa3c2d..bec878d 100644
--- a/src/Managing.Application.Tests/PositionTests.cs
+++ b/src/Managing.Application.Tests/PositionTests.cs
@@ -45,14 +45,14 @@ public class PositionTests : BaseTests
// _ = new GetAccountPositioqwnInfoListOutputDTO().DecodeOutput(hexPositions).d
//
var openTrade = await _exchangeService.GetTrade(_account, "", Ticker.GMX);
- var position = new Position("", "", TradeDirection.Long, Ticker.GMX, MoneyManagement, PositionInitiator.User,
+ var position = new Position(Guid.NewGuid(), "", TradeDirection.Long, Ticker.GMX, MoneyManagement, PositionInitiator.User,
DateTime.UtcNow, new User())
{
Open = openTrade
};
var command = new ClosePositionCommand(position);
- _ = _tradingService.Setup(m => m.GetPositionByIdentifierAsync(It.IsAny())).ReturnsAsync(position);
- _ = _tradingService.Setup(m => m.GetPositionByIdentifierAsync(It.IsAny())).ReturnsAsync(position);
+ _ = _tradingService.Setup(m => m.GetPositionByIdentifierAsync(It.IsAny())).ReturnsAsync(position);
+ _ = _tradingService.Setup(m => m.GetPositionByIdentifierAsync(It.IsAny())).ReturnsAsync(position);
var handler = new ClosePositionCommandHandler(
_exchangeService,
diff --git a/src/Managing.Application.Tests/ProfitAndLossTests.cs b/src/Managing.Application.Tests/ProfitAndLossTests.cs
index 0733bb2..6b4a6c2 100644
--- a/src/Managing.Application.Tests/ProfitAndLossTests.cs
+++ b/src/Managing.Application.Tests/ProfitAndLossTests.cs
@@ -214,7 +214,7 @@ namespace Managing.Application.Tests
private static Position GetFakeShortPosition()
{
- return new Position("", "FakeAccount", TradeDirection.Short, Ticker.BTC, null,
+ return new Position(Guid.NewGuid(), "FakeAccount", TradeDirection.Short, Ticker.BTC, null,
PositionInitiator.PaperTrading, DateTime.UtcNow, new User())
{
Open = new Trade(DateTime.Now, TradeDirection.Short, TradeStatus.Filled,
@@ -230,7 +230,7 @@ namespace Managing.Application.Tests
private static Position GetSolanaLongPosition()
{
- return new Position("", "FakeAccount", TradeDirection.Long, Ticker.BTC, null,
+ return new Position(Guid.NewGuid(), "FakeAccount", TradeDirection.Long, Ticker.BTC, null,
PositionInitiator.PaperTrading, DateTime.UtcNow, new User())
{
Open = new Trade(DateTime.Now, TradeDirection.Long, TradeStatus.Filled,
@@ -250,7 +250,7 @@ namespace Managing.Application.Tests
private static Position GetFakeLongPosition()
{
- return new Position("", "FakeAccount", TradeDirection.Long, Ticker.BTC, null,
+ return new Position(Guid.NewGuid(), "FakeAccount", TradeDirection.Long, Ticker.BTC, null,
PositionInitiator.PaperTrading, DateTime.UtcNow, new User())
{
Open = new Trade(DateTime.Now, TradeDirection.Short, TradeStatus.Filled,
diff --git a/src/Managing.Application.Workers/BundleBacktestWorker.cs b/src/Managing.Application.Workers/BundleBacktestWorker.cs
index 575fa3f..0c4e9da 100644
--- a/src/Managing.Application.Workers/BundleBacktestWorker.cs
+++ b/src/Managing.Application.Workers/BundleBacktestWorker.cs
@@ -226,24 +226,22 @@ public class BundleBacktestWorker : BaseWorker
if (runBacktestRequest.Config.Scenario != null)
{
var sReq = runBacktestRequest.Config.Scenario;
- scenario = new LightScenario(sReq.Name, sReq.LoopbackPeriod);
- foreach (var indicatorRequest in sReq.Indicators)
+ scenario = new LightScenario(sReq.Name, sReq.LoopbackPeriod)
{
- var indicator = new LightIndicator(indicatorRequest.Name, indicatorRequest.Type)
+ Indicators = sReq.Indicators?.Select(i => new LightIndicator(i.Name, i.Type)
{
- SignalType = indicatorRequest.SignalType,
- MinimumHistory = indicatorRequest.MinimumHistory,
- Period = indicatorRequest.Period,
- FastPeriods = indicatorRequest.FastPeriods,
- SlowPeriods = indicatorRequest.SlowPeriods,
- SignalPeriods = indicatorRequest.SignalPeriods,
- Multiplier = indicatorRequest.Multiplier,
- SmoothPeriods = indicatorRequest.SmoothPeriods,
- StochPeriods = indicatorRequest.StochPeriods,
- CyclePeriods = indicatorRequest.CyclePeriods,
- };
- scenario.AddIndicator(indicator);
- }
+ SignalType = i.SignalType,
+ MinimumHistory = i.MinimumHistory,
+ Period = i.Period,
+ FastPeriods = i.FastPeriods,
+ SlowPeriods = i.SlowPeriods,
+ SignalPeriods = i.SignalPeriods,
+ Multiplier = i.Multiplier,
+ SmoothPeriods = i.SmoothPeriods,
+ StochPeriods = i.StochPeriods,
+ CyclePeriods = i.CyclePeriods
+ }).ToList() ?? new List()
+ };
}
// Map TradingBotConfig
diff --git a/src/Managing.Application.Workers/StatisticService.cs b/src/Managing.Application.Workers/StatisticService.cs
index 6595f82..e500e7f 100644
--- a/src/Managing.Application.Workers/StatisticService.cs
+++ b/src/Managing.Application.Workers/StatisticService.cs
@@ -2,6 +2,7 @@
using Managing.Application.Abstractions.Services;
using Managing.Domain.Accounts;
using Managing.Domain.Bots;
+using Managing.Domain.Indicators;
using Managing.Domain.MoneyManagements;
using Managing.Domain.Scenarios;
using Managing.Domain.Shared.Helpers;
@@ -24,6 +25,7 @@ public class StatisticService : IStatisticService
private readonly IMessengerService _messengerService;
private readonly ICacheService _cacheService;
private readonly IAgentBalanceRepository _agentBalanceRepository;
+ private readonly IAgentSummaryRepository _agentSummaryRepository;
private readonly ILogger _logger;
public StatisticService(
@@ -37,7 +39,8 @@ public class StatisticService : IStatisticService
ITradaoService tradaoService,
IMessengerService messengerService,
ICacheService cacheService,
- IAgentBalanceRepository agentBalanceRepository)
+ IAgentBalanceRepository agentBalanceRepository,
+ IAgentSummaryRepository agentSummaryRepository)
{
_exchangeService = exchangeService;
_accountService = accountService;
@@ -50,6 +53,7 @@ public class StatisticService : IStatisticService
_messengerService = messengerService;
_cacheService = cacheService;
_agentBalanceRepository = agentBalanceRepository;
+ _agentSummaryRepository = agentSummaryRepository;
}
public async Task UpdateTopVolumeTicker(TradingExchanges exchange, int top)
@@ -497,4 +501,27 @@ public class StatisticService : IStatisticService
return (result, fetchedTotalCount);
}
+
+ public async Task SaveOrUpdateAgentSummary(AgentSummary agentSummary)
+ {
+ try
+ {
+ // Use the injected AgentSummaryRepository to save or update
+ await _agentSummaryRepository.SaveOrUpdateAsync(agentSummary);
+
+ _logger.LogInformation("AgentSummary saved/updated for user {UserId} with agent name {AgentName}",
+ agentSummary.UserId, agentSummary.AgentName);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error saving/updating AgentSummary for user {UserId} with agent name {AgentName}",
+ agentSummary.UserId, agentSummary.AgentName);
+ throw;
+ }
+ }
+
+ public async Task> GetAllAgentSummaries()
+ {
+ return await _agentSummaryRepository.GetAllAsync();
+ }
}
\ No newline at end of file
diff --git a/src/Managing.Application/Abstractions/Grains/IAgentGrain.cs b/src/Managing.Application/Abstractions/Grains/IAgentGrain.cs
new file mode 100644
index 0000000..b71ef86
--- /dev/null
+++ b/src/Managing.Application/Abstractions/Grains/IAgentGrain.cs
@@ -0,0 +1,27 @@
+namespace Managing.Application.Abstractions.Grains
+{
+ public interface IAgentGrain : IGrainWithIntegerKey
+ {
+ ///
+ /// Initializes the agent grain with user-specific data.
+ ///
+ /// The ID of the user (used as grain key).
+ /// The display name of the agent.
+ Task InitializeAsync(int userId, string agentName);
+
+ ///
+ /// Generates a summary of the agent's stats for the AgentRegistryGrain.
+ ///
+ Task UpdateSummary();
+
+ ///
+ /// Registers a new bot with this agent.
+ ///
+ Task RegisterBotAsync(Guid botId);
+
+ ///
+ /// Unregisters a bot from this agent.
+ ///
+ Task UnregisterBotAsync(Guid botId);
+ }
+}
\ No newline at end of file
diff --git a/src/Managing.Application/Abstractions/Grains/IScenarioRunnerGrain.cs b/src/Managing.Application/Abstractions/Grains/IScenarioRunnerGrain.cs
new file mode 100644
index 0000000..20d7dd3
--- /dev/null
+++ b/src/Managing.Application/Abstractions/Grains/IScenarioRunnerGrain.cs
@@ -0,0 +1,22 @@
+using Managing.Domain.Bots;
+using Managing.Domain.Candles;
+using Managing.Domain.Indicators;
+
+namespace Managing.Application.Abstractions.Grains;
+
+///
+/// Orleans grain interface for scenario execution and signal generation.
+/// This stateless grain handles candle management and signal generation for live trading.
+///
+public interface IScenarioRunnerGrain : IGrainWithGuidKey
+{
+ ///
+ /// Generates signals based on the current candles and scenario
+ ///
+ /// The trading bot configuration
+ /// Previous signals to consider
+ /// Start date
+ /// The generated signal or null if no signal
+ Task GetSignals(TradingBotConfig config, Dictionary previousSignals, DateTime startDate,
+ Candle candle);
+}
\ No newline at end of file
diff --git a/src/Managing.Application/Abstractions/IBotFactory.cs b/src/Managing.Application/Abstractions/IBotFactory.cs
deleted file mode 100644
index 7f96cc7..0000000
--- a/src/Managing.Application/Abstractions/IBotFactory.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-īģŋusing Managing.Domain.Bots;
-
-namespace Managing.Application.Abstractions
-{
- public interface IBotFactory
- {
- ///
- /// Creates a trading bot using the unified TradingBot class
- ///
- /// The trading bot configuration
- /// ITradingBot instance
- Task CreateTradingBot(TradingBotConfig config);
-
- ///
- /// Creates a trading bot for backtesting using the unified TradingBot class
- ///
- /// The trading bot configuration
- /// ITradingBot instance configured for backtesting
- Task CreateBacktestTradingBot(TradingBotConfig config);
- }
-}
\ No newline at end of file
diff --git a/src/Managing.Application/Abstractions/IBotService.cs b/src/Managing.Application/Abstractions/IBotService.cs
index 0960948..b634f55 100644
--- a/src/Managing.Application/Abstractions/IBotService.cs
+++ b/src/Managing.Application/Abstractions/IBotService.cs
@@ -1,38 +1,47 @@
using Managing.Domain.Bots;
-using Managing.Domain.Users;
-using Managing.Domain.Workflows;
+using Managing.Domain.Trades;
using static Managing.Common.Enums;
namespace Managing.Application.Abstractions;
public interface IBotService
{
- Task SaveOrUpdateBotBackup(User user, string identifier, BotStatus status, TradingBotBackup data);
- void AddSimpleBotToCache(IBot bot);
- void AddTradingBotToCache(ITradingBot bot);
- List GetActiveBots();
- Task> GetSavedBotsAsync();
- Task StartBotFromBackup(BotBackup backupBot);
- Task GetBotBackup(string identifier);
-
+ Task> GetBotsAsync();
+ Task> GetBotsByStatusAsync(BotStatus status);
+ Task StopBot(Guid identifier);
+ Task RestartBot(Guid identifier);
+ Task DeleteBot(Guid identifier);
+ Task UpdateBotConfiguration(Guid identifier, TradingBotConfig newConfig);
+ Task> GetActiveBotsNamesAsync();
+ Task> GetBotsByUser(int id);
+ Task> GetBotsByIdsAsync(IEnumerable botIds);
+ Task GetBotByName(string name);
+ Task GetBotByIdentifier(Guid identifier);
+ Task OpenPositionManuallyAsync(Guid identifier, TradeDirection direction);
+ Task ClosePositionAsync(Guid identifier, Guid positionId);
+ Task GetBotConfig(Guid identifier);
+ Task UpdateBotStatisticsAsync(Guid identifier);
+ Task SaveBotStatisticsAsync(Bot bot);
+
///
- /// Creates a trading bot using the unified TradingBot class
+ /// Gets paginated bots with filtering and sorting
///
- /// The trading bot configuration
- /// ITradingBot instance
- Task CreateTradingBot(TradingBotConfig config);
-
- ///
- /// Creates a trading bot for backtesting using the unified TradingBot class
- ///
- /// The trading bot configuration
- /// ITradingBot instance configured for backtesting
- Task CreateBacktestTradingBot(TradingBotConfig config);
-
- IBot CreateSimpleBot(string botName, Workflow workflow);
- Task StopBot(string botName);
- Task DeleteBot(string botName);
- Task RestartBot(string botName);
- Task ToggleIsForWatchingOnly(string botName);
- Task UpdateBotConfiguration(string identifier, TradingBotConfig newConfig);
+ /// Page number (1-based)
+ /// Number of items per page
+ /// Filter by status (optional)
+ /// Filter by name (partial match, case-insensitive)
+ /// Filter by ticker (partial match, case-insensitive)
+ /// Filter by agent name (partial match, case-insensitive)
+ /// Sort field
+ /// Sort direction ("Asc" or "Desc")
+ /// Tuple containing the bots for the current page and total count
+ Task<(IEnumerable 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");
}
\ No newline at end of file
diff --git a/src/Managing.Application/Abstractions/IScenarioService.cs b/src/Managing.Application/Abstractions/IScenarioService.cs
index 8eb0fe7..f4a7879 100644
--- a/src/Managing.Application/Abstractions/IScenarioService.cs
+++ b/src/Managing.Application/Abstractions/IScenarioService.cs
@@ -8,19 +8,7 @@ namespace Managing.Application.Abstractions
public interface IScenarioService
{
Task CreateScenario(string name, List strategies, int? loopbackPeriod = 1);
- Task> GetIndicatorsAsync();
-
- Task CreateStrategy(IndicatorType type,
- string name,
- int? period = null,
- int? fastPeriods = null,
- int? slowPeriods = null,
- int? signalPeriods = null,
- double? multiplier = null,
- int? stochPeriods = null,
- int? smoothPeriods = null,
- int? cyclePeriods = null);
-
+ Task> GetIndicatorsAsync();
Task UpdateScenario(string name, List strategies, int? loopbackPeriod);
Task UpdateStrategy(IndicatorType indicatorType, string name, int? period, int? fastPeriods,
@@ -29,12 +17,12 @@ namespace Managing.Application.Abstractions
Task> GetScenariosByUserAsync(User user);
Task CreateScenarioForUser(User user, string name, List strategies, int? loopbackPeriod = 1);
- Task> GetIndicatorsByUserAsync(User user);
+ Task> GetIndicatorsByUserAsync(User user);
Task DeleteIndicatorByUser(User user, string name);
Task DeleteScenarioByUser(User user, string name);
Task GetScenarioByUser(User user, string name);
- Task CreateIndicatorForUser(User user,
+ Task CreateIndicatorForUser(User user,
IndicatorType type,
string name,
int? period = null,
diff --git a/src/Managing.Application/Abstractions/ITradingBot.cs b/src/Managing.Application/Abstractions/ITradingBot.cs
index 9026ee4..452fed3 100644
--- a/src/Managing.Application/Abstractions/ITradingBot.cs
+++ b/src/Managing.Application/Abstractions/ITradingBot.cs
@@ -1,37 +1,28 @@
-īģŋusing Managing.Core.FixedSizedQueue;
-using Managing.Domain.Accounts;
+īģŋusing Managing.Domain.Accounts;
using Managing.Domain.Bots;
using Managing.Domain.Candles;
-using Managing.Domain.Scenarios;
-using Managing.Domain.Strategies.Base;
+using Managing.Domain.Indicators;
using Managing.Domain.Trades;
using static Managing.Common.Enums;
namespace Managing.Application.Abstractions
{
- public interface ITradingBot : IBot
+ public interface ITradingBot
{
TradingBotConfig Config { get; set; }
Account Account { get; set; }
- FixedSizeQueue OptimizedCandles { get; set; }
- HashSet Candles { get; set; }
- HashSet Signals { get; set; }
- List Positions { get; set; }
+ Dictionary Signals { get; set; }
+ Dictionary Positions { get; set; }
Dictionary WalletBalances { get; set; }
- Dictionary IndicatorsValues { get; set; }
- DateTime StartupTime { get; }
- DateTime CreateDate { get; }
DateTime PreloadSince { get; set; }
int PreloadedCandlesCount { get; set; }
-
+ long ExecutionCount { get; set; }
+ Candle LastCandle { get; set; }
Task Run();
- Task ToggleIsForWatchOnly();
int GetWinRate();
decimal GetProfitAndLoss();
decimal GetTotalFees();
- void LoadScenario(Scenario scenario);
- void UpdateIndicatorsValues();
Task LoadAccount();
Task OpenPositionManually(TradeDirection direction);
diff --git a/src/Managing.Application/Backtesting/Backtester.cs b/src/Managing.Application/Backtesting/Backtester.cs
index f42515b..e3da91c 100644
--- a/src/Managing.Application/Backtesting/Backtester.cs
+++ b/src/Managing.Application/Backtesting/Backtester.cs
@@ -21,7 +21,6 @@ namespace Managing.Application.Backtesting
private readonly IBacktestRepository _backtestRepository;
private readonly ILogger _logger;
private readonly IExchangeService _exchangeService;
- private readonly IBotFactory _botFactory;
private readonly IScenarioService _scenarioService;
private readonly IAccountService _accountService;
private readonly IMessengerService _messengerService;
@@ -31,7 +30,6 @@ namespace Managing.Application.Backtesting
public Backtester(
IExchangeService exchangeService,
- IBotFactory botFactory,
IBacktestRepository backtestRepository,
ILogger logger,
IScenarioService scenarioService,
@@ -42,7 +40,6 @@ namespace Managing.Application.Backtesting
IGrainFactory grainFactory)
{
_exchangeService = exchangeService;
- _botFactory = botFactory;
_backtestRepository = backtestRepository;
_logger = logger;
_scenarioService = scenarioService;
@@ -99,7 +96,6 @@ namespace Managing.Application.Backtesting
try
{
var candles = GetCandles(config.Ticker, config.Timeframe, startDate, endDate);
- throw new Exception();
return await RunBacktestWithCandles(config, candles, user, save, withCandles, requestId, metadata);
}
catch (Exception ex)
@@ -145,7 +141,7 @@ namespace Managing.Application.Backtesting
/// The lightweight backtest results
public async Task RunTradingBotBacktest(
TradingBotConfig config,
- List candles,
+ HashSet candles,
User user = null,
bool withCandles = false,
string requestId = null,
@@ -159,7 +155,7 @@ namespace Managing.Application.Backtesting
///
private async Task RunBacktestWithCandles(
TradingBotConfig config,
- List candles,
+ HashSet candles,
User user = null,
bool save = false,
bool withCandles = false,
@@ -201,7 +197,7 @@ namespace Managing.Application.Backtesting
return await _accountService.GetAccountByAccountName(config.AccountName, false, false);
}
- private List GetCandles(Ticker ticker, Timeframe timeframe,
+ private HashSet GetCandles(Ticker ticker, Timeframe timeframe,
DateTime startDate, DateTime endDate)
{
var candles = _exchangeService.GetCandlesInflux(TradingExchanges.Evm, ticker,
diff --git a/src/Managing.Application/Bots/Base/BotFactory.cs b/src/Managing.Application/Bots/Base/BotFactory.cs
deleted file mode 100644
index bc10ffc..0000000
--- a/src/Managing.Application/Bots/Base/BotFactory.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-īģŋusing Managing.Application.Abstractions;
-using Managing.Application.Abstractions.Services;
-using Managing.Application.ManageBot;
-using Managing.Domain.Bots;
-using Microsoft.Extensions.Logging;
-
-namespace Managing.Application.Bots.Base
-{
- public class BotFactory : IBotFactory
- {
- private readonly IExchangeService _exchangeService;
- private readonly IMessengerService _messengerService;
- private readonly IAccountService _accountService;
- private readonly ILogger _tradingBotLogger;
- private readonly ITradingService _tradingService;
- private readonly IBotService _botService;
- private readonly IBackupBotService _backupBotService;
-
- public BotFactory(
- IExchangeService exchangeService,
- ILogger tradingBotLogger,
- IMessengerService messengerService,
- IAccountService accountService,
- ITradingService tradingService,
- IBotService botService,
- IBackupBotService backupBotService)
- {
- _tradingBotLogger = tradingBotLogger;
- _exchangeService = exchangeService;
- _messengerService = messengerService;
- _accountService = accountService;
- _tradingService = tradingService;
- _botService = botService;
- _backupBotService = backupBotService;
- }
-
- public async Task CreateTradingBot(TradingBotConfig config)
- {
- // Delegate to BotService which handles scenario loading properly
- return await _botService.CreateTradingBot(config);
- }
-
- public async Task CreateBacktestTradingBot(TradingBotConfig config)
- {
- // Delegate to BotService which handles scenario loading properly
- return await _botService.CreateBacktestTradingBot(config);
- }
- }
-}
\ No newline at end of file
diff --git a/src/Managing.Application/Bots/Grains/AgentGrain.cs b/src/Managing.Application/Bots/Grains/AgentGrain.cs
new file mode 100644
index 0000000..2583075
--- /dev/null
+++ b/src/Managing.Application/Bots/Grains/AgentGrain.cs
@@ -0,0 +1,163 @@
+using Managing.Application.Abstractions;
+using Managing.Application.Abstractions.Grains;
+using Managing.Application.Abstractions.Services;
+using Managing.Application.Bots.Models;
+using Managing.Domain.Statistics;
+using Microsoft.Extensions.Logging;
+using static Managing.Common.Enums;
+
+namespace Managing.Application.Bots.Grains;
+
+public class AgentGrain : Grain, IAgentGrain, IRemindable
+{
+ private readonly IPersistentState _state;
+ private readonly ILogger _logger;
+ private readonly IBotService _botService;
+ private readonly IStatisticService _statisticService;
+ private const string _updateSummaryReminderName = "UpdateAgentSummary";
+
+ public AgentGrain(
+ [PersistentState("agent-state", "agent-store")]
+ IPersistentState state,
+ ILogger logger,
+ IBotService botService,
+ IStatisticService statisticService)
+ {
+ _state = state;
+ _logger = logger;
+ _botService = botService;
+ _statisticService = statisticService;
+ }
+
+ public override Task OnActivateAsync(CancellationToken cancellationToken)
+ {
+ _logger.LogInformation("AgentGrain activated for user {UserId}", this.GetPrimaryKeyLong());
+ return base.OnActivateAsync(cancellationToken);
+ }
+
+ public async Task InitializeAsync(int userId, string agentName)
+ {
+ _state.State.AgentName = agentName;
+ await _state.WriteStateAsync();
+ _logger.LogInformation("Agent {UserId} initialized with name {AgentName}", userId, agentName);
+ await RegisterReminderAsync();
+ }
+
+ private async Task RegisterReminderAsync()
+ {
+ try
+ {
+ // Register a reminder that fires every 5 minutes
+ await this.RegisterOrUpdateReminder(_updateSummaryReminderName, TimeSpan.FromMinutes(5),
+ TimeSpan.FromMinutes(1));
+ _logger.LogInformation("Reminder registered for agent {UserId} to update summary every 5 minutes",
+ this.GetPrimaryKeyLong());
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to register reminder for agent {UserId}", this.GetPrimaryKeyLong());
+ }
+ }
+
+ public async Task ReceiveReminder(string reminderName, TickStatus status)
+ {
+ if (reminderName == _updateSummaryReminderName)
+ {
+ try
+ {
+ _logger.LogInformation("Reminder triggered for agent {UserId} to update summary",
+ this.GetPrimaryKeyLong());
+ await UpdateSummary();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error updating agent summary from reminder for user {UserId}",
+ this.GetPrimaryKeyLong());
+ }
+ }
+ }
+
+ public async Task UpdateSummary()
+ {
+ try
+ {
+ // Get all bots for this agent
+ var bots = await _botService.GetBotsByIdsAsync(_state.State.BotIds);
+
+ // Calculate aggregated statistics from bot data
+ var totalPnL = bots.Sum(b => b.Pnl);
+ var totalWins = bots.Sum(b => b.TradeWins);
+ var totalLosses = bots.Sum(b => b.TradeLosses);
+
+ // Calculate ROI based on total volume traded with proper division by zero handling
+ var totalVolume = bots.Sum(b => b.Volume);
+ decimal totalROI;
+
+ if (totalVolume > 0)
+ {
+ totalROI = (totalPnL / totalVolume) * 100;
+ }
+ else if (totalVolume == 0 && totalPnL == 0)
+ {
+ // No trading activity yet
+ totalROI = 0;
+ }
+ else if (totalVolume == 0 && totalPnL != 0)
+ {
+ // Edge case: PnL exists but no volume (shouldn't happen in normal cases)
+ _logger.LogWarning("Agent {UserId} has PnL {PnL} but zero volume", this.GetPrimaryKeyLong(), totalPnL);
+ totalROI = 0;
+ }
+ else
+ {
+ // Fallback for any other edge cases
+ totalROI = 0;
+ }
+
+ // Calculate Runtime based on the farthest date from bot startup times
+ DateTime? runtime = null;
+ if (bots.Any())
+ {
+ runtime = bots.Max(b => b.StartupTime);
+ }
+
+ var summary = new AgentSummary
+ {
+ UserId = (int)this.GetPrimaryKeyLong(),
+ AgentName = _state.State.AgentName,
+ TotalPnL = totalPnL,
+ Wins = totalWins,
+ Losses = totalLosses,
+ TotalROI = totalROI,
+ Runtime = runtime,
+ ActiveStrategiesCount = bots.Count(b => b.Status == BotStatus.Up),
+ TotalVolume = totalVolume,
+ };
+
+ // Save summary to database
+ await _statisticService.SaveOrUpdateAgentSummary(summary);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error calculating agent summary for user {UserId}", this.GetPrimaryKeyLong());
+ }
+ }
+
+ public async Task RegisterBotAsync(Guid botId)
+ {
+ if (_state.State.BotIds.Add(botId))
+ {
+ await _state.WriteStateAsync();
+ _logger.LogInformation("Bot {BotId} registered to Agent {UserId}", botId, this.GetPrimaryKeyLong());
+ }
+ }
+
+ public async Task UnregisterBotAsync(Guid botId)
+ {
+ if (_state.State.BotIds.Remove(botId))
+ {
+ await _state.WriteStateAsync();
+ _logger.LogInformation("Bot {BotId} unregistered from Agent {UserId}", botId, this.GetPrimaryKeyLong());
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Managing.Application/Bots/Grains/BacktestTradingBotGrain.cs b/src/Managing.Application/Bots/Grains/BacktestTradingBotGrain.cs
index 8165cf3..8183dfc 100644
--- a/src/Managing.Application/Bots/Grains/BacktestTradingBotGrain.cs
+++ b/src/Managing.Application/Bots/Grains/BacktestTradingBotGrain.cs
@@ -1,7 +1,5 @@
using Managing.Application.Abstractions.Grains;
-using Managing.Application.Abstractions.Models;
using Managing.Application.Abstractions.Repositories;
-using Managing.Core.FixedSizedQueue;
using Managing.Domain.Backtests;
using Managing.Domain.Bots;
using Managing.Domain.Candles;
@@ -9,7 +7,6 @@ using Managing.Domain.Scenarios;
using Managing.Domain.Shared.Helpers;
using Managing.Domain.Strategies;
using Managing.Domain.Strategies.Base;
-using Managing.Domain.Trades;
using Managing.Domain.Users;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -52,7 +49,7 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
/// The complete backtest result
public async Task RunBacktestAsync(
TradingBotConfig config,
- List candles,
+ HashSet candles,
User user = null,
bool save = false,
bool withCandles = false,
@@ -66,7 +63,7 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
// Create a fresh TradingBotBase instance for this backtest
var tradingBot = await CreateTradingBotInstance(config);
- tradingBot.Start();
+ tradingBot.Account = user.Accounts.First(a => a.Name == config.AccountName);
var totalCandles = candles.Count;
var currentCandle = 0;
@@ -79,11 +76,15 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
tradingBot.WalletBalances.Clear();
tradingBot.WalletBalances.Add(candles.FirstOrDefault()!.Date, config.BotTradingBalance);
+ var fixedCandles = new HashSet();
// Process all candles following the exact pattern from GetBacktestingResult
foreach (var candle in candles)
{
- tradingBot.OptimizedCandles.Enqueue(candle);
- tradingBot.Candles.Add(candle);
+ fixedCandles.Add(candle);
+ tradingBot.LastCandle = candle;
+
+ // Update signals manually only for backtesting
+ await tradingBot.UpdateSignals(fixedCandles);
await tradingBot.Run();
currentCandle++;
@@ -97,43 +98,16 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
currentWalletBalance, currentCandle, totalCandles, candle.Date.ToString("yyyy-MM-dd HH:mm"));
break;
}
-
- // Log progress every 10% or every 1000 candles, whichever comes first
- var currentPercentage = (int)((double)currentCandle / totalCandles * 100);
- var shouldLog = currentPercentage >= lastLoggedPercentage + 10 ||
- currentCandle % 1000 == 0 ||
- currentCandle == totalCandles;
-
- if (shouldLog && currentPercentage > lastLoggedPercentage)
- {
- _logger.LogInformation(
- "Backtest progress: {CurrentCandle}/{TotalCandles} ({Percentage}%) - Processing candle from {CandleDate}",
- currentCandle, totalCandles, currentPercentage, candle.Date.ToString("yyyy-MM-dd HH:mm"));
- lastLoggedPercentage = currentPercentage;
- }
}
_logger.LogInformation("Backtest processing completed. Calculating final results...");
- // Set all candles for final calculations
- tradingBot.Candles = new HashSet(candles);
-
- // Only calculate indicators values if withCandles is true
- Dictionary indicatorsValues = null;
- if (withCandles)
- {
- // Convert LightScenario back to full Scenario for indicator calculations
- var fullScenario = config.Scenario.ToScenario();
- indicatorsValues = GetIndicatorsValues(fullScenario.Indicators, candles);
- }
-
- // Calculate final results following the exact pattern from GetBacktestingResult
var finalPnl = tradingBot.GetProfitAndLoss();
var winRate = tradingBot.GetWinRate();
var stats = TradingHelpers.GetStatistics(tradingBot.WalletBalances);
var growthPercentage =
TradingHelpers.GetGrowthFromInitalBalance(tradingBot.WalletBalances.FirstOrDefault().Value, finalPnl);
- var hodlPercentage = TradingHelpers.GetHodlPercentage(candles[0], candles.Last());
+ var hodlPercentage = TradingHelpers.GetHodlPercentage(candles.First(), candles.Last());
var fees = tradingBot.GetTotalFees();
var scoringParams = new BacktestScoringParams(
@@ -148,7 +122,7 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
maxDrawdown: stats.MaxDrawdown,
initialBalance: tradingBot.WalletBalances.FirstOrDefault().Value,
tradingBalance: config.BotTradingBalance,
- startDate: candles[0].Date,
+ startDate: candles.First().Date,
endDate: candles.Last().Date,
timeframe: config.Timeframe,
moneyManagement: config.MoneyManagement
@@ -160,8 +134,8 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
var finalRequestId = requestId ?? Guid.NewGuid().ToString();
// Create backtest result with conditional candles and indicators values
- var result = new Backtest(config, tradingBot.Positions, tradingBot.Signals.ToList(),
- withCandles ? candles : new List())
+ var result = new Backtest(config, tradingBot.Positions, tradingBot.Signals,
+ withCandles ? candles : new HashSet())
{
FinalPnl = finalPnl,
WinRate = winRate,
@@ -170,9 +144,6 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
Fees = fees,
WalletBalances = tradingBot.WalletBalances.ToList(),
Statistics = stats,
- IndicatorsValues = withCandles
- ? AggregateValues(indicatorsValues, tradingBot.IndicatorsValues)
- : new Dictionary(),
Score = scoringResult.Score,
ScoreMessage = scoringResult.GenerateSummaryMessage(),
Id = Guid.NewGuid().ToString(),
@@ -190,9 +161,6 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
// Send notification if backtest meets criteria
await SendBacktestNotificationIfCriteriaMet(result);
- // Clean up the trading bot instance
- tradingBot.Stop();
-
// Convert Backtest to LightBacktest for safe Orleans serialization
return ConvertToLightBacktest(result);
}
@@ -241,13 +209,6 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
// Create the trading bot instance
var logger = _scopeFactory.CreateScope().ServiceProvider.GetRequiredService>();
var tradingBot = new TradingBotBase(logger, _scopeFactory, config);
-
- // Set the user if available
- if (user != null)
- {
- tradingBot.User = user;
- }
-
return tradingBot;
}
@@ -276,8 +237,7 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
/// Aggregates indicator values (following Backtester.cs pattern)
///
private Dictionary AggregateValues(
- Dictionary indicatorsValues,
- Dictionary botStrategiesValues)
+ Dictionary indicatorsValues)
{
var result = new Dictionary();
foreach (var indicator in indicatorsValues)
@@ -291,23 +251,17 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
///
/// Gets indicators values (following Backtester.cs pattern)
///
- private Dictionary GetIndicatorsValues(List indicators,
- List candles)
+ private Dictionary GetIndicatorsValues(List indicators,
+ HashSet candles)
{
var indicatorsValues = new Dictionary();
- var fixedCandles = new FixedSizeQueue(10000);
- foreach (var candle in candles)
- {
- fixedCandles.Enqueue(candle);
- }
foreach (var indicator in indicators)
{
try
{
- var s = ScenarioHelpers.BuildIndicator(indicator, 10000);
- s.Candles = fixedCandles;
- indicatorsValues[indicator.Type] = s.GetIndicatorValues();
+ var builtIndicator = ScenarioHelpers.BuildIndicator(indicator);
+ indicatorsValues[indicator.Type] = builtIndicator.GetIndicatorValues(candles);
}
catch (Exception e)
{
@@ -325,79 +279,4 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
_isDisposed = true;
}
}
-
- public Task GetBacktestProgressAsync()
- {
- throw new NotImplementedException();
- }
-
- public Task StartAsync()
- {
- throw new NotImplementedException();
- }
-
- public Task StopAsync()
- {
- throw new NotImplementedException();
- }
-
- public Task GetStatusAsync()
- {
- throw new NotImplementedException();
- }
-
- public Task GetConfigurationAsync()
- {
- throw new NotImplementedException();
- }
-
- public Task OpenPositionManuallyAsync(TradeDirection direction)
- {
- throw new NotImplementedException();
- }
-
- public Task ToggleIsForWatchOnlyAsync()
- {
- throw new NotImplementedException();
- }
-
- public Task GetBotDataAsync()
- {
- throw new NotImplementedException();
- }
-
- public Task LoadBackupAsync(BotBackup backup)
- {
- throw new NotImplementedException();
- }
-
- public Task SaveBackupAsync()
- {
- throw new NotImplementedException();
- }
-
- public Task GetProfitAndLossAsync()
- {
- throw new NotImplementedException();
- }
-
- public Task GetWinRateAsync()
- {
- throw new NotImplementedException();
- }
-
- public Task GetExecutionCountAsync()
- {
- throw new NotImplementedException();
- }
-
- public Task GetStartupTimeAsync()
- {
- throw new NotImplementedException();
- }
-
- public Task GetCreateDateAsync()
- {
- throw new NotImplementedException();
- }
}
\ No newline at end of file
diff --git a/src/Managing.Application/Bots/Grains/LiveBotRegistryGrain.cs b/src/Managing.Application/Bots/Grains/LiveBotRegistryGrain.cs
new file mode 100644
index 0000000..4acdb5a
--- /dev/null
+++ b/src/Managing.Application/Bots/Grains/LiveBotRegistryGrain.cs
@@ -0,0 +1,179 @@
+using Managing.Application.Abstractions.Grains;
+using Microsoft.Extensions.Logging;
+using static Managing.Common.Enums;
+
+namespace Managing.Application.Bots.Grains;
+
+///
+/// Orleans grain for LiveBotRegistry operations.
+/// This grain acts as a central, durable directory for all LiveTradingBot grains.
+/// It maintains a persistent, up-to-date list of all known bot IDs and their status.
+///
+public class LiveBotRegistryGrain : Grain, ILiveBotRegistryGrain
+{
+ private readonly IPersistentState _state;
+ private readonly ILogger _logger;
+
+ public LiveBotRegistryGrain(
+ [PersistentState("bot-registry", "registry-store")]
+ IPersistentState state,
+ ILogger logger)
+ {
+ _state = state;
+ _logger = logger;
+ }
+
+ public override async Task OnActivateAsync(CancellationToken cancellationToken)
+ {
+ await base.OnActivateAsync(cancellationToken);
+ _logger.LogInformation("LiveBotRegistryGrain activated with {TotalBots} bots registered",
+ _state.State.TotalBotsCount);
+ }
+
+ public override async Task OnDeactivateAsync(DeactivationReason reason, CancellationToken cancellationToken)
+ {
+ _logger.LogInformation("LiveBotRegistryGrain deactivating. Reason: {Reason}. Total bots: {TotalBots}",
+ reason.Description, _state.State.TotalBotsCount);
+ await base.OnDeactivateAsync(reason, cancellationToken);
+ }
+
+ public async Task RegisterBot(Guid identifier, int userId)
+ {
+ try
+ {
+ if (_state.State.Bots.ContainsKey(identifier))
+ {
+ _logger.LogWarning("Bot {Identifier} is already registered in the registry", identifier);
+ return;
+ }
+
+ var entry = new BotRegistryEntry(identifier, userId);
+ _state.State.Bots[identifier] = entry;
+
+ // O(1) FIX: Increment the counters
+ _state.State.TotalBotsCount++;
+ _state.State.ActiveBotsCount++;
+ _state.State.LastUpdated = DateTime.UtcNow;
+
+ await _state.WriteStateAsync();
+
+ _logger.LogInformation(
+ "Bot {Identifier} registered successfully for user {UserId}. Total bots: {TotalBots}, Active bots: {ActiveBots}",
+ identifier, userId, _state.State.TotalBotsCount, _state.State.ActiveBotsCount);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to register bot {Identifier} for user {UserId}", identifier, userId);
+ throw;
+ }
+ }
+
+ public async Task UnregisterBot(Guid identifier)
+ {
+ try
+ {
+ if (!_state.State.Bots.TryGetValue(identifier, out var entryToRemove))
+ {
+ _logger.LogWarning("Bot {Identifier} is not registered in the registry", identifier);
+ return;
+ }
+
+ _state.State.Bots.Remove(identifier);
+
+ // O(1) FIX: Decrement the counters based on the removed entry's status
+ _state.State.TotalBotsCount--;
+ if (entryToRemove.Status == BotStatus.Up)
+ {
+ _state.State.ActiveBotsCount--;
+ }
+
+ _state.State.LastUpdated = DateTime.UtcNow;
+
+ await _state.WriteStateAsync();
+
+ _logger.LogInformation(
+ "Bot {Identifier} unregistered successfully from user {UserId}. Total bots: {TotalBots}",
+ identifier, entryToRemove.UserId, _state.State.TotalBotsCount);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to unregister bot {Identifier}", identifier);
+ throw;
+ }
+ }
+
+ public Task> GetAllBots()
+ {
+ var bots = _state.State.Bots.Values.ToList();
+ _logger.LogDebug("Retrieved {Count} bots from registry", bots.Count);
+ return Task.FromResult(bots);
+ }
+
+ public Task> GetBotsForUser(int userId)
+ {
+ var userBots = _state.State.Bots.Values
+ .Where(b => b.UserId == userId)
+ .ToList();
+
+ _logger.LogDebug("Retrieved {Count} bots for user {UserId}", userBots.Count, userId);
+ return Task.FromResult(userBots);
+ }
+
+ public async Task UpdateBotStatus(Guid identifier, BotStatus newStatus)
+ {
+ try
+ {
+ if (!_state.State.Bots.TryGetValue(identifier, out var entry))
+ {
+ _logger.LogWarning("Bot {Identifier} is not registered in the registry, cannot update status",
+ identifier);
+ return;
+ }
+
+ var previousStatus = entry.Status;
+
+ if (previousStatus == newStatus)
+ {
+ _logger.LogDebug("Bot {Identifier} status unchanged ({Status}), skipping state write", identifier,
+ newStatus);
+ return;
+ }
+
+ // O(1) FIX: Conditionally adjust the counter
+ if (newStatus == BotStatus.Up && previousStatus != BotStatus.Up)
+ {
+ _state.State.ActiveBotsCount++;
+ }
+ else if (newStatus != BotStatus.Up && previousStatus == BotStatus.Up)
+ {
+ _state.State.ActiveBotsCount--;
+ }
+
+ entry.Status = newStatus;
+ entry.LastStatusUpdate = DateTime.UtcNow;
+ _state.State.LastUpdated = DateTime.UtcNow;
+
+ await _state.WriteStateAsync();
+
+ _logger.LogInformation(
+ "Bot {Identifier} status updated from {PreviousStatus} to {NewStatus}. Active bots: {ActiveBots}",
+ identifier, previousStatus, newStatus, _state.State.ActiveBotsCount);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to update status for bot {Identifier} to {Status}", identifier, newStatus);
+ throw;
+ }
+ }
+
+ public Task GetBotStatus(Guid identifier)
+ {
+ if (!_state.State.Bots.TryGetValue(identifier, out var entry))
+ {
+ _logger.LogWarning("Bot {Identifier} is not registered in the registry, returning None", identifier);
+ return Task.FromResult(BotStatus.None);
+ }
+
+ return Task.FromResult(entry.Status);
+ }
+}
\ No newline at end of file
diff --git a/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs b/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs
index 6a47f5d..edd11bb 100644
--- a/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs
+++ b/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs
@@ -1,7 +1,12 @@
+using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Grains;
using Managing.Application.Abstractions.Models;
+using Managing.Core;
+using Managing.Domain.Accounts;
using Managing.Domain.Bots;
+using Managing.Domain.Shared.Helpers;
using Managing.Domain.Trades;
+using Managing.Domain.Users;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
@@ -13,125 +18,200 @@ namespace Managing.Application.Bots.Grains;
/// Uses composition with TradingBotBase to maintain separation of concerns.
/// This grain handles live trading scenarios with real-time market data and execution.
///
-public class LiveTradingBotGrain : Grain, ITradingBotGrain
+public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
{
+ private readonly IPersistentState _state;
private readonly ILogger _logger;
private readonly IServiceScopeFactory _scopeFactory;
private TradingBotBase? _tradingBot;
private IDisposable? _timer;
- private bool _isDisposed = false;
+ private string _reminderName = "RebootReminder";
public LiveTradingBotGrain(
+ [PersistentState("live-trading-bot", "bot-store")]
+ IPersistentState state,
ILogger logger,
IServiceScopeFactory scopeFactory)
{
_logger = logger;
_scopeFactory = scopeFactory;
+ _state = state;
}
public override async Task OnActivateAsync(CancellationToken cancellationToken)
{
- await base.OnActivateAsync(cancellationToken);
-
_logger.LogInformation("LiveTradingBotGrain {GrainId} activated", this.GetPrimaryKey());
-
- // Initialize the grain state if not already done
- if (!State.IsInitialized)
- {
- State.Identifier = this.GetPrimaryKey().ToString();
- State.CreateDate = DateTime.UtcNow;
- State.Status = BotStatus.Down;
- State.IsInitialized = true;
- await WriteStateAsync();
- }
+ await base.OnActivateAsync(cancellationToken);
+ await ResumeBotIfRequiredAsync();
}
public override async Task OnDeactivateAsync(DeactivationReason reason, CancellationToken cancellationToken)
{
- _logger.LogInformation("LiveTradingBotGrain {GrainId} deactivating. Reason: {Reason}",
+ _logger.LogInformation("LiveTradingBotGrain {GrainId} deactivating. Reason: {Reason}",
this.GetPrimaryKey(), reason.Description);
-
- // Stop the timer and trading bot
- await StopAsync();
-
+
+ StopAndDisposeTimer();
await base.OnDeactivateAsync(reason, cancellationToken);
}
+ public async Task CreateAsync(TradingBotConfig config, User user)
+ {
+ if (config == null || string.IsNullOrEmpty(config.Name))
+ {
+ throw new InvalidOperationException("Bot configuration is not properly initialized");
+ }
+
+ if (config.IsForBacktest)
+ {
+ throw new InvalidOperationException("LiveTradingBotGrain cannot be used for backtesting");
+ }
+
+ // This is a new bot, so we can assume it's not registered or active.
+ _state.State.Config = config;
+ _state.State.User = user;
+ _state.State.CreateDate = DateTime.UtcNow;
+ _state.State.Identifier = this.GetPrimaryKey();
+ await _state.WriteStateAsync();
+
+ var botRegistry = GrainFactory.GetGrain(0);
+ await botRegistry.RegisterBot(_state.State.Identifier, user.Id);
+
+ // Register the bot with the user's agent
+ var agentGrain = GrainFactory.GetGrain(user.Id);
+ await agentGrain.RegisterBotAsync(_state.State.Identifier);
+
+ await SaveBotAsync(BotStatus.None);
+
+ _logger.LogInformation("LiveTradingBotGrain {GrainId} created successfully", this.GetPrimaryKey());
+ }
+
+ private async Task ResumeBotIfRequiredAsync()
+ {
+ // Make the network call to the registry to get the source of truth
+ var botRegistry = GrainFactory.GetGrain(0);
+ var botId = this.GetPrimaryKey();
+ var botStatus = await botRegistry.GetBotStatus(botId);
+
+ _logger.LogInformation("LiveTradingBotGrain {GrainId} activated. Registry status: {Status}",
+ botId, botStatus);
+
+ if (botStatus == BotStatus.Up && _tradingBot == null)
+ {
+ // Now, we can proceed with resuming the bot.
+ await ResumeBotInternalAsync();
+ }
+ }
+
+ private async Task ResumeBotInternalAsync()
+ {
+ // The core of this method remains idempotent thanks to the _tradingBot null check
+ if (_tradingBot != null)
+ {
+ return;
+ }
+
+ try
+ {
+ // Load state from persisted grain state
+ _tradingBot = CreateTradingBotInstance(_state.State.Config);
+ LoadStateIntoBase();
+ await _tradingBot.Start();
+
+ // Start the in-memory timer and persistent reminder
+ RegisterAndStartTimer();
+ await RegisterReminder();
+ await SaveBotAsync(BotStatus.Up);
+ _logger.LogInformation("LiveTradingBotGrain {GrainId} resumed successfully", this.GetPrimaryKey());
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to resume bot {GrainId}", this.GetPrimaryKey());
+ // If resume fails, update the status to Down via the registry and stop
+ await UpdateBotRegistryStatus(BotStatus.Down);
+ throw;
+ }
+ }
+
public async Task StartAsync()
{
+ var botRegistry = GrainFactory.GetGrain(0);
+ var botId = this.GetPrimaryKey();
+ var status = await botRegistry.GetBotStatus(botId);
+
+ // This is the new idempotency check, using the registry as the source of truth
+ if (status == BotStatus.Up && _tradingBot != null)
+ {
+ await RegisterReminder();
+ _logger.LogInformation("LiveTradingBotGrain {GrainId} is already running", this.GetPrimaryKey());
+ return;
+ }
+
try
{
- if (State.Status == BotStatus.Up)
- {
- _logger.LogWarning("Bot {GrainId} is already running", this.GetPrimaryKey());
- return;
- }
-
- if (State.Config == null || string.IsNullOrEmpty(State.Config.Name))
- {
- throw new InvalidOperationException("Bot configuration is not properly initialized");
- }
-
- // Ensure this is not a backtest configuration
- if (State.Config.IsForBacktest)
- {
- throw new InvalidOperationException("LiveTradingBotGrain cannot be used for backtesting");
- }
-
- // Create the TradingBotBase instance using composition
- _tradingBot = await CreateTradingBotInstance();
-
- // Load backup if available
- if (State.User != null)
- {
- await LoadBackupFromState();
- }
-
- // Start the trading bot
- _tradingBot.Start();
-
- // Update state
- State.Status = BotStatus.Up;
- State.StartupTime = DateTime.UtcNow;
- await WriteStateAsync();
-
- // Start Orleans timer for periodic execution
- StartTimer();
+ // Resume the bot using the internal logic
+ await ResumeBotInternalAsync();
+ // Update registry status (if it was previously 'Down')
+ await UpdateBotRegistryStatus(BotStatus.Up);
_logger.LogInformation("LiveTradingBotGrain {GrainId} started successfully", this.GetPrimaryKey());
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to start LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
- State.Status = BotStatus.Down;
- await WriteStateAsync();
+ // Ensure registry status is correct even on failure
+ await UpdateBotRegistryStatus(BotStatus.Down);
throw;
}
}
+ private async Task RegisterReminder()
+ {
+ var reminderPeriod = TimeSpan.FromMinutes(2);
+ await this.RegisterOrUpdateReminder(_reminderName, reminderPeriod, reminderPeriod);
+ }
+
+ ///
+ /// Starts the Orleans timer for periodic bot execution
+ ///
+ private void RegisterAndStartTimer()
+ {
+ if (_tradingBot == null) return;
+
+ if (_timer != null) return;
+
+ _timer = this.RegisterGrainTimer(
+ async _ => await ExecuteBotCycle(),
+ new GrainTimerCreationOptions
+ {
+ Period = TimeSpan.FromMinutes(1),
+ DueTime = TimeSpan.FromMinutes(1),
+ KeepAlive = true
+ });
+ }
+
public async Task StopAsync()
{
+ // The check is now against the registry status
+ var botRegistry = GrainFactory.GetGrain(0);
+ var botStatus = await botRegistry.GetBotStatus(this.GetPrimaryKey());
+ if (botStatus == BotStatus.Down)
+ {
+ _logger.LogInformation("Bot {GrainId} is already stopped", this.GetPrimaryKey());
+ return;
+ }
+
try
{
- // Stop the timer
- _timer?.Dispose();
- _timer = null;
+ StopAndDisposeTimer();
+ await UnregisterReminder();
- // Stop the trading bot
- if (_tradingBot != null)
- {
- _tradingBot.Stop();
-
- // Save backup before stopping
- await SaveBackupToState();
-
- _tradingBot = null;
- }
-
- // Update state
- State.Status = BotStatus.Down;
- await WriteStateAsync();
+ // Sync state from the volatile TradingBotBase before destroying it
+ SyncStateFromBase();
+ await _state.WriteStateAsync();
+ await SaveBotAsync(BotStatus.Down);
+ _tradingBot = null;
+ await UpdateBotRegistryStatus(BotStatus.Down);
_logger.LogInformation("LiveTradingBotGrain {GrainId} stopped successfully", this.GetPrimaryKey());
}
catch (Exception ex)
@@ -141,50 +221,88 @@ public class LiveTradingBotGrain : Grain, ITradingBotGrain
}
}
- public Task GetStatusAsync()
+ private void StopAndDisposeTimer()
{
- return Task.FromResult(State.Status);
+ if (_timer != null)
+ {
+ // Stop the timer
+ _timer?.Dispose();
+ _timer = null;
+ }
}
- public Task GetConfigurationAsync()
+ private async Task UnregisterReminder()
{
- return Task.FromResult(State.Config);
+ var reminder = await this.GetReminder(_reminderName);
+ if (reminder != null)
+ {
+ await this.UnregisterReminder(reminder);
+ }
}
- public async Task UpdateConfigurationAsync(TradingBotConfig newConfig)
+ ///
+ /// Creates a TradingBotBase instance using composition
+ ///
+ private TradingBotBase CreateTradingBotInstance(TradingBotConfig config)
+ {
+ if (string.IsNullOrEmpty(config.AccountName))
+ {
+ throw new InvalidOperationException("Account name is required for live trading");
+ }
+
+ // Create the trading bot instance
+ var logger = _scopeFactory.CreateScope().ServiceProvider.GetRequiredService>();
+ var tradingBot = new TradingBotBase(logger, _scopeFactory, config);
+
+ // Restore state from grain state
+ tradingBot.Signals = _state.State.Signals;
+ tradingBot.Positions = _state.State.Positions;
+ tradingBot.WalletBalances = _state.State.WalletBalances;
+ tradingBot.PreloadedCandlesCount = _state.State.PreloadedCandlesCount;
+ tradingBot.ExecutionCount = _state.State.ExecutionCount;
+ tradingBot.Identifier = _state.State.Identifier;
+ tradingBot.LastPositionClosingTime = _state.State.LastPositionClosingTime;
+
+ return tradingBot;
+ }
+
+
+ ///
+ /// Executes one cycle of the trading bot
+ ///
+ private async Task ExecuteBotCycle()
{
try
{
if (_tradingBot == null)
{
- throw new InvalidOperationException("Bot is not running");
+ return;
}
- // Ensure this is not a backtest configuration
- if (newConfig.IsForBacktest)
- {
- throw new InvalidOperationException("LiveTradingBotGrain cannot be used for backtesting");
- }
+ // Execute the bot's Run method
+ await _tradingBot.Run();
+ SyncStateFromBase();
+ await _state.WriteStateAsync();
- // Update the configuration in the trading bot
- var success = await _tradingBot.UpdateConfiguration(newConfig);
-
- if (success)
- {
- // Update the state
- State.Config = newConfig;
- await WriteStateAsync();
- }
-
- return success;
+ // Save bot statistics to database
+ await SaveBotAsync(BotStatus.Up);
+ }
+ catch (ObjectDisposedException)
+ {
+ // Gracefully handle disposed service provider during shutdown
+ _logger.LogInformation("Service provider disposed during shutdown for LiveTradingBotGrain {GrainId}",
+ this.GetPrimaryKey());
+ return;
}
catch (Exception ex)
{
- _logger.LogError(ex, "Failed to update configuration for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
- return false;
+ // TODO : Turn off the bot if an error occurs
+ _logger.LogError(ex, "Error during bot execution cycle for LiveTradingBotGrain {GrainId}",
+ this.GetPrimaryKey());
}
}
+
public async Task OpenPositionManuallyAsync(TradeDirection direction)
{
try
@@ -198,12 +316,14 @@ public class LiveTradingBotGrain : Grain, ITradingBotGrain
}
catch (Exception ex)
{
- _logger.LogError(ex, "Failed to open manual position for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
+ _logger.LogError(ex, "Failed to open manual position for LiveTradingBotGrain {GrainId}",
+ this.GetPrimaryKey());
throw;
}
}
- public async Task ToggleIsForWatchOnlyAsync()
+
+ public Task GetBotDataAsync()
{
try
{
@@ -212,39 +332,20 @@ public class LiveTradingBotGrain : Grain, ITradingBotGrain
throw new InvalidOperationException("Bot is not running");
}
- await _tradingBot.ToggleIsForWatchOnly();
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Failed to toggle watch-only mode for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
- throw;
- }
- }
-
- public async Task GetBotDataAsync()
- {
- try
- {
- if (_tradingBot == null)
+ return Task.FromResult(new TradingBotResponse
{
- throw new InvalidOperationException("Bot is not running");
- }
-
- return new TradingBotResponse
- {
- Identifier = State.Identifier,
- Name = State.Name,
- Status = State.Status,
- Config = State.Config,
+ Identifier = _state.State.Identifier,
+ Name = _state.State.Name,
+ Config = _state.State.Config,
Positions = _tradingBot.Positions,
- Signals = _tradingBot.Signals.ToList(),
+ Signals = _tradingBot.Signals,
WalletBalances = _tradingBot.WalletBalances,
ProfitAndLoss = _tradingBot.GetProfitAndLoss(),
WinRate = _tradingBot.GetWinRate(),
- ExecutionCount = _tradingBot.ExecutionCount,
- StartupTime = State.StartupTime,
- CreateDate = State.CreateDate
- };
+ ExecutionCount = _state.State.ExecutionCount,
+ StartupTime = _state.State.StartupTime,
+ CreateDate = _state.State.CreateDate
+ });
}
catch (Exception ex)
{
@@ -253,244 +354,236 @@ public class LiveTradingBotGrain : Grain, ITradingBotGrain
}
}
- public async Task LoadBackupAsync(BotBackup backup)
+ private void LoadStateIntoBase()
{
- try
- {
- if (_tradingBot == null)
- {
- throw new InvalidOperationException("Bot is not running");
- }
+ if (_tradingBot == null)
+ _tradingBot = CreateTradingBotInstance(_state.State.Config);
- _tradingBot.LoadBackup(backup);
-
- // Update state from backup
- State.User = backup.User;
- State.Identifier = backup.Identifier;
- State.Status = backup.LastStatus;
- State.CreateDate = backup.Data.CreateDate;
- State.StartupTime = backup.Data.StartupTime;
- await WriteStateAsync();
+ if (_tradingBot == null) throw new InvalidOperationException("TradingBotBase instance could not be created");
- _logger.LogInformation("Backup loaded successfully for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Failed to load backup for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
- throw;
- }
+ _tradingBot.Signals = _state.State.Signals;
+ _tradingBot.Positions = _state.State.Positions;
+ _tradingBot.WalletBalances = _state.State.WalletBalances;
+ _tradingBot.PreloadedCandlesCount = _state.State.PreloadedCandlesCount;
+ _tradingBot.ExecutionCount = _state.State.ExecutionCount;
+ _tradingBot.Identifier = _state.State.Identifier;
+ _tradingBot.LastPositionClosingTime = _state.State.LastPositionClosingTime;
}
- public async Task SaveBackupAsync()
- {
- try
- {
- if (_tradingBot == null)
- {
- throw new InvalidOperationException("Bot is not running");
- }
-
- await _tradingBot.SaveBackup();
- await SaveBackupToState();
-
- _logger.LogInformation("Backup saved successfully for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Failed to save backup for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
- throw;
- }
- }
-
- public async Task GetProfitAndLossAsync()
- {
- try
- {
- if (_tradingBot == null)
- {
- throw new InvalidOperationException("Bot is not running");
- }
-
- return _tradingBot.GetProfitAndLoss();
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Failed to get P&L for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
- throw;
- }
- }
-
- public async Task GetWinRateAsync()
- {
- try
- {
- if (_tradingBot == null)
- {
- throw new InvalidOperationException("Bot is not running");
- }
-
- return _tradingBot.GetWinRate();
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Failed to get win rate for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
- throw;
- }
- }
-
- public Task GetExecutionCountAsync()
- {
- return Task.FromResult(State.ExecutionCount);
- }
-
- public Task GetStartupTimeAsync()
- {
- return Task.FromResult(State.StartupTime);
- }
-
- public Task GetCreateDateAsync()
- {
- return Task.FromResult(State.CreateDate);
- }
-
- ///
- /// Creates a TradingBotBase instance using composition
- ///
- private async Task CreateTradingBotInstance()
- {
- // Validate configuration for live trading
- if (State.Config == null)
- {
- throw new InvalidOperationException("Bot configuration is not initialized");
- }
-
- if (State.Config.IsForBacktest)
- {
- throw new InvalidOperationException("LiveTradingBotGrain cannot be used for backtesting");
- }
-
- if (string.IsNullOrEmpty(State.Config.AccountName))
- {
- throw new InvalidOperationException("Account name is required for live trading");
- }
-
- // Create the trading bot instance
- var logger = _scopeFactory.CreateScope().ServiceProvider.GetRequiredService>();
- var tradingBot = new TradingBotBase(logger, _scopeFactory, State.Config);
-
- // Set the user if available
- if (State.User != null)
- {
- tradingBot.User = State.User;
- }
-
- return tradingBot;
- }
-
- ///
- /// Starts the Orleans timer for periodic bot execution
- ///
- private void StartTimer()
+ private void SyncStateFromBase()
{
if (_tradingBot == null) return;
-
- var interval = _tradingBot.Interval;
- _timer = RegisterTimer(
- async _ => await ExecuteBotCycle(),
- null,
- TimeSpan.FromMilliseconds(interval),
- TimeSpan.FromMilliseconds(interval));
+ _state.State.Signals = _tradingBot.Signals;
+ _state.State.Positions = _tradingBot.Positions;
+ _state.State.WalletBalances = _tradingBot.WalletBalances;
+ _state.State.PreloadedCandlesCount = _tradingBot.PreloadedCandlesCount;
+ _state.State.ExecutionCount = _tradingBot.ExecutionCount;
+ _state.State.Identifier = _tradingBot.Identifier;
+ _state.State.LastPositionClosingTime = _tradingBot.LastPositionClosingTime;
+ _state.State.Config = _tradingBot.Config;
}
- ///
- /// Executes one cycle of the trading bot
- ///
- private async Task ExecuteBotCycle()
+ public async Task UpdateConfiguration(TradingBotConfig newConfig)
+ {
+ if (_tradingBot == null)
+ LoadStateIntoBase();
+
+ var result = await _tradingBot!.UpdateConfiguration(newConfig);
+
+ if (result)
+ {
+ var botRegistry = GrainFactory.GetGrain(0);
+ var botId = this.GetPrimaryKey();
+ var status = await botRegistry.GetBotStatus(botId);
+ _state.State.Config = newConfig;
+ await _state.WriteStateAsync();
+ await SaveBotAsync(status);
+ }
+
+ return result;
+ }
+
+ public Task GetAccount()
+ {
+ return Task.FromResult(_tradingBot.Account);
+ }
+
+ public Task GetConfiguration()
+ {
+ return Task.FromResult(_state.State.Config);
+ }
+
+ public async Task ClosePositionAsync(Guid positionId)
+ {
+ if (_tradingBot == null)
+ {
+ throw new InvalidOperationException("Bot is not running");
+ }
+
+ if (!_tradingBot.Positions.TryGetValue(positionId, out var position))
+ {
+ throw new InvalidOperationException($"Position with ID {positionId} not found");
+ }
+
+ var signal = _tradingBot.Signals.TryGetValue(position.SignalIdentifier, out var foundSignal)
+ ? foundSignal
+ : null;
+ if (signal == null)
+ {
+ throw new InvalidOperationException($"Signal with ID {position.SignalIdentifier} not found");
+ }
+
+ await _tradingBot.CloseTrade(signal, position, position.Open, _tradingBot.LastCandle.Close, true);
+
+ return position;
+ }
+
+ public async Task RestartAsync()
+ {
+ await StopAsync();
+ await StartAsync();
+ }
+
+ public async Task DeleteAsync()
{
try
{
- if (_tradingBot == null || State.Status != BotStatus.Up || _isDisposed)
+ // Stop the bot first if it's running
+ await StopAsync();
+
+ // Unregister from the bot registry
+ var botRegistry = GrainFactory.GetGrain(0);
+ await botRegistry.UnregisterBot(_state.State.Identifier);
+
+ // Unregister from the user's agent
+ if (_state.State.User != null)
{
- return;
+ var agentGrain = GrainFactory.GetGrain(_state.State.User.Id);
+ await agentGrain.UnregisterBotAsync(_state.State.Identifier);
}
- // Execute the bot's Run method
- await _tradingBot.Run();
-
- // Update execution count
- State.ExecutionCount++;
-
- await SaveBackupToState();
- }
- catch (ObjectDisposedException)
- {
- // Gracefully handle disposed service provider during shutdown
- _logger.LogInformation("Service provider disposed during shutdown for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
- return;
+ // Clear the state
+ _tradingBot = null;
+ await _state.ClearStateAsync();
+
+ _logger.LogInformation("LiveTradingBotGrain {GrainId} deleted successfully", this.GetPrimaryKey());
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error during bot execution cycle for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
+ _logger.LogError(ex, "Failed to delete LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
+ throw;
}
}
///
- /// Saves the current bot state to Orleans state storage
+ /// Updates the bot status in the central BotRegistry
///
- private async Task SaveBackupToState()
+ private async Task UpdateBotRegistryStatus(BotStatus status)
{
- if (_tradingBot == null) return;
-
try
{
- // Sync state from TradingBotBase
- State.Config = _tradingBot.Config;
- State.Signals = _tradingBot.Signals;
- State.Positions = _tradingBot.Positions;
- State.WalletBalances = _tradingBot.WalletBalances;
- State.PreloadSince = _tradingBot.PreloadSince;
- State.PreloadedCandlesCount = _tradingBot.PreloadedCandlesCount;
- State.Interval = _tradingBot.Interval;
- State.MaxSignals = _tradingBot._maxSignals;
- State.LastBackupTime = DateTime.UtcNow;
-
- await WriteStateAsync();
+ var botRegistry = GrainFactory.GetGrain(0);
+ var botId = this.GetPrimaryKey();
+ await botRegistry.UpdateBotStatus(botId, status);
+ _logger.LogDebug("Bot {BotId} status updated to {Status} in BotRegistry", botId, status);
}
catch (Exception ex)
{
- _logger.LogError(ex, "Failed to save state for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
+ _logger.LogError(ex, "Failed to update bot {BotId} status to {Status} in BotRegistry", this.GetPrimaryKey(),
+ status);
+ }
+ }
+
+ public async Task ReceiveReminder(string reminderName, TickStatus status)
+ {
+ _logger.LogInformation("Reminder '{ReminderName}' received for grain {GrainId}.", reminderName,
+ this.GetPrimaryKey());
+
+ if (reminderName == _reminderName)
+ {
+ // Now a single, clean call to the method that handles all the logic
+ await ResumeBotIfRequiredAsync();
}
}
///
- /// Loads bot state from Orleans state storage
+ /// Saves the current bot statistics to the database using BotService
///
- private async Task LoadBackupFromState()
+ private async Task SaveBotAsync(BotStatus status)
{
- if (_tradingBot == null) return;
-
try
{
- // Sync state to TradingBotBase
- _tradingBot.Signals = State.Signals;
- _tradingBot.Positions = State.Positions;
- _tradingBot.WalletBalances = State.WalletBalances;
- _tradingBot.PreloadSince = State.PreloadSince;
- _tradingBot.PreloadedCandlesCount = State.PreloadedCandlesCount;
- _tradingBot.Config = State.Config;
+ Bot bot = null;
+ if (_tradingBot == null || _state.State.User == null)
+ {
+ // Save bot statistics for saved bots
+ bot = new Bot
+ {
+ Identifier = _state.State.Identifier,
+ Name = _state.State.Config.Name,
+ Ticker = _state.State.Config.Ticker,
+ User = _state.State.User,
+ Status = status,
+ CreateDate = _state.State.CreateDate,
+ StartupTime = _state.State.StartupTime,
+ TradeWins = 0,
+ TradeLosses = 0,
+ Pnl = 0,
+ Roi = 0,
+ Volume = 0,
+ Fees = 0
+ };
+ }
+ else
+ {
+ // Calculate statistics using TradingBox helpers
+ var (tradeWins, tradeLosses) = TradingBox.GetWinLossCount(_tradingBot.Positions);
+ var pnl = _tradingBot.GetProfitAndLoss();
+ var fees = _tradingBot.GetTotalFees();
+ var volume = TradingBox.GetTotalVolumeTraded(_tradingBot.Positions);
+
+ // Calculate ROI based on total investment
+ var totalInvestment = _tradingBot.Positions.Values
+ .Sum(p => p.Open.Quantity * p.Open.Price);
+ var roi = totalInvestment > 0 ? (pnl / totalInvestment) * 100 : 0;
+
+ // Create complete Bot object with all statistics
+ bot = new Bot
+ {
+ Identifier = _state.State.Identifier,
+ Name = _state.State.Config.Name,
+ Ticker = _state.State.Config.Ticker,
+ User = _state.State.User,
+ Status = status,
+ StartupTime = _state.State.StartupTime,
+ CreateDate = _state.State.CreateDate,
+ TradeWins = tradeWins,
+ TradeLosses = tradeLosses,
+ Pnl = pnl,
+ Roi = roi,
+ Volume = volume,
+ Fees = fees
+ };
+ }
+
+ // Pass the complete Bot object to BotService for saving
+ var success = await ServiceScopeHelpers.WithScopedService(_scopeFactory,
+ async (botService) => { return await botService.SaveBotStatisticsAsync(bot); });
+
+ if (success)
+ {
+ _logger.LogDebug(
+ "Successfully saved bot statistics for bot {BotId}: Wins={Wins}, Losses={Losses}, PnL={PnL}, ROI={ROI}%, Volume={Volume}, Fees={Fees}",
+ _state.State.Identifier, bot.TradeWins, bot.TradeLosses, bot.Pnl, bot.Roi, bot.Volume, bot.Fees);
+ }
+ else
+ {
+ _logger.LogWarning("Failed to save bot statistics for bot {BotId}", _state.State.Identifier);
+ }
}
catch (Exception ex)
{
- _logger.LogError(ex, "Failed to load state for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
+ _logger.LogError(ex, "Failed to save bot statistics for bot {BotId}", _state.State.Identifier);
}
}
-
- public void Dispose()
- {
- if (!_isDisposed)
- {
- _timer?.Dispose();
- _isDisposed = true;
- }
- }
-}
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/src/Managing.Application/Bots/Models/AgentGrainState.cs b/src/Managing.Application/Bots/Models/AgentGrainState.cs
new file mode 100644
index 0000000..91458dd
--- /dev/null
+++ b/src/Managing.Application/Bots/Models/AgentGrainState.cs
@@ -0,0 +1,8 @@
+namespace Managing.Application.Bots.Models
+{
+ public class AgentGrainState
+ {
+ public string AgentName { get; set; }
+ public HashSet BotIds { get; set; } = new HashSet();
+ }
+}
\ No newline at end of file
diff --git a/src/Managing.Application/Bots/SimpleBot.cs b/src/Managing.Application/Bots/SimpleBot.cs
deleted file mode 100644
index 4ff1c8c..0000000
--- a/src/Managing.Application/Bots/SimpleBot.cs
+++ /dev/null
@@ -1,58 +0,0 @@
-īģŋusing Managing.Application.Abstractions;
-using Managing.Application.ManageBot;
-using Managing.Domain.Bots;
-using Managing.Domain.Workflows;
-using Microsoft.Extensions.Logging;
-using Newtonsoft.Json;
-
-namespace Managing.Application.Bots
-{
- public class SimpleBot : Bot
- {
- public readonly ILogger Logger;
- private readonly IBotService _botService;
- private readonly IBackupBotService _backupBotService;
- private Workflow _workflow;
-
- public SimpleBot(string name, ILogger logger, Workflow workflow, IBotService botService,
- IBackupBotService backupBotService) :
- base(name)
- {
- Logger = logger;
- _botService = botService;
- _backupBotService = backupBotService;
- _workflow = workflow;
- Interval = 100;
- }
-
- public override void Start()
- {
- Task.Run(() => InitWorker(Run));
- base.Start();
- }
-
- public async Task Run()
- {
- await Task.Run(
- async () =>
- {
- Logger.LogInformation(Identifier);
- Logger.LogInformation(DateTime.Now.ToString());
- await _workflow.Execute();
- await SaveBackup();
- Logger.LogInformation("__________________________________________________");
- });
- }
-
- public override async Task SaveBackup()
- {
- var data = JsonConvert.SerializeObject(_workflow);
- await _backupBotService.SaveOrUpdateBotBackup(User, Identifier, Status, new TradingBotBackup());
- }
-
- public override void LoadBackup(BotBackup backup)
- {
- _workflow = new Workflow();
- }
- }
-}
\ No newline at end of file
diff --git a/src/Managing.Application/Bots/TradingBotBase.cs b/src/Managing.Application/Bots/TradingBotBase.cs
index 0d379eb..a2a75c7 100644
--- a/src/Managing.Application/Bots/TradingBotBase.cs
+++ b/src/Managing.Application/Bots/TradingBotBase.cs
@@ -1,18 +1,17 @@
īģŋusing Managing.Application.Abstractions;
+using Managing.Application.Abstractions.Grains;
using Managing.Application.Abstractions.Services;
-using Managing.Application.ManageBot;
using Managing.Application.Trading;
using Managing.Application.Trading.Commands;
using Managing.Common;
using Managing.Core;
-using Managing.Core.FixedSizedQueue;
using Managing.Domain.Accounts;
using Managing.Domain.Bots;
using Managing.Domain.Candles;
+using Managing.Domain.Indicators;
using Managing.Domain.Scenarios;
using Managing.Domain.Shared.Helpers;
using Managing.Domain.Strategies;
-using Managing.Domain.Strategies.Base;
using Managing.Domain.Synth.Models;
using Managing.Domain.Trades;
using Microsoft.Extensions.DependencyInjection;
@@ -22,137 +21,100 @@ using static Managing.Common.Enums;
namespace Managing.Application.Bots;
-public class TradingBotBase : Bot, ITradingBot
+public class TradingBotBase : ITradingBot
{
public readonly ILogger Logger;
private readonly IServiceScopeFactory _scopeFactory;
public TradingBotConfig Config { get; set; }
public Account Account { get; set; }
- public HashSet Indicators { get; set; }
- public FixedSizeQueue OptimizedCandles { get; set; }
- public HashSet Candles { get; set; }
- public HashSet Signals { get; set; }
- public List Positions { get; set; }
+ public Dictionary Signals { get; set; }
+ public Dictionary Positions { get; set; }
public Dictionary WalletBalances { get; set; }
- public Dictionary IndicatorsValues { get; set; }
public DateTime PreloadSince { get; set; }
public int PreloadedCandlesCount { get; set; }
+ public long ExecutionCount { get; set; } = 0;
+ public Guid Identifier { get; set; } = Guid.Empty;
+ public Candle LastCandle { get; set; }
+ public DateTime? LastPositionClosingTime { get; set; }
- public int _maxSignals = 10; // Maximum number of signals to keep in memory
public TradingBotBase(
ILogger logger,
IServiceScopeFactory scopeFactory,
TradingBotConfig config
)
- : base(config.Name)
{
_scopeFactory = scopeFactory;
Logger = logger;
-
- if (config.BotTradingBalance <= Constants.GMX.Config.MinimumPositionAmount)
- {
- throw new ArgumentException(
- $"Initial trading balance must be greater than {Constants.GMX.Config.MinimumPositionAmount}",
- nameof(config.BotTradingBalance));
- }
-
Config = config;
-
- Indicators = new HashSet();
- Signals = new HashSet();
- OptimizedCandles = new FixedSizeQueue(600);
- Candles = new HashSet();
- Positions = new List();
+ Signals = new Dictionary();
+ Positions = new Dictionary();
WalletBalances = new Dictionary();
- IndicatorsValues = new Dictionary();
-
- // Load indicators if scenario is provided in config
- if (Config.Scenario != null)
- {
- // Convert LightScenario to full Scenario for indicator loading
- var fullScenario = Config.Scenario.ToScenario();
- LoadIndicators(fullScenario);
- }
- else
- {
- throw new ArgumentException(
- "Scenario object must be provided in TradingBotConfig. ScenarioName alone is not sufficient.");
- }
+ PreloadSince = CandleExtensions.GetBotPreloadSinceFromTimeframe(config.Timeframe);
+ }
+ public async Task Start()
+ {
if (!Config.IsForBacktest)
{
- Interval = CandleExtensions.GetIntervalFromTimeframe(Config.Timeframe);
- PreloadSince = CandleExtensions.GetBotPreloadSinceFromTimeframe(Config.Timeframe);
+ // Start async initialization in the background without blocking
+ try
+ {
+ // Load account asynchronously
+ await LoadAccount();
+
+ // Load last candle asynchronously
+ await LoadLastCandle();
+
+ if (Account == null)
+ {
+ await LogWarning($"Account {Config.AccountName} not found. Bot cannot start.");
+ throw new ArgumentException("Account not found");
+ }
+
+ // Cancel orders
+ // await CancelAllOrders();
+
+ // Send startup message only for fresh starts (not reboots)
+ if (!true)
+ {
+ var indicatorNames = Config.Scenario.Indicators.Select(i => i.Type.ToString()).ToList();
+ var startupMessage = $"đ **Bot Started Successfully!**\n\n" +
+ $"đ **Trading Setup:**\n" +
+ $"đ¯ Ticker: `{Config.Ticker}`\n" +
+ $"â° Timeframe: `{Config.Timeframe}`\n" +
+ $"đŽ Scenario: `{Config.Scenario?.Name ?? "Unknown"}`\n" +
+ $"đ° Balance: `${Config.BotTradingBalance:F2}`\n" +
+ $"đ Mode: `{(Config.IsForWatchingOnly ? "Watch Only" : "Live Trading")}`\n\n" +
+ $"đ **Active Indicators:** `{string.Join(", ", indicatorNames)}`\n\n" +
+ $"â
Ready to monitor signals and execute trades!\n" +
+ $"đĸ I'll notify you when signals are triggered.";
+
+ await LogInformation(startupMessage);
+ }
+ else
+ {
+ await LogInformation($"đ **Bot Restarted**\n" +
+ $"đ Resuming operations with {Signals.Count} signals and {Positions.Count} positions\n" +
+ $"â
Ready to continue trading");
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, "Error during bot startup: {Message}", ex.Message);
+ }
}
}
- public override void Start()
+ private async Task LoadLastCandle()
{
- base.Start();
-
- if (!Config.IsForBacktest)
+ await ServiceScopeHelpers.WithScopedService(_scopeFactory, async exchangeService =>
{
- // Scenario and indicators should already be loaded in constructor
- // This is just a safety check
- if (Config.Scenario == null || !Indicators.Any())
- {
- throw new InvalidOperationException(
- "Scenario or indicators not loaded properly in constructor. This indicates a configuration error.");
- }
-
- // Start async initialization in the background without blocking
- Task.Run(async () =>
- {
- try
- {
- // Load account asynchronously
- await LoadAccount();
-
- // Preload candles and cancel orders
- await PreloadCandles();
- await CancelAllOrders();
-
- // Send startup message only for fresh starts (not reboots)
- var timeSinceCreation = DateTime.UtcNow - CreateDate;
- var isReboot = timeSinceCreation.TotalMinutes > 3;
-
- if (!isReboot)
- {
- StartupTime = DateTime.UtcNow;
-
- var indicatorNames = Indicators.Select(i => i.Type.ToString()).ToList();
- var startupMessage = $"đ **Bot Started Successfully!**\n\n" +
- $"đ **Trading Setup:**\n" +
- $"đ¯ Ticker: `{Config.Ticker}`\n" +
- $"â° Timeframe: `{Config.Timeframe}`\n" +
- $"đŽ Scenario: `{Config.Scenario?.Name ?? "Unknown"}`\n" +
- $"đ° Balance: `${Config.BotTradingBalance:F2}`\n" +
- $"đ Mode: `{(Config.IsForWatchingOnly ? "Watch Only" : "Live Trading")}`\n\n" +
- $"đ **Active Indicators:** `{string.Join(", ", indicatorNames)}`\n\n" +
- $"â
Ready to monitor signals and execute trades!\n" +
- $"đĸ I'll notify you when signals are triggered.";
-
- await LogInformation(startupMessage);
- }
- else
- {
- await LogInformation($"đ **Bot Restarted**\n" +
- $"đ Resuming operations with {Signals.Count} signals and {Positions.Count} positions\n" +
- $"â
Ready to continue trading");
- }
-
- // Initialize the worker after everything is loaded
- await InitWorker(Run);
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error during bot startup: {Message}", ex.Message);
- Stop(); // Stop the bot if initialization fails
- }
- });
- }
+ var candles = await exchangeService.GetCandlesInflux(Account.Exchange, Config.Ticker, PreloadSince,
+ Config.Timeframe, DateTime.UtcNow, 2);
+ LastCandle = candles.Last();
+ });
}
public async Task LoadAccount()
@@ -165,52 +127,6 @@ public class TradingBotBase : Bot, ITradingBot
});
}
-
- public void LoadScenario(Scenario scenario)
- {
- if (scenario == null)
- {
- var errorMessage = "Null scenario provided";
- Logger.LogWarning(errorMessage);
-
- // If called during construction, throw exception instead of Stop()
- if (Status == BotStatus.Down)
- {
- throw new ArgumentException(errorMessage);
- }
- else
- {
- Stop();
- }
- }
- else
- {
- // Convert full Scenario to LightScenario for storage and load indicators
- Config.Scenario = LightScenario.FromScenario(scenario);
- LoadIndicators(ScenarioHelpers.GetIndicatorsFromScenario(scenario));
-
- Logger.LogInformation($"Loaded scenario '{scenario.Name}' with {Indicators.Count} indicators");
- }
- }
-
- public void LoadIndicators(Scenario scenario)
- {
- LoadIndicators(ScenarioHelpers.GetIndicatorsFromScenario(scenario));
- }
-
- public void LoadIndicators(IEnumerable indicators)
- {
- // Clear existing indicators to prevent duplicates
- Indicators.Clear();
-
- foreach (var indicator in indicators)
- {
- Indicators.Add(indicator);
- }
-
- Logger.LogInformation($"Loaded {Indicators.Count} indicators for bot '{Name}'");
- }
-
public async Task Run()
{
if (!Config.IsForBacktest)
@@ -219,42 +135,31 @@ public class TradingBotBase : Bot, ITradingBot
await ServiceScopeHelpers.WithScopedService(_scopeFactory, async exchangeService =>
{
var balance = await exchangeService.GetBalance(Account, false);
- if (balance < Constants.GMX.Config.MinimumPositionAmount && Positions.All(p => p.IsFinished()))
+ if (balance < Constants.GMX.Config.MinimumPositionAmount && Positions.All(p => p.Value.IsFinished()))
{
await LogWarning(
- $"Balance on broker is below {Constants.GMX.Config.MinimumPositionAmount} USD (actual: {balance}). Stopping bot {Identifier} and saving backup.");
- await SaveBackup();
- Stop();
+ $"Balance on broker is below {Constants.GMX.Config.MinimumPositionAmount} USD (actual: {balance}). Stopping bot {Identifier}.");
return;
}
});
+
+ await LoadLastCandle();
}
- var previousLastCandle = OptimizedCandles.LastOrDefault();
-
+ // Update signals for live trading only
if (!Config.IsForBacktest)
- await UpdateCandles();
-
- var currentLastCandle = OptimizedCandles.LastOrDefault();
-
- if (currentLastCandle != previousLastCandle || Config.IsForBacktest)
- await UpdateSignals(OptimizedCandles);
- else
- Logger.LogInformation($"No need to update signals for {Config.Ticker}");
+ {
+ await UpdateSignals();
+ }
if (!Config.IsForWatchingOnly)
await ManagePositions();
+ UpdateWalletBalances();
if (!Config.IsForBacktest)
{
- await SaveBackup();
- UpdateIndicatorsValues();
- }
+ ExecutionCount++;
- UpdateWalletBalances();
- if (!Config.IsForBacktest) // Log every 10th execution
- {
- Logger.LogInformation($"Candle date : {OptimizedCandles.Last().Date:u}");
Logger.LogInformation($"Signals : {Signals.Count}");
Logger.LogInformation($"ExecutionCount : {ExecutionCount}");
Logger.LogInformation($"Positions : {Positions.Count}");
@@ -262,59 +167,10 @@ public class TradingBotBase : Bot, ITradingBot
}
}
- public void UpdateIndicatorsValues()
- {
- foreach (var strategy in Indicators)
- {
- IndicatorsValues[strategy.Type] = ((Indicator)strategy).GetIndicatorValues();
- }
- }
-
- private async Task PreloadCandles()
- {
- if (OptimizedCandles.Any())
- return;
-
- var haveSignal = Signals.Any();
- if (haveSignal)
- {
- if (Signals.Count > _maxSignals)
- PreloadSince = Signals.TakeLast(_maxSignals).First().Date;
- else
- PreloadSince = Signals.First().Date;
- }
-
- if (Positions.Any() && PreloadSince > Positions.FirstOrDefault()?.Open.Date)
- PreloadSince = Positions.First().Open.Date;
-
- await ServiceScopeHelpers.WithScopedService(_scopeFactory, async exchangeService =>
- {
- var candles =
- await exchangeService.GetCandlesInflux(TradingExchanges.Evm, Config.Ticker, PreloadSince,
- Config.Timeframe);
-
- foreach (var candle in candles.Where(c => c.Date < DateTime.Now.ToUniversalTime()))
- {
- if (!OptimizedCandles.Any(c => c.Date == candle.Date))
- {
- OptimizedCandles.Enqueue(candle);
- Candles.Add(candle);
-
- if (!haveSignal)
- {
- await UpdateSignals(OptimizedCandles);
- }
- }
- }
- });
-
- PreloadedCandlesCount = OptimizedCandles.Count();
- }
-
- private async Task UpdateSignals(FixedSizeQueue candles)
+ public async Task UpdateSignals(HashSet? candles = null)
{
// If position open and not flipped, do not update signals
- if (!Config.FlipPosition && Positions.Any(p => !p.IsFinished())) return;
+ if (!Config.FlipPosition && Positions.Any(p => !p.Value.IsFinished())) return;
// Check if we're in cooldown period for any direction
if (IsInCooldownPeriod())
@@ -323,130 +179,43 @@ public class TradingBotBase : Bot, ITradingBot
return;
}
- var signal = TradingBox.GetSignal(candles.ToHashSet(), Indicators, Signals, Config.Scenario.LoopbackPeriod);
- if (signal == null) return;
-
- await AddSignal(signal);
- }
-
- private async Task AddSignal(LightSignal signal)
- {
- if (Config.IsForWatchingOnly || (ExecutionCount < 1 && !Config.IsForBacktest))
- signal.Status = SignalStatus.Expired;
-
- var indicatorNames = Indicators.Select(i => i.Type.ToString()).ToList();
- var signalText = $"đ¯ **New Trading Signal**\n\n" +
- $"đ **Signal Details:**\n" +
- $"đ Action: `{signal.Direction}` {Config.Ticker}\n" +
- $"â° Timeframe: `{Config.Timeframe}`\n" +
- $"đ¯ Confidence: `{signal.Confidence}`\n" +
- $"đ Indicators: `{string.Join(", ", indicatorNames)}`\n" +
- $"đ Signal ID: `{signal.Identifier}`";
-
- // Apply Synth-based signal filtering if enabled
- if ((Config.UseSynthApi || !Config.IsForBacktest) && ExecutionCount > 0)
+ if (Config.IsForBacktest && candles != null)
{
- await ServiceScopeHelpers.WithScopedServices(_scopeFactory,
- async (tradingService, exchangeService) =>
- {
- var currentPrice = Config.IsForBacktest
- ? OptimizedCandles.Last().Close
- : await exchangeService.GetPrice(Account, Config.Ticker, DateTime.UtcNow);
-
- var signalValidationResult = await tradingService.ValidateSynthSignalAsync(signal, currentPrice,
- Config,
- Config.IsForBacktest);
-
- if (signalValidationResult.Confidence == Confidence.None ||
- signalValidationResult.Confidence == Confidence.Low ||
- signalValidationResult.IsBlocked)
- {
- // TODO : remove this when Synth is stable
- // signal.Status = SignalStatus.Expired;
- signalText += $"\n\nđĢ *Synth Signal Filter*\n" +
- $"Signal `{signal.Identifier}` blocked by Synth risk assessment\n\n" +
- $"đ *Risk Analysis Details*\n" +
- $"SL Probability: `{signalValidationResult.StopLossProbability:P2}`\n" +
- $"TP Probability: `{signalValidationResult.TakeProfitProbability:P2}`\n" +
- $"TP/SL Ratio: `{signalValidationResult.TpSlRatio:F2}x`\n" +
- $"Win/Loss: `{signalValidationResult.WinLossRatio:F2}:1`\n" +
- $"Expected Value: `${signalValidationResult.ExpectedMonetaryValue:F2}`\n" +
- $"Expected Utility: `{signalValidationResult.ExpectedUtility:F4}`\n" +
- $"Kelly Criterion: `{signalValidationResult.KellyFraction:P2}`\n" +
- $"Kelly Capped: `{signalValidationResult.KellyCappedFraction:P2}`\n" +
- $"Risk Assessment: `{signalValidationResult.GetUtilityRiskAssessment()}`\n" +
- $"Time Horizon: `{signalValidationResult.TimeHorizonSeconds / 3600:F1}h`\n\n" +
- $"đ *Context*\n`{signalValidationResult.ValidationContext}`";
- }
- else
- {
- signal.Confidence = signalValidationResult.Confidence;
- signalText += $"\n\nâ
*Synth Risk Assessment Passed*\n" +
- $"Confidence: `{signalValidationResult.Confidence}`\n\n" +
- $"đ *Risk Analysis Details*\n" +
- $"SL Probability: `{signalValidationResult.StopLossProbability:P2}`\n" +
- $"TP Probability: `{signalValidationResult.TakeProfitProbability:P2}`\n" +
- $"TP/SL Ratio: `{signalValidationResult.TpSlRatio:F2}x`\n" +
- $"Win/Loss: `{signalValidationResult.WinLossRatio:F2}:1`\n" +
- $"Expected Value: `${signalValidationResult.ExpectedMonetaryValue:F2}`\n" +
- $"Expected Utility: `{signalValidationResult.ExpectedUtility:F4}`\n" +
- $"Kelly Criterion: `{signalValidationResult.KellyFraction:P2}`\n" +
- $"Kelly Capped: `{signalValidationResult.KellyCappedFraction:P2}`\n" +
- $"Risk Assessment: `{signalValidationResult.GetUtilityRiskAssessment()}`\n" +
- $"Time Horizon: `{signalValidationResult.TimeHorizonSeconds / 3600:F1}h`\n\n" +
- $"đ *Context*\n`{signalValidationResult.ValidationContext}`";
- }
- });
+ var backtestSignal =
+ TradingBox.GetSignal(candles, Config.Scenario, Signals, Config.Scenario.LoopbackPeriod);
+ if (backtestSignal == null) return;
+ await AddSignal(backtestSignal);
}
-
- Signals.Add(signal);
-
- Logger.LogInformation(signalText);
-
- if (Config.IsForWatchingOnly && !Config.IsForBacktest && ExecutionCount > 0)
+ else
{
- await ServiceScopeHelpers.WithScopedService(_scopeFactory, async messengerService =>
+ await ServiceScopeHelpers.WithScopedService(_scopeFactory, async grainFactory =>
{
- await messengerService.SendSignal(signalText, Account.Exchange, Config.Ticker, signal.Direction,
- Config.Timeframe);
+ var scenarioRunnerGrain = grainFactory.GetGrain(Guid.NewGuid());
+ var signal = await scenarioRunnerGrain.GetSignals(Config, Signals, PreloadSince, LastCandle);
+ if (signal == null) return;
+ await AddSignal(signal);
});
}
}
- protected async Task UpdateCandles()
- {
- if (OptimizedCandles.Count == 0 || ExecutionCount == 0)
- return;
-
- var lastCandle = OptimizedCandles.Last();
- await ServiceScopeHelpers.WithScopedService(_scopeFactory, async exchangeService =>
- {
- var newCandle =
- await exchangeService.GetCandlesInflux(TradingExchanges.Evm, Config.Ticker, lastCandle.Date,
- Config.Timeframe);
-
- foreach (var candle in newCandle.Where(c => c.Date < DateTime.Now.ToUniversalTime()))
- {
- OptimizedCandles.Enqueue(candle);
- Candles.Add(candle);
- }
- });
- }
-
private async Task RecreateSignalFromPosition(Position position)
{
try
{
- // Get the candle that corresponds to the position opening time
- var positionCandle = OptimizedCandles.FirstOrDefault(c => c.Date <= position.Open.Date)
- ?? OptimizedCandles.LastOrDefault();
-
- if (positionCandle == null)
+ // Create a dummy candle for the position opening time
+ var positionCandle = new Candle
{
- await LogWarning(
- $"Cannot find candle for position {position.Identifier} opened at {position.Open.Date}");
- return null;
- }
+ Date = position.Open.Date,
+ OpenTime = position.Open.Date,
+ Open = position.Open.Price,
+ Close = position.Open.Price,
+ High = position.Open.Price,
+ Low = position.Open.Price,
+ Volume = 0,
+ Exchange = TradingExchanges.Evm,
+ Ticker = Config.Ticker.ToString(),
+ Timeframe = Config.Timeframe
+ };
// Create a new signal based on position information
var recreatedSignal = new LightSignal(
@@ -469,7 +238,7 @@ public class TradingBotBase : Bot, ITradingBot
recreatedSignal.Status = SignalStatus.PositionOpen;
// Add the recreated signal to our collection
- Signals.Add(recreatedSignal);
+ Signals.Add(recreatedSignal.Identifier, recreatedSignal);
await LogInformation(
$"đ **Signal Recovery Success**\nRecreated signal: `{recreatedSignal.Identifier}`\nFor position: `{position.Identifier}`");
@@ -485,9 +254,9 @@ public class TradingBotBase : Bot, ITradingBot
private async Task ManagePositions()
{
// First, process all existing positions that are not finished
- foreach (var position in Positions.Where(p => !p.IsFinished()))
+ foreach (var position in Positions.Values.Where(p => !p.IsFinished()))
{
- var signalForPosition = Signals.FirstOrDefault(s => s.Identifier == position.SignalIdentifier);
+ var signalForPosition = Signals[position.SignalIdentifier];
if (signalForPosition == null)
{
await LogInformation(
@@ -516,12 +285,20 @@ public class TradingBotBase : Bot, ITradingBot
// Then, open positions for signals waiting for a position open
// But first, check if we already have a position for any of these signals
- var signalsWaitingForPosition = Signals.Where(s => s.Status == SignalStatus.WaitingForPosition).ToList();
+ var signalsWaitingForPosition = Signals.Values.Where(s => s.Status == SignalStatus.WaitingForPosition);
foreach (var signal in signalsWaitingForPosition)
{
+ if (signal.Date < LastCandle.Date)
+ {
+ await LogWarning(
+ $"â **Signal Expired**\nSignal `{signal.Identifier}` is older than last candle `{LastCandle.Date}`\nStatus: `Expired`");
+ SetSignalStatus(signal.Identifier, SignalStatus.Expired);
+ continue;
+ }
+
// Check if we already have a position for this signal (in case it was added but not processed yet)
- var existingPosition = Positions.FirstOrDefault(p => p.SignalIdentifier == signal.Identifier);
+ var existingPosition = Positions.Values.FirstOrDefault(p => p.SignalIdentifier == signal.Identifier);
if (existingPosition != null)
{
@@ -537,7 +314,7 @@ public class TradingBotBase : Bot, ITradingBot
if (newlyCreatedPosition != null)
{
- Positions.Add(newlyCreatedPosition);
+ Positions[newlyCreatedPosition.Identifier] = newlyCreatedPosition;
}
else
{
@@ -549,14 +326,10 @@ public class TradingBotBase : Bot, ITradingBot
private void UpdateWalletBalances()
{
- var lastCandle = OptimizedCandles.LastOrDefault();
- if (lastCandle == null) return;
-
- var date = lastCandle.Date;
+ var date = DateTime.UtcNow;
if (WalletBalances.Count == 0)
{
- // WalletBalances[date] = await ExchangeService.GetBalance(Account, IsForBacktest);
WalletBalances[date] = Config.BotTradingBalance;
return;
}
@@ -626,7 +399,7 @@ public class TradingBotBase : Bot, ITradingBot
{
if (orders.Count() >= 3)
{
- var currentTime = Config.IsForBacktest ? OptimizedCandles.Last().Date : DateTime.UtcNow;
+ var currentTime = Config.IsForBacktest ? LastCandle?.Date ?? DateTime.UtcNow : DateTime.UtcNow;
var timeSinceRequest = currentTime - positionForSignal.Open.Date;
var waitTimeMinutes = 10;
@@ -683,8 +456,8 @@ public class TradingBotBase : Bot, ITradingBot
await ServiceScopeHelpers.WithScopedService(_scopeFactory, async exchangeService =>
{
lastCandle = Config.IsForBacktest
- ? OptimizedCandles.Last()
- : exchangeService.GetCandle(Account, Config.Ticker, DateTime.UtcNow);
+ ? LastCandle
+ : await exchangeService.GetCandle(Account, Config.Ticker, DateTime.UtcNow);
});
var currentTime = Config.IsForBacktest ? lastCandle.Date : DateTime.UtcNow;
@@ -812,14 +585,9 @@ public class TradingBotBase : Bot, ITradingBot
if (riskResult.ShouldAutoClose && !string.IsNullOrEmpty(riskResult.EmergencyMessage))
{
await LogWarning(riskResult.EmergencyMessage);
-
- var signalForAutoClose =
- Signals.FirstOrDefault(s => s.Identifier == positionForSignal.SignalIdentifier);
- if (signalForAutoClose != null)
- {
- await CloseTrade(signalForAutoClose, positionForSignal, positionForSignal.StopLoss,
- currentPrice, true);
- }
+ await CloseTrade(Signals[positionForSignal.SignalIdentifier], positionForSignal,
+ positionForSignal.StopLoss,
+ currentPrice, true);
}
}
}
@@ -836,19 +604,20 @@ public class TradingBotBase : Bot, ITradingBot
Logger.LogInformation($"Opening position for {signal.Identifier}");
// Check for any existing open position (not finished) for this ticker
- var openedPosition = Positions.FirstOrDefault(p => !p.IsFinished() && p.SignalIdentifier != signal.Identifier);
+ var openedPosition =
+ Positions.Values.FirstOrDefault(p => !p.IsFinished() && p.SignalIdentifier != signal.Identifier);
decimal lastPrice = await ServiceScopeHelpers.WithScopedService(_scopeFactory,
async exchangeService =>
{
return Config.IsForBacktest
- ? OptimizedCandles.Last().Close
+ ? LastCandle?.Close ?? 0
: await exchangeService.GetPrice(Account, Config.Ticker, DateTime.UtcNow);
});
if (openedPosition != null)
{
- var previousSignal = Signals.First(s => s.Identifier == openedPosition.SignalIdentifier);
+ var previousSignal = Signals[openedPosition.SignalIdentifier];
if (openedPosition.OriginDirection == signal.Direction)
{
@@ -919,7 +688,7 @@ public class TradingBotBase : Bot, ITradingBot
Config.Ticker,
PositionInitiator.Bot,
signal.Date,
- User,
+ Account.User,
Config.BotTradingBalance,
Config.IsForBacktest,
lastPrice,
@@ -971,6 +740,7 @@ public class TradingBotBase : Bot, ITradingBot
private async Task CanOpenPosition(LightSignal signal)
{
// Early return if we're in backtest mode and haven't executed yet
+ // TODO : check if its a startup cycle
if (!Config.IsForBacktest && ExecutionCount == 0)
{
await LogInformation("âŗ **Bot Not Ready**\nCannot open position\nBot hasn't executed first cycle yet");
@@ -997,7 +767,7 @@ public class TradingBotBase : Bot, ITradingBot
await ServiceScopeHelpers.WithScopedService(_scopeFactory, async exchangeService =>
{
currentPrice = Config.IsForBacktest
- ? OptimizedCandles.Last().Close
+ ? LastCandle?.Close ?? 0
: await exchangeService.GetPrice(Account, Config.Ticker, DateTime.UtcNow);
});
@@ -1029,6 +799,7 @@ public class TradingBotBase : Bot, ITradingBot
// Get the last N finished positions regardless of direction
var recentPositions = Positions
+ .Values
.Where(p => p.IsFinished())
.OrderByDescending(p => p.Open.Date)
.Take(Config.MaxLossStreak)
@@ -1072,7 +843,7 @@ public class TradingBotBase : Bot, ITradingBot
}
// Handle existing position on broker
- var previousPosition = Positions.LastOrDefault();
+ var previousPosition = Positions.Values.LastOrDefault();
List orders = null;
await ServiceScopeHelpers.WithScopedService(_scopeFactory,
async exchangeService =>
@@ -1219,14 +990,14 @@ public class TradingBotBase : Bot, ITradingBot
private async Task HandleClosedPosition(Position position)
{
- if (Positions.Any(p => p.Identifier == position.Identifier))
+ if (Positions.ContainsKey(position.Identifier))
{
Candle currentCandle = null;
await ServiceScopeHelpers.WithScopedService(_scopeFactory, async exchangeService =>
{
currentCandle = Config.IsForBacktest
- ? OptimizedCandles.LastOrDefault()
- : exchangeService.GetCandle(Account, Config.Ticker, DateTime.UtcNow);
+ ? LastCandle
+ : await exchangeService.GetCandle(Account, Config.Ticker, DateTime.UtcNow);
});
if (currentCandle != null)
@@ -1235,7 +1006,7 @@ public class TradingBotBase : Bot, ITradingBot
await ServiceScopeHelpers.WithScopedService(_scopeFactory, async exchangeService =>
{
recentCandles = Config.IsForBacktest
- ? OptimizedCandles.TakeLast(4).ToList()
+ ? new List() { LastCandle }
: (await exchangeService.GetCandlesInflux(TradingExchanges.Evm, Config.Ticker,
DateTime.UtcNow.AddHours(-4), Config.Timeframe)).ToList();
});
@@ -1337,6 +1108,10 @@ public class TradingBotBase : Bot, ITradingBot
}
await SetPositionStatus(position.SignalIdentifier, PositionStatus.Finished);
+
+ // Update the last position closing time for cooldown period tracking
+ LastPositionClosingTime = Config.IsForBacktest ? currentCandle.Date : DateTime.UtcNow;
+
Logger.LogInformation(
$"â
**Position Closed Successfully**\nPosition: `{position.SignalIdentifier}`\nPnL: `${position.ProfitAndLoss?.Realized:F2}`");
@@ -1419,10 +1194,10 @@ public class TradingBotBase : Bot, ITradingBot
{
try
{
- var position = Positions.First(p => p.SignalIdentifier == signalIdentifier);
+ var position = Positions.Values.First(p => p.SignalIdentifier == signalIdentifier);
if (!position.Status.Equals(positionStatus))
{
- Positions.First(p => p.SignalIdentifier == signalIdentifier).Status = positionStatus;
+ Positions.Values.First(p => p.SignalIdentifier == signalIdentifier).Status = positionStatus;
await LogInformation(
$"đ **Position Status Change**\nPosition: `{signalIdentifier}`\nStatus: `{position.Status}` â `{positionStatus}`");
}
@@ -1436,9 +1211,9 @@ public class TradingBotBase : Bot, ITradingBot
}
}
- private void UpdatePositionPnl(string identifier, decimal realized)
+ private void UpdatePositionPnl(Guid identifier, decimal realized)
{
- Positions.First(p => p.Identifier == identifier).ProfitAndLoss = new ProfitAndLoss()
+ Positions[identifier].ProfitAndLoss = new ProfitAndLoss()
{
Realized = realized
};
@@ -1446,17 +1221,17 @@ public class TradingBotBase : Bot, ITradingBot
private void SetSignalStatus(string signalIdentifier, SignalStatus signalStatus)
{
- if (Signals.Any(s => s.Identifier == signalIdentifier && s.Status != signalStatus))
+ if (Signals.ContainsKey(signalIdentifier) && Signals[signalIdentifier].Status != signalStatus)
{
- Signals.First(s => s.Identifier == signalIdentifier).Status = signalStatus;
+ Signals[signalIdentifier].Status = signalStatus;
Logger.LogInformation($"Signal {signalIdentifier} is now {signalStatus}");
}
}
public int GetWinRate()
{
- var succeededPositions = Positions.Where(p => p.IsFinished()).Count(p => p.ProfitAndLoss?.Realized > 0);
- var total = Positions.Where(p => p.IsFinished()).Count();
+ var succeededPositions = Positions.Values.Where(p => p.IsFinished()).Count(p => p.ProfitAndLoss?.Realized > 0);
+ var total = Positions.Values.Where(p => p.IsFinished()).Count();
if (total == 0)
return 0;
@@ -1466,7 +1241,8 @@ public class TradingBotBase : Bot, ITradingBot
public decimal GetProfitAndLoss()
{
- var pnl = Positions.Where(p => p.ProfitAndLoss != null && p.IsFinished()).Sum(p => p.ProfitAndLoss.Realized);
+ var pnl = Positions.Values.Where(p => p.ProfitAndLoss != null && p.IsFinished())
+ .Sum(p => p.ProfitAndLoss.Realized);
return pnl - GetTotalFees();
}
@@ -1480,7 +1256,7 @@ public class TradingBotBase : Bot, ITradingBot
{
decimal totalFees = 0;
- foreach (var position in Positions.Where(p => p.Open.Price > 0 && p.Open.Quantity > 0))
+ foreach (var position in Positions.Values.Where(p => p.Open.Price > 0 && p.Open.Quantity > 0))
{
totalFees += CalculatePositionFees(position);
}
@@ -1543,7 +1319,7 @@ public class TradingBotBase : Bot, ITradingBot
{
Config.IsForWatchingOnly = !Config.IsForWatchingOnly;
await LogInformation(
- $"đ **Watch Mode Toggle**\nBot: `{Name}`\nWatch Only: `{(Config.IsForWatchingOnly ? "ON" : "OFF")}`");
+ $"đ **Watch Mode Toggle**\nBot: `{Config.Name}`\nWatch Only: `{(Config.IsForWatchingOnly ? "ON" : "OFF")}`");
}
private async Task LogInformation(string message)
@@ -1565,7 +1341,7 @@ public class TradingBotBase : Bot, ITradingBot
private async Task LogWarning(string message)
{
- message = $"[{Identifier}] {message}";
+ message = $"[{Config.Name}] {message}";
SentrySdk.CaptureException(new Exception(message));
try
@@ -1583,7 +1359,7 @@ public class TradingBotBase : Bot, ITradingBot
if (!Config.IsForBacktest)
{
var user = Account.User;
- var messageWithBotName = $"đ¤ **{user.AgentName} - {Name}**\n{message}";
+ var messageWithBotName = $"đ¤ **{user.AgentName} - {Config.Name}**\n{message}";
await ServiceScopeHelpers.WithScopedService(_scopeFactory,
async messengerService =>
{
@@ -1592,48 +1368,6 @@ public class TradingBotBase : Bot, ITradingBot
}
}
- public override async Task SaveBackup()
- {
- if (Config.IsForBacktest)
- return;
-
- var data = new TradingBotBackup
- {
- Config = Config,
- Signals = Signals.TakeLast(_maxSignals).ToHashSet(),
- Positions = Positions,
- WalletBalances = WalletBalances,
- StartupTime = StartupTime,
- CreateDate = CreateDate
- };
-
- // Use ServiceScopeHelpers for thread safety and DRY code
- await ServiceScopeHelpers.WithScopedService(_scopeFactory,
- async backBotService => { await backBotService.SaveOrUpdateBotBackup(User, Identifier, Status, data); });
- }
-
- public override void LoadBackup(BotBackup backup)
- {
- var data = backup.Data;
-
- // Load the configuration directly
- Config = data.Config;
-
- // Ensure IsForBacktest is always false when loading from backup
- Config.IsForBacktest = false;
-
- // Load runtime state
- Signals = data.Signals ?? new HashSet();
- Positions = data.Positions ?? new List();
- WalletBalances = data.WalletBalances ?? new Dictionary();
- PreloadSince = data.StartupTime;
- CreateDate = data.CreateDate;
- Identifier = backup.Identifier;
- User = backup.User;
- Status = backup.LastStatus;
- StartupTime = data.StartupTime;
- }
-
///
/// Manually opens a position using the bot's settings and a generated signal.
/// Relies on the bot's MoneyManagement for Stop Loss and Take Profit placement.
@@ -1643,14 +1377,13 @@ public class TradingBotBase : Bot, ITradingBot
/// Throws if no candles are available or position opening fails.
public async Task OpenPositionManually(TradeDirection direction)
{
- var lastCandle = OptimizedCandles.LastOrDefault();
- if (lastCandle == null)
+ if (LastCandle == null)
{
throw new Exception("No candles available to open position");
}
// Create a fake signal for manual position opening
- var signal = new LightSignal(Config.Ticker, direction, Confidence.Low, lastCandle, lastCandle.Date,
+ var signal = new LightSignal(Config.Ticker, direction, Confidence.Low, LastCandle, LastCandle.Date,
TradingExchanges.GmxV2,
IndicatorType.Stc, SignalType.Signal, "Manual Signal");
signal.Status = SignalStatus.WaitingForPosition; // Ensure status is correct
@@ -1669,13 +1402,85 @@ public class TradingBotBase : Bot, ITradingBot
}
// Add the position to the list after successful creation
- Positions.Add(position);
+ Positions[position.Identifier] = position;
Logger.LogInformation(
$"đ¤ **Manual Position Opened**\nPosition: `{position.Identifier}`\nSignal: `{signal.Identifier}`\nAdded to positions list");
return position;
}
+ public async Task AddSignal(LightSignal signal)
+ {
+ try
+ {
+ // Set signal status based on configuration
+ if (Config.IsForWatchingOnly || (ExecutionCount < 1 && !Config.IsForBacktest))
+ {
+ signal.Status = SignalStatus.Expired;
+ }
+
+ var indicatorNames = Config.Scenario.Indicators.Select(i => i.Type.ToString()).ToList();
+ var signalText = $"đ¯ **New Trading Signal**\n\n" +
+ $"đ **Signal Details:**\n" +
+ $"đ Action: `{signal.Direction}` {Config.Ticker}\n" +
+ $"â° Timeframe: `{Config.Timeframe}`\n" +
+ $"đ¯ Confidence: `{signal.Confidence}`\n" +
+ $"đ Indicators: `{string.Join(", ", indicatorNames)}`\n" +
+ $"đ Signal ID: `{signal.Identifier}`";
+
+ // Apply Synth-based signal filtering if enabled
+ if ((Config.UseSynthApi || !Config.IsForBacktest) && ExecutionCount > 0)
+ {
+ await ServiceScopeHelpers.WithScopedServices(_scopeFactory,
+ async (tradingService, exchangeService) =>
+ {
+ var currentPrice = await exchangeService.GetPrice(Account, Config.Ticker, DateTime.UtcNow);
+
+ var signalValidationResult = await tradingService.ValidateSynthSignalAsync(
+ signal,
+ currentPrice,
+ Config,
+ Config.IsForBacktest);
+
+ if (signalValidationResult.Confidence == Confidence.None ||
+ signalValidationResult.Confidence == Confidence.Low ||
+ signalValidationResult.IsBlocked)
+ {
+ signal.Status = SignalStatus.Expired;
+ Logger.LogInformation($"Signal {signal.Identifier} blocked by Synth risk assessment");
+ }
+ else
+ {
+ signal.Confidence = signalValidationResult.Confidence;
+ Logger.LogInformation(
+ $"Signal {signal.Identifier} passed Synth risk assessment with confidence {signalValidationResult.Confidence}");
+ }
+ });
+ }
+
+ Signals.Add(signal.Identifier, signal);
+
+ Logger.LogInformation(signalText);
+
+ if (Config.IsForWatchingOnly && !Config.IsForBacktest && ExecutionCount > 0)
+ {
+ await ServiceScopeHelpers.WithScopedService(_scopeFactory, async messengerService =>
+ {
+ await messengerService.SendSignal(signalText, Account.Exchange, Config.Ticker, signal.Direction,
+ Config.Timeframe);
+ });
+ }
+
+ Logger.LogInformation(
+ $"Processed signal for {Config.Ticker}: {signal.Direction} with status {signal.Status}");
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, "Failed to add signal for {Ticker}", Config.Ticker);
+ throw;
+ }
+ }
+
///
/// Checks if a position has exceeded the maximum time limit for being open.
///
@@ -1695,25 +1500,14 @@ public class TradingBotBase : Bot, ITradingBot
return timeOpen >= maxTimeAllowed;
}
- ///
- /// Updates the trading bot configuration with new settings.
- ///
- /// The new configuration to apply
- /// True if the configuration was successfully updated, false otherwise
- public async Task UpdateConfiguration(TradingBotConfig newConfig)
- {
- return await UpdateConfiguration(newConfig, allowNameChange: false);
- }
-
///
/// Updates the trading bot configuration with new settings.
/// This method validates the new configuration and applies it to the running bot.
///
/// The new configuration to apply
- /// Whether to allow changing the bot name/identifier
/// True if the configuration was successfully updated, false otherwise
/// Thrown when the new configuration is invalid
- public async Task UpdateConfiguration(TradingBotConfig newConfig, bool allowNameChange = false)
+ public async Task UpdateConfiguration(TradingBotConfig newConfig)
{
try
{
@@ -1845,7 +1639,7 @@ public class TradingBotBase : Bot, ITradingBot
changes.Add($"đ Scenario Name: {Config.ScenarioName ?? "None"} â {newConfig.ScenarioName ?? "None"}");
}
- if (allowNameChange && Config.Name != newConfig.Name)
+ if (Config.Name != newConfig.Name)
{
changes.Add($"đˇī¸ Name: {Config.Name} â {newConfig.Name}");
}
@@ -1865,9 +1659,6 @@ public class TradingBotBase : Bot, ITradingBot
changes.Add($"đ Timeframe: {Config.Timeframe} â {newConfig.Timeframe}");
}
- // Capture current indicators before any changes for scenario comparison
- var oldIndicators = Indicators?.ToList() ?? new List();
-
// Check if the actual Scenario object changed (not just the name)
var scenarioChanged = false;
if (Config.Scenario != newConfig.Scenario)
@@ -1885,19 +1676,17 @@ public class TradingBotBase : Bot, ITradingBot
// Protect critical properties that shouldn't change for running bots
var protectedIsForBacktest = Config.IsForBacktest;
- var protectedName = allowNameChange ? newConfig.Name : Config.Name;
// Update the configuration
Config = newConfig;
// Restore protected properties
Config.IsForBacktest = protectedIsForBacktest;
- Config.Name = protectedName;
// Update bot name and identifier if allowed
- if (allowNameChange && !string.IsNullOrEmpty(newConfig.Name))
+ if (!string.IsNullOrEmpty(newConfig.Name))
{
- Name = newConfig.Name;
+ Config.Name = newConfig.Name;
}
// If account changed, reload it
@@ -1911,13 +1700,9 @@ public class TradingBotBase : Bot, ITradingBot
{
if (newConfig.Scenario != null)
{
- // Convert LightScenario to full Scenario for loading
- var fullScenario = newConfig.Scenario.ToScenario();
- LoadScenario(fullScenario);
-
// Compare indicators after scenario change
- var newIndicators = Indicators?.ToList() ?? new List();
- var indicatorChanges = CompareIndicators(oldIndicators, newIndicators);
+ var newIndicators = newConfig.Scenario.Indicators?.ToList() ?? new List();
+ var indicatorChanges = ScenarioHelpers.CompareIndicators(Config.Scenario.Indicators, newIndicators);
if (indicatorChanges.Any())
{
@@ -1942,12 +1727,6 @@ public class TradingBotBase : Bot, ITradingBot
"âī¸ **Configuration Update**\nâ
No changes detected - configuration already up to date");
}
- // Save the updated configuration as backup
- if (!Config.IsForBacktest)
- {
- await SaveBackup();
- }
-
return true;
}
catch (Exception ex)
@@ -1989,63 +1768,6 @@ public class TradingBotBase : Bot, ITradingBot
};
}
- ///
- /// Compares two lists of indicators and returns a list of changes (added, removed, modified).
- ///
- /// The previous list of indicators
- /// The new list of indicators
- /// A list of change descriptions
- private List CompareIndicators(List oldIndicators, List newIndicators)
- {
- var changes = new List();
-
- // Create dictionaries for easier comparison using Type as key
- var oldIndicatorDict = oldIndicators.ToDictionary(i => i.Type, i => i);
- var newIndicatorDict = newIndicators.ToDictionary(i => i.Type, i => i);
-
- // Find removed indicators
- var removedTypes = oldIndicatorDict.Keys.Except(newIndicatorDict.Keys);
- foreach (var removedType in removedTypes)
- {
- var indicator = oldIndicatorDict[removedType];
- changes.Add($"â **Removed Indicator:** {removedType} ({indicator.GetType().Name})");
- }
-
- // Find added indicators
- var addedTypes = newIndicatorDict.Keys.Except(oldIndicatorDict.Keys);
- foreach (var addedType in addedTypes)
- {
- var indicator = newIndicatorDict[addedType];
- changes.Add($"â **Added Indicator:** {addedType} ({indicator.GetType().Name})");
- }
-
- // Find modified indicators (same type but potentially different configuration)
- var commonTypes = oldIndicatorDict.Keys.Intersect(newIndicatorDict.Keys);
- foreach (var commonType in commonTypes)
- {
- var oldIndicator = oldIndicatorDict[commonType];
- var newIndicator = newIndicatorDict[commonType];
-
- // Compare indicators by serializing them (simple way to detect configuration changes)
- var oldSerialized = JsonConvert.SerializeObject(oldIndicator, Formatting.None);
- var newSerialized = JsonConvert.SerializeObject(newIndicator, Formatting.None);
-
- if (oldSerialized != newSerialized)
- {
- changes.Add($"đ **Modified Indicator:** {commonType} ({newIndicator.GetType().Name})");
- }
- }
-
- // Add summary if there are changes
- if (changes.Any())
- {
- var summary =
- $"đ **Indicator Changes:** {addedTypes.Count()} added, {removedTypes.Count()} removed, {commonTypes.Count(c => JsonConvert.SerializeObject(oldIndicatorDict[c]) != JsonConvert.SerializeObject(newIndicatorDict[c]))} modified";
- changes.Insert(0, summary);
- }
-
- return changes;
- }
///
/// Checks if the bot is currently in a cooldown period for any direction.
@@ -2053,38 +1775,24 @@ public class TradingBotBase : Bot, ITradingBot
/// True if in cooldown period for any direction, false otherwise
private bool IsInCooldownPeriod()
{
- var lastPosition = Positions.LastOrDefault(p => p.IsFinished());
-
- if (lastPosition == null)
+ if (LastPositionClosingTime == null)
{
- return false; // No previous position, no cooldown
+ return false; // No previous position closing time, no cooldown
}
- var cooldownCandle = OptimizedCandles.TakeLast((int)Config.CooldownPeriod).FirstOrDefault();
- if (cooldownCandle == null)
- {
- return false; // Not enough candles to determine cooldown
- }
-
- var positionClosingDate = GetPositionClosingDate(lastPosition);
- if (positionClosingDate == null)
- {
- return false; // Cannot determine closing date
- }
-
- var isInCooldown = positionClosingDate >= cooldownCandle.Date;
+ // Calculate cooldown end time based on last position closing time
+ var baseIntervalSeconds = CandleExtensions.GetBaseIntervalInSeconds(Config.Timeframe);
+ var cooldownEndTime = LastPositionClosingTime.Value.AddSeconds(baseIntervalSeconds * Config.CooldownPeriod);
+ var isInCooldown = LastCandle.Date < cooldownEndTime;
if (isInCooldown)
{
- var intervalMilliseconds = CandleExtensions.GetIntervalFromTimeframe(Config.Timeframe);
- var intervalMinutes = intervalMilliseconds / (1000.0 * 60.0); // Convert milliseconds to minutes
- var cooldownEndTime = cooldownCandle.Date.AddMinutes(intervalMinutes * Config.CooldownPeriod);
- var remainingTime = cooldownEndTime - DateTime.UtcNow;
+ var remainingTime = cooldownEndTime - LastCandle.Date;
Logger.LogWarning(
$"âŗ **Cooldown Period Active**\n" +
$"Cannot open new positions\n" +
- $"Last position closed: `{positionClosingDate:HH:mm:ss}`\n" +
+ $"Last position closed: `{LastPositionClosingTime:HH:mm:ss}`\n" +
$"Cooldown period: `{Config.CooldownPeriod}` candles\n" +
$"Cooldown ends: `{cooldownEndTime:HH:mm:ss}`\n" +
$"Remaining time: `{remainingTime.TotalMinutes:F1} minutes`");
diff --git a/src/Managing.Application/Bots/TradingBotGrainState.cs b/src/Managing.Application/Bots/TradingBotGrainState.cs
index 50f9905..6c72299 100644
--- a/src/Managing.Application/Bots/TradingBotGrainState.cs
+++ b/src/Managing.Application/Bots/TradingBotGrainState.cs
@@ -1,7 +1,7 @@
using Managing.Domain.Bots;
+using Managing.Domain.Indicators;
using Managing.Domain.Trades;
using Managing.Domain.Users;
-using static Managing.Common.Enums;
namespace Managing.Application.Bots;
@@ -23,13 +23,13 @@ public class TradingBotGrainState
/// Collection of trading signals generated by the bot
///
[Id(1)]
- public HashSet Signals { get; set; } = new();
+ public Dictionary Signals { get; set; } = new();
///
- /// List of trading positions opened by the bot
+ /// Dictionary of trading positions opened by the bot, keyed by position identifier
///
[Id(2)]
- public List Positions { get; set; } = new();
+ public Dictionary Positions { get; set; } = new();
///
/// Historical wallet balances tracked over time
@@ -37,12 +37,6 @@ public class TradingBotGrainState
[Id(3)]
public Dictionary WalletBalances { get; set; } = new();
- ///
- /// Current status of the bot (Running, Stopped, etc.)
- ///
- [Id(4)]
- public BotStatus Status { get; set; } = BotStatus.Down;
-
///
/// When the bot was started
///
@@ -71,7 +65,7 @@ public class TradingBotGrainState
/// Bot identifier/name
///
[Id(9)]
- public string Identifier { get; set; } = string.Empty;
+ public Guid Identifier { get; set; } = Guid.Empty;
///
/// Bot display name
@@ -114,4 +108,10 @@ public class TradingBotGrainState
///
[Id(16)]
public DateTime LastBackupTime { get; set; } = DateTime.UtcNow;
+
+ ///
+ /// Last time a position was closed (for cooldown period tracking)
+ ///
+ [Id(17)]
+ public DateTime? LastPositionClosingTime { get; set; }
}
\ No newline at end of file
diff --git a/src/Managing.Application/GeneticService.cs b/src/Managing.Application/GeneticService.cs
index 292d845..50c7221 100644
--- a/src/Managing.Application/GeneticService.cs
+++ b/src/Managing.Application/GeneticService.cs
@@ -8,6 +8,7 @@ using Managing.Domain.Bots;
using Managing.Domain.MoneyManagements;
using Managing.Domain.Risk;
using Managing.Domain.Scenarios;
+using Managing.Domain.Strategies;
using Managing.Domain.Users;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -624,9 +625,9 @@ public class TradingBotChromosome : ChromosomeBase
return clone;
}
- public List GetSelectedIndicators()
+ public List GetSelectedIndicators()
{
- var selected = new List();
+ var selected = new List();
var genes = GetGenes();
// Check all indicator selection slots (genes 5 to 5+N-1 where N is number of eligible indicators)
@@ -634,7 +635,7 @@ public class TradingBotChromosome : ChromosomeBase
{
if (genes[5 + i].Value.ToString() == "1")
{
- var indicator = new GeneticIndicator
+ var indicator = new LightIndicator(_eligibleIndicators[i].ToString(), _eligibleIndicators[i])
{
Type = _eligibleIndicators[i]
};
@@ -713,39 +714,24 @@ public class TradingBotChromosome : ChromosomeBase
// Enforce proper risk-reward constraints
var minStopLoss = 0.2; // Minimum 0.2% to cover fees
var maxStopLoss = takeProfit / 1.1; // Ensure risk-reward ratio is at least 1.1:1
-
+
// Generate a random stop loss between min and max
var randomStopLoss = GetRandomInRange((minStopLoss, maxStopLoss));
-
+
// Use the random value instead of clamping the original
stopLoss = randomStopLoss;
-
+
// Log the generated values (for debugging)
- Console.WriteLine($"Generated: TP={takeProfit:F2}%, SL={stopLoss:F2}% (RR={takeProfit/stopLoss:F2}:1)");
+ Console.WriteLine($"Generated: TP={takeProfit:F2}%, SL={stopLoss:F2}% (RR={takeProfit / stopLoss:F2}:1)");
// Get loopback period from gene 4
var loopbackPeriod = Convert.ToInt32(genes[4].Value);
// Build scenario using selected indicators
- var scenario = new Scenario($"Genetic_{request.RequestId}_Scenario", loopbackPeriod);
-
- foreach (var geneticIndicator in selectedIndicators)
+ var scenario = new LightScenario($"Genetic_{request.RequestId}_Scenario", loopbackPeriod)
{
- var indicator = ScenarioHelpers.BuildIndicator(
- type: geneticIndicator.Type,
- name: $"Genetic_{geneticIndicator.Type}_{Guid.NewGuid():N}",
- period: geneticIndicator.Period,
- fastPeriods: geneticIndicator.FastPeriods,
- slowPeriods: geneticIndicator.SlowPeriods,
- signalPeriods: geneticIndicator.SignalPeriods,
- multiplier: geneticIndicator.Multiplier,
- stochPeriods: geneticIndicator.StochPeriods,
- smoothPeriods: geneticIndicator.SmoothPeriods,
- cyclePeriods: geneticIndicator.CyclePeriods
- );
-
- scenario.AddIndicator(indicator);
- }
+ Indicators = selectedIndicators
+ };
var mm = new MoneyManagement
{
@@ -776,7 +762,7 @@ public class TradingBotChromosome : ChromosomeBase
UseForPositionSizing = false,
UseForSignalFiltering = false,
UseForDynamicStopLoss = false,
- Scenario = LightScenario.FromScenario(scenario),
+ Scenario = scenario,
MoneyManagement = mm,
RiskManagement = new RiskManagement
{
@@ -853,7 +839,7 @@ public class TradingBotChromosome : ChromosomeBase
ReplaceGene(1, new Gene(stopLoss));
// Log the initial values (for debugging)
- Console.WriteLine($"Initialized: TP={takeProfit:F2}%, SL={stopLoss:F2}% (RR={takeProfit/stopLoss:F2}:1)");
+ Console.WriteLine($"Initialized: TP={takeProfit:F2}%, SL={stopLoss:F2}% (RR={takeProfit / stopLoss:F2}:1)");
// Initialize remaining genes normally
for (int i = 2; i < Length; i++)
@@ -863,22 +849,6 @@ public class TradingBotChromosome : ChromosomeBase
}
}
-///
-/// Genetic indicator with parameters
-///
-public class GeneticIndicator
-{
- public IndicatorType Type { get; set; }
- public int? Period { get; set; }
- public int? FastPeriods { get; set; }
- public int? SlowPeriods { get; set; }
- public int? SignalPeriods { get; set; }
- public double? Multiplier { get; set; }
- public int? StochPeriods { get; set; }
- public int? SmoothPeriods { get; set; }
- public int? CyclePeriods { get; set; }
-}
-
///
/// Multi-objective fitness function for trading bot optimization
///
@@ -889,7 +859,8 @@ public class TradingBotFitness : IFitness
private GeneticAlgorithm _geneticAlgorithm;
private readonly ILogger _logger;
- public TradingBotFitness(IServiceScopeFactory serviceScopeFactory, GeneticRequest request, ILogger logger)
+ public TradingBotFitness(IServiceScopeFactory serviceScopeFactory, GeneticRequest request,
+ ILogger logger)
{
_serviceScopeFactory = serviceScopeFactory;
_request = request;
diff --git a/src/Managing.Application/ManageBot/BackupBotService.cs b/src/Managing.Application/ManageBot/BackupBotService.cs
deleted file mode 100644
index 9524c8c..0000000
--- a/src/Managing.Application/ManageBot/BackupBotService.cs
+++ /dev/null
@@ -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 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 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);
- }
- }
- }
-}
\ No newline at end of file
diff --git a/src/Managing.Application/ManageBot/BotService.cs b/src/Managing.Application/ManageBot/BotService.cs
index 6802ec9..afe4395 100644
--- a/src/Managing.Application/ManageBot/BotService.cs
+++ b/src/Managing.Application/ManageBot/BotService.cs
@@ -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 _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 _botTasks =
- new ConcurrentDictionary();
- public BotService(IBotRepository botRepository, IExchangeService exchangeService,
- IMessengerService messengerService, IAccountService accountService, ILogger tradingBotLogger,
- ITradingService tradingService, IMoneyManagementService moneyManagementService, IUserService userService,
- IBackupBotService backupBotService, IServiceScopeFactory scopeFactory, IGrainFactory grainFactory)
+ public BotService(IBotRepository botRepository,
+ IMessengerService messengerService, ILogger 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 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> GetSavedBotsAsync()
+ public async Task> GetBotsAsync()
{
return await _botRepository.GetBotsAsync();
}
- public async Task StartBotFromBackup(BotBackup backupBot)
+ public async Task> 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 StopBot(Guid identifier)
+ {
+ try
{
- var moneyManagement = _moneyManagementService
- .GetMoneyMangement(scalpingConfig.MoneyManagement.Name).Result;
- if (moneyManagement != null)
- {
- scalpingConfig.MoneyManagement = moneyManagement;
- }
+ var grain = _grainFactory.GetGrain(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 DeleteBot(Guid identifier)
+ {
+ var grain = _grainFactory.GetGrain(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 RestartBot(Guid identifier)
+ {
+ try
+ {
+ var registryGrain = _grainFactory.GetGrain(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(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 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 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 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 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 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;
}
///
@@ -319,128 +155,198 @@ namespace Managing.Application.ManageBot
/// The bot identifier
/// The new configuration to apply
/// True if the configuration was successfully updated, false otherwise
- public async Task UpdateBotConfiguration(string identifier, TradingBotConfig newConfig)
+ public async Task UpdateBotConfiguration(Guid identifier, TradingBotConfig newConfig)
{
- if (_botTasks.TryGetValue(identifier, out var botTaskWrapper) &&
- botTaskWrapper.BotInstance is TradingBotBase tradingBot)
+ var grain = _grainFactory.GetGrain(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 CreateTradingBot(TradingBotConfig config)
+ public async Task 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(identifier);
+ return await grain.GetConfiguration();
}
- public async Task CreateBacktestTradingBot(TradingBotConfig config)
+ public async Task> 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> GetBotsByUser(int id)
+ {
+ return await _botRepository.GetBotsByUserIdAsync(id);
+ }
+
+ public async Task> GetBotsByIdsAsync(IEnumerable botIds)
+ {
+ return await _botRepository.GetBotsByIdsAsync(botIds);
+ }
+
+ public async Task GetBotByName(string name)
+ {
+ return await _botRepository.GetBotByNameAsync(name);
+ }
+
+ public async Task GetBotByIdentifier(Guid identifier)
+ {
+ return await _botRepository.GetBotByIdentifierAsync(identifier);
+ }
+
+ public async Task OpenPositionManuallyAsync(Guid identifier, TradeDirection direction)
+ {
+ var grain = _grainFactory.GetGrain(identifier);
+ return await grain.OpenPositionManuallyAsync(direction);
+ }
+
+ public async Task ClosePositionAsync(Guid identifier, Guid positionId)
+ {
+ var grain = _grainFactory.GetGrain(identifier);
+ return await grain.ClosePositionAsync(positionId);
+ }
+
+ public async Task UpdateBotStatisticsAsync(Guid identifier)
+ {
+ try
{
- var scenario = await _tradingService.GetScenarioByNameAsync(config.ScenarioName);
- if (scenario != null)
+ var grain = _grainFactory.GetGrain(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 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 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 Bots, int TotalCount)>(
+ _scopeFactory,
+ async repo =>
+ {
+ return await repo.GetBotsPaginatedAsync(
+ pageNumber,
+ pageSize,
+ status,
+ name,
+ ticker,
+ agentName,
+ sortBy,
+ sortDirection);
+ });
}
}
}
\ No newline at end of file
diff --git a/src/Managing.Application/ManageBot/Commands/DeleteBotCommand.cs b/src/Managing.Application/ManageBot/Commands/DeleteBotCommand.cs
index 60ecaf8..685461b 100644
--- a/src/Managing.Application/ManageBot/Commands/DeleteBotCommand.cs
+++ b/src/Managing.Application/ManageBot/Commands/DeleteBotCommand.cs
@@ -4,10 +4,10 @@ namespace Managing.Application.ManageBot.Commands;
public class DeleteBotCommand : IRequest
{
- public string Name { get; }
+ public Guid Identifier { get; }
- public DeleteBotCommand(string name)
+ public DeleteBotCommand(Guid identifier)
{
- Name = name;
+ Identifier = identifier;
}
}
diff --git a/src/Managing.Application/ManageBot/Commands/GetActiveBotsCommand.cs b/src/Managing.Application/ManageBot/Commands/GetActiveBotsCommand.cs
index ce89a51..f1f7237 100644
--- a/src/Managing.Application/ManageBot/Commands/GetActiveBotsCommand.cs
+++ b/src/Managing.Application/ManageBot/Commands/GetActiveBotsCommand.cs
@@ -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>
+ public class GetBotsByStatusCommand : IRequest>
{
- public GetActiveBotsCommand()
+ public BotStatus Status { get; }
+
+ public GetBotsByStatusCommand(BotStatus status)
{
+ Status = status;
}
}
}
diff --git a/src/Managing.Application/ManageBot/Commands/GetAllAgentSummariesCommand.cs b/src/Managing.Application/ManageBot/Commands/GetAllAgentSummariesCommand.cs
new file mode 100644
index 0000000..f1bc8e8
--- /dev/null
+++ b/src/Managing.Application/ManageBot/Commands/GetAllAgentSummariesCommand.cs
@@ -0,0 +1,21 @@
+using Managing.Domain.Statistics;
+using MediatR;
+
+namespace Managing.Application.ManageBot.Commands
+{
+ ///
+ /// Command to retrieve all agent summaries with complete data
+ ///
+ public class GetAllAgentSummariesCommand : IRequest>
+ {
+ ///
+ /// Optional time filter to apply (24H, 3D, 1W, 1M, 1Y, Total)
+ ///
+ public string TimeFilter { get; }
+
+ public GetAllAgentSummariesCommand(string timeFilter = "Total")
+ {
+ TimeFilter = timeFilter;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Managing.Application/ManageBot/Commands/GetAllAgentsCommand.cs b/src/Managing.Application/ManageBot/Commands/GetAllAgentsCommand.cs
index ac54de6..c728cd4 100644
--- a/src/Managing.Application/ManageBot/Commands/GetAllAgentsCommand.cs
+++ b/src/Managing.Application/ManageBot/Commands/GetAllAgentsCommand.cs
@@ -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
///
/// Command to retrieve all active agents and their strategies
///
- public class GetAllAgentsCommand : IRequest>>
+ public class GetAllAgentsCommand : IRequest>>
{
///
/// Optional time filter to apply (24H, 3D, 1W, 1M, 1Y, Total)
diff --git a/src/Managing.Application/ManageBot/Commands/GetBotsByUserAndStatusCommand.cs b/src/Managing.Application/ManageBot/Commands/GetBotsByUserAndStatusCommand.cs
new file mode 100644
index 0000000..50e8633
--- /dev/null
+++ b/src/Managing.Application/ManageBot/Commands/GetBotsByUserAndStatusCommand.cs
@@ -0,0 +1,18 @@
+using Managing.Domain.Bots;
+using MediatR;
+using static Managing.Common.Enums;
+
+namespace Managing.Application.ManageBot.Commands
+{
+ public class GetBotsByUserAndStatusCommand : IRequest>
+ {
+ public int UserId { get; }
+ public BotStatus Status { get; }
+
+ public GetBotsByUserAndStatusCommand(int userId, BotStatus status)
+ {
+ UserId = userId;
+ Status = status;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Managing.Application/ManageBot/Commands/GetPaginatedAgentSummariesCommand.cs b/src/Managing.Application/ManageBot/Commands/GetPaginatedAgentSummariesCommand.cs
new file mode 100644
index 0000000..21509e9
--- /dev/null
+++ b/src/Managing.Application/ManageBot/Commands/GetPaginatedAgentSummariesCommand.cs
@@ -0,0 +1,51 @@
+using Managing.Domain.Statistics;
+using MediatR;
+using static Managing.Common.Enums;
+
+namespace Managing.Application.ManageBot.Commands
+{
+ ///
+ /// Command to retrieve paginated agent summaries with sorting and filtering
+ ///
+ public class GetPaginatedAgentSummariesCommand : IRequest<(IEnumerable Results, int TotalCount)>
+ {
+ ///
+ /// Page number (1-based)
+ ///
+ public int Page { get; }
+
+ ///
+ /// Number of items per page
+ ///
+ public int PageSize { get; }
+
+ ///
+ /// Field to sort by
+ ///
+ public SortableFields SortBy { get; }
+
+ ///
+ /// Sort order (asc or desc)
+ ///
+ public string SortOrder { get; }
+
+ ///
+ /// Optional list of agent names to filter by
+ ///
+ public IEnumerable? AgentNames { get; }
+
+ public GetPaginatedAgentSummariesCommand(
+ int page = 1,
+ int pageSize = 10,
+ SortableFields sortBy = SortableFields.TotalPnL,
+ string sortOrder = "desc",
+ IEnumerable? agentNames = null)
+ {
+ Page = page;
+ PageSize = pageSize;
+ SortBy = sortBy;
+ SortOrder = sortOrder;
+ AgentNames = agentNames;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Managing.Application/ManageBot/Commands/GetUserStrategiesCommand.cs b/src/Managing.Application/ManageBot/Commands/GetUserStrategiesCommand.cs
index e226931..8bf70e3 100644
--- a/src/Managing.Application/ManageBot/Commands/GetUserStrategiesCommand.cs
+++ b/src/Managing.Application/ManageBot/Commands/GetUserStrategiesCommand.cs
@@ -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
///
/// Command to retrieve all strategies owned by a specific user
///
- public class GetUserStrategiesCommand : IRequest>
+ public class GetUserStrategiesCommand : IRequest>
{
- public string UserName { get; }
+ public string AgentName { get; }
- public GetUserStrategiesCommand(string userName)
+ public GetUserStrategiesCommand(string agentName)
{
- UserName = userName;
+ AgentName = agentName;
}
}
}
\ No newline at end of file
diff --git a/src/Managing.Application/ManageBot/Commands/GetUserStrategyCommand.cs b/src/Managing.Application/ManageBot/Commands/GetUserStrategyCommand.cs
index b5a457c..ccaa9b0 100644
--- a/src/Managing.Application/ManageBot/Commands/GetUserStrategyCommand.cs
+++ b/src/Managing.Application/ManageBot/Commands/GetUserStrategyCommand.cs
@@ -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
///
/// Command to retrieve a specific strategy owned by a user
///
- public class GetUserStrategyCommand : IRequest
+ public class GetUserStrategyCommand : IRequest
{
///
/// The username of the agent/user that owns the strategy
diff --git a/src/Managing.Application/ManageBot/Commands/ManualPositionCommand.cs b/src/Managing.Application/ManageBot/Commands/ManualPositionCommand.cs
new file mode 100644
index 0000000..1fca903
--- /dev/null
+++ b/src/Managing.Application/ManageBot/Commands/ManualPositionCommand.cs
@@ -0,0 +1,16 @@
+using Managing.Domain.Trades;
+using MediatR;
+
+namespace Managing.Application.ManageBot.Commands;
+
+public class ManualPositionCommand : IRequest
+{
+ public Guid PositionId { get; set; }
+ public Guid Identifier { get; set; }
+
+ public ManualPositionCommand(Guid identifier, Guid positionId)
+ {
+ Identifier = identifier;
+ PositionId = positionId;
+ }
+}
\ No newline at end of file
diff --git a/src/Managing.Application/ManageBot/Commands/RestartBotCommand.cs b/src/Managing.Application/ManageBot/Commands/RestartBotCommand.cs
index d92a34a..c0692c7 100644
--- a/src/Managing.Application/ManageBot/Commands/RestartBotCommand.cs
+++ b/src/Managing.Application/ManageBot/Commands/RestartBotCommand.cs
@@ -3,15 +3,13 @@ using static Managing.Common.Enums;
namespace Managing.Application.ManageBot.Commands
{
- public class RestartBotCommand : IRequest
+ public class RestartBotCommand : IRequest
{
- 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;
}
}
}
diff --git a/src/Managing.Application/ManageBot/Commands/StartBotCommand.cs b/src/Managing.Application/ManageBot/Commands/StartBotCommand.cs
index adf11c3..32961ea 100644
--- a/src/Managing.Application/ManageBot/Commands/StartBotCommand.cs
+++ b/src/Managing.Application/ManageBot/Commands/StartBotCommand.cs
@@ -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
+ public class StartBotCommand : IRequest
{
- 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;
}
}
}
\ No newline at end of file
diff --git a/src/Managing.Application/ManageBot/Commands/StopBotCommand.cs b/src/Managing.Application/ManageBot/Commands/StopBotCommand.cs
index 5ed673c..915d929 100644
--- a/src/Managing.Application/ManageBot/Commands/StopBotCommand.cs
+++ b/src/Managing.Application/ManageBot/Commands/StopBotCommand.cs
@@ -1,12 +1,13 @@
īģŋusing MediatR;
+using static Managing.Common.Enums;
namespace Managing.Application.ManageBot.Commands
{
- public class StopBotCommand : IRequest
+ public class StopBotCommand : IRequest
{
- public string Identifier { get; }
+ public Guid Identifier { get; }
- public StopBotCommand(string identifier)
+ public StopBotCommand(Guid identifier)
{
Identifier = identifier;
}
diff --git a/src/Managing.Application/ManageBot/Commands/ToggleIsForWatchingCommand.cs b/src/Managing.Application/ManageBot/Commands/ToggleIsForWatchingCommand.cs
deleted file mode 100644
index 40ee74a..0000000
--- a/src/Managing.Application/ManageBot/Commands/ToggleIsForWatchingCommand.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-īģŋusing MediatR;
-
-namespace Managing.Application.ManageBot.Commands
-{
- public class ToggleIsForWatchingCommand : IRequest
- {
- public string Name { get; }
-
- public ToggleIsForWatchingCommand(string name)
- {
- Name = name;
- }
- }
-}
diff --git a/src/Managing.Application/ManageBot/Commands/UpdateBotConfigCommand.cs b/src/Managing.Application/ManageBot/Commands/UpdateBotConfigCommand.cs
index 9f4a523..598b686 100644
--- a/src/Managing.Application/ManageBot/Commands/UpdateBotConfigCommand.cs
+++ b/src/Managing.Application/ManageBot/Commands/UpdateBotConfigCommand.cs
@@ -6,12 +6,12 @@ namespace Managing.Application.ManageBot.Commands
///
/// Command to update the configuration of a running trading bot
///
- public class UpdateBotConfigCommand : IRequest
+ public class UpdateBotConfigCommand : IRequest
{
- 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;
diff --git a/src/Managing.Application/ManageBot/DeleteBotCommandHandler.cs b/src/Managing.Application/ManageBot/DeleteBotCommandHandler.cs
index 7ae2734..9faab26 100644
--- a/src/Managing.Application/ManageBot/DeleteBotCommandHandler.cs
+++ b/src/Managing.Application/ManageBot/DeleteBotCommandHandler.cs
@@ -18,6 +18,6 @@ public class DeleteBotCommandHandler : IRequestHandler
public Task Handle(DeleteBotCommand request, CancellationToken cancellationToken)
{
- return _botService.DeleteBot(request.Name);
+ return _botService.DeleteBot(request.Identifier);
}
}
\ No newline at end of file
diff --git a/src/Managing.Application/ManageBot/GetActiveBotsCommandHandler.cs b/src/Managing.Application/ManageBot/GetActiveBotsCommandHandler.cs
index 930049e..d879ed6 100644
--- a/src/Managing.Application/ManageBot/GetActiveBotsCommandHandler.cs
+++ b/src/Managing.Application/ManageBot/GetActiveBotsCommandHandler.cs
@@ -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>
+ public class GetBotsByStatusCommandHandler(IBotService botService)
+ : IRequestHandler>
{
- public Task> Handle(GetActiveBotsCommand request, CancellationToken cancellationToken)
+ public async Task> Handle(GetBotsByStatusCommand request, CancellationToken cancellationToken)
{
- return Task.FromResult(botService.GetActiveBots());
+ return await botService.GetBotsByStatusAsync(request.Status);
}
}
}
\ No newline at end of file
diff --git a/src/Managing.Application/ManageBot/GetAgentStatusesCommandHandler.cs b/src/Managing.Application/ManageBot/GetAgentStatusesCommandHandler.cs
index cdb4d34..1f5277a 100644
--- a/src/Managing.Application/ManageBot/GetAgentStatusesCommandHandler.cs
+++ b/src/Managing.Application/ManageBot/GetAgentStatusesCommandHandler.cs
@@ -20,11 +20,11 @@ namespace Managing.Application.ManageBot
_accountService = accountService;
}
- public Task> Handle(GetAgentStatusesCommand request,
+ public async Task> Handle(GetAgentStatusesCommand request,
CancellationToken cancellationToken)
{
var result = new List();
- 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;
}
}
-}
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/src/Managing.Application/ManageBot/GetAllAgentSummariesCommandHandler.cs b/src/Managing.Application/ManageBot/GetAllAgentSummariesCommandHandler.cs
new file mode 100644
index 0000000..b0a6655
--- /dev/null
+++ b/src/Managing.Application/ManageBot/GetAllAgentSummariesCommandHandler.cs
@@ -0,0 +1,54 @@
+using Managing.Application.Abstractions.Services;
+using Managing.Application.ManageBot.Commands;
+using Managing.Domain.Statistics;
+using MediatR;
+
+namespace Managing.Application.ManageBot
+{
+ ///
+ /// Handler for retrieving all agent summaries with complete data
+ ///
+ public class GetAllAgentSummariesCommandHandler : IRequestHandler>
+ {
+ private readonly IStatisticService _statisticService;
+
+ public GetAllAgentSummariesCommandHandler(IStatisticService statisticService)
+ {
+ _statisticService = statisticService;
+ }
+
+ public async Task> 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;
+ }
+
+ ///
+ /// Gets the cutoff date based on the time filter
+ ///
+ 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
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Managing.Application/ManageBot/GetAllAgentsCommandHandler.cs b/src/Managing.Application/ManageBot/GetAllAgentsCommandHandler.cs
deleted file mode 100644
index 72ba073..0000000
--- a/src/Managing.Application/ManageBot/GetAllAgentsCommandHandler.cs
+++ /dev/null
@@ -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
-{
- ///
- /// Handler for retrieving all agents and their strategies
- ///
- public class GetAllAgentsCommandHandler : IRequestHandler>>
- {
- private readonly IBotService _botService;
- private readonly IAccountService _accountService;
-
- public GetAllAgentsCommandHandler(IBotService botService, IAccountService accountService)
- {
- _botService = botService;
- _accountService = accountService;
- }
-
- public Task>> Handle(GetAllAgentsCommand request,
- CancellationToken cancellationToken)
- {
- var result = new Dictionary>();
- 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();
- }
-
- result[bot.User].Add(bot);
- }
-
- return Task.FromResult(result);
- }
-
- ///
- /// Checks if a bot has had trading activity within the specified time range
- ///
- 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));
- }
- }
-}
\ No newline at end of file
diff --git a/src/Managing.Application/ManageBot/GetBotsByUserAndStatusCommandHandler.cs b/src/Managing.Application/ManageBot/GetBotsByUserAndStatusCommandHandler.cs
new file mode 100644
index 0000000..e77ae3b
--- /dev/null
+++ b/src/Managing.Application/ManageBot/GetBotsByUserAndStatusCommandHandler.cs
@@ -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>
+ {
+ public async Task> 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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Managing.Application/ManageBot/GetOnlineAgentNamesCommandHandler.cs b/src/Managing.Application/ManageBot/GetOnlineAgentNamesCommandHandler.cs
index b3ecbd4..2ba5ce6 100644
--- a/src/Managing.Application/ManageBot/GetOnlineAgentNamesCommandHandler.cs
+++ b/src/Managing.Application/ManageBot/GetOnlineAgentNamesCommandHandler.cs
@@ -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
{
///
/// Handler for retrieving only online agent names
///
- public class GetOnlineAgentNamesCommandHandler : IRequestHandler>
+ public class GetOnlineAgentNamesCommandHandler : IRequestHandler>
{
private readonly IBotService _botService;
- private readonly IAccountService _accountService;
- public GetOnlineAgentNamesCommandHandler(IBotService botService, IAccountService accountService)
+ public GetOnlineAgentNamesCommandHandler(IBotService botService)
{
_botService = botService;
- _accountService = accountService;
}
- public Task> Handle(GetOnlineAgentNamesCommand request,
+ public async Task> Handle(GetOnlineAgentNamesCommand request,
CancellationToken cancellationToken)
{
- var onlineAgentNames = new List();
- 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();
}
}
}
\ No newline at end of file
diff --git a/src/Managing.Application/ManageBot/GetPaginatedAgentSummariesCommandHandler.cs b/src/Managing.Application/ManageBot/GetPaginatedAgentSummariesCommandHandler.cs
new file mode 100644
index 0000000..21edbb7
--- /dev/null
+++ b/src/Managing.Application/ManageBot/GetPaginatedAgentSummariesCommandHandler.cs
@@ -0,0 +1,33 @@
+using Managing.Application.Abstractions.Repositories;
+using Managing.Application.ManageBot.Commands;
+using Managing.Domain.Statistics;
+using MediatR;
+
+namespace Managing.Application.ManageBot
+{
+ ///
+ /// Handler for retrieving paginated agent summaries with sorting and filtering
+ ///
+ public class GetPaginatedAgentSummariesCommandHandler : IRequestHandler Results, int TotalCount)>
+ {
+ private readonly IAgentSummaryRepository _agentSummaryRepository;
+
+ public GetPaginatedAgentSummariesCommandHandler(IAgentSummaryRepository agentSummaryRepository)
+ {
+ _agentSummaryRepository = agentSummaryRepository;
+ }
+
+ public async Task<(IEnumerable Results, int TotalCount)> Handle(
+ GetPaginatedAgentSummariesCommand request,
+ CancellationToken cancellationToken)
+ {
+ return await _agentSummaryRepository.GetPaginatedAsync(
+ request.Page,
+ request.PageSize,
+ request.SortBy,
+ request.SortOrder,
+ request.AgentNames);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Managing.Application/ManageBot/GetUserStrategiesCommandHandler.cs b/src/Managing.Application/ManageBot/GetUserStrategiesCommandHandler.cs
index 0fbbc01..422bfdc 100644
--- a/src/Managing.Application/ManageBot/GetUserStrategiesCommandHandler.cs
+++ b/src/Managing.Application/ManageBot/GetUserStrategiesCommandHandler.cs
@@ -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>
+ public class GetUserStrategiesCommandHandler : IRequestHandler>
{
private readonly IBotService _botService;
+ private readonly IUserService _userService;
- public GetUserStrategiesCommandHandler(IBotService botService)
+ public GetUserStrategiesCommandHandler(IBotService botService, IUserService userService)
{
_botService = botService;
+ _userService = userService;
}
- public Task> Handle(GetUserStrategiesCommand request, CancellationToken cancellationToken)
+ public async Task> 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);
}
}
}
\ No newline at end of file
diff --git a/src/Managing.Application/ManageBot/GetUserStrategyCommandHandler.cs b/src/Managing.Application/ManageBot/GetUserStrategyCommandHandler.cs
index 1afefba..208acb1 100644
--- a/src/Managing.Application/ManageBot/GetUserStrategyCommandHandler.cs
+++ b/src/Managing.Application/ManageBot/GetUserStrategyCommandHandler.cs
@@ -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
///
/// Handler for retrieving a specific strategy owned by a user
///
- public class GetUserStrategyCommandHandler : IRequestHandler
+ public class GetUserStrategyCommandHandler : IRequestHandler
{
private readonly IBotService _botService;
@@ -16,17 +17,14 @@ namespace Managing.Application.ManageBot
_botService = botService;
}
- public Task Handle(GetUserStrategyCommand request, CancellationToken cancellationToken)
+ public async Task 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;
}
}
}
\ No newline at end of file
diff --git a/src/Managing.Application/ManageBot/LoadBackupBotCommandHandler.cs b/src/Managing.Application/ManageBot/LoadBackupBotCommandHandler.cs
deleted file mode 100644
index c870375..0000000
--- a/src/Managing.Application/ManageBot/LoadBackupBotCommandHandler.cs
+++ /dev/null
@@ -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
-{
- private readonly IBotService _botService;
- private readonly ILogger _logger;
-
- public LoadBackupBotCommandHandler(
- ILogger logger, IBotService botService)
- {
- _logger = logger;
- _botService = botService;
- }
-
- public async Task Handle(LoadBackupBotCommand request, CancellationToken cancellationToken)
- {
- var backupBots = (await _botService.GetSavedBotsAsync()).ToList();
- _logger.LogInformation("Loading {Count} backup bots.", backupBots.Count);
-
- var result = new Dictionary();
- 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(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
-{
-}
\ No newline at end of file
diff --git a/src/Managing.Application/ManageBot/ManualPositionCommandHandler.cs b/src/Managing.Application/ManageBot/ManualPositionCommandHandler.cs
new file mode 100644
index 0000000..11c7d26
--- /dev/null
+++ b/src/Managing.Application/ManageBot/ManualPositionCommandHandler.cs
@@ -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
+{
+ private readonly IBotService _botService;
+
+ public ManualPositionCommandHandler(IBotService botService)
+ {
+ _botService = botService;
+ }
+
+ public async Task 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);
+ }
+}
\ No newline at end of file
diff --git a/src/Managing.Application/ManageBot/RestartBotCommandHandler.cs b/src/Managing.Application/ManageBot/RestartBotCommandHandler.cs
index dd9a4a2..baf5fe9 100644
--- a/src/Managing.Application/ManageBot/RestartBotCommandHandler.cs
+++ b/src/Managing.Application/ManageBot/RestartBotCommandHandler.cs
@@ -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
+ public class RestartBotCommandHandler : IRequestHandler
{
private readonly IBotService _botService;
@@ -13,9 +14,9 @@ namespace Managing.Application.ManageBot
_botService = botService;
}
- public Task Handle(RestartBotCommand request, CancellationToken cancellationToken)
+ public async Task Handle(RestartBotCommand request, CancellationToken cancellationToken)
{
- return _botService.RestartBot(request.Name);
+ return await _botService.RestartBot(request.Identifier);
}
}
}
\ No newline at end of file
diff --git a/src/Managing.Application/ManageBot/StartBotCommandHandler.cs b/src/Managing.Application/ManageBot/StartBotCommandHandler.cs
index d1b4877..12e660d 100644
--- a/src/Managing.Application/ManageBot/StartBotCommandHandler.cs
+++ b/src/Managing.Application/ManageBot/StartBotCommandHandler.cs
@@ -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
+ public class StartBotCommandHandler : IRequestHandler
{
- 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 Handle(StartBotCommand request, CancellationToken cancellationToken)
+ public async Task 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();
- }
-
- ///
- /// Logs the bot configuration for debugging and audit purposes
- ///
- /// The trading bot instance
- /// Context information for the log
- 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(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;
}
}
}
\ No newline at end of file
diff --git a/src/Managing.Application/ManageBot/StopBotCommandHandler.cs b/src/Managing.Application/ManageBot/StopBotCommandHandler.cs
index df0a3a6..1cc8e80 100644
--- a/src/Managing.Application/ManageBot/StopBotCommandHandler.cs
+++ b/src/Managing.Application/ManageBot/StopBotCommandHandler.cs
@@ -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
+ public class StopBotCommandHandler : IRequestHandler
{
private readonly IBotService _botService;
@@ -13,9 +14,9 @@ namespace Managing.Application.ManageBot
_botService = botService;
}
- public Task