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 Handle(StopBotCommand request, CancellationToken cancellationToken) + public async Task Handle(StopBotCommand request, CancellationToken cancellationToken) { - return _botService.StopBot(request.Identifier); + return await _botService.StopBot(request.Identifier); } } } \ No newline at end of file diff --git a/src/Managing.Application/ManageBot/ToggleIsForWatchingCommandHandler.cs b/src/Managing.Application/ManageBot/ToggleIsForWatchingCommandHandler.cs deleted file mode 100644 index 4b25eb9..0000000 --- a/src/Managing.Application/ManageBot/ToggleIsForWatchingCommandHandler.cs +++ /dev/null @@ -1,23 +0,0 @@ -īģŋusing Managing.Application.Abstractions; -using Managing.Application.ManageBot.Commands; -using MediatR; - -namespace Managing.Application.ManageBot -{ - public class ToggleIsForWatchingCommandHandler : IRequestHandler - { - private readonly IBotService _botService; - - public ToggleIsForWatchingCommandHandler(IBotService botService) - { - _botService = botService; - } - - public Task Handle(ToggleIsForWatchingCommand request, CancellationToken cancellationToken) - { - _botService.ToggleIsForWatchingOnly(request.Name); - var bot = _botService.GetActiveBots().FirstOrDefault(b => b.Name == request.Name); - return Task.FromResult(bot?.Config.IsForWatchingOnly.ToString()); - } - } -} \ No newline at end of file diff --git a/src/Managing.Application/ManageBot/UpdateBotConfigCommandHandler.cs b/src/Managing.Application/ManageBot/UpdateBotConfigCommandHandler.cs index f5c58d5..89dc61a 100644 --- a/src/Managing.Application/ManageBot/UpdateBotConfigCommandHandler.cs +++ b/src/Managing.Application/ManageBot/UpdateBotConfigCommandHandler.cs @@ -7,7 +7,7 @@ namespace Managing.Application.ManageBot /// /// Handler for updating trading bot configurations /// - public class UpdateBotConfigCommandHandler : IRequestHandler + public class UpdateBotConfigCommandHandler : IRequestHandler { private readonly IBotService _botService; @@ -16,44 +16,27 @@ namespace Managing.Application.ManageBot _botService = botService; } - public async Task Handle(UpdateBotConfigCommand request, CancellationToken cancellationToken) + public async Task Handle(UpdateBotConfigCommand request, CancellationToken cancellationToken) { try { - if (string.IsNullOrEmpty(request.Identifier)) - { - throw new ArgumentException("Bot identifier is required"); - } - if (request.NewConfig == null) { throw new ArgumentException("New configuration is required"); } - // Get the bot from active bots - var activeBots = _botService.GetActiveBots(); - var bot = activeBots.FirstOrDefault(b => b.Identifier == request.Identifier); + var bot = await _botService.GetBotByIdentifier(request.Identifier); if (bot == null) { - return $"Bot with identifier {request.Identifier} not found or is not running"; + throw new Exception($"Bot with identifier {request.Identifier} not found"); } - // Update the bot configuration - var updateResult = await bot.UpdateConfiguration(request.NewConfig); - - if (updateResult) - { - return $"Bot configuration updated successfully for {request.Identifier}"; - } - else - { - return $"Failed to update bot configuration for {request.Identifier}"; - } + return await _botService.UpdateBotConfiguration(request.Identifier, request.NewConfig); } catch (Exception ex) { - return $"Error updating bot configuration: {ex.Message}"; + throw new Exception($"Error updating bot configuration: {ex.Message}"); } } } diff --git a/src/Managing.Application/Managing.Application.csproj b/src/Managing.Application/Managing.Application.csproj index 2d023db..b8c7340 100644 --- a/src/Managing.Application/Managing.Application.csproj +++ b/src/Managing.Application/Managing.Application.csproj @@ -7,31 +7,33 @@ - - - + + + - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - + + + + diff --git a/src/Managing.Application/Scenarios/ScenarioRunnerGrain.cs b/src/Managing.Application/Scenarios/ScenarioRunnerGrain.cs new file mode 100644 index 0000000..2bf41d3 --- /dev/null +++ b/src/Managing.Application/Scenarios/ScenarioRunnerGrain.cs @@ -0,0 +1,99 @@ +using Managing.Application.Abstractions.Grains; +using Managing.Application.Abstractions.Services; +using Managing.Core; +using Managing.Domain.Bots; +using Managing.Domain.Candles; +using Managing.Domain.Indicators; +using Managing.Domain.Shared.Helpers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Orleans.Concurrency; +using static Managing.Common.Enums; + +namespace Managing.Application.Scenarios; + +/// +/// Orleans grain for scenario execution and signal generation. +/// This stateless grain handles candle management and signal generation for live trading. +/// +[StatelessWorker] +public class ScenarioRunnerGrain : Grain, IScenarioRunnerGrain +{ + private readonly ILogger _logger; + private readonly IServiceScopeFactory _scopeFactory; + + public ScenarioRunnerGrain( + ILogger logger, + IServiceScopeFactory scopeFactory) + { + _logger = logger; + _scopeFactory = scopeFactory; + } + + private async Task> GetCandlesAsync(TradingBotConfig config, DateTime startDate) + { + try + { + var newCandles = await ServiceScopeHelpers.WithScopedService>( + _scopeFactory, async exchangeService => + { + return await exchangeService.GetCandlesInflux( + TradingExchanges.Evm, + config.Ticker, + startDate, + config.Timeframe, + 500); + }); + + _logger.LogInformation($"Updated {newCandles.Count} candles for {config.Ticker}"); + return newCandles; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to update candles for {Ticker}", config.Ticker); + throw; + } + } + + public async Task GetSignals(TradingBotConfig config, Dictionary previousSignals, + DateTime startDate, Candle candle) + { + try + { + // return new LightSignal(config.Ticker, TradeDirection.Long, Confidence.High, + // candle, candle.Date, TradingExchanges.Evm, IndicatorType.Composite, + // SignalType.Signal, "Generated Signal"); + + var candlesHashSet = await GetCandlesAsync(config, startDate); + if (candlesHashSet.LastOrDefault()!.Date <= candle.Date) + { + return null; // No new candles, no need to generate a signal + } + + var signal = TradingBox.GetSignal( + candlesHashSet, + config.Scenario, + previousSignals, + config.Scenario?.LoopbackPeriod ?? 1); + + if (signal != null && signal.Date >= candle.Date) + { + _logger.LogInformation( + $"Generated signal for {config.Ticker}: {signal.Direction} with confidence {signal.Confidence}"); + + return new LightSignal(signal.Ticker, signal.Direction, Confidence.High, + candle, candle.Date, signal.Exchange, signal.IndicatorType, + signal.SignalType, signal.IndicatorName); + } + else + { + return null; // No signal generated + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to update signals for {Ticker}", config.Ticker); + throw; + } + } +} \ No newline at end of file diff --git a/src/Managing.Application/Scenarios/ScenarioService.cs b/src/Managing.Application/Scenarios/ScenarioService.cs index 1ce7518..94ec165 100644 --- a/src/Managing.Application/Scenarios/ScenarioService.cs +++ b/src/Managing.Application/Scenarios/ScenarioService.cs @@ -1,4 +1,5 @@ -īģŋusing Managing.Application.Abstractions; +īģŋusing System.Data; +using Managing.Application.Abstractions; using Managing.Application.Abstractions.Services; using Managing.Domain.Scenarios; using Managing.Domain.Strategies; @@ -25,7 +26,7 @@ namespace Managing.Application.Scenarios foreach (var strategy in strategies) { - scenario.AddIndicator(await _tradingService.GetStrategyByNameAsync(strategy)); + scenario.AddIndicator(await _tradingService.GetIndicatorByNameAsync(strategy)); } try @@ -41,41 +42,14 @@ namespace Managing.Application.Scenarios return scenario; } - public async 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) - { - var strategy = ScenarioHelpers.BuildIndicator( - type, - name, - period, - fastPeriods, - slowPeriods, - signalPeriods, - multiplier, - stochPeriods, - smoothPeriods, - cyclePeriods); - await _tradingService.InsertStrategyAsync(strategy); - return strategy; - } - public async Task> GetScenariosAsync() { return await _tradingService.GetScenariosAsync(); } - public async Task> GetIndicatorsAsync() + public async Task> GetIndicatorsAsync() { - return await _tradingService.GetStrategiesAsync(); + return await _tradingService.GetIndicatorsAsync(); } public async Task DeleteScenarioAsync(string name) @@ -100,7 +74,7 @@ namespace Managing.Application.Scenarios scenario.Indicators.Clear(); foreach (var strategy in strategies) { - scenario.AddIndicator(await _tradingService.GetStrategyByNameAsync(strategy)); + scenario.AddIndicator(await _tradingService.GetIndicatorByNameAsync(strategy)); } scenario.LoopbackPeriod = loopbackPeriod ?? 1; @@ -120,7 +94,7 @@ namespace Managing.Application.Scenarios { try { - var strategy = await _tradingService.GetStrategyByNameAsync(name); + var strategy = await _tradingService.GetIndicatorByNameAsync(name); strategy.Type = indicatorType; strategy.Period = period; strategy.FastPeriods = fastPeriods; @@ -130,7 +104,7 @@ namespace Managing.Application.Scenarios strategy.StochPeriods = stochPeriods; strategy.SmoothPeriods = smoothPeriods; strategy.CyclePeriods = cyclePeriods; - await _tradingService.UpdateStrategyAsync(strategy); + await _tradingService.UpdateIndicatorAsync(strategy); return true; } catch (Exception e) @@ -156,7 +130,7 @@ namespace Managing.Application.Scenarios foreach (var strategyName in strategies) { - var strategy = await _tradingService.GetStrategyByNameAsync(strategyName); + var strategy = await _tradingService.GetIndicatorByNameAsync(strategyName); if (strategy != null && strategy.User?.Name == user.Name) { scenario.AddIndicator(strategy); @@ -167,7 +141,7 @@ namespace Managing.Application.Scenarios return scenario; } - public async Task> GetIndicatorsByUserAsync(User user) + public async Task> GetIndicatorsByUserAsync(User user) { var indicators = await GetIndicatorsAsync(); return indicators.Where(s => s.User?.Name == user.Name); @@ -175,10 +149,10 @@ namespace Managing.Application.Scenarios public async Task DeleteIndicatorByUser(User user, string name) { - var strategy = await _tradingService.GetStrategyByNameAsync(name); + var strategy = await _tradingService.GetIndicatorByNameAsync(name); if (strategy != null && strategy.User?.Name == user.Name) { - await _tradingService.DeleteStrategyAsync(strategy.Name); + await _tradingService.DeleteIndicatorAsync(strategy.Name); return true; } @@ -229,23 +203,35 @@ namespace Managing.Application.Scenarios return scenario != null && scenario.User?.Name == user.Name ? scenario : null; } - public async Task CreateIndicatorForUser(User user, IndicatorType type, string name, + public async Task CreateIndicatorForUser(User user, 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) { - // Create a new strategy using the existing implementation - var strategy = await CreateStrategy(type, name, period, fastPeriods, slowPeriods, signalPeriods, - multiplier, stochPeriods, smoothPeriods, cyclePeriods); + var existingIndicator = await _tradingService.GetIndicatorByNameUserAsync(name, user); - // Set the user - strategy.User = user; - - // Update the strategy to save the user property - await _tradingService.UpdateStrategyAsync(strategy); - - return strategy; + if (existingIndicator != null) + { + throw new DuplicateNameException("An indicator with this name already exists for the user."); + } + else + { + var indicator = new IndicatorBase(name, type) + { + Period = period, + FastPeriods = fastPeriods, + SlowPeriods = slowPeriods, + SignalPeriods = signalPeriods, + Multiplier = multiplier, + StochPeriods = stochPeriods, + SmoothPeriods = smoothPeriods, + CyclePeriods = cyclePeriods, + User = user + }; + await _tradingService.InsertIndicatorAsync(indicator); + return indicator; + } } public async Task DeleteStrategiesByUser(User user) @@ -255,7 +241,7 @@ namespace Managing.Application.Scenarios var strategies = await GetIndicatorsByUserAsync(user); foreach (var strategy in strategies) { - await _tradingService.DeleteStrategyAsync(strategy.Name); + await _tradingService.DeleteIndicatorAsync(strategy.Name); } return true; @@ -281,7 +267,7 @@ namespace Managing.Application.Scenarios foreach (var strategyName in strategies) { - var strategy = await _tradingService.GetStrategyByNameAsync(strategyName); + var strategy = await _tradingService.GetIndicatorByNameAsync(strategyName); if (strategy != null && strategy.User?.Name == user.Name) { scenario.AddIndicator(strategy); @@ -296,7 +282,7 @@ namespace Managing.Application.Scenarios int? fastPeriods, int? slowPeriods, int? signalPeriods, double? multiplier, int? stochPeriods, int? smoothPeriods, int? cyclePeriods) { - var strategy = await _tradingService.GetStrategyByNameAsync(name); + var strategy = await _tradingService.GetIndicatorByNameAsync(name); if (strategy == null || strategy.User?.Name != user.Name) { return false; @@ -311,7 +297,7 @@ namespace Managing.Application.Scenarios public async Task GetScenarioByNameAndUserAsync(string scenarioName, User user) { - var scenario = await _tradingService.GetScenarioByNameAsync(scenarioName); + var scenario = await _tradingService.GetScenarioByNameUserAsync(scenarioName, user); if (scenario == null) { throw new InvalidOperationException($"Scenario {scenarioName} not found for user {user.Name}"); diff --git a/src/Managing.Application/Shared/SettingsService.cs b/src/Managing.Application/Shared/SettingsService.cs index d17fa81..d43fdf2 100644 --- a/src/Managing.Application/Shared/SettingsService.cs +++ b/src/Managing.Application/Shared/SettingsService.cs @@ -90,7 +90,7 @@ public class SettingsService : ISettingsService private async Task SetupStochSTCTrend() { var name = "STCTrend"; - var strategy = await _scenarioService.CreateStrategy(IndicatorType.Stc, + var strategy = await _scenarioService.CreateIndicatorForUser(null, IndicatorType.Stc, name, fastPeriods: 23, slowPeriods: 50, @@ -101,7 +101,7 @@ public class SettingsService : ISettingsService private async Task SetupMacd() { var name = "MacdCross"; - var strategy = await _scenarioService.CreateStrategy(IndicatorType.MacdCross, + var strategy = await _scenarioService.CreateIndicatorForUser(null, IndicatorType.MacdCross, name, fastPeriods: 12, slowPeriods: 26, @@ -112,7 +112,7 @@ public class SettingsService : ISettingsService private async Task SetupRsiDiv() { var name = "RsiDiv6"; - var strategy = await _scenarioService.CreateStrategy(IndicatorType.RsiDivergence, + var strategy = await _scenarioService.CreateIndicatorForUser(null, IndicatorType.RsiDivergence, name, period: 6); await _scenarioService.CreateScenario(name, new List { strategy.Name }); @@ -121,7 +121,7 @@ public class SettingsService : ISettingsService private async Task SetupRsiDivConfirm() { var name = "RsiDivConfirm6"; - var strategy = await _scenarioService.CreateStrategy(IndicatorType.RsiDivergenceConfirm, + var strategy = await _scenarioService.CreateIndicatorForUser(null, IndicatorType.RsiDivergenceConfirm, name, period: 6); await _scenarioService.CreateScenario(name, new List { strategy.Name }); @@ -130,7 +130,7 @@ public class SettingsService : ISettingsService private async Task SetupSuperTrend() { var name = "SuperTrend"; - var strategy = await _scenarioService.CreateStrategy(IndicatorType.SuperTrend, + var strategy = await _scenarioService.CreateIndicatorForUser(null, IndicatorType.SuperTrend, name, period: 10, multiplier: 3); @@ -140,7 +140,7 @@ public class SettingsService : ISettingsService private async Task SetupChandelierExit() { var name = "ChandelierExit"; - var strategy = await _scenarioService.CreateStrategy(IndicatorType.ChandelierExit, + var strategy = await _scenarioService.CreateIndicatorForUser(null, IndicatorType.ChandelierExit, name, period: 22, multiplier: 3); @@ -150,7 +150,7 @@ public class SettingsService : ISettingsService private async Task SetupStochRsiTrend() { var name = "StochRsiTrend"; - var strategy = await _scenarioService.CreateStrategy(IndicatorType.StochRsiTrend, + var strategy = await _scenarioService.CreateIndicatorForUser(null, IndicatorType.StochRsiTrend, name, period: 14, stochPeriods: 14, @@ -162,7 +162,7 @@ public class SettingsService : ISettingsService private async Task SetupEmaTrend() { var name = "Ema200Trend"; - var strategy = await _scenarioService.CreateStrategy(IndicatorType.EmaTrend, + var strategy = await _scenarioService.CreateIndicatorForUser(null, IndicatorType.EmaTrend, name, period: 200); await _scenarioService.CreateScenario(name, new List { strategy.Name }); @@ -171,7 +171,7 @@ public class SettingsService : ISettingsService private async Task SetupEmaCross() { var name = "Ema200Cross"; - var strategy = await _scenarioService.CreateStrategy(IndicatorType.EmaCross, + var strategy = await _scenarioService.CreateIndicatorForUser(null, IndicatorType.EmaCross, name, period: 200); await _scenarioService.CreateScenario(name, new List { strategy.Name }); diff --git a/src/Managing.Application/Synth/SynthPredictionService.cs b/src/Managing.Application/Synth/SynthPredictionService.cs index 3e208a4..f73ad9a 100644 --- a/src/Managing.Application/Synth/SynthPredictionService.cs +++ b/src/Managing.Application/Synth/SynthPredictionService.cs @@ -1,6 +1,7 @@ using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Services; using Managing.Domain.Bots; +using Managing.Domain.Indicators; using Managing.Domain.Risk; using Managing.Domain.Synth.Models; using Microsoft.Extensions.Logging; @@ -867,7 +868,7 @@ public class SynthPredictionService : ISynthPredictionService /// Monitors liquidation risk for an open position /// public async Task MonitorPositionRiskAsync(Ticker ticker, TradeDirection direction, - decimal currentPrice, decimal liquidationPrice, string positionIdentifier, TradingBotConfig botConfig) + decimal currentPrice, decimal liquidationPrice, Guid positionIdentifier, TradingBotConfig botConfig) { var result = new SynthRiskResult(); var config = BuildConfigurationForTimeframe(botConfig.Timeframe, botConfig); diff --git a/src/Managing.Application/Trading/OpenPositionCommandHandler.cs b/src/Managing.Application/Trading/OpenPositionCommandHandler.cs index 69b831a..77cf795 100644 --- a/src/Managing.Application/Trading/OpenPositionCommandHandler.cs +++ b/src/Managing.Application/Trading/OpenPositionCommandHandler.cs @@ -27,7 +27,7 @@ namespace Managing.Application.Trading } var initiator = request.IsForPaperTrading ? PositionInitiator.PaperTrading : request.Initiator; - var position = new Position(Guid.NewGuid().ToString(), request.AccountName, request.Direction, + var position = new Position(Guid.NewGuid(), request.AccountName, request.Direction, request.Ticker, request.MoneyManagement, initiator, request.Date, request.User); diff --git a/src/Managing.Application/Trading/TradingService.cs b/src/Managing.Application/Trading/TradingService.cs index e528e51..3657ae6 100644 --- a/src/Managing.Application/Trading/TradingService.cs +++ b/src/Managing.Application/Trading/TradingService.cs @@ -1,9 +1,9 @@ īģŋusing Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Services; -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.Statistics; @@ -11,6 +11,7 @@ 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 Microsoft.Extensions.Logging; using static Managing.Common.Enums; @@ -56,12 +57,12 @@ public class TradingService : ITradingService await _tradingRepository.DeleteScenarioAsync(name); } - public async Task DeleteStrategyAsync(string name) + public async Task DeleteIndicatorAsync(string name) { await _tradingRepository.DeleteIndicatorAsync(name); } - public async Task GetPositionByIdentifierAsync(string identifier) + public async Task GetPositionByIdentifierAsync(Guid identifier) { return await _tradingRepository.GetPositionByIdentifierAsync(identifier); } @@ -87,12 +88,12 @@ public class TradingService : ITradingService return await _tradingRepository.GetScenariosAsync(); } - public async Task> GetStrategiesAsync() + public async Task> GetIndicatorsAsync() { return await _tradingRepository.GetStrategiesAsync(); } - public async Task GetStrategyByNameAsync(string strategy) + public async Task GetIndicatorByNameAsync(string strategy) { return await _tradingRepository.GetStrategyByNameAsync(strategy); } @@ -107,9 +108,9 @@ public class TradingService : ITradingService await _tradingRepository.InsertScenarioAsync(scenario); } - public async Task InsertStrategyAsync(Indicator indicator) + public async Task InsertIndicatorAsync(IndicatorBase indicatorBase) { - await _tradingRepository.InsertStrategyAsync(indicator); + await _tradingRepository.InsertIndicatorAsync(indicatorBase); } public async Task ManagePosition(Account account, Position position) @@ -170,7 +171,6 @@ public class TradingService : ITradingService } - public async Task UpdatePositionAsync(Position position) { await _tradingRepository.UpdatePositionAsync(position); @@ -235,9 +235,9 @@ public class TradingService : ITradingService await _tradingRepository.UpdateScenarioAsync(scenario); } - public async Task UpdateStrategyAsync(Indicator indicator) + public async Task UpdateIndicatorAsync(IndicatorBase indicatorBase) { - await _tradingRepository.UpdateStrategyAsync(indicator); + await _tradingRepository.UpdateStrategyAsync(indicatorBase); } public async Task> GetBrokerPositions(Account account) @@ -372,7 +372,7 @@ public class TradingService : ITradingService } public async Task MonitorSynthPositionRiskAsync(Ticker ticker, TradeDirection direction, - decimal currentPrice, decimal liquidationPrice, string positionIdentifier, TradingBotConfig botConfig) + decimal currentPrice, decimal liquidationPrice, Guid positionIdentifier, TradingBotConfig botConfig) { return await _synthPredictionService.MonitorPositionRiskAsync(ticker, direction, currentPrice, liquidationPrice, positionIdentifier, botConfig); @@ -386,7 +386,7 @@ public class TradingService : ITradingService /// A dictionary of indicator types to their calculated values. public Dictionary CalculateIndicatorsValuesAsync( Scenario scenario, - List candles) + HashSet candles) { var indicatorsValues = new Dictionary(); @@ -395,27 +395,15 @@ public class TradingService : ITradingService return indicatorsValues; } - // Convert candles to FixedSizeQueue for indicators - var fixedCandles = new FixedSizeQueue(10000); - foreach (var candle in candles) - { - fixedCandles.Enqueue(candle); - } - // Build indicators from scenario foreach (var indicator in scenario.Indicators) { try { - // Build the indicator using ScenarioHelpers - var builtIndicator = ScenarioHelpers.BuildIndicator(indicator, 10000); - builtIndicator.Candles = fixedCandles; - - indicatorsValues[indicator.Type] = builtIndicator.GetIndicatorValues(); + indicatorsValues[indicator.Type] = indicator.GetIndicatorValues(candles); } catch (Exception ex) { - // Log the error but continue with other indicators _logger.LogError(ex, "Error calculating indicator {IndicatorName}: {ErrorMessage}", indicator.Name, ex.Message); } @@ -423,4 +411,14 @@ public class TradingService : ITradingService return indicatorsValues; } + + public async Task GetIndicatorByNameUserAsync(string name, User user) + { + return await _tradingRepository.GetStrategyByNameUserAsync(name, user); + } + + public async Task GetScenarioByNameUserAsync(string scenarioName, User user) + { + return await _tradingRepository.GetScenarioByNameUserAsync(scenarioName, user); + } } \ No newline at end of file diff --git a/src/Managing.Application/Users/UserService.cs b/src/Managing.Application/Users/UserService.cs index a1a6290..8e2f17b 100644 --- a/src/Managing.Application/Users/UserService.cs +++ b/src/Managing.Application/Users/UserService.cs @@ -1,8 +1,10 @@ īģŋusing System.Text.RegularExpressions; +using Managing.Application.Abstractions.Grains; using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Services; using Managing.Common; using Managing.Domain.Accounts; +using Managing.Domain.Statistics; using Managing.Domain.Users; using Microsoft.Extensions.Logging; @@ -15,6 +17,8 @@ public class UserService : IUserService private readonly IAccountService _accountService; private readonly ILogger _logger; private readonly ICacheService _cacheService; + private readonly IGrainFactory _grainFactory; + private readonly IAgentSummaryRepository _agentSummaryRepository; private string[] authorizedAddresses = [ @@ -33,15 +37,19 @@ public class UserService : IUserService public UserService( IEvmManager evmManager, IUserRepository userRepository, - IAccountService accountService, + IAccountService accountService, ILogger logger, - ICacheService cacheService) + ICacheService cacheService, + IGrainFactory grainFactory, + IAgentSummaryRepository agentSummaryRepository) { _evmManager = evmManager; _userRepository = userRepository; _accountService = accountService; _logger = logger; _cacheService = cacheService; + _grainFactory = grainFactory; + _agentSummaryRepository = agentSummaryRepository; } public async Task Authenticate(string name, string address, string message, string signature) @@ -120,26 +128,32 @@ public class UserService : IUserService return user; } - public async Task GetUserByAddressAsync(string address) + public async Task GetUserByAddressAsync(string address, bool useCache = true) { var cacheKey = $"user-by-address-{address}"; - - // Check cache first - var cachedUser = _cacheService.GetValue(cacheKey); - if (cachedUser != null) + + // Check cache first if caching is enabled + if (useCache) { - return cachedUser; + var cachedUser = _cacheService.GetValue(cacheKey); + if (cachedUser != null) + { + return cachedUser; + } } - // Cache miss - fetch from database + // Fetch from database (either cache miss or cache disabled) var account = await _accountService.GetAccountByKey(address, true, false); var user = await _userRepository.GetUserByNameAsync(account.User.Name); // Use proper async version to avoid DbContext concurrency issues user.Accounts = (await _accountService.GetAccountsByUserAsync(user)).ToList(); - // Save to cache for 10 minutes (JWT middleware calls this on every request) - _cacheService.SaveValue(cacheKey, user, TimeSpan.FromMinutes(10)); + // Save to cache for 10 minutes if caching is enabled (JWT middleware calls this on every request) + if (useCache) + { + _cacheService.SaveValue(cacheKey, user, TimeSpan.FromMinutes(10)); + } return user; } @@ -154,8 +168,25 @@ public class UserService : IUserService } else { + user = await GetUserByName(user.Name); user.AgentName = agentName; await _userRepository.UpdateUser(user); + + // Initialize the AgentGrain for this user + try + { + var agentGrain = _grainFactory.GetGrain(user.Id); + await agentGrain.InitializeAsync(user.Id, agentName); + _logger.LogInformation("AgentGrain initialized for user {UserId} with agent name {AgentName}", user.Id, + agentName); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to initialize AgentGrain for user {UserId} with agent name {AgentName}", + user.Id, agentName); + // Don't throw here to avoid breaking the user update process + } + return user; } } @@ -176,6 +207,7 @@ public class UserService : IUserService throw new Exception("URL must point to a JPEG or PNG image"); } + user = await GetUserByName(user.Name); user.AvatarUrl = avatarUrl; await _userRepository.UpdateUser(user); return user; @@ -193,13 +225,41 @@ public class UserService : IUserService } } + user = await GetUserByName(user.Name); user.TelegramChannel = telegramChannel; await _userRepository.UpdateUser(user); return user; } - public async Task GetUser(string name) + public async Task GetUserByName(string name) { return await _userRepository.GetUserByNameAsync(name); } + + public async Task GetUserByAgentName(string agentName) + { + var user = await _userRepository.GetUserByAgentNameAsync(agentName); + if (user == null) + { + throw new Exception($"User with agent name {agentName} not found"); + } + + return user; + } + + public async Task SaveOrUpdateAgentSummary(AgentSummary agentSummary) + { + try + { + 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, "Failed to save/update AgentSummary for user {UserId} with agent name {AgentName}", + agentSummary.UserId, agentSummary.AgentName); + throw; + } + } } \ No newline at end of file diff --git a/src/Managing.Application/Workers/BalanceTrackingWorker.cs b/src/Managing.Application/Workers/BalanceTrackingWorker.cs index d5bf03a..d5a8627 100644 --- a/src/Managing.Application/Workers/BalanceTrackingWorker.cs +++ b/src/Managing.Application/Workers/BalanceTrackingWorker.cs @@ -1,7 +1,7 @@ -using Managing.Application.Abstractions; using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Services; using Managing.Application.ManageBot.Commands; +using Managing.Domain.Bots; using Managing.Domain.Statistics; using MediatR; using Microsoft.Extensions.Logging; @@ -46,20 +46,21 @@ public class BalanceTrackingWorker : BaseWorker _logger.LogInformation("Starting balance tracking..."); // Get all active bots - var bots = await _mediator.Send(new GetActiveBotsCommand()); + var bots = await _mediator.Send(new GetBotsByStatusCommand(BotStatus.Up)); - if (bots.Count == 0) + var botCount = bots.Count(); + if (botCount == 0) { _logger.LogWarning("No active bots found. Skipping balance tracking."); return; } - _logger.LogInformation($"Found {bots.Count} active bots. Proceeding with balance tracking."); + _logger.LogInformation($"Found {botCount} active bots. Proceeding with balance tracking."); await TrackBalances(bots); _logger.LogInformation("Completed balance tracking"); } - private async Task TrackBalances(List bots) + private async Task TrackBalances(IEnumerable bots) { // Group bots by agent/user var botsByAgent = bots @@ -98,9 +99,9 @@ public class BalanceTrackingWorker : BaseWorker // Calculate total allocated balance for all bots foreach (var bot in agentBots) { - totalBotAllocatedBalance += bot.Config.BotTradingBalance; + totalBotAllocatedBalance += bot.Volume; _logger.LogInformation( - $"Bot {bot.Name} allocated balance: {bot.Config.BotTradingBalance} USD"); + $"Bot {bot.Name} allocated balance: {bot.Volume} USD"); } // Get account balances for this agent (only once per agent) @@ -140,20 +141,7 @@ public class BalanceTrackingWorker : BaseWorker // Process all bots in a single iteration foreach (var bot in agentBots) { - // Get wallet balance - var latestBotBalance = bot.WalletBalances - .OrderByDescending(x => x.Key) - .FirstOrDefault(); - - if (latestBotBalance.Key != default) - { - botsAllocationUsdValue += latestBotBalance.Value; - _logger.LogInformation( - $"Bot {bot.Name} wallet balance: {latestBotBalance.Value} USD at {latestBotBalance.Key}"); - } - - // Calculate PnL - totalPnL += bot.GetProfitAndLoss(); + totalPnL += bot.Pnl; } totalAgentValue = totalAccountUsdValue + botsAllocationUsdValue; diff --git a/src/Managing.Application/Workers/BotManagerWorker.cs b/src/Managing.Application/Workers/BotManagerWorker.cs deleted file mode 100644 index c0d5896..0000000 --- a/src/Managing.Application/Workers/BotManagerWorker.cs +++ /dev/null @@ -1,22 +0,0 @@ -īģŋusing Managing.Application.ManageBot; -using MediatR; -using Microsoft.Extensions.Logging; -using static Managing.Common.Enums; - -namespace Managing.Application.Workers; - -public class BotManagerWorker( - ILogger logger, - IServiceProvider serviceProvider, - IMediator mediadior) - : BaseWorker(WorkerType.BotManager, - logger, - TimeSpan.FromMinutes(5), - serviceProvider) -{ - protected override async Task Run(CancellationToken cancellationToken) - { - var loadBackupBotCommand = new LoadBackupBotCommand(); - await mediadior.Send(loadBackupBotCommand, cancellationToken); - } -} \ No newline at end of file diff --git a/src/Managing.Bootstrap/ApiBootstrap.cs b/src/Managing.Bootstrap/ApiBootstrap.cs index 72bd403..55c5d44 100644 --- a/src/Managing.Bootstrap/ApiBootstrap.cs +++ b/src/Managing.Bootstrap/ApiBootstrap.cs @@ -8,7 +8,6 @@ using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Services; using Managing.Application.Accounts; using Managing.Application.Backtesting; -using Managing.Application.Bots.Base; using Managing.Application.ManageBot; using Managing.Application.ManageBot.Commands; using Managing.Application.MoneyManagements; @@ -22,6 +21,7 @@ using Managing.Application.Users; using Managing.Application.Workers; using Managing.Application.Workers.Abstractions; using Managing.Domain.Trades; +using Managing.Infrastructure.Database.PostgreSql; using Managing.Infrastructure.Databases; using Managing.Infrastructure.Databases.InfluxDb; using Managing.Infrastructure.Databases.InfluxDb.Abstractions; @@ -74,31 +74,43 @@ public static class ApiBootstrap public static IHostBuilder ConfigureOrleans(this IHostBuilder hostBuilder, IConfiguration configuration, bool isProduction) { - var postgreSqlConnectionString = configuration.GetSection("Databases:PostgreSql")["ConnectionString"]; + var postgreSqlConnectionString = configuration.GetSection("PostgreSql")["Orleans"]; return hostBuilder.UseOrleans(siloBuilder => { - // Configure clustering - if (isProduction && !string.IsNullOrEmpty(postgreSqlConnectionString)) - { - // Production clustering configuration - siloBuilder - .UseAdoNetClustering(options => - { - options.ConnectionString = postgreSqlConnectionString; - options.Invariant = "Npgsql"; - }) - .UseAdoNetReminderService(options => - { - options.ConnectionString = postgreSqlConnectionString; - options.Invariant = "Npgsql"; - }); - } - else - { - // Development clustering configuration - siloBuilder.UseLocalhostClustering(); - } + // Configure clustering with improved networking + siloBuilder + .UseAdoNetClustering(options => + { + options.ConnectionString = postgreSqlConnectionString; + options.Invariant = "Npgsql"; + }) + .UseAdoNetReminderService(options => + { + options.ConnectionString = postgreSqlConnectionString; + options.Invariant = "Npgsql"; + }); + + // Configure networking for better silo communication + siloBuilder + .ConfigureEndpoints(siloPort: 11111, gatewayPort: 30000) + .Configure(options => + { + // Configure cluster options with unique identifiers + options.ServiceId = "ManagingApp"; + options.ClusterId = configuration["ASPNETCORE_ENVIRONMENT"] ?? "Development"; + }) + .Configure(options => + { + // Configure grain collection to prevent memory issues + options.CollectionAge = TimeSpan.FromMinutes(10); + options.CollectionQuantum = TimeSpan.FromMinutes(1); + }) + .Configure(options => + { + // Configure messaging for better reliability + options.ResponseTimeout = TimeSpan.FromSeconds(30); + }); siloBuilder .ConfigureLogging(logging => logging.SetMinimumLevel(LogLevel.Information)); @@ -113,10 +125,28 @@ public static class ApiBootstrap options.HostSelf = true; options.CounterUpdateIntervalMs = 10000; // 10 seconds options.HideTrace = true; // Hide trace to reduce dashboard overhead + options.Host = "0.0.0.0"; // Allow external connections + options.Username = "admin"; + options.Password = "admin"; }); } - siloBuilder.AddMemoryGrainStorageAsDefault() + siloBuilder + .AddAdoNetGrainStorage("bot-store", options => + { + options.ConnectionString = postgreSqlConnectionString; + options.Invariant = "Npgsql"; + }) + .AddAdoNetGrainStorage("registry-store", options => + { + options.ConnectionString = postgreSqlConnectionString; + options.Invariant = "Npgsql"; + }) + .AddAdoNetGrainStorage("agent-store", options => + { + options.ConnectionString = postgreSqlConnectionString; + options.Invariant = "Npgsql"; + }) .ConfigureServices(services => { // Register existing services for Orleans DI @@ -125,13 +155,6 @@ public static class ApiBootstrap services.AddTransient(); services.AddTransient(); services.AddTransient(); - services.AddTransient(); - }) - .Configure(options => - { - // Configure cluster options - options.ServiceId = "ManagingApp"; - options.ClusterId = configuration["ASPNETCORE_ENVIRONMENT"] ?? "Development"; }); }) ; @@ -148,7 +171,6 @@ public static class ApiBootstrap services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddSingleton(); services.AddScoped(); services.AddScoped(); @@ -170,7 +192,6 @@ public static class ApiBootstrap services.AddTransient(); services.AddTransient(); - services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -206,6 +227,7 @@ public static class ApiBootstrap services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -230,11 +252,6 @@ public static class ApiBootstrap private static IServiceCollection AddWorkers(this IServiceCollection services, IConfiguration configuration) { - if (configuration.GetValue("WorkerBotManager", false)) - { - services.AddHostedService(); - } - if (configuration.GetValue("WorkerBalancesTracking", false)) { services.AddHostedService(); diff --git a/src/Managing.Bootstrap/Managing.Bootstrap.csproj b/src/Managing.Bootstrap/Managing.Bootstrap.csproj index 62b37ea..4f04609 100644 --- a/src/Managing.Bootstrap/Managing.Bootstrap.csproj +++ b/src/Managing.Bootstrap/Managing.Bootstrap.csproj @@ -7,28 +7,29 @@ - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - - - + + + + + + + diff --git a/src/Managing.Bootstrap/WorkersBootstrap.cs b/src/Managing.Bootstrap/WorkersBootstrap.cs index d45e1ae..806602a 100644 --- a/src/Managing.Bootstrap/WorkersBootstrap.cs +++ b/src/Managing.Bootstrap/WorkersBootstrap.cs @@ -4,7 +4,6 @@ using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Services; using Managing.Application.Accounts; using Managing.Application.Backtesting; -using Managing.Application.Bots.Base; using Managing.Application.ManageBot; using Managing.Application.MoneyManagements; using Managing.Application.Scenarios; @@ -59,7 +58,6 @@ public static class WorkersBootstrap services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddSingleton(); services.AddScoped(); services.AddScoped(); @@ -82,7 +80,6 @@ public static class WorkersBootstrap services.AddTransient(); services.AddTransient(); - services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Managing.Common/Enums.cs b/src/Managing.Common/Enums.cs index ede4162..249ec09 100644 --- a/src/Managing.Common/Enums.cs +++ b/src/Managing.Common/Enums.cs @@ -76,10 +76,9 @@ public static class Enums public enum BotStatus { + None, Down, - Starting, Up, - Backup } public enum SignalStatus @@ -473,4 +472,18 @@ public static class Enums Offline, Online } + + /// + /// Fields that can be used for sorting in data queries + /// + public enum SortableFields + { + TotalPnL, + TotalROI, + Wins, + Losses, + AgentName, + CreatedAt, + UpdatedAt + } } \ No newline at end of file diff --git a/src/Managing.Domain/Backtests/Backtest.cs b/src/Managing.Domain/Backtests/Backtest.cs index 60be133..d65f877 100644 --- a/src/Managing.Domain/Backtests/Backtest.cs +++ b/src/Managing.Domain/Backtests/Backtest.cs @@ -3,10 +3,9 @@ using System.ComponentModel.DataAnnotations; using Exilion.TradingAtomics; using Managing.Domain.Bots; using Managing.Domain.Candles; -using Managing.Domain.Strategies.Base; +using Managing.Domain.Indicators; using Managing.Domain.Trades; using Managing.Domain.Users; -using static Managing.Common.Enums; namespace Managing.Domain.Backtests; @@ -14,16 +13,15 @@ public class Backtest { public Backtest( TradingBotConfig config, - List positions, - List signals, - List candles = null) + Dictionary positions, + Dictionary signals, + HashSet candles = null) { Config = config; Positions = positions; Signals = signals; - Candles = candles != null ? candles : new List(); + Candles = candles != null ? candles : new HashSet(); WalletBalances = new List>(); - IndicatorsValues = new Dictionary(); // Initialize start and end dates if candles are provided if (candles != null && candles.Count > 0) @@ -44,16 +42,15 @@ public class Backtest [Required] public decimal GrowthPercentage { get; set; } [Required] public decimal HodlPercentage { get; set; } [Required] public TradingBotConfig Config { get; } - [Required] public List Positions { get; } - [Required] public List Signals { get; } - [Required] public List Candles { get; set; } + [Required] public Dictionary Positions { get; } + [Required] public Dictionary Signals { get; } + [Required] public HashSet Candles { get; set; } [Required] public DateTime StartDate { get; set; } [Required] public DateTime EndDate { get; set; } [Required] public PerformanceMetrics Statistics { get; set; } [Required] public decimal Fees { get; set; } [Required] public List> WalletBalances { get; set; } [Required] public User User { get; set; } - [Required] public Dictionary IndicatorsValues { get; set; } [Required] public double Score { get; set; } public string RequestId { get; set; } public object? Metadata { get; set; } diff --git a/src/Managing.Domain/Bots/Bot.cs b/src/Managing.Domain/Bots/Bot.cs index 6f13a76..6f324ef 100644 --- a/src/Managing.Domain/Bots/Bot.cs +++ b/src/Managing.Domain/Bots/Bot.cs @@ -3,123 +3,22 @@ using static Managing.Common.Enums; namespace Managing.Domain.Bots { - /// - /// A bot define what code should be run. - /// To run a code you have to herit from this class and implement the Run() method - /// - public abstract class Bot : IBot + public class Bot { - public int ExecutionCount; - public string Identifier { get; set; } - public string Name { get; set; } - public int Interval { get; set; } - public BotStatus Status { get; set; } public User User { get; set; } + public Guid Identifier { get; set; } + public string Name { get; set; } + public Ticker Ticker { get; set; } + public BotStatus Status { get; set; } + public DateTime StartupTime { get; set; } + public DateTime CreateDate { get; set; } - /// - /// The time when the bot was first started (creation date) - /// - public DateTime StartupTime { get; protected set; } - - /// - /// The time when the bot was created - /// - public DateTime CreateDate { get; protected set; } - - private CancellationTokenSource CancellationToken { get; set; } - - public Bot(string name) - { - Identifier = $"{name}-{DateTime.Now:yyyyMMdd-hhmm}-{Guid.NewGuid()}"; - Name = name; - Status = BotStatus.Down; - CancellationToken = new CancellationTokenSource(); - ExecutionCount = 0; - Interval = 3000; - CreateDate = DateTime.UtcNow; // Set the creation time when the bot is instantiated - StartupTime = DateTime.UtcNow; // Set the startup time to creation date initially - } - - public virtual void Start() - { - Status = BotStatus.Up; - // StartupTime remains unchanged on first start (it's already set to creation date) - } - - public async Task InitWorker(Func action) - { - try - { - await Task.Run(async () => - { - while (Status == BotStatus.Up && !CancellationToken.IsCancellationRequested) - { - try - { - await action(); - ExecutionCount++; - if (CancellationToken.IsCancellationRequested) - break; - } - catch (TaskCanceledException) when (CancellationToken.IsCancellationRequested) - { - // Graceful shutdown when cancellation is requested - break; - } - catch (Exception ex) - { - SentrySdk.CaptureException(ex); - Console.WriteLine(ex.Message); - } - finally - { - await Task.Delay(Interval, CancellationToken.Token); - } - } - }, CancellationToken.Token); - } - catch (TaskCanceledException ex) - { - Console.WriteLine($"Bot was cancelled: {ex.Message}"); - } - } - - public void Stop() - { - Status = BotStatus.Down; - _ = Task.Run(async () => await SaveBackup()); - // CancellationToken.Cancel(); - } - - public void Restart() - { - Status = BotStatus.Up; - StartupTime = DateTime.UtcNow; // Update the startup time when the bot is restarted - } - - public string GetStatus() - { - return Status.ToString(); - } - - public string GetName() - { - return Name; - } - - /// - /// Gets the total runtime of the bot since it was started - /// - /// TimeSpan representing the runtime, or TimeSpan.Zero if the bot is not running - public TimeSpan GetRuntime() - { - if (Status != BotStatus.Up) - return TimeSpan.Zero; - - return DateTime.UtcNow - StartupTime; - } - - public abstract Task SaveBackup(); - public abstract void LoadBackup(BotBackup backup); + public int TradeWins { get; set; } + public int TradeLosses { get; set; } + public decimal Pnl { get; set; } + public decimal Roi { get; set; } + public decimal Volume { get; set; } + public decimal Fees { get; set; } + } } \ No newline at end of file diff --git a/src/Managing.Domain/Bots/BotBackup.cs b/src/Managing.Domain/Bots/BotBackup.cs deleted file mode 100644 index de35272..0000000 --- a/src/Managing.Domain/Bots/BotBackup.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Managing.Domain.Users; -using Newtonsoft.Json; -using Orleans; -using static Managing.Common.Enums; - -namespace Managing.Domain.Bots; - -[GenerateSerializer] -public class BotBackup -{ - [Id(0)] - public string Identifier { get; set; } - - [Id(1)] - public User User { get; set; } - - [Id(2)] - public TradingBotBackup Data { get; set; } - - [Id(3)] - public BotStatus LastStatus { get; set; } - - [Id(4)] - public DateTime CreateDate { get; set; } - - /// - /// Serializes the TradingBotBackup data to JSON string - /// - /// JSON string representation of the data - public string SerializeData() - { - if (Data == null) return null; - - return JsonConvert.SerializeObject(Data); - } - - /// - /// Deserializes JSON string to TradingBotBackup data - /// - /// JSON string to deserialize - public void DeserializeData(string jsonData) - { - if (string.IsNullOrEmpty(jsonData)) - { - Data = null; - return; - } - - Data = JsonConvert.DeserializeObject(jsonData); - } -} \ No newline at end of file diff --git a/src/Managing.Domain/Bots/IBot.cs b/src/Managing.Domain/Bots/IBot.cs deleted file mode 100644 index 70c2f1d..0000000 --- a/src/Managing.Domain/Bots/IBot.cs +++ /dev/null @@ -1,30 +0,0 @@ -īģŋusing Managing.Domain.Users; - -namespace Managing.Domain.Bots -{ - public interface IBot - { - User User { get; set; } - string Name { get; set; } - void Start(); - void Stop(); - void Restart(); - string GetStatus(); - string GetName(); - - /// - /// Gets the total runtime of the bot since it was started - /// - /// TimeSpan representing the runtime, or TimeSpan.Zero if the bot is not running - TimeSpan GetRuntime(); - - /// - /// The time when the bot was first started (creation date) - /// - DateTime StartupTime { get; } - - string Identifier { get; set; } - Task SaveBackup(); - void LoadBackup(BotBackup backup); - } -} \ No newline at end of file diff --git a/src/Managing.Domain/Bots/TradingBotBackup.cs b/src/Managing.Domain/Bots/TradingBotBackup.cs index 871b701..fb1a937 100644 --- a/src/Managing.Domain/Bots/TradingBotBackup.cs +++ b/src/Managing.Domain/Bots/TradingBotBackup.cs @@ -1,3 +1,4 @@ +using Managing.Domain.Indicators; using Managing.Domain.Trades; using Orleans; @@ -19,10 +20,10 @@ public class TradingBotBackup public HashSet Signals { get; set; } /// - /// Runtime state: Open and closed positions for the bot + /// Runtime state: Open and closed positions for the bot, keyed by position identifier /// [Id(2)] - public List Positions { get; set; } + public Dictionary Positions { get; set; } /// /// Runtime state: Historical wallet balances over time @@ -41,4 +42,4 @@ public class TradingBotBackup /// [Id(5)] public DateTime CreateDate { get; set; } -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/Managing.Domain/Candles/CandleExtensions.cs b/src/Managing.Domain/Candles/CandleExtensions.cs index 1163f46..25f1af5 100644 --- a/src/Managing.Domain/Candles/CandleExtensions.cs +++ b/src/Managing.Domain/Candles/CandleExtensions.cs @@ -32,18 +32,42 @@ public static class CandleExtensions public static int GetIntervalFromTimeframe(Timeframe timeframe) { - var seconds = timeframe switch - { - Timeframe.OneDay => 86400, - Timeframe.FourHour => 14400, - Timeframe.OneHour => 3600, - Timeframe.ThirtyMinutes => 1800, - Timeframe.FifteenMinutes => 900, - Timeframe.FiveMinutes => 300, - _ => 300, - }; - // Run every 1/5th of the candle duration - return seconds / 5 * 1000; // milliseconds + var seconds = GetBaseIntervalInSeconds(timeframe); + // Run every 1/5th of the candle duration + return seconds / 5 * 1000; // milliseconds + } + + /// + /// Gets the interval in minutes for the given timeframe. + /// This is useful for cooldown period calculations. + /// + /// The timeframe to get the interval for + /// The interval in minutes + public static double GetIntervalInMinutes(Timeframe timeframe) + { + var seconds = GetBaseIntervalInSeconds(timeframe); + // Run every 1/5th of the candle duration + return (seconds / 5.0) / 60.0; // minutes + } + + /// + /// Gets the base interval in seconds for the given timeframe. + /// This is the core interval logic that can be used for various calculations. + /// + /// The timeframe to get the base interval for + /// The base interval in seconds + public static int GetBaseIntervalInSeconds(Timeframe timeframe) + { + return timeframe switch + { + Timeframe.OneDay => 86400, + Timeframe.FourHour => 14400, + Timeframe.OneHour => 3600, + Timeframe.ThirtyMinutes => 1800, + Timeframe.FifteenMinutes => 900, + Timeframe.FiveMinutes => 300, + _ => 300, + }; } public static int GetUnixInterval(this Timeframe timeframe) diff --git a/src/Managing.Domain/Strategies/Base/EmaBaseIndicator.cs b/src/Managing.Domain/Indicators/Base/EmaBaseIndicatorBase.cs similarity index 86% rename from src/Managing.Domain/Strategies/Base/EmaBaseIndicator.cs rename to src/Managing.Domain/Indicators/Base/EmaBaseIndicatorBase.cs index db0d209..eb47a71 100644 --- a/src/Managing.Domain/Strategies/Base/EmaBaseIndicator.cs +++ b/src/Managing.Domain/Indicators/Base/EmaBaseIndicatorBase.cs @@ -4,9 +4,9 @@ using Skender.Stock.Indicators; namespace Managing.Domain.Strategies.Base; -public abstract class EmaBaseIndicator : Indicator +public abstract class EmaBaseIndicatorBase : IndicatorBase { - protected EmaBaseIndicator(string name, Enums.IndicatorType type) : base(name, type) + protected EmaBaseIndicatorBase(string name, Enums.IndicatorType type) : base(name, type) { } diff --git a/src/Managing.Domain/Strategies/Base/IndicatorsResultBase.cs b/src/Managing.Domain/Indicators/Base/IndicatorsResultBase.cs similarity index 100% rename from src/Managing.Domain/Strategies/Base/IndicatorsResultBase.cs rename to src/Managing.Domain/Indicators/Base/IndicatorsResultBase.cs diff --git a/src/Managing.Domain/Strategies/Context/StDevContext.cs b/src/Managing.Domain/Indicators/Context/StDevContext.cs similarity index 89% rename from src/Managing.Domain/Strategies/Context/StDevContext.cs rename to src/Managing.Domain/Indicators/Context/StDevContext.cs index 3b7eac5..4b1ee0d 100644 --- a/src/Managing.Domain/Strategies/Context/StDevContext.cs +++ b/src/Managing.Domain/Indicators/Context/StDevContext.cs @@ -1,5 +1,6 @@ īģŋusing Managing.Core; using Managing.Domain.Candles; +using Managing.Domain.Indicators; using Managing.Domain.Shared.Rules; using Managing.Domain.Strategies.Base; using Skender.Stock.Indicators; @@ -7,7 +8,7 @@ using static Managing.Common.Enums; namespace Managing.Domain.Strategies.Context; -public class StDevContext : Indicator +public class StDevContext : IndicatorBase { public List Signals { get; set; } @@ -17,17 +18,17 @@ public class StDevContext : Indicator Period = period; } - public override List Run() + public override List Run(HashSet candles) { - if (Candles.Count <= Period) + if (candles.Count <= Period) { return null; } try { - var stDev = Candles.GetStdDev(Period.Value).ToList(); - var stDevCandles = MapStDev(stDev, Candles.TakeLast(Period.Value)); + var stDev = candles.GetStdDev(Period.Value).ToList(); + var stDevCandles = MapStDev(stDev, candles.TakeLast(Period.Value)); if (stDev.Count == 0) return null; @@ -73,11 +74,11 @@ public class StDevContext : Indicator } } - public override IndicatorsResultBase GetIndicatorValues() + public override IndicatorsResultBase GetIndicatorValues(HashSet candles) { var test = new IndicatorsResultBase() { - StdDev = Candles.GetStdDev(Period.Value).ToList() + StdDev = candles.GetStdDev(Period.Value).ToList() }; return test; diff --git a/src/Managing.Domain/Strategies/IIndicator.cs b/src/Managing.Domain/Indicators/IIndicator.cs similarity index 54% rename from src/Managing.Domain/Strategies/IIndicator.cs rename to src/Managing.Domain/Indicators/IIndicator.cs index 7f6c319..a3526c9 100644 --- a/src/Managing.Domain/Strategies/IIndicator.cs +++ b/src/Managing.Domain/Indicators/IIndicator.cs @@ -1,5 +1,5 @@ -īģŋusing Managing.Core.FixedSizedQueue; -using Managing.Domain.Candles; +īģŋusing Managing.Domain.Candles; +using Managing.Domain.Indicators; using Managing.Domain.Strategies.Base; using static Managing.Common.Enums; @@ -14,11 +14,12 @@ namespace Managing.Domain.Strategies int? FastPeriods { get; set; } int? SlowPeriods { get; set; } int? SignalPeriods { get; set; } - FixedSizeQueue Candles { get; set; } + double? Multiplier { get; set; } + int? StochPeriods { get; set; } + int? SmoothPeriods { get; set; } + int? CyclePeriods { get; set; } - List Run(); - IndicatorsResultBase GetIndicatorValues(); - void UpdateCandles(HashSet newCandles); - string GetName(); + List Run(HashSet candles); + IndicatorsResultBase GetIndicatorValues(HashSet candles); } } \ No newline at end of file diff --git a/src/Managing.Domain/Indicators/IndicatorBase.cs b/src/Managing.Domain/Indicators/IndicatorBase.cs new file mode 100644 index 0000000..95569b1 --- /dev/null +++ b/src/Managing.Domain/Indicators/IndicatorBase.cs @@ -0,0 +1,54 @@ +īģŋusing Managing.Domain.Candles; +using Managing.Domain.Indicators; +using Managing.Domain.Scenarios; +using Managing.Domain.Strategies.Base; +using Managing.Domain.Users; +using static Managing.Common.Enums; + +namespace Managing.Domain.Strategies +{ + public class IndicatorBase : IIndicator + { + public IndicatorBase(string name, IndicatorType type) + { + Name = name; + Type = type; + SignalType = ScenarioHelpers.GetSignalType(type); + } + + public string Name { get; set; } + public IndicatorType Type { get; set; } + + public SignalType SignalType { get; set; } + + public int MinimumHistory { 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? SmoothPeriods { get; set; } + + public int? StochPeriods { get; set; } + + public int? CyclePeriods { get; set; } + + public User User { get; set; } + + public virtual List Run(HashSet candles) + { + throw new NotImplementedException(); + } + + public virtual IndicatorsResultBase GetIndicatorValues(HashSet candles) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/Managing.Domain/Strategies/LightIndicator.cs b/src/Managing.Domain/Indicators/LightIndicator.cs similarity index 64% rename from src/Managing.Domain/Strategies/LightIndicator.cs rename to src/Managing.Domain/Indicators/LightIndicator.cs index 843c224..458bf57 100644 --- a/src/Managing.Domain/Strategies/LightIndicator.cs +++ b/src/Managing.Domain/Indicators/LightIndicator.cs @@ -45,29 +45,35 @@ public class LightIndicator /// /// Converts a full Indicator to a LightIndicator /// - public static LightIndicator FromIndicator(Indicator indicator) + public static LightIndicator BaseToLight(IndicatorBase indicatorBase) { - return new LightIndicator(indicator.Name, indicator.Type) + return new LightIndicator(indicatorBase.Name, indicatorBase.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 + 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 }; } /// /// Converts a LightIndicator back to a full Indicator /// - public Indicator ToIndicator() + public IIndicator ToInterface() { - return new Indicator(Name, Type) + // Use the factory method to create the correct indicator type + return ScenarioHelpers.BuildIndicator(this); + } + + public IndicatorBase LightToBase() + { + var baseIndicator = new IndicatorBase(Name, Type) { SignalType = SignalType, MinimumHistory = MinimumHistory, @@ -80,5 +86,7 @@ public class LightIndicator StochPeriods = StochPeriods, CyclePeriods = CyclePeriods }; + + return baseIndicator; } } \ No newline at end of file diff --git a/src/Managing.Domain/Indicators/LightSignal.cs b/src/Managing.Domain/Indicators/LightSignal.cs new file mode 100644 index 0000000..9c92cf4 --- /dev/null +++ b/src/Managing.Domain/Indicators/LightSignal.cs @@ -0,0 +1,71 @@ +using System.ComponentModel.DataAnnotations; +using System.Globalization; +using Managing.Domain.Candles; +using Orleans; +using static Managing.Common.Enums; + +namespace Managing.Domain.Indicators; + +[GenerateSerializer] +public class LightSignal +{ + public LightSignal(Ticker ticker, TradeDirection direction, Confidence confidence, Candle candle, DateTime date, + TradingExchanges exchange, IndicatorType indicatorType, SignalType signalType, string indicatorName) + { + Direction = direction; + Confidence = confidence; + // Cast to base Candle type to avoid Orleans serialization issues with nested types + Candle = candle as Candle ?? new Candle + { + Exchange = candle.Exchange, + Ticker = candle.Ticker, + OpenTime = candle.OpenTime, + Date = candle.Date, + Open = candle.Open, + Close = candle.Close, + High = candle.High, + Low = candle.Low, + Timeframe = candle.Timeframe, + Volume = candle.Volume + }; + Date = date; + Ticker = ticker; + Exchange = exchange; + Status = SignalStatus.WaitingForPosition; + IndicatorType = indicatorType; + IndicatorName = indicatorName; + SignalType = signalType; + + Identifier = + $"{indicatorName}-{indicatorType}-{direction}-{ticker}-{Candle?.Close.ToString(CultureInfo.InvariantCulture)}-{date:yyyyMMdd-HHmmss}"; + } + + [Id(0)] [Required] public SignalStatus Status { get; set; } + + [Id(1)] [Required] public TradeDirection Direction { get; } + + [Id(2)] [Required] public Confidence Confidence { get; set; } + + [Id(3)] [Required] public Timeframe Timeframe { get; } + + [Id(4)] [Required] public DateTime Date { get; private set; } + + [Id(5)] [Required] public Candle Candle { get; } + + [Id(6)] [Required] public string Identifier { get; } + + [Id(7)] [Required] public Ticker Ticker { get; } + + [Id(8)] [Required] public TradingExchanges Exchange { get; set; } + + [Id(9)] [Required] public IndicatorType IndicatorType { get; set; } + + [Id(10)] [Required] public SignalType SignalType { get; set; } + + [Id(11)] [Required] public string IndicatorName { get; set; } + + public void SetConfidence(Confidence confidence) + { + Confidence = confidence; + } +} \ No newline at end of file diff --git a/src/Managing.Domain/Strategies/Rules/CloseHigherThanThePreviousHigh.cs b/src/Managing.Domain/Indicators/Rules/CloseHigherThanThePreviousHigh.cs similarity index 100% rename from src/Managing.Domain/Strategies/Rules/CloseHigherThanThePreviousHigh.cs rename to src/Managing.Domain/Indicators/Rules/CloseHigherThanThePreviousHigh.cs diff --git a/src/Managing.Domain/Strategies/Rules/CloseLowerThanThePreviousHigh.cs b/src/Managing.Domain/Indicators/Rules/CloseLowerThanThePreviousHigh.cs similarity index 100% rename from src/Managing.Domain/Strategies/Rules/CloseLowerThanThePreviousHigh.cs rename to src/Managing.Domain/Indicators/Rules/CloseLowerThanThePreviousHigh.cs diff --git a/src/Managing.Domain/Strategies/Rules/RSIShouldBeBullish.cs b/src/Managing.Domain/Indicators/Rules/RSIShouldBeBullish.cs similarity index 100% rename from src/Managing.Domain/Strategies/Rules/RSIShouldBeBullish.cs rename to src/Managing.Domain/Indicators/Rules/RSIShouldBeBullish.cs diff --git a/src/Managing.Domain/Strategies/Signal.cs b/src/Managing.Domain/Indicators/Signal.cs similarity index 97% rename from src/Managing.Domain/Strategies/Signal.cs rename to src/Managing.Domain/Indicators/Signal.cs index e918e83..b314943 100644 --- a/src/Managing.Domain/Strategies/Signal.cs +++ b/src/Managing.Domain/Indicators/Signal.cs @@ -1,5 +1,6 @@ īģŋusing System.ComponentModel.DataAnnotations; using Managing.Domain.Candles; +using Managing.Domain.Indicators; using Managing.Domain.Users; using static Managing.Common.Enums; diff --git a/src/Managing.Domain/Strategies/Signals/ChandelierExitIndicator.cs b/src/Managing.Domain/Indicators/Signals/ChandelierExitIndicatorBase.cs similarity index 81% rename from src/Managing.Domain/Strategies/Signals/ChandelierExitIndicator.cs rename to src/Managing.Domain/Indicators/Signals/ChandelierExitIndicatorBase.cs index a5bdb6d..d21adb8 100644 --- a/src/Managing.Domain/Strategies/Signals/ChandelierExitIndicator.cs +++ b/src/Managing.Domain/Indicators/Signals/ChandelierExitIndicatorBase.cs @@ -1,5 +1,6 @@ īģŋusing Managing.Core; using Managing.Domain.Candles; +using Managing.Domain.Indicators; using Managing.Domain.Shared.Rules; using Managing.Domain.Strategies.Base; using Skender.Stock.Indicators; @@ -7,11 +8,11 @@ using static Managing.Common.Enums; namespace Managing.Domain.Strategies.Signals; -public class ChandelierExitIndicator : Indicator +public class ChandelierExitIndicatorBase : IndicatorBase { public List Signals { get; set; } - public ChandelierExitIndicator(string name, int period, double multiplier) : base(name, + public ChandelierExitIndicatorBase(string name, int period, double multiplier) : base(name, IndicatorType.ChandelierExit) { Signals = new List(); @@ -20,17 +21,17 @@ public class ChandelierExitIndicator : Indicator MinimumHistory = 1 + Period.Value; } - public override List Run() + public override List Run(HashSet candles) { - if (Candles.Count <= MinimumHistory) + if (candles.Count <= MinimumHistory) { return null; } try { - GetSignals(ChandelierType.Long); - GetSignals(ChandelierType.Short); + GetSignals(ChandelierType.Long, candles); + GetSignals(ChandelierType.Short, candles); return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList(); } @@ -40,20 +41,20 @@ public class ChandelierExitIndicator : Indicator } } - public override IndicatorsResultBase GetIndicatorValues() + public override IndicatorsResultBase GetIndicatorValues(HashSet candles) { return new IndicatorsResultBase() { - ChandelierLong = Candles.GetChandelier(Period.Value, Multiplier.Value, ChandelierType.Long).ToList(), - ChandelierShort = Candles.GetChandelier(Period.Value, Multiplier.Value, ChandelierType.Short).ToList() + ChandelierLong = candles.GetChandelier(Period.Value, Multiplier.Value, ChandelierType.Long).ToList(), + ChandelierShort = candles.GetChandelier(Period.Value, Multiplier.Value, ChandelierType.Short).ToList() }; } - private void GetSignals(ChandelierType chandelierType) + private void GetSignals(ChandelierType chandelierType, HashSet candles) { - var chandelier = Candles.GetChandelier(Period.Value, Multiplier.Value, chandelierType) + var chandelier = candles.GetChandelier(Period.Value, Multiplier.Value, chandelierType) .Where(s => s.ChandelierExit.HasValue).ToList(); - var chandelierCandle = MapChandelierToCandle(chandelier, Candles.TakeLast(MinimumHistory)); + var chandelierCandle = MapChandelierToCandle(chandelier, candles.TakeLast(MinimumHistory)); var previousCandle = chandelierCandle[0]; foreach (var currentCandle in chandelierCandle.Skip(1)) diff --git a/src/Managing.Domain/Strategies/Signals/DualEmaCrossIndicator.cs b/src/Managing.Domain/Indicators/Signals/DualEmaCrossIndicatorBase.cs similarity index 82% rename from src/Managing.Domain/Strategies/Signals/DualEmaCrossIndicator.cs rename to src/Managing.Domain/Indicators/Signals/DualEmaCrossIndicatorBase.cs index 70d08eb..4af58bf 100644 --- a/src/Managing.Domain/Strategies/Signals/DualEmaCrossIndicator.cs +++ b/src/Managing.Domain/Indicators/Signals/DualEmaCrossIndicatorBase.cs @@ -1,5 +1,6 @@ using Managing.Core; using Managing.Domain.Candles; +using Managing.Domain.Indicators; using Managing.Domain.Shared.Rules; using Managing.Domain.Strategies.Base; using Skender.Stock.Indicators; @@ -7,11 +8,12 @@ using static Managing.Common.Enums; namespace Managing.Domain.Strategies.Signals; -public class DualEmaCrossIndicator : EmaBaseIndicator +public class DualEmaCrossIndicatorBase : EmaBaseIndicatorBase { public List Signals { get; set; } - public DualEmaCrossIndicator(string name, int fastPeriod, int slowPeriod) : base(name, IndicatorType.DualEmaCross) + public DualEmaCrossIndicatorBase(string name, int fastPeriod, int slowPeriod) : base(name, + IndicatorType.DualEmaCross) { Signals = new List(); FastPeriods = fastPeriod; @@ -19,28 +21,28 @@ public class DualEmaCrossIndicator : EmaBaseIndicator MinimumHistory = Math.Max(fastPeriod, slowPeriod) * 2; } - public override IndicatorsResultBase GetIndicatorValues() + public override IndicatorsResultBase GetIndicatorValues(HashSet candles) { return new IndicatorsResultBase() { - FastEma = Candles.GetEma(FastPeriods.Value).ToList(), - SlowEma = Candles.GetEma(SlowPeriods.Value).ToList() + FastEma = candles.GetEma(FastPeriods.Value).ToList(), + SlowEma = candles.GetEma(SlowPeriods.Value).ToList() }; } - public override List Run() + public override List Run(HashSet candles) { - if (Candles.Count <= MinimumHistory) + if (candles.Count <= MinimumHistory) { return null; } try { - var fastEma = Candles.GetEma(FastPeriods.Value).ToList(); - var slowEma = Candles.GetEma(SlowPeriods.Value).ToList(); + var fastEma = candles.GetEma(FastPeriods.Value).ToList(); + var slowEma = candles.GetEma(SlowPeriods.Value).ToList(); - var dualEmaCandles = MapDualEmaToCandle(fastEma, slowEma, Candles.TakeLast(MinimumHistory)); + var dualEmaCandles = MapDualEmaToCandle(fastEma, slowEma, candles.TakeLast(MinimumHistory)); if (dualEmaCandles.Count < 2) return null; diff --git a/src/Managing.Domain/Strategies/Signals/EmaCrossIndicator.cs b/src/Managing.Domain/Indicators/Signals/EmaCrossIndicator.cs similarity index 80% rename from src/Managing.Domain/Strategies/Signals/EmaCrossIndicator.cs rename to src/Managing.Domain/Indicators/Signals/EmaCrossIndicator.cs index f45795e..581e8ad 100644 --- a/src/Managing.Domain/Strategies/Signals/EmaCrossIndicator.cs +++ b/src/Managing.Domain/Indicators/Signals/EmaCrossIndicator.cs @@ -1,4 +1,6 @@ īģŋusing Managing.Core; +using Managing.Domain.Candles; +using Managing.Domain.Indicators; using Managing.Domain.Shared.Rules; using Managing.Domain.Strategies.Base; using Skender.Stock.Indicators; @@ -6,7 +8,7 @@ using static Managing.Common.Enums; namespace Managing.Domain.Strategies.Signals; -public class EmaCrossIndicator : EmaBaseIndicator +public class EmaCrossIndicator : EmaBaseIndicatorBase { public List Signals { get; set; } @@ -16,25 +18,25 @@ public class EmaCrossIndicator : EmaBaseIndicator Period = period; } - public override IndicatorsResultBase GetIndicatorValues() + public override IndicatorsResultBase GetIndicatorValues(HashSet candles) { return new IndicatorsResultBase() { - Ema = Candles.GetEma(Period.Value).ToList() + Ema = candles.GetEma(Period.Value).ToList() }; } - public override List Run() + public override List Run(HashSet candles) { - if (Candles.Count <= Period) + if (candles.Count <= Period) { return null; } try { - var ema = Candles.GetEma(Period.Value).ToList(); - var emaCandles = MapEmaToCandle(ema, Candles.TakeLast(Period.Value)); + var ema = candles.GetEma(Period.Value).ToList(); + var emaCandles = MapEmaToCandle(ema, candles.TakeLast(Period.Value)); if (ema.Count == 0) return null; diff --git a/src/Managing.Domain/Indicators/Signals/EmaCrossIndicatorBase.cs b/src/Managing.Domain/Indicators/Signals/EmaCrossIndicatorBase.cs new file mode 100644 index 0000000..327317b --- /dev/null +++ b/src/Managing.Domain/Indicators/Signals/EmaCrossIndicatorBase.cs @@ -0,0 +1,79 @@ +īģŋusing Managing.Core; +using Managing.Domain.Candles; +using Managing.Domain.Indicators; +using Managing.Domain.Shared.Rules; +using Managing.Domain.Strategies.Base; +using Skender.Stock.Indicators; +using static Managing.Common.Enums; + +namespace Managing.Domain.Strategies.Signals; + +public class EmaCrossIndicatorBase : EmaBaseIndicatorBase +{ + public List Signals { get; set; } + + public EmaCrossIndicatorBase(string name, int period) : base(name, IndicatorType.EmaCross) + { + Signals = new List(); + Period = period; + } + + public override IndicatorsResultBase GetIndicatorValues(HashSet candles) + { + return new IndicatorsResultBase() + { + Ema = candles.GetEma(Period.Value).ToList() + }; + } + + public override List Run(HashSet candles) + { + if (candles.Count <= Period) + { + return null; + } + + try + { + var ema = candles.GetEma(Period.Value).ToList(); + var emaCandles = MapEmaToCandle(ema, candles.TakeLast(Period.Value).ToHashSet()); + + if (ema.Count == 0) + return null; + + var previousCandle = emaCandles[0]; + foreach (var currentCandle in emaCandles.Skip(1)) + { + if (previousCandle.Close > (decimal)currentCandle.Ema && + currentCandle.Close < (decimal)currentCandle.Ema) + { + AddSignal(currentCandle, TradeDirection.Short, Confidence.Medium); + } + + if (previousCandle.Close < (decimal)currentCandle.Ema && + currentCandle.Close > (decimal)currentCandle.Ema) + { + AddSignal(currentCandle, TradeDirection.Long, Confidence.Medium); + } + + previousCandle = currentCandle; + } + + return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList(); + } + catch (RuleException) + { + return null; + } + } + + private void AddSignal(CandleEma candleSignal, TradeDirection direction, Confidence confidence) + { + var signal = new LightSignal(MiscExtensions.ParseEnum(candleSignal.Ticker), direction, confidence, + candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name); + if (!Signals.Any(s => s.Identifier == signal.Identifier)) + { + Signals.AddItem(signal); + } + } +} \ No newline at end of file diff --git a/src/Managing.Domain/Strategies/Signals/LaggingSTC.cs b/src/Managing.Domain/Indicators/Signals/LaggingSTC.cs similarity index 90% rename from src/Managing.Domain/Strategies/Signals/LaggingSTC.cs rename to src/Managing.Domain/Indicators/Signals/LaggingSTC.cs index edf7850..6d58c2f 100644 --- a/src/Managing.Domain/Strategies/Signals/LaggingSTC.cs +++ b/src/Managing.Domain/Indicators/Signals/LaggingSTC.cs @@ -1,5 +1,6 @@ īģŋusing Managing.Core; using Managing.Domain.Candles; +using Managing.Domain.Indicators; using Managing.Domain.Shared.Rules; using Managing.Domain.Strategies.Base; using Skender.Stock.Indicators; @@ -14,7 +15,7 @@ namespace Managing.Domain.Strategies.Signals; /// 2. Long signals on STC rebound from oversold (25- → â‰Ĩ25) with recent compressed volatility (max <11) /// 3. Avoids look-ahead bias through proper rolling window implementation /// -public class LaggingSTC : Indicator +public class LaggingSTC : IndicatorBase { public List Signals { get; set; } @@ -27,17 +28,17 @@ public class LaggingSTC : Indicator CyclePeriods = cyclePeriods; } - public override List Run() + public override List Run(HashSet candles) { - if (Candles.Count <= 2 * (SlowPeriods + CyclePeriods)) + if (candles.Count <= 2 * (SlowPeriods + CyclePeriods)) { return null; } try { - var stc = Candles.GetStc(FastPeriods.Value, FastPeriods.Value, SlowPeriods.Value).ToList(); - var stcCandles = MapStcToCandle(stc, Candles.TakeLast(CyclePeriods.Value * 3)); + var stc = candles.GetStc(FastPeriods.Value, FastPeriods.Value, SlowPeriods.Value).ToList(); + var stcCandles = MapStcToCandle(stc, candles.TakeLast(CyclePeriods.Value * 3)); if (stcCandles.Count == 0) return null; @@ -89,9 +90,9 @@ public class LaggingSTC : Indicator } } - public override IndicatorsResultBase GetIndicatorValues() + public override IndicatorsResultBase GetIndicatorValues(HashSet candles) { - var stc = Candles.GetStc(FastPeriods.Value, FastPeriods.Value, SlowPeriods.Value).ToList(); + var stc = candles.GetStc(FastPeriods.Value, FastPeriods.Value, SlowPeriods.Value).ToList(); return new IndicatorsResultBase { Stc = stc diff --git a/src/Managing.Domain/Strategies/Signals/MacdCrossIndicator.cs b/src/Managing.Domain/Indicators/Signals/MacdCrossIndicatorBase.cs similarity index 85% rename from src/Managing.Domain/Strategies/Signals/MacdCrossIndicator.cs rename to src/Managing.Domain/Indicators/Signals/MacdCrossIndicatorBase.cs index 125a5ca..b54a848 100644 --- a/src/Managing.Domain/Strategies/Signals/MacdCrossIndicator.cs +++ b/src/Managing.Domain/Indicators/Signals/MacdCrossIndicatorBase.cs @@ -1,5 +1,6 @@ īģŋusing Managing.Core; using Managing.Domain.Candles; +using Managing.Domain.Indicators; using Managing.Domain.Shared.Rules; using Managing.Domain.Strategies.Base; using Skender.Stock.Indicators; @@ -7,11 +8,11 @@ using static Managing.Common.Enums; namespace Managing.Domain.Strategies.Signals; -public class MacdCrossIndicator : Indicator +public class MacdCrossIndicatorBase : IndicatorBase { public List Signals { get; set; } - public MacdCrossIndicator(string name, int fastPeriods, int slowPeriods, int signalPeriods) : + public MacdCrossIndicatorBase(string name, int fastPeriods, int slowPeriods, int signalPeriods) : base(name, IndicatorType.MacdCross) { Signals = new List(); @@ -20,17 +21,17 @@ public class MacdCrossIndicator : Indicator SignalPeriods = signalPeriods; } - public override List Run() + public override List Run(HashSet candles) { - if (Candles.Count <= 2 * (SlowPeriods + SignalPeriods)) + if (candles.Count <= 2 * (SlowPeriods + SignalPeriods)) { return null; } try { - var macd = Candles.GetMacd(FastPeriods.Value, SlowPeriods.Value, SignalPeriods.Value).ToList(); - var macdCandle = MapMacdToCandle(macd, Candles.TakeLast(SignalPeriods.Value)); + var macd = candles.GetMacd(FastPeriods.Value, SlowPeriods.Value, SignalPeriods.Value).ToList(); + var macdCandle = MapMacdToCandle(macd, candles.TakeLast(SignalPeriods.Value)); if (macd.Count == 0) return null; @@ -67,11 +68,11 @@ public class MacdCrossIndicator : Indicator } } - public override IndicatorsResultBase GetIndicatorValues() + public override IndicatorsResultBase GetIndicatorValues(HashSet candles) { return new IndicatorsResultBase() { - Macd = Candles.GetMacd(FastPeriods.Value, SlowPeriods.Value, SignalPeriods.Value).ToList() + Macd = candles.GetMacd(FastPeriods.Value, SlowPeriods.Value, SignalPeriods.Value).ToList() }; } diff --git a/src/Managing.Domain/Strategies/Signals/RsiDivergenceConfirmIndicator.cs b/src/Managing.Domain/Indicators/Signals/RsiDivergenceConfirmIndicatorBase.cs similarity index 89% rename from src/Managing.Domain/Strategies/Signals/RsiDivergenceConfirmIndicator.cs rename to src/Managing.Domain/Indicators/Signals/RsiDivergenceConfirmIndicatorBase.cs index 1172649..b080b97 100644 --- a/src/Managing.Domain/Strategies/Signals/RsiDivergenceConfirmIndicator.cs +++ b/src/Managing.Domain/Indicators/Signals/RsiDivergenceConfirmIndicatorBase.cs @@ -1,4 +1,5 @@ īģŋusing Managing.Core; +using Managing.Domain.Indicators; using Managing.Domain.Shared.Rules; using Managing.Domain.Strategies.Base; using Skender.Stock.Indicators; @@ -7,11 +8,11 @@ using Candle = Managing.Domain.Candles.Candle; namespace Managing.Domain.Strategies.Signals; -public class RsiDivergenceConfirmIndicator : Indicator +public class RsiDivergenceConfirmIndicatorBase : IndicatorBase { public List Signals { get; set; } - public RsiDivergenceConfirmIndicator(string name, int period) : base(name, IndicatorType.RsiDivergenceConfirm) + public RsiDivergenceConfirmIndicatorBase(string name, int period) : base(name, IndicatorType.RsiDivergenceConfirm) { Period = period; Signals = new List(); @@ -21,25 +22,25 @@ public class RsiDivergenceConfirmIndicator : Indicator /// Get RSI signals /// /// - public override List Run() + public override List Run(HashSet candles) { - if (Candles.Count <= Period) + if (candles.Count <= Period) { return null; } - var ticker = Candles.First().Ticker; + var ticker = candles.First().Ticker; try { - var rsiResult = Candles.TakeLast(10 * Period.Value).GetRsi(Period.Value).ToList(); - var candlesRsi = MapRsiToCandle(rsiResult, Candles.TakeLast(10 * Period.Value)); + var rsiResult = candles.TakeLast(10 * Period.Value).GetRsi(Period.Value).ToList(); + var candlesRsi = MapRsiToCandle(rsiResult, candles.TakeLast(10 * Period.Value)); if (candlesRsi.Count(c => c.Rsi > 0) == 0) return null; - GetLongSignals(candlesRsi); - GetShortSignals(candlesRsi); + GetLongSignals(candlesRsi, candles); + GetShortSignals(candlesRsi, candles); return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList(); } @@ -49,15 +50,15 @@ public class RsiDivergenceConfirmIndicator : Indicator } } - public override IndicatorsResultBase GetIndicatorValues() + public override IndicatorsResultBase GetIndicatorValues(HashSet candles) { return new IndicatorsResultBase() { - Rsi = Candles.GetRsi(Period.Value).ToList() + Rsi = candles.GetRsi(Period.Value).ToList() }; } - private void GetLongSignals(List candlesRsi) + private void GetLongSignals(List candlesRsi, HashSet candles) { // Set the low and high for first candle var firstCandleRsi = candlesRsi.First(c => c.Rsi > 0); @@ -124,13 +125,13 @@ public class RsiDivergenceConfirmIndicator : Indicator highRsi.AddItem(currentCandle); } - CheckIfConfimation(currentCandle, TradeDirection.Long); + CheckIfConfimation(currentCandle, TradeDirection.Long, candles); previousCandle = currentCandle; } } - private void GetShortSignals(List candlesRsi) + private void GetShortSignals(List candlesRsi, HashSet candles) { // Set the low and high for first candle var firstCandleRsi = candlesRsi.First(c => c.Rsi > 0); @@ -198,15 +199,15 @@ public class RsiDivergenceConfirmIndicator : Indicator lowRsi.AddItem(currentCandle); } - CheckIfConfimation(currentCandle, TradeDirection.Short); + CheckIfConfimation(currentCandle, TradeDirection.Short, candles); previousCandle = currentCandle; } } - private void CheckIfConfimation(CandleRsi currentCandle, TradeDirection direction) + private void CheckIfConfimation(CandleRsi currentCandle, TradeDirection direction, HashSet candles) { - var lastCandleOnPeriod = Candles.TakeLast(Period.Value).ToList(); + var lastCandleOnPeriod = candles.TakeLast(Period.Value).ToList(); var signalsOnPeriod = Signals.Where(s => s.Date >= lastCandleOnPeriod[0].Date && s.Date < currentCandle.Date && s.Direction == direction diff --git a/src/Managing.Domain/Strategies/Signals/RsiDivergenceIndicator.cs b/src/Managing.Domain/Indicators/Signals/RsiDivergenceIndicatorBase.cs similarity index 87% rename from src/Managing.Domain/Strategies/Signals/RsiDivergenceIndicator.cs rename to src/Managing.Domain/Indicators/Signals/RsiDivergenceIndicatorBase.cs index 2ceacc3..e1a27be 100644 --- a/src/Managing.Domain/Strategies/Signals/RsiDivergenceIndicator.cs +++ b/src/Managing.Domain/Indicators/Signals/RsiDivergenceIndicatorBase.cs @@ -1,4 +1,5 @@ īģŋusing Managing.Core; +using Managing.Domain.Indicators; using Managing.Domain.Shared.Rules; using Managing.Domain.Strategies.Base; using Skender.Stock.Indicators; @@ -7,14 +8,14 @@ using Candle = Managing.Domain.Candles.Candle; namespace Managing.Domain.Strategies.Signals; -public class RsiDivergenceIndicator : Indicator +public class RsiDivergenceIndicatorBase : IndicatorBase { public List Signals { get; set; } public TradeDirection Direction { get; set; } private const int UpperBand = 70; private const int LowerBand = 30; - public RsiDivergenceIndicator(string name, int period) : base(name, IndicatorType.RsiDivergence) + public RsiDivergenceIndicatorBase(string name, int period) : base(name, IndicatorType.RsiDivergence) { Period = period; Signals = new List(); @@ -24,25 +25,25 @@ public class RsiDivergenceIndicator : Indicator /// Get RSI signals /// /// - public override List Run() + public override List Run(HashSet candles) { - if (!Period.HasValue || Candles.Count <= Period) + if (!Period.HasValue || candles.Count <= Period) { return null; } - var ticker = Candles.First().Ticker; + var ticker = candles.First().Ticker; try { - var rsiResult = Candles.TakeLast(10 * Period.Value).GetRsi(Period.Value).ToList(); - var candlesRsi = MapRsiToCandle(rsiResult, Candles.TakeLast(10 * Period.Value)); + var rsiResult = candles.TakeLast(10 * Period.Value).GetRsi(Period.Value).ToList(); + var candlesRsi = MapRsiToCandle(rsiResult, candles.TakeLast(10 * Period.Value)); if (candlesRsi.Count(c => c.Rsi > 0) == 0) return null; - GetLongSignals(candlesRsi); - GetShortSignals(candlesRsi); + GetLongSignals(candlesRsi, candles); + GetShortSignals(candlesRsi, candles); return Signals; } @@ -52,15 +53,15 @@ public class RsiDivergenceIndicator : Indicator } } - public override IndicatorsResultBase GetIndicatorValues() + public override IndicatorsResultBase GetIndicatorValues(HashSet candles) { return new IndicatorsResultBase() { - Rsi = Candles.GetRsi(Period.Value).ToList() + Rsi = candles.GetRsi(Period.Value).ToList() }; } - private void GetLongSignals(List candlesRsi) + private void GetLongSignals(List candlesRsi, HashSet candles) { // Set the low and high for first candle var firstCandleRsi = candlesRsi.First(c => c.Rsi > 0); @@ -100,7 +101,7 @@ public class RsiDivergenceIndicator : Indicator // Price go down but RSI go up if (currentCandle.Close < lowPrices.TakeLast(Period.Value).Min(p => p.Close)) { - AddSignal(currentCandle, TradeDirection.Long); + AddSignal(currentCandle, TradeDirection.Long, candles); } } else @@ -131,7 +132,7 @@ public class RsiDivergenceIndicator : Indicator } } - private void GetShortSignals(List candlesRsi) + private void GetShortSignals(List candlesRsi, HashSet candles) { // Set the low and high for first candle var firstCandleRsi = candlesRsi.First(c => c.Rsi > 0); @@ -173,7 +174,7 @@ public class RsiDivergenceIndicator : Indicator // Price go up but RSI go down if (currentCandle.Close > highPrices.TakeLast(Period.Value).Max(p => p.Close)) { - AddSignal(currentCandle, TradeDirection.Short); + AddSignal(currentCandle, TradeDirection.Short, candles); } } else @@ -203,14 +204,14 @@ public class RsiDivergenceIndicator : Indicator } } - private void AddSignal(CandleRsi candleSignal, TradeDirection direction) + private void AddSignal(CandleRsi candleSignal, TradeDirection direction, HashSet candles) { var signal = new LightSignal(MiscExtensions.ParseEnum(candleSignal.Ticker), direction, Confidence.Low, candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name); if (Signals.Count(s => s.Identifier == signal.Identifier) < 1) { - var lastCandleOnPeriod = Candles.TakeLast(Period.Value).ToList(); + var lastCandleOnPeriod = candles.TakeLast(Period.Value).ToList(); var signalsOnPeriod = Signals.Where(s => s.Date >= lastCandleOnPeriod[0].Date).ToList(); if (signalsOnPeriod.Count == 1) diff --git a/src/Managing.Domain/Strategies/Signals/StcIndicator.cs b/src/Managing.Domain/Indicators/Signals/StcIndicatorBase.cs similarity index 84% rename from src/Managing.Domain/Strategies/Signals/StcIndicator.cs rename to src/Managing.Domain/Indicators/Signals/StcIndicatorBase.cs index d89dcd3..a2ee494 100644 --- a/src/Managing.Domain/Strategies/Signals/StcIndicator.cs +++ b/src/Managing.Domain/Indicators/Signals/StcIndicatorBase.cs @@ -1,5 +1,6 @@ īģŋusing Managing.Core; using Managing.Domain.Candles; +using Managing.Domain.Indicators; using Managing.Domain.Shared.Rules; using Managing.Domain.Strategies.Base; using Skender.Stock.Indicators; @@ -7,11 +8,12 @@ using static Managing.Common.Enums; namespace Managing.Domain.Strategies.Signals; -public class StcIndicator : Indicator +public class StcIndicatorBase : IndicatorBase { public List Signals { get; set; } - public StcIndicator(string name, int cyclePeriods, int fastPeriods, int slowPeriods) : base(name, IndicatorType.Stc) + public StcIndicatorBase(string name, int cyclePeriods, int fastPeriods, int slowPeriods) : base(name, + IndicatorType.Stc) { Signals = new List(); FastPeriods = fastPeriods; @@ -19,9 +21,9 @@ public class StcIndicator : Indicator CyclePeriods = cyclePeriods; } - public override List Run() + public override List Run(HashSet candles) { - if (Candles.Count <= 2 * (SlowPeriods + CyclePeriods)) + if (candles.Count <= 2 * (SlowPeriods + CyclePeriods)) { return null; } @@ -30,10 +32,10 @@ public class StcIndicator : Indicator { if (FastPeriods != null) { - var stc = Candles.GetStc(FastPeriods.Value, FastPeriods.Value, SlowPeriods.Value).ToList(); + var stc = candles.GetStc(FastPeriods.Value, FastPeriods.Value, SlowPeriods.Value).ToList(); if (CyclePeriods != null) { - var stcCandles = MapStcToCandle(stc, Candles.TakeLast(CyclePeriods.Value)); + var stcCandles = MapStcToCandle(stc, candles.TakeLast(CyclePeriods.Value)); if (stc.Count == 0) return null; @@ -64,11 +66,11 @@ public class StcIndicator : Indicator } } - public override IndicatorsResultBase GetIndicatorValues() + public override IndicatorsResultBase GetIndicatorValues(HashSet candles) { if (FastPeriods != null && SlowPeriods != null) { - var stc = Candles.GetStc(FastPeriods.Value, FastPeriods.Value, SlowPeriods.Value).ToList(); + var stc = candles.GetStc(FastPeriods.Value, FastPeriods.Value, SlowPeriods.Value).ToList(); return new IndicatorsResultBase { Stc = stc diff --git a/src/Managing.Domain/Strategies/Signals/SuperTrendCrossEma.cs b/src/Managing.Domain/Indicators/Signals/SuperTrendCrossEma.cs similarity index 92% rename from src/Managing.Domain/Strategies/Signals/SuperTrendCrossEma.cs rename to src/Managing.Domain/Indicators/Signals/SuperTrendCrossEma.cs index 278c5e3..e60444d 100644 --- a/src/Managing.Domain/Strategies/Signals/SuperTrendCrossEma.cs +++ b/src/Managing.Domain/Indicators/Signals/SuperTrendCrossEma.cs @@ -1,5 +1,6 @@ īģŋusing Managing.Core; using Managing.Domain.Candles; +using Managing.Domain.Indicators; using Managing.Domain.Shared.Rules; using Managing.Domain.Strategies.Base; using Skender.Stock.Indicators; @@ -7,7 +8,7 @@ using static Managing.Common.Enums; namespace Managing.Domain.Strategies.Signals; -public class SuperTrendCrossEma : Indicator +public class SuperTrendCrossEma : IndicatorBase { public List Signals { get; set; } @@ -19,7 +20,7 @@ public class SuperTrendCrossEma : Indicator MinimumHistory = 100 + Period.Value; } - public override List Run() + public override List Run(HashSet candles) { // Validate sufficient historical data for all indicators const int emaPeriod = 50; @@ -27,7 +28,7 @@ public class SuperTrendCrossEma : Indicator const int adxThreshold = 25; // Minimum ADX level to confirm a trend int minimumRequiredHistory = Math.Max(Math.Max(emaPeriod, adxPeriod), Period.Value * 2); // Ensure enough data - if (Candles.Count < minimumRequiredHistory) + if (candles.Count < minimumRequiredHistory) { return null; } @@ -35,20 +36,20 @@ public class SuperTrendCrossEma : Indicator try { // 1. Calculate indicators - var superTrend = Candles.GetSuperTrend(Period.Value, Multiplier.Value) + var superTrend = candles.GetSuperTrend(Period.Value, Multiplier.Value) .Where(s => s.SuperTrend.HasValue) .ToList(); - var ema50 = Candles.GetEma(emaPeriod) + var ema50 = candles.GetEma(emaPeriod) .Where(e => e.Ema.HasValue) .ToList(); - var adxResults = Candles.GetAdx(adxPeriod) + var adxResults = candles.GetAdx(adxPeriod) .Where(a => a.Adx.HasValue && a.Pdi.HasValue && a.Mdi.HasValue) // Ensure all values exist .ToList(); // 2. Create merged dataset with price + indicators - var superTrendCandles = MapSuperTrendToCandle(superTrend, Candles.TakeLast(minimumRequiredHistory)); + var superTrendCandles = MapSuperTrendToCandle(superTrend, candles.TakeLast(minimumRequiredHistory)); if (superTrendCandles.Count == 0) return null; @@ -157,11 +158,11 @@ public class SuperTrendCrossEma : Indicator return superTrends; } - public override IndicatorsResultBase GetIndicatorValues() + public override IndicatorsResultBase GetIndicatorValues(HashSet candles) { return new IndicatorsResultBase() { - SuperTrend = Candles.GetSuperTrend(Period.Value, Multiplier.Value).Where(s => s.SuperTrend.HasValue) + SuperTrend = candles.GetSuperTrend(Period.Value, Multiplier.Value).Where(s => s.SuperTrend.HasValue) .ToList() }; } diff --git a/src/Managing.Domain/Strategies/Signals/SuperTrendIndicator.cs b/src/Managing.Domain/Indicators/Signals/SuperTrendIndicatorBase.cs similarity index 82% rename from src/Managing.Domain/Strategies/Signals/SuperTrendIndicator.cs rename to src/Managing.Domain/Indicators/Signals/SuperTrendIndicatorBase.cs index 4b5015f..06f3dc0 100644 --- a/src/Managing.Domain/Strategies/Signals/SuperTrendIndicator.cs +++ b/src/Managing.Domain/Indicators/Signals/SuperTrendIndicatorBase.cs @@ -1,5 +1,6 @@ īģŋusing Managing.Core; using Managing.Domain.Candles; +using Managing.Domain.Indicators; using Managing.Domain.Shared.Rules; using Managing.Domain.Strategies.Base; using Skender.Stock.Indicators; @@ -7,11 +8,11 @@ using static Managing.Common.Enums; namespace Managing.Domain.Strategies.Signals; -public class SuperTrendIndicator : Indicator +public class SuperTrendIndicatorBase : IndicatorBase { public List Signals { get; set; } - public SuperTrendIndicator(string name, int period, double multiplier) : base(name, IndicatorType.SuperTrend) + public SuperTrendIndicatorBase(string name, int period, double multiplier) : base(name, IndicatorType.SuperTrend) { Signals = new List(); Period = period; @@ -19,18 +20,17 @@ public class SuperTrendIndicator : Indicator MinimumHistory = 100 + Period.Value; } - public override List Run() + public override List Run(HashSet candles) { - if (Candles.Count <= MinimumHistory) + if (candles.Count <= MinimumHistory) { return null; } try { - var superTrend = Candles.GetSuperTrend(Period.Value, Multiplier.Value).Where(s => s.SuperTrend.HasValue) - .ToList(); - var superTrendCandle = MapSuperTrendToCandle(superTrend, Candles.TakeLast(MinimumHistory)); + var superTrend = candles.GetSuperTrend(Period.Value, Multiplier.Value).Where(s => s.SuperTrend.HasValue); + var superTrendCandle = MapSuperTrendToCandle(superTrend, candles.TakeLast(MinimumHistory)); if (superTrendCandle.Count == 0) return null; @@ -70,16 +70,17 @@ public class SuperTrendIndicator : Indicator } } - public override IndicatorsResultBase GetIndicatorValues() + public override IndicatorsResultBase GetIndicatorValues(HashSet candles) { return new IndicatorsResultBase() { - SuperTrend = Candles.GetSuperTrend(Period.Value, Multiplier.Value).Where(s => s.SuperTrend.HasValue) + SuperTrend = candles.GetSuperTrend(Period.Value, Multiplier.Value).Where(s => s.SuperTrend.HasValue) .ToList() }; } - private List MapSuperTrendToCandle(List superTrend, IEnumerable candles) + private List MapSuperTrendToCandle(IEnumerable superTrend, + IEnumerable candles) { var superTrends = new List(); foreach (var candle in candles) diff --git a/src/Managing.Domain/Strategies/Signals/ThreeWhiteSoldiersIndicator.cs b/src/Managing.Domain/Indicators/Signals/ThreeWhiteSoldiersIndicatorBase.cs similarity index 79% rename from src/Managing.Domain/Strategies/Signals/ThreeWhiteSoldiersIndicator.cs rename to src/Managing.Domain/Indicators/Signals/ThreeWhiteSoldiersIndicatorBase.cs index 0440255..712e60e 100644 --- a/src/Managing.Domain/Strategies/Signals/ThreeWhiteSoldiersIndicator.cs +++ b/src/Managing.Domain/Indicators/Signals/ThreeWhiteSoldiersIndicatorBase.cs @@ -1,4 +1,5 @@ īģŋusing Managing.Domain.Candles; +using Managing.Domain.Indicators; using Managing.Domain.Shared.Rules; using Managing.Domain.Strategies.Base; using Managing.Domain.Strategies.Rules; @@ -6,9 +7,9 @@ using static Managing.Common.Enums; namespace Managing.Domain.Strategies.Signals { - public class ThreeWhiteSoldiersIndicator : Indicator + public class ThreeWhiteSoldiersIndicatorBase : IndicatorBase { - public ThreeWhiteSoldiersIndicator(string name, int period) + public ThreeWhiteSoldiersIndicatorBase(string name, int period) : base(name, IndicatorType.ThreeWhiteSoldiers) { Period = period; @@ -16,18 +17,18 @@ namespace Managing.Domain.Strategies.Signals public TradeDirection Direction { get; } - public override List Run() + public override List Run(HashSet candles) { var signals = new List(); - if (Candles.Count <= 3) + if (candles.Count <= 3) { return null; } try { - var lastFourCandles = Candles.TakeLast(4); + var lastFourCandles = candles.TakeLast(4); Candle previousCandles = null; foreach (var currentCandle in lastFourCandles) @@ -52,7 +53,7 @@ namespace Managing.Domain.Strategies.Signals } } - public override IndicatorsResultBase GetIndicatorValues() + public override IndicatorsResultBase GetIndicatorValues(HashSet candles) { throw new NotImplementedException(); } diff --git a/src/Managing.Domain/Strategies/Trends/EmaTrendIndicator.cs b/src/Managing.Domain/Indicators/Trends/EmaTrendIndicatorBase.cs similarity index 74% rename from src/Managing.Domain/Strategies/Trends/EmaTrendIndicator.cs rename to src/Managing.Domain/Indicators/Trends/EmaTrendIndicatorBase.cs index f3446c1..db87015 100644 --- a/src/Managing.Domain/Strategies/Trends/EmaTrendIndicator.cs +++ b/src/Managing.Domain/Indicators/Trends/EmaTrendIndicatorBase.cs @@ -1,4 +1,6 @@ īģŋusing Managing.Core; +using Managing.Domain.Candles; +using Managing.Domain.Indicators; using Managing.Domain.Shared.Rules; using Managing.Domain.Strategies.Base; using Skender.Stock.Indicators; @@ -6,27 +8,27 @@ using static Managing.Common.Enums; namespace Managing.Domain.Strategies.Trends; -public class EmaTrendIndicator : EmaBaseIndicator +public class EmaTrendIndicatorBase : EmaBaseIndicatorBase { public List Signals { get; set; } - public EmaTrendIndicator(string name, int period) : base(name, IndicatorType.EmaTrend) + public EmaTrendIndicatorBase(string name, int period) : base(name, IndicatorType.EmaTrend) { Signals = new List(); Period = period; } - public override List Run() + public override List Run(HashSet candles) { - if (Candles.Count <= 2 * Period) + if (candles.Count <= 2 * Period) { return null; } try { - var ema = Candles.GetEma(Period.Value).ToList(); - var emaCandles = MapEmaToCandle(ema, Candles.TakeLast(Period.Value)); + var ema = candles.GetEma(Period.Value).ToList(); + var emaCandles = MapEmaToCandle(ema, candles.TakeLast(Period.Value)); if (ema.Count == 0) return null; @@ -54,11 +56,11 @@ public class EmaTrendIndicator : EmaBaseIndicator } } - public override IndicatorsResultBase GetIndicatorValues() + public override IndicatorsResultBase GetIndicatorValues(HashSet candles) { return new IndicatorsResultBase() { - Ema = Candles.GetEma(Period.Value).ToList() + Ema = candles.GetEma(Period.Value).ToList() }; } diff --git a/src/Managing.Domain/Strategies/Trends/StochRsiTrendIndicator.cs b/src/Managing.Domain/Indicators/Trends/StochRsiTrendIndicatorBase.cs similarity index 82% rename from src/Managing.Domain/Strategies/Trends/StochRsiTrendIndicator.cs rename to src/Managing.Domain/Indicators/Trends/StochRsiTrendIndicatorBase.cs index 00a51bb..b8ff7ec 100644 --- a/src/Managing.Domain/Strategies/Trends/StochRsiTrendIndicator.cs +++ b/src/Managing.Domain/Indicators/Trends/StochRsiTrendIndicatorBase.cs @@ -1,5 +1,6 @@ īģŋusing Managing.Core; using Managing.Domain.Candles; +using Managing.Domain.Indicators; using Managing.Domain.Shared.Rules; using Managing.Domain.Strategies.Base; using Skender.Stock.Indicators; @@ -7,11 +8,11 @@ using static Managing.Common.Enums; namespace Managing.Domain.Strategies.Trends; -public class StochRsiTrendIndicator : Indicator +public class StochRsiTrendIndicatorBase : IndicatorBase { public List Signals { get; set; } - public StochRsiTrendIndicator( + public StochRsiTrendIndicatorBase( string name, int period, int stochPeriod, @@ -25,21 +26,21 @@ public class StochRsiTrendIndicator : Indicator Period = period; } - public override List Run() + public override List Run(HashSet candles) { - if (Candles.Count <= 10 * Period + 50) + if (candles.Count <= 10 * Period + 50) { return null; } try { - var stochRsi = Candles + var stochRsi = candles .GetStochRsi(Period.Value, StochPeriods.Value, SignalPeriods.Value, SmoothPeriods.Value) - .RemoveWarmupPeriods().ToList(); - var stochRsiCandles = MapStochRsiToCandle(stochRsi, Candles.TakeLast(Period.Value)); + .RemoveWarmupPeriods(); + var stochRsiCandles = MapStochRsiToCandle(stochRsi, candles.TakeLast(Period.Value)); - if (stochRsi.Count == 0) + if (stochRsi.Count() == 0) return null; var previousCandle = stochRsiCandles[0]; @@ -65,16 +66,16 @@ public class StochRsiTrendIndicator : Indicator } } - public override IndicatorsResultBase GetIndicatorValues() + public override IndicatorsResultBase GetIndicatorValues(HashSet candles) { return new IndicatorsResultBase() { - StochRsi = Candles.GetStochRsi(Period.Value, StochPeriods.Value, SignalPeriods.Value, SmoothPeriods.Value) + StochRsi = candles.GetStochRsi(Period.Value, StochPeriods.Value, SignalPeriods.Value, SmoothPeriods.Value) .ToList() }; } - private List MapStochRsiToCandle(List ema, IEnumerable candles) + private List MapStochRsiToCandle(IEnumerable ema, IEnumerable candles) { var emaList = new List(); foreach (var candle in candles) diff --git a/src/Managing.Domain/Scenarios/LightScenario.cs b/src/Managing.Domain/Scenarios/LightScenario.cs index 6ef3cce..1ba1a3b 100644 --- a/src/Managing.Domain/Scenarios/LightScenario.cs +++ b/src/Managing.Domain/Scenarios/LightScenario.cs @@ -30,7 +30,7 @@ public class LightScenario { var lightScenario = new LightScenario(scenario.Name, scenario.LoopbackPeriod) { - Indicators = scenario.Indicators?.Select(LightIndicator.FromIndicator).ToList() ?? + Indicators = scenario.Indicators?.Select(LightIndicator.BaseToLight).ToList() ?? new List() }; return lightScenario; @@ -43,15 +43,8 @@ public class LightScenario { var scenario = new Scenario(Name, LoopbackPeriod) { - Indicators = Indicators?.Select(li => li.ToIndicator()).ToList() ?? new List() + Indicators = Indicators?.Select(li => li.LightToBase()).ToList() }; return scenario; } - - public void AddIndicator(LightIndicator indicator) - { - if (Indicators == null) - Indicators = new List(); - Indicators.Add(indicator); - } } \ No newline at end of file diff --git a/src/Managing.Domain/Scenarios/Scenario.cs b/src/Managing.Domain/Scenarios/Scenario.cs index ae974eb..5857137 100644 --- a/src/Managing.Domain/Scenarios/Scenario.cs +++ b/src/Managing.Domain/Scenarios/Scenario.cs @@ -10,23 +10,19 @@ namespace Managing.Domain.Scenarios public Scenario(string name, int? loopbackPeriod = 1) { Name = name; - Indicators = new List(); + Indicators = new List(); LoopbackPeriod = loopbackPeriod; } - [Id(0)] - public string Name { get; set; } - - [Id(1)] - public List Indicators { get; set; } - - [Id(2)] - public int? LoopbackPeriod { get; set; } - - [Id(3)] - public User User { get; set; } + [Id(0)] public string Name { get; set; } - public void AddIndicator(Indicator indicator) + [Id(1)] public List Indicators { get; set; } + + [Id(2)] public int? LoopbackPeriod { get; set; } + + [Id(3)] public User User { get; set; } + + public void AddIndicator(IndicatorBase indicator) { Indicators.Add(indicator); } diff --git a/src/Managing.Domain/Scenarios/ScenarioHelpers.cs b/src/Managing.Domain/Scenarios/ScenarioHelpers.cs index ba73692..4a78792 100644 --- a/src/Managing.Domain/Scenarios/ScenarioHelpers.cs +++ b/src/Managing.Domain/Scenarios/ScenarioHelpers.cs @@ -1,52 +1,97 @@ -īģŋusing Managing.Core.FixedSizedQueue; -using Managing.Domain.Candles; -using Managing.Domain.Strategies; +īģŋusing Managing.Domain.Strategies; using Managing.Domain.Strategies.Context; using Managing.Domain.Strategies.Signals; using Managing.Domain.Strategies.Trends; +using Newtonsoft.Json; using static Managing.Common.Enums; namespace Managing.Domain.Scenarios; public static class ScenarioHelpers { - public static IEnumerable GetIndicatorsFromScenario(Scenario scenario) + /// + /// 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 + public static List CompareIndicators(List oldIndicators, List newIndicators) { - var strategies = new List(); - foreach (var strategy in scenario.Indicators) + 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 result = BuildIndicator(strategy); - strategies.Add(result); + var indicator = oldIndicatorDict[removedType]; + changes.Add($"➖ **Removed Indicator:** {removedType} ({indicator.GetType().Name})"); } - return strategies; + // 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; } - public static IIndicator BuildIndicator(Indicator indicator, int size = 600) + public static IIndicator BuildIndicator(LightIndicator indicator) { IIndicator result = indicator.Type switch { IndicatorType.StDev => new StDevContext(indicator.Name, indicator.Period.Value), - IndicatorType.RsiDivergence => new RsiDivergenceIndicator(indicator.Name, + IndicatorType.RsiDivergence => new RsiDivergenceIndicatorBase(indicator.Name, indicator.Period.Value), - IndicatorType.RsiDivergenceConfirm => new RsiDivergenceConfirmIndicator(indicator.Name, + IndicatorType.RsiDivergenceConfirm => new RsiDivergenceConfirmIndicatorBase(indicator.Name, indicator.Period.Value), - IndicatorType.MacdCross => new MacdCrossIndicator(indicator.Name, + IndicatorType.MacdCross => new MacdCrossIndicatorBase(indicator.Name, indicator.FastPeriods.Value, indicator.SlowPeriods.Value, indicator.SignalPeriods.Value), - IndicatorType.EmaCross => new EmaCrossIndicator(indicator.Name, indicator.Period.Value), - IndicatorType.DualEmaCross => new DualEmaCrossIndicator(indicator.Name, + IndicatorType.EmaCross => new EmaCrossIndicatorBase(indicator.Name, indicator.Period.Value), + IndicatorType.DualEmaCross => new DualEmaCrossIndicatorBase(indicator.Name, indicator.FastPeriods.Value, indicator.SlowPeriods.Value), - IndicatorType.ThreeWhiteSoldiers => new ThreeWhiteSoldiersIndicator(indicator.Name, + IndicatorType.ThreeWhiteSoldiers => new ThreeWhiteSoldiersIndicatorBase(indicator.Name, indicator.Period.Value), - IndicatorType.SuperTrend => new SuperTrendIndicator(indicator.Name, + IndicatorType.SuperTrend => new SuperTrendIndicatorBase(indicator.Name, indicator.Period.Value, indicator.Multiplier.Value), - IndicatorType.ChandelierExit => new ChandelierExitIndicator(indicator.Name, + IndicatorType.ChandelierExit => new ChandelierExitIndicatorBase(indicator.Name, indicator.Period.Value, indicator.Multiplier.Value), - IndicatorType.EmaTrend => new EmaTrendIndicator(indicator.Name, indicator.Period.Value), - IndicatorType.StochRsiTrend => new StochRsiTrendIndicator(indicator.Name, + IndicatorType.EmaTrend => new EmaTrendIndicatorBase(indicator.Name, indicator.Period.Value), + IndicatorType.StochRsiTrend => new StochRsiTrendIndicatorBase(indicator.Name, indicator.Period.Value, indicator.StochPeriods.Value, indicator.SignalPeriods.Value, indicator.SmoothPeriods.Value), - IndicatorType.Stc => new StcIndicator(indicator.Name, indicator.CyclePeriods.Value, + IndicatorType.Stc => new StcIndicatorBase(indicator.Name, indicator.CyclePeriods.Value, indicator.FastPeriods.Value, indicator.SlowPeriods.Value), IndicatorType.LaggingStc => new LaggingSTC(indicator.Name, indicator.CyclePeriods.Value, indicator.FastPeriods.Value, indicator.SlowPeriods.Value), @@ -55,11 +100,10 @@ public static class ScenarioHelpers _ => throw new NotImplementedException(), }; - result.Candles = new FixedSizeQueue(size); return result; } - public static Indicator BuildIndicator( + public static IIndicator BuildIndicator( IndicatorType type, string name, int? period = null, @@ -71,7 +115,7 @@ public static class ScenarioHelpers int? smoothPeriods = null, int? cyclePeriods = null) { - var indicator = new Indicator(name, type); + IIndicator indicator = null; switch (type) { diff --git a/src/Managing.Domain/Shared/Helpers/TradingBox.cs b/src/Managing.Domain/Shared/Helpers/TradingBox.cs index 24de0fd..424d288 100644 --- a/src/Managing.Domain/Shared/Helpers/TradingBox.cs +++ b/src/Managing.Domain/Shared/Helpers/TradingBox.cs @@ -1,6 +1,8 @@ īģŋusing Managing.Core; using Managing.Domain.Candles; +using Managing.Domain.Indicators; using Managing.Domain.MoneyManagements; +using Managing.Domain.Scenarios; using Managing.Domain.Strategies; using Managing.Domain.Trades; using static Managing.Common.Enums; @@ -49,28 +51,28 @@ public static class TradingBox { private static readonly IndicatorComboConfig _defaultConfig = new(); - public static LightSignal GetSignal(HashSet newCandles, HashSet strategies, - HashSet previousSignal, int? loopbackPeriod = 1) + public static LightSignal GetSignal(HashSet newCandles, LightScenario scenario, + Dictionary previousSignal, int? loopbackPeriod = 1) { - return GetSignal(newCandles, strategies, previousSignal, _defaultConfig, loopbackPeriod); + return GetSignal(newCandles, scenario, previousSignal, _defaultConfig, loopbackPeriod); } - public static LightSignal GetSignal(HashSet newCandles, HashSet strategies, - HashSet previousSignal, IndicatorComboConfig config, int? loopbackPeriod = 1) + public static LightSignal GetSignal(HashSet newCandles, LightScenario lightScenario, + Dictionary previousSignal, IndicatorComboConfig config, int? loopbackPeriod = 1) { var signalOnCandles = new List(); var limitedCandles = newCandles.ToList().TakeLast(600).ToList(); - foreach (var strategy in strategies) + foreach (var indicator in lightScenario.Indicators) { - strategy.UpdateCandles(limitedCandles.ToHashSet()); - var signals = strategy.Run(); + IIndicator indicatorInstance = indicator.ToInterface(); + var signals = indicatorInstance.Run(newCandles); - if (signals == null || signals.Count == 0) + if (signals == null || signals.Count() == 0) { // For trend and context strategies, lack of signal might be meaningful // Signal strategies are expected to be sparse, so we continue - if (strategy.SignalType == SignalType.Signal) + if (indicator.SignalType == SignalType.Signal) { continue; } @@ -96,10 +98,10 @@ public static class TradingBox foreach (var signal in signals.Where(s => s.Date >= loopbackStartDate)) { - var hasExistingSignal = previousSignal.Any(s => s.Identifier == signal.Identifier); + var hasExistingSignal = previousSignal.ContainsKey(signal.Identifier); if (!hasExistingSignal) { - bool shouldAdd = previousSignal.Count == 0 || previousSignal.Last().Date < signal.Date; + bool shouldAdd = previousSignal.Count == 0 || previousSignal.Values.Last().Date < signal.Date; if (shouldAdd) { signalOnCandles.Add(signal); @@ -122,22 +124,22 @@ public static class TradingBox } var data = newCandles.First(); - return ComputeSignals(strategies, latestSignalsPerIndicator, MiscExtensions.ParseEnum(data.Ticker), + return ComputeSignals(lightScenario, latestSignalsPerIndicator, MiscExtensions.ParseEnum(data.Ticker), data.Timeframe, config); } - public static LightSignal ComputeSignals(HashSet strategies, HashSet signalOnCandles, + public static LightSignal ComputeSignals(LightScenario scenario, HashSet signalOnCandles, Ticker ticker, Timeframe timeframe) { - return ComputeSignals(strategies, signalOnCandles, ticker, timeframe, _defaultConfig); + return ComputeSignals(scenario, signalOnCandles, ticker, timeframe, _defaultConfig); } - public static LightSignal ComputeSignals(HashSet strategies, HashSet signalOnCandles, + public static LightSignal ComputeSignals(LightScenario scenario, HashSet signalOnCandles, Ticker ticker, Timeframe timeframe, IndicatorComboConfig config) { - if (strategies.Count == 1) + if (scenario.Indicators.Count == 1) { // Only one strategy, return the single signal return signalOnCandles.Single(); @@ -146,7 +148,7 @@ public static class TradingBox signalOnCandles = signalOnCandles.OrderBy(s => s.Date).ToHashSet(); // Check if all strategies produced signals - this is required for composite signals - var strategyNames = strategies.Select(s => s.Name).ToHashSet(); + var strategyNames = scenario.Indicators.Select(s => s.Name).ToHashSet(); var signalIndicatorNames = signalOnCandles.Select(s => s.IndicatorName).ToHashSet(); if (!strategyNames.SetEquals(signalIndicatorNames)) @@ -161,7 +163,7 @@ public static class TradingBox var contextSignals = signalOnCandles.Where(s => s.SignalType == SignalType.Context).ToList(); // Context validation - evaluates market conditions based on confidence levels - if (!ValidateContextStrategies(strategies, contextSignals, config)) + if (!ValidateContextStrategies(scenario, contextSignals, config)) { return null; // Context strategies are blocking the trade } @@ -233,10 +235,10 @@ public static class TradingBox /// /// Validates context strategies based on confidence levels indicating market condition quality /// - private static bool ValidateContextStrategies(HashSet allStrategies, List contextSignals, + private static bool ValidateContextStrategies(LightScenario scenario, List contextSignals, IndicatorComboConfig config) { - var contextStrategiesCount = allStrategies.Count(s => s.SignalType == SignalType.Context); + var contextStrategiesCount = scenario.Indicators.Count(s => s.SignalType == SignalType.Context); if (contextStrategiesCount == 0) { @@ -453,11 +455,11 @@ public static class TradingBox /// /// List of positions to analyze /// The total volume traded in decimal - public static decimal GetTotalVolumeTraded(List positions) + public static decimal GetTotalVolumeTraded(Dictionary positions) { decimal totalVolume = 0; - foreach (var position in positions) + foreach (var position in positions.Values) { // Add entry volume totalVolume += position.Open.Quantity * position.Open.Price; @@ -487,12 +489,12 @@ public static class TradingBox /// /// List of positions to analyze /// The volume traded in the last 24 hours in decimal - public static decimal GetLast24HVolumeTraded(List positions) + public static decimal GetLast24HVolumeTraded(Dictionary positions) { decimal last24hVolume = 0; DateTime cutoff = DateTime.UtcNow.AddHours(-24); - foreach (var position in positions) + foreach (var position in positions.Values) { // Check if any part of this position was traded in the last 24 hours @@ -528,24 +530,20 @@ public static class TradingBox /// /// List of positions to analyze /// A tuple containing (wins, losses) - public static (int Wins, int Losses) GetWinLossCount(List positions) + public static (int Wins, int Losses) GetWinLossCount(Dictionary positions) { int wins = 0; int losses = 0; - foreach (var position in positions) + foreach (var position in positions.Values) { - // Only count finished positions - if (position.IsFinished()) + if (position.ProfitAndLoss != null && position.ProfitAndLoss.Realized > 0) { - if (position.ProfitAndLoss != null && position.ProfitAndLoss.Realized > 0) - { - wins++; - } - else - { - losses++; - } + wins++; + } + else + { + losses++; } } @@ -557,13 +555,13 @@ public static class TradingBox /// /// List of positions to analyze /// The ROI for the last 24 hours as a percentage - public static decimal GetLast24HROI(List positions) + public static decimal GetLast24HROI(Dictionary positions) { decimal profitLast24h = 0; decimal investmentLast24h = 0; DateTime cutoff = DateTime.UtcNow.AddHours(-24); - foreach (var position in positions) + foreach (var position in positions.Values) { // Only count positions that were opened or closed within the last 24 hours if (position.IsFinished() && diff --git a/src/Managing.Domain/Shared/Helpers/TradingHelpers.cs b/src/Managing.Domain/Shared/Helpers/TradingHelpers.cs index f2e8967..28813c0 100644 --- a/src/Managing.Domain/Shared/Helpers/TradingHelpers.cs +++ b/src/Managing.Domain/Shared/Helpers/TradingHelpers.cs @@ -1,5 +1,6 @@ īģŋusing Exilion.TradingAtomics; using Managing.Domain.Accounts; +using Managing.Domain.Candles; using Managing.Domain.Statistics; using static Managing.Common.Enums; @@ -7,7 +8,7 @@ namespace Managing.Domain.Shared.Helpers; public static class TradingHelpers { - public static decimal GetHodlPercentage(Candles.Candle candle1, Candles.Candle candle2) + public static decimal GetHodlPercentage(Candle candle1, Candle candle2) { return candle2.Close * 100 / candle1.Close - 100; } diff --git a/src/Managing.Domain/Statistics/AgentSummary.cs b/src/Managing.Domain/Statistics/AgentSummary.cs new file mode 100644 index 0000000..b56f794 --- /dev/null +++ b/src/Managing.Domain/Statistics/AgentSummary.cs @@ -0,0 +1,47 @@ +using Managing.Domain.Users; +using Orleans; + +namespace Managing.Domain.Statistics; + +[GenerateSerializer] +public class AgentSummary +{ + [Id(0)] + public int Id { get; set; } + + [Id(1)] + public int UserId { get; set; } + + [Id(2)] + public string AgentName { get; set; } + + [Id(3)] + public decimal TotalPnL { get; set; } + + [Id(4)] + public decimal TotalROI { get; set; } + + [Id(5)] + public int Wins { get; set; } + + [Id(6)] + public int Losses { get; set; } + + [Id(7)] + public DateTime? Runtime { get; set; } + + [Id(8)] + public DateTime CreatedAt { get; set; } + + [Id(9)] + public DateTime UpdatedAt { get; set; } + + [Id(10)] + public User User { get; set; } + + [Id(11)] + public int ActiveStrategiesCount { get; set; } + + [Id(12)] + public decimal TotalVolume { get; set; } +} \ No newline at end of file diff --git a/src/Managing.Domain/Statistics/Spotlight.cs b/src/Managing.Domain/Statistics/Spotlight.cs index 9b6ace7..ce7be83 100644 --- a/src/Managing.Domain/Statistics/Spotlight.cs +++ b/src/Managing.Domain/Statistics/Spotlight.cs @@ -1,5 +1,6 @@ īģŋusing System.ComponentModel.DataAnnotations; using Managing.Common; +using Managing.Domain.Indicators; using Managing.Domain.Scenarios; namespace Managing.Domain.Statistics; diff --git a/src/Managing.Domain/Strategies/Indicator.cs b/src/Managing.Domain/Strategies/Indicator.cs deleted file mode 100644 index 0fb7210..0000000 --- a/src/Managing.Domain/Strategies/Indicator.cs +++ /dev/null @@ -1,82 +0,0 @@ -īģŋusing Managing.Core.FixedSizedQueue; -using Managing.Domain.Candles; -using Managing.Domain.Scenarios; -using Managing.Domain.Strategies.Base; -using Managing.Domain.Users; -using static Managing.Common.Enums; - -namespace Managing.Domain.Strategies -{ - public class Indicator : IIndicator - { - public Indicator(string name, IndicatorType type) - { - Name = name; - Type = type; - SignalType = ScenarioHelpers.GetSignalType(type); - Candles = new FixedSizeQueue(500); - } - - public string Name { get; set; } - - public FixedSizeQueue Candles { get; set; } - - public IndicatorType Type { get; set; } - - public SignalType SignalType { get; set; } - - public int MinimumHistory { 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? SmoothPeriods { get; set; } - - public int? StochPeriods { get; set; } - - public int? CyclePeriods { get; set; } - - public User User { get; set; } - - public virtual List Run() - { - return new List(); - } - - public virtual IndicatorsResultBase GetIndicatorValues() - { - return new IndicatorsResultBase(); - } - - public void UpdateCandles(HashSet newCandles) - { - if (newCandles == null || newCandles.Count == 0) - { - return; - } - - lock (Candles) - { - foreach (var item in newCandles.ToList()) - { - if (Candles.All(c => c.Date != item.Date)) - { - Candles.Enqueue(item); - } - } - } - } - - public string GetName() - { - return Name; - } - } -} \ No newline at end of file diff --git a/src/Managing.Domain/Strategies/LightSignal.cs b/src/Managing.Domain/Strategies/LightSignal.cs deleted file mode 100644 index 0b75d8f..0000000 --- a/src/Managing.Domain/Strategies/LightSignal.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.Globalization; -using Managing.Core; -using Managing.Domain.Candles; -using Orleans; -using static Managing.Common.Enums; - -[GenerateSerializer] -public class LightSignal : ValueObject -{ - public LightSignal(Ticker ticker, TradeDirection direction, Confidence confidence, Candle candle, DateTime date, - TradingExchanges exchange, IndicatorType indicatorType, SignalType signalType, string indicatorName) - { - Direction = direction; - Confidence = confidence; - Candle = candle; - Date = date; - Ticker = ticker; - Exchange = exchange; - Status = SignalStatus.WaitingForPosition; - IndicatorType = indicatorType; - IndicatorName = indicatorName; - SignalType = signalType; - - Identifier = - $"{indicatorName}-{indicatorType}-{direction}-{ticker}-{candle?.Close.ToString(CultureInfo.InvariantCulture)}-{date:yyyyMMdd-HHmmss}"; - } - - [Id(0)] - [Required] public SignalStatus Status { get; set; } - - [Id(1)] - [Required] public TradeDirection Direction { get; } - - [Id(2)] - [Required] public Confidence Confidence { get; set; } - - [Id(3)] - [Required] public Timeframe Timeframe { get; } - - [Id(4)] - [Required] public DateTime Date { get; private set; } - - [Id(5)] - [Required] public Candle Candle { get; } - - [Id(6)] - [Required] public string Identifier { get; } - - [Id(7)] - [Required] public Ticker Ticker { get; } - - [Id(8)] - [Required] public TradingExchanges Exchange { get; set; } - - [Id(9)] - [Required] public IndicatorType IndicatorType { get; set; } - - [Id(10)] - [Required] public SignalType SignalType { get; set; } - - [Id(11)] - [Required] public string IndicatorName { get; set; } - - protected override IEnumerable GetEqualityComponents() - { - yield return Direction; - yield return Confidence; - yield return Date; - } - - public void SetConfidence(Confidence confidence) - { - Confidence = confidence; - } -} \ No newline at end of file diff --git a/src/Managing.Domain/Trades/Position.cs b/src/Managing.Domain/Trades/Position.cs index da1ca82..9c65883 100644 --- a/src/Managing.Domain/Trades/Position.cs +++ b/src/Managing.Domain/Trades/Position.cs @@ -9,7 +9,7 @@ namespace Managing.Domain.Trades [GenerateSerializer] public class Position { - public Position(string identifier, string accountName, TradeDirection originDirection, Ticker ticker, + public Position(Guid identifier, string accountName, TradeDirection originDirection, Ticker ticker, LightMoneyManagement moneyManagement, PositionInitiator initiator, DateTime date, User user) { Identifier = identifier; @@ -23,23 +23,20 @@ namespace Managing.Domain.Trades User = user; } - [Id(0)] - [Required] public string AccountName { get; set; } - - [Id(1)] - [Required] public DateTime Date { get; set; } - - [Id(2)] - [Required] public TradeDirection OriginDirection { get; set; } - - [Id(3)] - [Required] public Ticker Ticker { get; set; } - - [Id(4)] - [Required] public LightMoneyManagement MoneyManagement { get; set; } - + [Id(0)] [Required] public string AccountName { get; set; } + + [Id(1)] [Required] public DateTime Date { get; set; } + + [Id(2)] [Required] public TradeDirection OriginDirection { get; set; } + + [Id(3)] [Required] public Ticker Ticker { get; set; } + + [Id(4)] [Required] public LightMoneyManagement MoneyManagement { get; set; } + [Id(5)] - [Required] [JsonPropertyName("Open")] public Trade Open { get; set; } + [Required] + [JsonPropertyName("Open")] + public Trade Open { get; set; } [Id(6)] [Required] @@ -52,25 +49,22 @@ namespace Managing.Domain.Trades public Trade TakeProfit1 { get; set; } [Id(8)] - [JsonPropertyName("TakeProfit2")] public Trade TakeProfit2 { get; set; } + [JsonPropertyName("TakeProfit2")] + public Trade TakeProfit2 { get; set; } [Id(9)] - [JsonPropertyName("ProfitAndLoss")] public ProfitAndLoss ProfitAndLoss { get; set; } - - [Id(10)] - [Required] public PositionStatus Status { get; set; } - - [Id(11)] - public string SignalIdentifier { get; set; } - - [Id(12)] - [Required] public string Identifier { get; set; } - - [Id(13)] - [Required] public PositionInitiator Initiator { get; set; } - - [Id(14)] - [Required] public User User { get; set; } + [JsonPropertyName("ProfitAndLoss")] + public ProfitAndLoss ProfitAndLoss { get; set; } + + [Id(10)] [Required] public PositionStatus Status { get; set; } + + [Id(11)] public string SignalIdentifier { get; set; } + + [Id(12)] [Required] public Guid Identifier { get; set; } + + [Id(13)] [Required] public PositionInitiator Initiator { get; set; } + + [Id(14)] [Required] public User User { get; set; } public bool IsFinished() { diff --git a/src/Managing.Domain/Users/User.cs b/src/Managing.Domain/Users/User.cs index f87f9ce..798c1b5 100644 --- a/src/Managing.Domain/Users/User.cs +++ b/src/Managing.Domain/Users/User.cs @@ -7,17 +7,20 @@ namespace Managing.Domain.Users; public class User { [Id(0)] + public int Id { get; set; } + + [Id(1)] public string Name { get; set; } - [Id(1)] + [Id(2)] public List Accounts { get; set; } - [Id(2)] + [Id(3)] public string AgentName { get; set; } - [Id(3)] + [Id(4)] public string AvatarUrl { get; set; } - [Id(4)] + [Id(5)] public string TelegramChannel { get; set; } } \ No newline at end of file diff --git a/src/Managing.Infrastructure.Database/InfluxDb/CandleRepository.cs b/src/Managing.Infrastructure.Database/InfluxDb/CandleRepository.cs index 66a951f..272b841 100644 --- a/src/Managing.Infrastructure.Database/InfluxDb/CandleRepository.cs +++ b/src/Managing.Infrastructure.Database/InfluxDb/CandleRepository.cs @@ -23,11 +23,12 @@ public class CandleRepository : ICandleRepository _logger = logger; } - public async Task> GetCandles( + public async Task> GetCandles( TradingExchanges exchange, Ticker ticker, Timeframe timeframe, - DateTime start) + DateTime start, + int? limit = null) { var results = await _influxDbRepository.QueryAsync(async query => { @@ -37,20 +38,25 @@ public class CandleRepository : ICandleRepository $"|> filter(fn: (r) => r[\"ticker\"] == \"{ticker}\")" + $"|> filter(fn: (r) => r[\"timeframe\"] == \"{timeframe}\")" + $"|> pivot(rowKey:[\"_time\"], columnKey: [\"_field\"], valueColumn: \"_value\")"; + if (limit != null) + { + flux += $"|> tail(n:{limit})"; + } var prices = await query.QueryAsync(flux, _influxDbRepository.Organization); - return prices.Select(price => PriceHelpers.Map(price)).ToList(); + return prices.Select(price => PriceHelpers.Map(price)).ToHashSet(); }); return results; } - public async Task> GetCandles( + public async Task> GetCandles( TradingExchanges exchange, Ticker ticker, Timeframe timeframe, DateTime start, - DateTime end) + DateTime end, + int? limit = null) { var results = await _influxDbRepository.QueryAsync(async query => { @@ -60,9 +66,13 @@ public class CandleRepository : ICandleRepository $"|> filter(fn: (r) => r[\"ticker\"] == \"{ticker}\")" + $"|> filter(fn: (r) => r[\"timeframe\"] == \"{timeframe}\")" + $"|> pivot(rowKey:[\"_time\"], columnKey: [\"_field\"], valueColumn: \"_value\")"; + if (limit != null) + { + flux += $"|> tail(n:{limit})"; + } var prices = await query.QueryAsync(flux, _influxDbRepository.Organization); - return prices.Select(price => PriceHelpers.Map(price)).ToList(); + return prices.Select(price => PriceHelpers.Map(price)).ToHashSet(); }); return results; diff --git a/src/Managing.Infrastructure.Database/Migrations/20250723194312_InitialCreate.Designer.cs b/src/Managing.Infrastructure.Database/Migrations/20250723194312_InitialCreate.Designer.cs deleted file mode 100644 index 70152d4..0000000 --- a/src/Managing.Infrastructure.Database/Migrations/20250723194312_InitialCreate.Designer.cs +++ /dev/null @@ -1,1224 +0,0 @@ -īģŋ// -using System; -using Managing.Infrastructure.Databases.PostgreSql; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Managing.Infrastructure.Databases.Migrations -{ - [DbContext(typeof(ManagingDbContext))] - [Migration("20250723194312_InitialCreate")] - partial class InitialCreate - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.11") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AccountEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Exchange") - .IsRequired() - .HasColumnType("text"); - - b.Property("Key") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Secret") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.Property("UserId") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("Key"); - - b.HasIndex("Name") - .IsUnique(); - - b.HasIndex("UserId"); - - b.ToTable("Accounts"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BacktestEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ConfigJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("EndDate") - .HasColumnType("timestamp with time zone"); - - b.Property("Fees") - .HasColumnType("decimal(18,8)"); - - b.Property("FinalPnl") - .HasColumnType("decimal(18,8)"); - - b.Property("GrowthPercentage") - .HasColumnType("decimal(18,8)"); - - b.Property("HodlPercentage") - .HasColumnType("decimal(18,8)"); - - b.Property("Identifier") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Metadata") - .HasColumnType("text"); - - b.Property("MoneyManagementJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("OptimizedMoneyManagementJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("PositionsJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("RequestId") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Score") - .HasColumnType("double precision"); - - b.Property("ScoreMessage") - .IsRequired() - .HasMaxLength(1000) - .HasColumnType("character varying(1000)"); - - b.Property("SignalsJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("StartDate") - .HasColumnType("timestamp with time zone"); - - b.Property("StatisticsJson") - .HasColumnType("jsonb"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("WinRate") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("EndDate"); - - b.HasIndex("FinalPnl"); - - b.HasIndex("Identifier") - .IsUnique(); - - b.HasIndex("RequestId"); - - b.HasIndex("Score"); - - b.HasIndex("StartDate"); - - b.HasIndex("UserName"); - - b.HasIndex("WinRate"); - - b.HasIndex("RequestId", "Score"); - - b.HasIndex("UserName", "Score"); - - b.ToTable("Backtests"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotBackupEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreateDate") - .HasColumnType("timestamp with time zone"); - - b.Property("Data") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("Identifier") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("LastStatus") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("CreateDate"); - - b.HasIndex("Identifier") - .IsUnique(); - - b.HasIndex("LastStatus"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "CreateDate"); - - b.ToTable("BotBackups"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BundleBacktestRequestEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("BacktestRequestsJson") - .IsRequired() - .HasColumnType("text"); - - b.Property("CompletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CompletedBacktests") - .HasColumnType("integer"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CurrentBacktest") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("ErrorMessage") - .HasColumnType("text"); - - b.Property("EstimatedTimeRemainingSeconds") - .HasColumnType("integer"); - - b.Property("FailedBacktests") - .HasColumnType("integer"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("ProgressInfo") - .HasColumnType("text"); - - b.Property("RequestId") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("ResultsJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("Status") - .IsRequired() - .HasColumnType("text"); - - b.Property("TotalBacktests") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("CompletedAt"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("RequestId") - .IsUnique(); - - b.HasIndex("Status"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "CreatedAt"); - - b.ToTable("BundleBacktestRequests"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.FeeEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Cost") - .HasColumnType("decimal(18,8)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Exchange") - .IsRequired() - .HasColumnType("text"); - - b.Property("LastUpdate") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("Exchange") - .IsUnique(); - - b.HasIndex("LastUpdate"); - - b.ToTable("Fees"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.FundingRateEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Date") - .HasColumnType("timestamp with time zone"); - - b.Property("Direction") - .HasColumnType("integer"); - - b.Property("Exchange") - .HasColumnType("integer"); - - b.Property("OpenInterest") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.Property("Rate") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.Property("Ticker") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("Date"); - - b.HasIndex("Direction"); - - b.HasIndex("Exchange"); - - b.HasIndex("Ticker"); - - b.HasIndex("Exchange", "Date"); - - b.HasIndex("Ticker", "Exchange"); - - b.HasIndex("Ticker", "Exchange", "Date") - .IsUnique(); - - b.ToTable("FundingRates"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.GeneticRequestEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Balance") - .HasColumnType("decimal(18,8)"); - - b.Property("BestChromosome") - .HasMaxLength(4000) - .HasColumnType("character varying(4000)"); - - b.Property("BestFitness") - .HasColumnType("double precision"); - - b.Property("BestFitnessSoFar") - .HasColumnType("double precision"); - - b.Property("BestIndividual") - .HasMaxLength(4000) - .HasColumnType("character varying(4000)"); - - b.Property("CompletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CrossoverMethod") - .IsRequired() - .HasColumnType("text"); - - b.Property("CurrentGeneration") - .HasColumnType("integer"); - - b.Property("EligibleIndicatorsJson") - .HasMaxLength(2000) - .HasColumnType("character varying(2000)"); - - b.Property("ElitismPercentage") - .HasColumnType("integer"); - - b.Property("EndDate") - .HasColumnType("timestamp with time zone"); - - b.Property("ErrorMessage") - .HasMaxLength(2000) - .HasColumnType("character varying(2000)"); - - b.Property("Generations") - .HasColumnType("integer"); - - b.Property("MaxTakeProfit") - .HasColumnType("double precision"); - - b.Property("MutationMethod") - .IsRequired() - .HasColumnType("text"); - - b.Property("MutationRate") - .HasColumnType("double precision"); - - b.Property("PopulationSize") - .HasColumnType("integer"); - - b.Property("ProgressInfo") - .HasMaxLength(4000) - .HasColumnType("character varying(4000)"); - - b.Property("RequestId") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("SelectionMethod") - .IsRequired() - .HasColumnType("text"); - - b.Property("StartDate") - .HasColumnType("timestamp with time zone"); - - b.Property("Status") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("Ticker") - .IsRequired() - .HasColumnType("text"); - - b.Property("Timeframe") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("RequestId") - .IsUnique(); - - b.HasIndex("Status"); - - b.HasIndex("UserId"); - - b.ToTable("GeneticRequests"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CyclePeriods") - .HasColumnType("integer"); - - b.Property("FastPeriods") - .HasColumnType("integer"); - - b.Property("MinimumHistory") - .HasColumnType("integer"); - - b.Property("Multiplier") - .HasColumnType("double precision"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Period") - .HasColumnType("integer"); - - b.Property("SignalPeriods") - .HasColumnType("integer"); - - b.Property("SignalType") - .IsRequired() - .HasColumnType("text"); - - b.Property("SlowPeriods") - .HasColumnType("integer"); - - b.Property("SmoothPeriods") - .HasColumnType("integer"); - - b.Property("StochPeriods") - .HasColumnType("integer"); - - b.Property("Timeframe") - .IsRequired() - .HasColumnType("text"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("Name"); - - b.HasIndex("Type"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "Name"); - - b.ToTable("Indicators"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.MoneyManagementEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Leverage") - .HasColumnType("decimal(18,8)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("StopLoss") - .HasColumnType("decimal(18,8)"); - - b.Property("TakeProfit") - .HasColumnType("decimal(18,8)"); - - b.Property("Timeframe") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("Name"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "Name"); - - b.ToTable("MoneyManagements"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AccountName") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Date") - .HasColumnType("timestamp with time zone"); - - b.Property("Identifier") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Initiator") - .IsRequired() - .HasColumnType("text"); - - b.Property("MoneyManagementJson") - .HasColumnType("text"); - - b.Property("OpenTradeId") - .HasColumnType("integer"); - - b.Property("OriginDirection") - .IsRequired() - .HasColumnType("text"); - - b.Property("ProfitAndLoss") - .HasColumnType("decimal(18,8)"); - - b.Property("SignalIdentifier") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Status") - .IsRequired() - .HasColumnType("text"); - - b.Property("StopLossTradeId") - .HasColumnType("integer"); - - b.Property("TakeProfit1TradeId") - .HasColumnType("integer"); - - b.Property("TakeProfit2TradeId") - .HasColumnType("integer"); - - b.Property("Ticker") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("Date"); - - b.HasIndex("Identifier") - .IsUnique(); - - b.HasIndex("Initiator"); - - b.HasIndex("OpenTradeId"); - - b.HasIndex("Status"); - - b.HasIndex("StopLossTradeId"); - - b.HasIndex("TakeProfit1TradeId"); - - b.HasIndex("TakeProfit2TradeId"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "Identifier"); - - b.ToTable("Positions"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("LoopbackPeriod") - .HasColumnType("integer"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("Name"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "Name"); - - b.ToTable("Scenarios"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioIndicatorEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("IndicatorId") - .HasColumnType("integer"); - - b.Property("ScenarioId") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("IndicatorId"); - - b.HasIndex("ScenarioId"); - - b.HasIndex("ScenarioId", "IndicatorId") - .IsUnique(); - - b.ToTable("ScenarioIndicators"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SignalEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CandleJson") - .HasColumnType("text"); - - b.Property("Confidence") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Date") - .HasColumnType("timestamp with time zone"); - - b.Property("Direction") - .IsRequired() - .HasColumnType("text"); - - b.Property("Identifier") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("IndicatorName") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("SignalType") - .IsRequired() - .HasColumnType("text"); - - b.Property("Status") - .IsRequired() - .HasColumnType("text"); - - b.Property("Ticker") - .IsRequired() - .HasColumnType("text"); - - b.Property("Timeframe") - .IsRequired() - .HasColumnType("text"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("Date"); - - b.HasIndex("Identifier"); - - b.HasIndex("Status"); - - b.HasIndex("Ticker"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "Date"); - - b.HasIndex("Identifier", "Date", "UserName") - .IsUnique(); - - b.ToTable("Signals"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SpotlightOverviewEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DateTime") - .HasColumnType("timestamp with time zone"); - - b.Property("Identifier") - .HasColumnType("uuid"); - - b.Property("ScenarioCount") - .HasColumnType("integer"); - - b.Property("SpotlightsJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("DateTime"); - - b.HasIndex("Identifier") - .IsUnique(); - - b.HasIndex("ScenarioCount"); - - b.HasIndex("DateTime", "ScenarioCount"); - - b.ToTable("SpotlightOverviews"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TopVolumeTickerEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Date") - .HasColumnType("timestamp with time zone"); - - b.Property("Exchange") - .HasColumnType("integer"); - - b.Property("Rank") - .HasColumnType("integer"); - - b.Property("Ticker") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Volume") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.HasKey("Id"); - - b.HasIndex("Date"); - - b.HasIndex("Exchange"); - - b.HasIndex("Rank"); - - b.HasIndex("Ticker"); - - b.HasIndex("Date", "Rank"); - - b.HasIndex("Exchange", "Date"); - - b.ToTable("TopVolumeTickers"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Date") - .HasColumnType("timestamp with time zone"); - - b.Property("Direction") - .IsRequired() - .HasColumnType("text"); - - b.Property("ExchangeOrderId") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Fee") - .HasColumnType("decimal(18,8)"); - - b.Property("Leverage") - .HasColumnType("decimal(18,8)"); - - b.Property("Message") - .HasColumnType("text"); - - b.Property("Price") - .HasColumnType("decimal(18,8)"); - - b.Property("Quantity") - .HasColumnType("decimal(18,8)"); - - b.Property("Status") - .IsRequired() - .HasColumnType("text"); - - b.Property("Ticker") - .IsRequired() - .HasColumnType("text"); - - b.Property("TradeType") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("Date"); - - b.HasIndex("ExchangeOrderId"); - - b.HasIndex("Status"); - - b.ToTable("Trades"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TraderEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Address") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("AverageLoss") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.Property("AverageWin") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("IsBestTrader") - .HasColumnType("boolean"); - - b.Property("Pnl") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.Property("Roi") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.Property("TradeCount") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Winrate") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("Address"); - - b.HasIndex("IsBestTrader"); - - b.HasIndex("Pnl"); - - b.HasIndex("Roi"); - - b.HasIndex("Winrate"); - - b.HasIndex("Address", "IsBestTrader") - .IsUnique(); - - b.HasIndex("IsBestTrader", "Roi"); - - b.HasIndex("IsBestTrader", "Winrate"); - - b.ToTable("Traders"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AgentName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("AvatarUrl") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("TelegramChannel") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.ToTable("Users"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AccountEntity", b => - { - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.SetNull); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.GeneticRequestEntity", b => - { - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.SetNull); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => - { - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "OpenTrade") - .WithMany() - .HasForeignKey("OpenTradeId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "StopLossTrade") - .WithMany() - .HasForeignKey("StopLossTradeId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "TakeProfit1Trade") - .WithMany() - .HasForeignKey("TakeProfit1TradeId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "TakeProfit2Trade") - .WithMany() - .HasForeignKey("TakeProfit2TradeId") - .OnDelete(DeleteBehavior.SetNull); - - b.Navigation("OpenTrade"); - - b.Navigation("StopLossTrade"); - - b.Navigation("TakeProfit1Trade"); - - b.Navigation("TakeProfit2Trade"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioIndicatorEntity", b => - { - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", "Indicator") - .WithMany("ScenarioIndicators") - .HasForeignKey("IndicatorId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", "Scenario") - .WithMany("ScenarioIndicators") - .HasForeignKey("ScenarioId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Indicator"); - - b.Navigation("Scenario"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => - { - b.Navigation("ScenarioIndicators"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => - { - b.Navigation("ScenarioIndicators"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/Managing.Infrastructure.Database/Migrations/20250723221025_UpdateBotBackupDataToText.Designer.cs b/src/Managing.Infrastructure.Database/Migrations/20250723221025_UpdateBotBackupDataToText.Designer.cs deleted file mode 100644 index 971118b..0000000 --- a/src/Managing.Infrastructure.Database/Migrations/20250723221025_UpdateBotBackupDataToText.Designer.cs +++ /dev/null @@ -1,1224 +0,0 @@ -īģŋ// -using System; -using Managing.Infrastructure.Databases.PostgreSql; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Managing.Infrastructure.Databases.Migrations -{ - [DbContext(typeof(ManagingDbContext))] - [Migration("20250723221025_UpdateBotBackupDataToText")] - partial class UpdateBotBackupDataToText - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.11") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AccountEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Exchange") - .IsRequired() - .HasColumnType("text"); - - b.Property("Key") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Secret") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.Property("UserId") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("Key"); - - b.HasIndex("Name") - .IsUnique(); - - b.HasIndex("UserId"); - - b.ToTable("Accounts"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BacktestEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ConfigJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("EndDate") - .HasColumnType("timestamp with time zone"); - - b.Property("Fees") - .HasColumnType("decimal(18,8)"); - - b.Property("FinalPnl") - .HasColumnType("decimal(18,8)"); - - b.Property("GrowthPercentage") - .HasColumnType("decimal(18,8)"); - - b.Property("HodlPercentage") - .HasColumnType("decimal(18,8)"); - - b.Property("Identifier") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Metadata") - .HasColumnType("text"); - - b.Property("MoneyManagementJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("OptimizedMoneyManagementJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("PositionsJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("RequestId") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Score") - .HasColumnType("double precision"); - - b.Property("ScoreMessage") - .IsRequired() - .HasMaxLength(1000) - .HasColumnType("character varying(1000)"); - - b.Property("SignalsJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("StartDate") - .HasColumnType("timestamp with time zone"); - - b.Property("StatisticsJson") - .HasColumnType("jsonb"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("WinRate") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("EndDate"); - - b.HasIndex("FinalPnl"); - - b.HasIndex("Identifier") - .IsUnique(); - - b.HasIndex("RequestId"); - - b.HasIndex("Score"); - - b.HasIndex("StartDate"); - - b.HasIndex("UserName"); - - b.HasIndex("WinRate"); - - b.HasIndex("RequestId", "Score"); - - b.HasIndex("UserName", "Score"); - - b.ToTable("Backtests"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotBackupEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreateDate") - .HasColumnType("timestamp with time zone"); - - b.Property("Data") - .IsRequired() - .HasColumnType("text"); - - b.Property("Identifier") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("LastStatus") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("CreateDate"); - - b.HasIndex("Identifier") - .IsUnique(); - - b.HasIndex("LastStatus"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "CreateDate"); - - b.ToTable("BotBackups"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BundleBacktestRequestEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("BacktestRequestsJson") - .IsRequired() - .HasColumnType("text"); - - b.Property("CompletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CompletedBacktests") - .HasColumnType("integer"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CurrentBacktest") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("ErrorMessage") - .HasColumnType("text"); - - b.Property("EstimatedTimeRemainingSeconds") - .HasColumnType("integer"); - - b.Property("FailedBacktests") - .HasColumnType("integer"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("ProgressInfo") - .HasColumnType("text"); - - b.Property("RequestId") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("ResultsJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("Status") - .IsRequired() - .HasColumnType("text"); - - b.Property("TotalBacktests") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("CompletedAt"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("RequestId") - .IsUnique(); - - b.HasIndex("Status"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "CreatedAt"); - - b.ToTable("BundleBacktestRequests"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.FeeEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Cost") - .HasColumnType("decimal(18,8)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Exchange") - .IsRequired() - .HasColumnType("text"); - - b.Property("LastUpdate") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("Exchange") - .IsUnique(); - - b.HasIndex("LastUpdate"); - - b.ToTable("Fees"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.FundingRateEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Date") - .HasColumnType("timestamp with time zone"); - - b.Property("Direction") - .HasColumnType("integer"); - - b.Property("Exchange") - .HasColumnType("integer"); - - b.Property("OpenInterest") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.Property("Rate") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.Property("Ticker") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("Date"); - - b.HasIndex("Direction"); - - b.HasIndex("Exchange"); - - b.HasIndex("Ticker"); - - b.HasIndex("Exchange", "Date"); - - b.HasIndex("Ticker", "Exchange"); - - b.HasIndex("Ticker", "Exchange", "Date") - .IsUnique(); - - b.ToTable("FundingRates"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.GeneticRequestEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Balance") - .HasColumnType("decimal(18,8)"); - - b.Property("BestChromosome") - .HasMaxLength(4000) - .HasColumnType("character varying(4000)"); - - b.Property("BestFitness") - .HasColumnType("double precision"); - - b.Property("BestFitnessSoFar") - .HasColumnType("double precision"); - - b.Property("BestIndividual") - .HasMaxLength(4000) - .HasColumnType("character varying(4000)"); - - b.Property("CompletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CrossoverMethod") - .IsRequired() - .HasColumnType("text"); - - b.Property("CurrentGeneration") - .HasColumnType("integer"); - - b.Property("EligibleIndicatorsJson") - .HasMaxLength(2000) - .HasColumnType("character varying(2000)"); - - b.Property("ElitismPercentage") - .HasColumnType("integer"); - - b.Property("EndDate") - .HasColumnType("timestamp with time zone"); - - b.Property("ErrorMessage") - .HasMaxLength(2000) - .HasColumnType("character varying(2000)"); - - b.Property("Generations") - .HasColumnType("integer"); - - b.Property("MaxTakeProfit") - .HasColumnType("double precision"); - - b.Property("MutationMethod") - .IsRequired() - .HasColumnType("text"); - - b.Property("MutationRate") - .HasColumnType("double precision"); - - b.Property("PopulationSize") - .HasColumnType("integer"); - - b.Property("ProgressInfo") - .HasMaxLength(4000) - .HasColumnType("character varying(4000)"); - - b.Property("RequestId") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("SelectionMethod") - .IsRequired() - .HasColumnType("text"); - - b.Property("StartDate") - .HasColumnType("timestamp with time zone"); - - b.Property("Status") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("Ticker") - .IsRequired() - .HasColumnType("text"); - - b.Property("Timeframe") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("RequestId") - .IsUnique(); - - b.HasIndex("Status"); - - b.HasIndex("UserId"); - - b.ToTable("GeneticRequests"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CyclePeriods") - .HasColumnType("integer"); - - b.Property("FastPeriods") - .HasColumnType("integer"); - - b.Property("MinimumHistory") - .HasColumnType("integer"); - - b.Property("Multiplier") - .HasColumnType("double precision"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Period") - .HasColumnType("integer"); - - b.Property("SignalPeriods") - .HasColumnType("integer"); - - b.Property("SignalType") - .IsRequired() - .HasColumnType("text"); - - b.Property("SlowPeriods") - .HasColumnType("integer"); - - b.Property("SmoothPeriods") - .HasColumnType("integer"); - - b.Property("StochPeriods") - .HasColumnType("integer"); - - b.Property("Timeframe") - .IsRequired() - .HasColumnType("text"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("Name"); - - b.HasIndex("Type"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "Name"); - - b.ToTable("Indicators"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.MoneyManagementEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Leverage") - .HasColumnType("decimal(18,8)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("StopLoss") - .HasColumnType("decimal(18,8)"); - - b.Property("TakeProfit") - .HasColumnType("decimal(18,8)"); - - b.Property("Timeframe") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("Name"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "Name"); - - b.ToTable("MoneyManagements"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AccountName") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Date") - .HasColumnType("timestamp with time zone"); - - b.Property("Identifier") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Initiator") - .IsRequired() - .HasColumnType("text"); - - b.Property("MoneyManagementJson") - .HasColumnType("text"); - - b.Property("OpenTradeId") - .HasColumnType("integer"); - - b.Property("OriginDirection") - .IsRequired() - .HasColumnType("text"); - - b.Property("ProfitAndLoss") - .HasColumnType("decimal(18,8)"); - - b.Property("SignalIdentifier") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Status") - .IsRequired() - .HasColumnType("text"); - - b.Property("StopLossTradeId") - .HasColumnType("integer"); - - b.Property("TakeProfit1TradeId") - .HasColumnType("integer"); - - b.Property("TakeProfit2TradeId") - .HasColumnType("integer"); - - b.Property("Ticker") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("Date"); - - b.HasIndex("Identifier") - .IsUnique(); - - b.HasIndex("Initiator"); - - b.HasIndex("OpenTradeId"); - - b.HasIndex("Status"); - - b.HasIndex("StopLossTradeId"); - - b.HasIndex("TakeProfit1TradeId"); - - b.HasIndex("TakeProfit2TradeId"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "Identifier"); - - b.ToTable("Positions"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("LoopbackPeriod") - .HasColumnType("integer"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("Name"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "Name"); - - b.ToTable("Scenarios"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioIndicatorEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("IndicatorId") - .HasColumnType("integer"); - - b.Property("ScenarioId") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("IndicatorId"); - - b.HasIndex("ScenarioId"); - - b.HasIndex("ScenarioId", "IndicatorId") - .IsUnique(); - - b.ToTable("ScenarioIndicators"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SignalEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CandleJson") - .HasColumnType("text"); - - b.Property("Confidence") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Date") - .HasColumnType("timestamp with time zone"); - - b.Property("Direction") - .IsRequired() - .HasColumnType("text"); - - b.Property("Identifier") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("IndicatorName") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("SignalType") - .IsRequired() - .HasColumnType("text"); - - b.Property("Status") - .IsRequired() - .HasColumnType("text"); - - b.Property("Ticker") - .IsRequired() - .HasColumnType("text"); - - b.Property("Timeframe") - .IsRequired() - .HasColumnType("text"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("Date"); - - b.HasIndex("Identifier"); - - b.HasIndex("Status"); - - b.HasIndex("Ticker"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "Date"); - - b.HasIndex("Identifier", "Date", "UserName") - .IsUnique(); - - b.ToTable("Signals"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SpotlightOverviewEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DateTime") - .HasColumnType("timestamp with time zone"); - - b.Property("Identifier") - .HasColumnType("uuid"); - - b.Property("ScenarioCount") - .HasColumnType("integer"); - - b.Property("SpotlightsJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("DateTime"); - - b.HasIndex("Identifier") - .IsUnique(); - - b.HasIndex("ScenarioCount"); - - b.HasIndex("DateTime", "ScenarioCount"); - - b.ToTable("SpotlightOverviews"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TopVolumeTickerEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Date") - .HasColumnType("timestamp with time zone"); - - b.Property("Exchange") - .HasColumnType("integer"); - - b.Property("Rank") - .HasColumnType("integer"); - - b.Property("Ticker") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Volume") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.HasKey("Id"); - - b.HasIndex("Date"); - - b.HasIndex("Exchange"); - - b.HasIndex("Rank"); - - b.HasIndex("Ticker"); - - b.HasIndex("Date", "Rank"); - - b.HasIndex("Exchange", "Date"); - - b.ToTable("TopVolumeTickers"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Date") - .HasColumnType("timestamp with time zone"); - - b.Property("Direction") - .IsRequired() - .HasColumnType("text"); - - b.Property("ExchangeOrderId") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Fee") - .HasColumnType("decimal(18,8)"); - - b.Property("Leverage") - .HasColumnType("decimal(18,8)"); - - b.Property("Message") - .HasColumnType("text"); - - b.Property("Price") - .HasColumnType("decimal(18,8)"); - - b.Property("Quantity") - .HasColumnType("decimal(18,8)"); - - b.Property("Status") - .IsRequired() - .HasColumnType("text"); - - b.Property("Ticker") - .IsRequired() - .HasColumnType("text"); - - b.Property("TradeType") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("Date"); - - b.HasIndex("ExchangeOrderId"); - - b.HasIndex("Status"); - - b.ToTable("Trades"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TraderEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Address") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("AverageLoss") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.Property("AverageWin") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("IsBestTrader") - .HasColumnType("boolean"); - - b.Property("Pnl") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.Property("Roi") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.Property("TradeCount") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Winrate") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("Address"); - - b.HasIndex("IsBestTrader"); - - b.HasIndex("Pnl"); - - b.HasIndex("Roi"); - - b.HasIndex("Winrate"); - - b.HasIndex("Address", "IsBestTrader") - .IsUnique(); - - b.HasIndex("IsBestTrader", "Roi"); - - b.HasIndex("IsBestTrader", "Winrate"); - - b.ToTable("Traders"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AgentName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("AvatarUrl") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("TelegramChannel") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.ToTable("Users"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AccountEntity", b => - { - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.SetNull); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.GeneticRequestEntity", b => - { - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.SetNull); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => - { - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "OpenTrade") - .WithMany() - .HasForeignKey("OpenTradeId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "StopLossTrade") - .WithMany() - .HasForeignKey("StopLossTradeId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "TakeProfit1Trade") - .WithMany() - .HasForeignKey("TakeProfit1TradeId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "TakeProfit2Trade") - .WithMany() - .HasForeignKey("TakeProfit2TradeId") - .OnDelete(DeleteBehavior.SetNull); - - b.Navigation("OpenTrade"); - - b.Navigation("StopLossTrade"); - - b.Navigation("TakeProfit1Trade"); - - b.Navigation("TakeProfit2Trade"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioIndicatorEntity", b => - { - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", "Indicator") - .WithMany("ScenarioIndicators") - .HasForeignKey("IndicatorId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", "Scenario") - .WithMany("ScenarioIndicators") - .HasForeignKey("ScenarioId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Indicator"); - - b.Navigation("Scenario"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => - { - b.Navigation("ScenarioIndicators"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => - { - b.Navigation("ScenarioIndicators"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/Managing.Infrastructure.Database/Migrations/20250724141819_AddWorkerEntity.Designer.cs b/src/Managing.Infrastructure.Database/Migrations/20250724141819_AddWorkerEntity.Designer.cs deleted file mode 100644 index c4e11c9..0000000 --- a/src/Managing.Infrastructure.Database/Migrations/20250724141819_AddWorkerEntity.Designer.cs +++ /dev/null @@ -1,1259 +0,0 @@ -īģŋ// -using System; -using Managing.Infrastructure.Databases.PostgreSql; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Managing.Infrastructure.Databases.Migrations -{ - [DbContext(typeof(ManagingDbContext))] - [Migration("20250724141819_AddWorkerEntity")] - partial class AddWorkerEntity - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.11") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AccountEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Exchange") - .IsRequired() - .HasColumnType("text"); - - b.Property("Key") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Secret") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.Property("UserId") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("Key"); - - b.HasIndex("Name") - .IsUnique(); - - b.HasIndex("UserId"); - - b.ToTable("Accounts"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BacktestEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ConfigJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("EndDate") - .HasColumnType("timestamp with time zone"); - - b.Property("Fees") - .HasColumnType("decimal(18,8)"); - - b.Property("FinalPnl") - .HasColumnType("decimal(18,8)"); - - b.Property("GrowthPercentage") - .HasColumnType("decimal(18,8)"); - - b.Property("HodlPercentage") - .HasColumnType("decimal(18,8)"); - - b.Property("Identifier") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Metadata") - .HasColumnType("text"); - - b.Property("MoneyManagementJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("OptimizedMoneyManagementJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("PositionsJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("RequestId") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Score") - .HasColumnType("double precision"); - - b.Property("ScoreMessage") - .IsRequired() - .HasMaxLength(1000) - .HasColumnType("character varying(1000)"); - - b.Property("SignalsJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("StartDate") - .HasColumnType("timestamp with time zone"); - - b.Property("StatisticsJson") - .HasColumnType("jsonb"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("WinRate") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("EndDate"); - - b.HasIndex("FinalPnl"); - - b.HasIndex("Identifier") - .IsUnique(); - - b.HasIndex("RequestId"); - - b.HasIndex("Score"); - - b.HasIndex("StartDate"); - - b.HasIndex("UserName"); - - b.HasIndex("WinRate"); - - b.HasIndex("RequestId", "Score"); - - b.HasIndex("UserName", "Score"); - - b.ToTable("Backtests"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotBackupEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreateDate") - .HasColumnType("timestamp with time zone"); - - b.Property("Data") - .IsRequired() - .HasColumnType("text"); - - b.Property("Identifier") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("LastStatus") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("CreateDate"); - - b.HasIndex("Identifier") - .IsUnique(); - - b.HasIndex("LastStatus"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "CreateDate"); - - b.ToTable("BotBackups"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BundleBacktestRequestEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("BacktestRequestsJson") - .IsRequired() - .HasColumnType("text"); - - b.Property("CompletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CompletedBacktests") - .HasColumnType("integer"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CurrentBacktest") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("ErrorMessage") - .HasColumnType("text"); - - b.Property("EstimatedTimeRemainingSeconds") - .HasColumnType("integer"); - - b.Property("FailedBacktests") - .HasColumnType("integer"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("ProgressInfo") - .HasColumnType("text"); - - b.Property("RequestId") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("ResultsJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("Status") - .IsRequired() - .HasColumnType("text"); - - b.Property("TotalBacktests") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("CompletedAt"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("RequestId") - .IsUnique(); - - b.HasIndex("Status"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "CreatedAt"); - - b.ToTable("BundleBacktestRequests"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.FeeEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Cost") - .HasColumnType("decimal(18,8)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Exchange") - .IsRequired() - .HasColumnType("text"); - - b.Property("LastUpdate") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("Exchange") - .IsUnique(); - - b.HasIndex("LastUpdate"); - - b.ToTable("Fees"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.FundingRateEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Date") - .HasColumnType("timestamp with time zone"); - - b.Property("Direction") - .HasColumnType("integer"); - - b.Property("Exchange") - .HasColumnType("integer"); - - b.Property("OpenInterest") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.Property("Rate") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.Property("Ticker") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("Date"); - - b.HasIndex("Direction"); - - b.HasIndex("Exchange"); - - b.HasIndex("Ticker"); - - b.HasIndex("Exchange", "Date"); - - b.HasIndex("Ticker", "Exchange"); - - b.HasIndex("Ticker", "Exchange", "Date") - .IsUnique(); - - b.ToTable("FundingRates"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.GeneticRequestEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Balance") - .HasColumnType("decimal(18,8)"); - - b.Property("BestChromosome") - .HasMaxLength(4000) - .HasColumnType("character varying(4000)"); - - b.Property("BestFitness") - .HasColumnType("double precision"); - - b.Property("BestFitnessSoFar") - .HasColumnType("double precision"); - - b.Property("BestIndividual") - .HasMaxLength(4000) - .HasColumnType("character varying(4000)"); - - b.Property("CompletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CrossoverMethod") - .IsRequired() - .HasColumnType("text"); - - b.Property("CurrentGeneration") - .HasColumnType("integer"); - - b.Property("EligibleIndicatorsJson") - .HasMaxLength(2000) - .HasColumnType("character varying(2000)"); - - b.Property("ElitismPercentage") - .HasColumnType("integer"); - - b.Property("EndDate") - .HasColumnType("timestamp with time zone"); - - b.Property("ErrorMessage") - .HasMaxLength(2000) - .HasColumnType("character varying(2000)"); - - b.Property("Generations") - .HasColumnType("integer"); - - b.Property("MaxTakeProfit") - .HasColumnType("double precision"); - - b.Property("MutationMethod") - .IsRequired() - .HasColumnType("text"); - - b.Property("MutationRate") - .HasColumnType("double precision"); - - b.Property("PopulationSize") - .HasColumnType("integer"); - - b.Property("ProgressInfo") - .HasMaxLength(4000) - .HasColumnType("character varying(4000)"); - - b.Property("RequestId") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("SelectionMethod") - .IsRequired() - .HasColumnType("text"); - - b.Property("StartDate") - .HasColumnType("timestamp with time zone"); - - b.Property("Status") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("Ticker") - .IsRequired() - .HasColumnType("text"); - - b.Property("Timeframe") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("RequestId") - .IsUnique(); - - b.HasIndex("Status"); - - b.HasIndex("UserId"); - - b.ToTable("GeneticRequests"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CyclePeriods") - .HasColumnType("integer"); - - b.Property("FastPeriods") - .HasColumnType("integer"); - - b.Property("MinimumHistory") - .HasColumnType("integer"); - - b.Property("Multiplier") - .HasColumnType("double precision"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Period") - .HasColumnType("integer"); - - b.Property("SignalPeriods") - .HasColumnType("integer"); - - b.Property("SignalType") - .IsRequired() - .HasColumnType("text"); - - b.Property("SlowPeriods") - .HasColumnType("integer"); - - b.Property("SmoothPeriods") - .HasColumnType("integer"); - - b.Property("StochPeriods") - .HasColumnType("integer"); - - b.Property("Timeframe") - .IsRequired() - .HasColumnType("text"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("Name"); - - b.HasIndex("Type"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "Name"); - - b.ToTable("Indicators"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.MoneyManagementEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Leverage") - .HasColumnType("decimal(18,8)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("StopLoss") - .HasColumnType("decimal(18,8)"); - - b.Property("TakeProfit") - .HasColumnType("decimal(18,8)"); - - b.Property("Timeframe") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("Name"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "Name"); - - b.ToTable("MoneyManagements"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AccountName") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Date") - .HasColumnType("timestamp with time zone"); - - b.Property("Identifier") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Initiator") - .IsRequired() - .HasColumnType("text"); - - b.Property("MoneyManagementJson") - .HasColumnType("text"); - - b.Property("OpenTradeId") - .HasColumnType("integer"); - - b.Property("OriginDirection") - .IsRequired() - .HasColumnType("text"); - - b.Property("ProfitAndLoss") - .HasColumnType("decimal(18,8)"); - - b.Property("SignalIdentifier") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Status") - .IsRequired() - .HasColumnType("text"); - - b.Property("StopLossTradeId") - .HasColumnType("integer"); - - b.Property("TakeProfit1TradeId") - .HasColumnType("integer"); - - b.Property("TakeProfit2TradeId") - .HasColumnType("integer"); - - b.Property("Ticker") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("Date"); - - b.HasIndex("Identifier") - .IsUnique(); - - b.HasIndex("Initiator"); - - b.HasIndex("OpenTradeId"); - - b.HasIndex("Status"); - - b.HasIndex("StopLossTradeId"); - - b.HasIndex("TakeProfit1TradeId"); - - b.HasIndex("TakeProfit2TradeId"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "Identifier"); - - b.ToTable("Positions"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("LoopbackPeriod") - .HasColumnType("integer"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("Name"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "Name"); - - b.ToTable("Scenarios"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioIndicatorEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("IndicatorId") - .HasColumnType("integer"); - - b.Property("ScenarioId") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("IndicatorId"); - - b.HasIndex("ScenarioId"); - - b.HasIndex("ScenarioId", "IndicatorId") - .IsUnique(); - - b.ToTable("ScenarioIndicators"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SignalEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CandleJson") - .HasColumnType("text"); - - b.Property("Confidence") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Date") - .HasColumnType("timestamp with time zone"); - - b.Property("Direction") - .IsRequired() - .HasColumnType("text"); - - b.Property("Identifier") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("IndicatorName") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("SignalType") - .IsRequired() - .HasColumnType("text"); - - b.Property("Status") - .IsRequired() - .HasColumnType("text"); - - b.Property("Ticker") - .IsRequired() - .HasColumnType("text"); - - b.Property("Timeframe") - .IsRequired() - .HasColumnType("text"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("Date"); - - b.HasIndex("Identifier"); - - b.HasIndex("Status"); - - b.HasIndex("Ticker"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "Date"); - - b.HasIndex("Identifier", "Date", "UserName") - .IsUnique(); - - b.ToTable("Signals"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SpotlightOverviewEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DateTime") - .HasColumnType("timestamp with time zone"); - - b.Property("Identifier") - .HasColumnType("uuid"); - - b.Property("ScenarioCount") - .HasColumnType("integer"); - - b.Property("SpotlightsJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("DateTime"); - - b.HasIndex("Identifier") - .IsUnique(); - - b.HasIndex("ScenarioCount"); - - b.HasIndex("DateTime", "ScenarioCount"); - - b.ToTable("SpotlightOverviews"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TopVolumeTickerEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Date") - .HasColumnType("timestamp with time zone"); - - b.Property("Exchange") - .HasColumnType("integer"); - - b.Property("Rank") - .HasColumnType("integer"); - - b.Property("Ticker") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Volume") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.HasKey("Id"); - - b.HasIndex("Date"); - - b.HasIndex("Exchange"); - - b.HasIndex("Rank"); - - b.HasIndex("Ticker"); - - b.HasIndex("Date", "Rank"); - - b.HasIndex("Exchange", "Date"); - - b.ToTable("TopVolumeTickers"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Date") - .HasColumnType("timestamp with time zone"); - - b.Property("Direction") - .IsRequired() - .HasColumnType("text"); - - b.Property("ExchangeOrderId") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Fee") - .HasColumnType("decimal(18,8)"); - - b.Property("Leverage") - .HasColumnType("decimal(18,8)"); - - b.Property("Message") - .HasColumnType("text"); - - b.Property("Price") - .HasColumnType("decimal(18,8)"); - - b.Property("Quantity") - .HasColumnType("decimal(18,8)"); - - b.Property("Status") - .IsRequired() - .HasColumnType("text"); - - b.Property("Ticker") - .IsRequired() - .HasColumnType("text"); - - b.Property("TradeType") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("Date"); - - b.HasIndex("ExchangeOrderId"); - - b.HasIndex("Status"); - - b.ToTable("Trades"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TraderEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Address") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("AverageLoss") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.Property("AverageWin") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("IsBestTrader") - .HasColumnType("boolean"); - - b.Property("Pnl") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.Property("Roi") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.Property("TradeCount") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Winrate") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("Address"); - - b.HasIndex("IsBestTrader"); - - b.HasIndex("Pnl"); - - b.HasIndex("Roi"); - - b.HasIndex("Winrate"); - - b.HasIndex("Address", "IsBestTrader") - .IsUnique(); - - b.HasIndex("IsBestTrader", "Roi"); - - b.HasIndex("IsBestTrader", "Winrate"); - - b.ToTable("Traders"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AgentName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("AvatarUrl") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("TelegramChannel") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.ToTable("Users"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.WorkerEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("DelayTicks") - .HasColumnType("bigint"); - - b.Property("ExecutionCount") - .HasColumnType("integer"); - - b.Property("IsActive") - .HasColumnType("boolean"); - - b.Property("LastRunTime") - .HasColumnType("timestamp with time zone"); - - b.Property("StartTime") - .HasColumnType("timestamp with time zone"); - - b.Property("WorkerType") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("WorkerType") - .IsUnique(); - - b.ToTable("Workers"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AccountEntity", b => - { - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.SetNull); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.GeneticRequestEntity", b => - { - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.SetNull); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => - { - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "OpenTrade") - .WithMany() - .HasForeignKey("OpenTradeId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "StopLossTrade") - .WithMany() - .HasForeignKey("StopLossTradeId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "TakeProfit1Trade") - .WithMany() - .HasForeignKey("TakeProfit1TradeId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "TakeProfit2Trade") - .WithMany() - .HasForeignKey("TakeProfit2TradeId") - .OnDelete(DeleteBehavior.SetNull); - - b.Navigation("OpenTrade"); - - b.Navigation("StopLossTrade"); - - b.Navigation("TakeProfit1Trade"); - - b.Navigation("TakeProfit2Trade"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioIndicatorEntity", b => - { - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", "Indicator") - .WithMany("ScenarioIndicators") - .HasForeignKey("IndicatorId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", "Scenario") - .WithMany("ScenarioIndicators") - .HasForeignKey("ScenarioId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Indicator"); - - b.Navigation("Scenario"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => - { - b.Navigation("ScenarioIndicators"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => - { - b.Navigation("ScenarioIndicators"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/Managing.Infrastructure.Database/Migrations/20250724141819_AddWorkerEntity.cs b/src/Managing.Infrastructure.Database/Migrations/20250724141819_AddWorkerEntity.cs deleted file mode 100644 index a5208f7..0000000 --- a/src/Managing.Infrastructure.Database/Migrations/20250724141819_AddWorkerEntity.cs +++ /dev/null @@ -1,47 +0,0 @@ -īģŋusing System; -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Managing.Infrastructure.Databases.Migrations -{ - /// - public partial class AddWorkerEntity : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Workers", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - WorkerType = table.Column(type: "text", nullable: false), - StartTime = table.Column(type: "timestamp with time zone", nullable: false), - LastRunTime = table.Column(type: "timestamp with time zone", nullable: true), - ExecutionCount = table.Column(type: "integer", nullable: false), - DelayTicks = table.Column(type: "bigint", nullable: false), - IsActive = table.Column(type: "boolean", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Workers", x => x.Id); - }); - - migrationBuilder.CreateIndex( - name: "IX_Workers_WorkerType", - table: "Workers", - column: "WorkerType", - unique: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Workers"); - } - } -} diff --git a/src/Managing.Infrastructure.Database/Migrations/20250724160015_AddSynthEntities.Designer.cs b/src/Managing.Infrastructure.Database/Migrations/20250724160015_AddSynthEntities.Designer.cs deleted file mode 100644 index e3a4b4f..0000000 --- a/src/Managing.Infrastructure.Database/Migrations/20250724160015_AddSynthEntities.Designer.cs +++ /dev/null @@ -1,1349 +0,0 @@ -īģŋ// -using System; -using Managing.Infrastructure.Databases.PostgreSql; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Managing.Infrastructure.Databases.Migrations -{ - [DbContext(typeof(ManagingDbContext))] - [Migration("20250724160015_AddSynthEntities")] - partial class AddSynthEntities - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.11") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AccountEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Exchange") - .IsRequired() - .HasColumnType("text"); - - b.Property("Key") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Secret") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.Property("UserId") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("Key"); - - b.HasIndex("Name") - .IsUnique(); - - b.HasIndex("UserId"); - - b.ToTable("Accounts"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BacktestEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ConfigJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("EndDate") - .HasColumnType("timestamp with time zone"); - - b.Property("Fees") - .HasColumnType("decimal(18,8)"); - - b.Property("FinalPnl") - .HasColumnType("decimal(18,8)"); - - b.Property("GrowthPercentage") - .HasColumnType("decimal(18,8)"); - - b.Property("HodlPercentage") - .HasColumnType("decimal(18,8)"); - - b.Property("Identifier") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Metadata") - .HasColumnType("text"); - - b.Property("MoneyManagementJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("OptimizedMoneyManagementJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("PositionsJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("RequestId") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Score") - .HasColumnType("double precision"); - - b.Property("ScoreMessage") - .IsRequired() - .HasMaxLength(1000) - .HasColumnType("character varying(1000)"); - - b.Property("SignalsJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("StartDate") - .HasColumnType("timestamp with time zone"); - - b.Property("StatisticsJson") - .HasColumnType("jsonb"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("WinRate") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("EndDate"); - - b.HasIndex("FinalPnl"); - - b.HasIndex("Identifier") - .IsUnique(); - - b.HasIndex("RequestId"); - - b.HasIndex("Score"); - - b.HasIndex("StartDate"); - - b.HasIndex("UserName"); - - b.HasIndex("WinRate"); - - b.HasIndex("RequestId", "Score"); - - b.HasIndex("UserName", "Score"); - - b.ToTable("Backtests"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotBackupEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreateDate") - .HasColumnType("timestamp with time zone"); - - b.Property("Data") - .IsRequired() - .HasColumnType("text"); - - b.Property("Identifier") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("LastStatus") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("CreateDate"); - - b.HasIndex("Identifier") - .IsUnique(); - - b.HasIndex("LastStatus"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "CreateDate"); - - b.ToTable("BotBackups"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BundleBacktestRequestEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("BacktestRequestsJson") - .IsRequired() - .HasColumnType("text"); - - b.Property("CompletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CompletedBacktests") - .HasColumnType("integer"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CurrentBacktest") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("ErrorMessage") - .HasColumnType("text"); - - b.Property("EstimatedTimeRemainingSeconds") - .HasColumnType("integer"); - - b.Property("FailedBacktests") - .HasColumnType("integer"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("ProgressInfo") - .HasColumnType("text"); - - b.Property("RequestId") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("ResultsJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("Status") - .IsRequired() - .HasColumnType("text"); - - b.Property("TotalBacktests") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("CompletedAt"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("RequestId") - .IsUnique(); - - b.HasIndex("Status"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "CreatedAt"); - - b.ToTable("BundleBacktestRequests"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.FeeEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Cost") - .HasColumnType("decimal(18,8)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Exchange") - .IsRequired() - .HasColumnType("text"); - - b.Property("LastUpdate") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("Exchange") - .IsUnique(); - - b.HasIndex("LastUpdate"); - - b.ToTable("Fees"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.FundingRateEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Date") - .HasColumnType("timestamp with time zone"); - - b.Property("Direction") - .HasColumnType("integer"); - - b.Property("Exchange") - .HasColumnType("integer"); - - b.Property("OpenInterest") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.Property("Rate") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.Property("Ticker") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("Date"); - - b.HasIndex("Direction"); - - b.HasIndex("Exchange"); - - b.HasIndex("Ticker"); - - b.HasIndex("Exchange", "Date"); - - b.HasIndex("Ticker", "Exchange"); - - b.HasIndex("Ticker", "Exchange", "Date") - .IsUnique(); - - b.ToTable("FundingRates"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.GeneticRequestEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Balance") - .HasColumnType("decimal(18,8)"); - - b.Property("BestChromosome") - .HasMaxLength(4000) - .HasColumnType("character varying(4000)"); - - b.Property("BestFitness") - .HasColumnType("double precision"); - - b.Property("BestFitnessSoFar") - .HasColumnType("double precision"); - - b.Property("BestIndividual") - .HasMaxLength(4000) - .HasColumnType("character varying(4000)"); - - b.Property("CompletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CrossoverMethod") - .IsRequired() - .HasColumnType("text"); - - b.Property("CurrentGeneration") - .HasColumnType("integer"); - - b.Property("EligibleIndicatorsJson") - .HasMaxLength(2000) - .HasColumnType("character varying(2000)"); - - b.Property("ElitismPercentage") - .HasColumnType("integer"); - - b.Property("EndDate") - .HasColumnType("timestamp with time zone"); - - b.Property("ErrorMessage") - .HasMaxLength(2000) - .HasColumnType("character varying(2000)"); - - b.Property("Generations") - .HasColumnType("integer"); - - b.Property("MaxTakeProfit") - .HasColumnType("double precision"); - - b.Property("MutationMethod") - .IsRequired() - .HasColumnType("text"); - - b.Property("MutationRate") - .HasColumnType("double precision"); - - b.Property("PopulationSize") - .HasColumnType("integer"); - - b.Property("ProgressInfo") - .HasMaxLength(4000) - .HasColumnType("character varying(4000)"); - - b.Property("RequestId") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("SelectionMethod") - .IsRequired() - .HasColumnType("text"); - - b.Property("StartDate") - .HasColumnType("timestamp with time zone"); - - b.Property("Status") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("Ticker") - .IsRequired() - .HasColumnType("text"); - - b.Property("Timeframe") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("RequestId") - .IsUnique(); - - b.HasIndex("Status"); - - b.HasIndex("UserId"); - - b.ToTable("GeneticRequests"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CyclePeriods") - .HasColumnType("integer"); - - b.Property("FastPeriods") - .HasColumnType("integer"); - - b.Property("MinimumHistory") - .HasColumnType("integer"); - - b.Property("Multiplier") - .HasColumnType("double precision"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Period") - .HasColumnType("integer"); - - b.Property("SignalPeriods") - .HasColumnType("integer"); - - b.Property("SignalType") - .IsRequired() - .HasColumnType("text"); - - b.Property("SlowPeriods") - .HasColumnType("integer"); - - b.Property("SmoothPeriods") - .HasColumnType("integer"); - - b.Property("StochPeriods") - .HasColumnType("integer"); - - b.Property("Timeframe") - .IsRequired() - .HasColumnType("text"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("Name"); - - b.HasIndex("Type"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "Name"); - - b.ToTable("Indicators"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.MoneyManagementEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Leverage") - .HasColumnType("decimal(18,8)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("StopLoss") - .HasColumnType("decimal(18,8)"); - - b.Property("TakeProfit") - .HasColumnType("decimal(18,8)"); - - b.Property("Timeframe") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("Name"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "Name"); - - b.ToTable("MoneyManagements"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AccountName") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Date") - .HasColumnType("timestamp with time zone"); - - b.Property("Identifier") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Initiator") - .IsRequired() - .HasColumnType("text"); - - b.Property("MoneyManagementJson") - .HasColumnType("text"); - - b.Property("OpenTradeId") - .HasColumnType("integer"); - - b.Property("OriginDirection") - .IsRequired() - .HasColumnType("text"); - - b.Property("ProfitAndLoss") - .HasColumnType("decimal(18,8)"); - - b.Property("SignalIdentifier") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Status") - .IsRequired() - .HasColumnType("text"); - - b.Property("StopLossTradeId") - .HasColumnType("integer"); - - b.Property("TakeProfit1TradeId") - .HasColumnType("integer"); - - b.Property("TakeProfit2TradeId") - .HasColumnType("integer"); - - b.Property("Ticker") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("Date"); - - b.HasIndex("Identifier") - .IsUnique(); - - b.HasIndex("Initiator"); - - b.HasIndex("OpenTradeId"); - - b.HasIndex("Status"); - - b.HasIndex("StopLossTradeId"); - - b.HasIndex("TakeProfit1TradeId"); - - b.HasIndex("TakeProfit2TradeId"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "Identifier"); - - b.ToTable("Positions"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("LoopbackPeriod") - .HasColumnType("integer"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("Name"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "Name"); - - b.ToTable("Scenarios"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioIndicatorEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("IndicatorId") - .HasColumnType("integer"); - - b.Property("ScenarioId") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("IndicatorId"); - - b.HasIndex("ScenarioId"); - - b.HasIndex("ScenarioId", "IndicatorId") - .IsUnique(); - - b.ToTable("ScenarioIndicators"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SignalEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CandleJson") - .HasColumnType("text"); - - b.Property("Confidence") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Date") - .HasColumnType("timestamp with time zone"); - - b.Property("Direction") - .IsRequired() - .HasColumnType("text"); - - b.Property("Identifier") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("IndicatorName") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("SignalType") - .IsRequired() - .HasColumnType("text"); - - b.Property("Status") - .IsRequired() - .HasColumnType("text"); - - b.Property("Ticker") - .IsRequired() - .HasColumnType("text"); - - b.Property("Timeframe") - .IsRequired() - .HasColumnType("text"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("Date"); - - b.HasIndex("Identifier"); - - b.HasIndex("Status"); - - b.HasIndex("Ticker"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "Date"); - - b.HasIndex("Identifier", "Date", "UserName") - .IsUnique(); - - b.ToTable("Signals"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SpotlightOverviewEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DateTime") - .HasColumnType("timestamp with time zone"); - - b.Property("Identifier") - .HasColumnType("uuid"); - - b.Property("ScenarioCount") - .HasColumnType("integer"); - - b.Property("SpotlightsJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("DateTime"); - - b.HasIndex("Identifier") - .IsUnique(); - - b.HasIndex("ScenarioCount"); - - b.HasIndex("DateTime", "ScenarioCount"); - - b.ToTable("SpotlightOverviews"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SynthMinersLeaderboardEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Asset") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("CacheKey") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("IsBacktest") - .HasColumnType("boolean"); - - b.Property("MinersData") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("SignalDate") - .HasColumnType("timestamp with time zone"); - - b.Property("TimeIncrement") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("CacheKey") - .IsUnique(); - - b.HasIndex("CreatedAt"); - - b.ToTable("SynthMinersLeaderboards"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SynthPredictionEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Asset") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("CacheKey") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("IsBacktest") - .HasColumnType("boolean"); - - b.Property("MinerUid") - .HasColumnType("integer"); - - b.Property("PredictionData") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("SignalDate") - .HasColumnType("timestamp with time zone"); - - b.Property("TimeIncrement") - .HasColumnType("integer"); - - b.Property("TimeLength") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("CacheKey") - .IsUnique(); - - b.HasIndex("CreatedAt"); - - b.ToTable("SynthPredictions"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TopVolumeTickerEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Date") - .HasColumnType("timestamp with time zone"); - - b.Property("Exchange") - .HasColumnType("integer"); - - b.Property("Rank") - .HasColumnType("integer"); - - b.Property("Ticker") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Volume") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.HasKey("Id"); - - b.HasIndex("Date"); - - b.HasIndex("Exchange"); - - b.HasIndex("Rank"); - - b.HasIndex("Ticker"); - - b.HasIndex("Date", "Rank"); - - b.HasIndex("Exchange", "Date"); - - b.ToTable("TopVolumeTickers"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Date") - .HasColumnType("timestamp with time zone"); - - b.Property("Direction") - .IsRequired() - .HasColumnType("text"); - - b.Property("ExchangeOrderId") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Fee") - .HasColumnType("decimal(18,8)"); - - b.Property("Leverage") - .HasColumnType("decimal(18,8)"); - - b.Property("Message") - .HasColumnType("text"); - - b.Property("Price") - .HasColumnType("decimal(18,8)"); - - b.Property("Quantity") - .HasColumnType("decimal(18,8)"); - - b.Property("Status") - .IsRequired() - .HasColumnType("text"); - - b.Property("Ticker") - .IsRequired() - .HasColumnType("text"); - - b.Property("TradeType") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("Date"); - - b.HasIndex("ExchangeOrderId"); - - b.HasIndex("Status"); - - b.ToTable("Trades"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TraderEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Address") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("AverageLoss") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.Property("AverageWin") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("IsBestTrader") - .HasColumnType("boolean"); - - b.Property("Pnl") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.Property("Roi") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.Property("TradeCount") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Winrate") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("Address"); - - b.HasIndex("IsBestTrader"); - - b.HasIndex("Pnl"); - - b.HasIndex("Roi"); - - b.HasIndex("Winrate"); - - b.HasIndex("Address", "IsBestTrader") - .IsUnique(); - - b.HasIndex("IsBestTrader", "Roi"); - - b.HasIndex("IsBestTrader", "Winrate"); - - b.ToTable("Traders"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AgentName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("AvatarUrl") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("TelegramChannel") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.ToTable("Users"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.WorkerEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("DelayTicks") - .HasColumnType("bigint"); - - b.Property("ExecutionCount") - .HasColumnType("integer"); - - b.Property("IsActive") - .HasColumnType("boolean"); - - b.Property("LastRunTime") - .HasColumnType("timestamp with time zone"); - - b.Property("StartTime") - .HasColumnType("timestamp with time zone"); - - b.Property("WorkerType") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("WorkerType") - .IsUnique(); - - b.ToTable("Workers"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AccountEntity", b => - { - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.SetNull); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.GeneticRequestEntity", b => - { - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.SetNull); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => - { - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "OpenTrade") - .WithMany() - .HasForeignKey("OpenTradeId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "StopLossTrade") - .WithMany() - .HasForeignKey("StopLossTradeId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "TakeProfit1Trade") - .WithMany() - .HasForeignKey("TakeProfit1TradeId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "TakeProfit2Trade") - .WithMany() - .HasForeignKey("TakeProfit2TradeId") - .OnDelete(DeleteBehavior.SetNull); - - b.Navigation("OpenTrade"); - - b.Navigation("StopLossTrade"); - - b.Navigation("TakeProfit1Trade"); - - b.Navigation("TakeProfit2Trade"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioIndicatorEntity", b => - { - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", "Indicator") - .WithMany("ScenarioIndicators") - .HasForeignKey("IndicatorId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", "Scenario") - .WithMany("ScenarioIndicators") - .HasForeignKey("ScenarioId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Indicator"); - - b.Navigation("Scenario"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => - { - b.Navigation("ScenarioIndicators"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => - { - b.Navigation("ScenarioIndicators"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/Managing.Infrastructure.Database/Migrations/20250724160015_AddSynthEntities.cs b/src/Managing.Infrastructure.Database/Migrations/20250724160015_AddSynthEntities.cs deleted file mode 100644 index 3ded012..0000000 --- a/src/Managing.Infrastructure.Database/Migrations/20250724160015_AddSynthEntities.cs +++ /dev/null @@ -1,85 +0,0 @@ -īģŋusing System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Managing.Infrastructure.Databases.Migrations -{ - /// - public partial class AddSynthEntities : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "SynthMinersLeaderboards", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Asset = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), - TimeIncrement = table.Column(type: "integer", nullable: false), - SignalDate = table.Column(type: "timestamp with time zone", nullable: true), - IsBacktest = table.Column(type: "boolean", nullable: false), - MinersData = table.Column(type: "jsonb", nullable: false), - CacheKey = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_SynthMinersLeaderboards", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "SynthPredictions", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Asset = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), - MinerUid = table.Column(type: "integer", nullable: false), - TimeIncrement = table.Column(type: "integer", nullable: false), - TimeLength = table.Column(type: "integer", nullable: false), - SignalDate = table.Column(type: "timestamp with time zone", nullable: true), - IsBacktest = table.Column(type: "boolean", nullable: false), - PredictionData = table.Column(type: "jsonb", nullable: false), - CacheKey = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_SynthPredictions", x => x.Id); - }); - - migrationBuilder.CreateIndex( - name: "IX_SynthMinersLeaderboards_CacheKey", - table: "SynthMinersLeaderboards", - column: "CacheKey", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_SynthMinersLeaderboards_CreatedAt", - table: "SynthMinersLeaderboards", - column: "CreatedAt"); - - migrationBuilder.CreateIndex( - name: "IX_SynthPredictions_CacheKey", - table: "SynthPredictions", - column: "CacheKey", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_SynthPredictions_CreatedAt", - table: "SynthPredictions", - column: "CreatedAt"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "SynthMinersLeaderboards"); - - migrationBuilder.DropTable( - name: "SynthPredictions"); - } - } -} diff --git a/src/Managing.Infrastructure.Database/Migrations/20250724164138_UpdateScoreMessageToText.Designer.cs b/src/Managing.Infrastructure.Database/Migrations/20250724164138_UpdateScoreMessageToText.Designer.cs deleted file mode 100644 index f1152b7..0000000 --- a/src/Managing.Infrastructure.Database/Migrations/20250724164138_UpdateScoreMessageToText.Designer.cs +++ /dev/null @@ -1,1349 +0,0 @@ -īģŋ// -using System; -using Managing.Infrastructure.Databases.PostgreSql; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Managing.Infrastructure.Databases.Migrations -{ - [DbContext(typeof(ManagingDbContext))] - [Migration("20250724164138_UpdateScoreMessageToText")] - partial class UpdateScoreMessageToText - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.11") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AccountEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Exchange") - .IsRequired() - .HasColumnType("text"); - - b.Property("Key") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Secret") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.Property("UserId") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("Key"); - - b.HasIndex("Name") - .IsUnique(); - - b.HasIndex("UserId"); - - b.ToTable("Accounts"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BacktestEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ConfigJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("EndDate") - .HasColumnType("timestamp with time zone"); - - b.Property("Fees") - .HasColumnType("decimal(18,8)"); - - b.Property("FinalPnl") - .HasColumnType("decimal(18,8)"); - - b.Property("GrowthPercentage") - .HasColumnType("decimal(18,8)"); - - b.Property("HodlPercentage") - .HasColumnType("decimal(18,8)"); - - b.Property("Identifier") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Metadata") - .HasColumnType("text"); - - b.Property("MoneyManagementJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("OptimizedMoneyManagementJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("PositionsJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("RequestId") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Score") - .HasColumnType("double precision"); - - b.Property("ScoreMessage") - .IsRequired() - .HasMaxLength(1000) - .HasColumnType("text"); - - b.Property("SignalsJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("StartDate") - .HasColumnType("timestamp with time zone"); - - b.Property("StatisticsJson") - .HasColumnType("jsonb"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("WinRate") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("EndDate"); - - b.HasIndex("FinalPnl"); - - b.HasIndex("Identifier") - .IsUnique(); - - b.HasIndex("RequestId"); - - b.HasIndex("Score"); - - b.HasIndex("StartDate"); - - b.HasIndex("UserName"); - - b.HasIndex("WinRate"); - - b.HasIndex("RequestId", "Score"); - - b.HasIndex("UserName", "Score"); - - b.ToTable("Backtests"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotBackupEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreateDate") - .HasColumnType("timestamp with time zone"); - - b.Property("Data") - .IsRequired() - .HasColumnType("text"); - - b.Property("Identifier") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("LastStatus") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("CreateDate"); - - b.HasIndex("Identifier") - .IsUnique(); - - b.HasIndex("LastStatus"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "CreateDate"); - - b.ToTable("BotBackups"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BundleBacktestRequestEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("BacktestRequestsJson") - .IsRequired() - .HasColumnType("text"); - - b.Property("CompletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CompletedBacktests") - .HasColumnType("integer"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CurrentBacktest") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("ErrorMessage") - .HasColumnType("text"); - - b.Property("EstimatedTimeRemainingSeconds") - .HasColumnType("integer"); - - b.Property("FailedBacktests") - .HasColumnType("integer"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("ProgressInfo") - .HasColumnType("text"); - - b.Property("RequestId") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("ResultsJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("Status") - .IsRequired() - .HasColumnType("text"); - - b.Property("TotalBacktests") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("CompletedAt"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("RequestId") - .IsUnique(); - - b.HasIndex("Status"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "CreatedAt"); - - b.ToTable("BundleBacktestRequests"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.FeeEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Cost") - .HasColumnType("decimal(18,8)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Exchange") - .IsRequired() - .HasColumnType("text"); - - b.Property("LastUpdate") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("Exchange") - .IsUnique(); - - b.HasIndex("LastUpdate"); - - b.ToTable("Fees"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.FundingRateEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Date") - .HasColumnType("timestamp with time zone"); - - b.Property("Direction") - .HasColumnType("integer"); - - b.Property("Exchange") - .HasColumnType("integer"); - - b.Property("OpenInterest") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.Property("Rate") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.Property("Ticker") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("Date"); - - b.HasIndex("Direction"); - - b.HasIndex("Exchange"); - - b.HasIndex("Ticker"); - - b.HasIndex("Exchange", "Date"); - - b.HasIndex("Ticker", "Exchange"); - - b.HasIndex("Ticker", "Exchange", "Date") - .IsUnique(); - - b.ToTable("FundingRates"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.GeneticRequestEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Balance") - .HasColumnType("decimal(18,8)"); - - b.Property("BestChromosome") - .HasMaxLength(4000) - .HasColumnType("character varying(4000)"); - - b.Property("BestFitness") - .HasColumnType("double precision"); - - b.Property("BestFitnessSoFar") - .HasColumnType("double precision"); - - b.Property("BestIndividual") - .HasMaxLength(4000) - .HasColumnType("character varying(4000)"); - - b.Property("CompletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CrossoverMethod") - .IsRequired() - .HasColumnType("text"); - - b.Property("CurrentGeneration") - .HasColumnType("integer"); - - b.Property("EligibleIndicatorsJson") - .HasMaxLength(2000) - .HasColumnType("character varying(2000)"); - - b.Property("ElitismPercentage") - .HasColumnType("integer"); - - b.Property("EndDate") - .HasColumnType("timestamp with time zone"); - - b.Property("ErrorMessage") - .HasMaxLength(2000) - .HasColumnType("character varying(2000)"); - - b.Property("Generations") - .HasColumnType("integer"); - - b.Property("MaxTakeProfit") - .HasColumnType("double precision"); - - b.Property("MutationMethod") - .IsRequired() - .HasColumnType("text"); - - b.Property("MutationRate") - .HasColumnType("double precision"); - - b.Property("PopulationSize") - .HasColumnType("integer"); - - b.Property("ProgressInfo") - .HasMaxLength(4000) - .HasColumnType("character varying(4000)"); - - b.Property("RequestId") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("SelectionMethod") - .IsRequired() - .HasColumnType("text"); - - b.Property("StartDate") - .HasColumnType("timestamp with time zone"); - - b.Property("Status") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("Ticker") - .IsRequired() - .HasColumnType("text"); - - b.Property("Timeframe") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("RequestId") - .IsUnique(); - - b.HasIndex("Status"); - - b.HasIndex("UserId"); - - b.ToTable("GeneticRequests"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CyclePeriods") - .HasColumnType("integer"); - - b.Property("FastPeriods") - .HasColumnType("integer"); - - b.Property("MinimumHistory") - .HasColumnType("integer"); - - b.Property("Multiplier") - .HasColumnType("double precision"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Period") - .HasColumnType("integer"); - - b.Property("SignalPeriods") - .HasColumnType("integer"); - - b.Property("SignalType") - .IsRequired() - .HasColumnType("text"); - - b.Property("SlowPeriods") - .HasColumnType("integer"); - - b.Property("SmoothPeriods") - .HasColumnType("integer"); - - b.Property("StochPeriods") - .HasColumnType("integer"); - - b.Property("Timeframe") - .IsRequired() - .HasColumnType("text"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("Name"); - - b.HasIndex("Type"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "Name"); - - b.ToTable("Indicators"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.MoneyManagementEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Leverage") - .HasColumnType("decimal(18,8)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("StopLoss") - .HasColumnType("decimal(18,8)"); - - b.Property("TakeProfit") - .HasColumnType("decimal(18,8)"); - - b.Property("Timeframe") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("Name"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "Name"); - - b.ToTable("MoneyManagements"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AccountName") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Date") - .HasColumnType("timestamp with time zone"); - - b.Property("Identifier") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Initiator") - .IsRequired() - .HasColumnType("text"); - - b.Property("MoneyManagementJson") - .HasColumnType("text"); - - b.Property("OpenTradeId") - .HasColumnType("integer"); - - b.Property("OriginDirection") - .IsRequired() - .HasColumnType("text"); - - b.Property("ProfitAndLoss") - .HasColumnType("decimal(18,8)"); - - b.Property("SignalIdentifier") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Status") - .IsRequired() - .HasColumnType("text"); - - b.Property("StopLossTradeId") - .HasColumnType("integer"); - - b.Property("TakeProfit1TradeId") - .HasColumnType("integer"); - - b.Property("TakeProfit2TradeId") - .HasColumnType("integer"); - - b.Property("Ticker") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("Date"); - - b.HasIndex("Identifier") - .IsUnique(); - - b.HasIndex("Initiator"); - - b.HasIndex("OpenTradeId"); - - b.HasIndex("Status"); - - b.HasIndex("StopLossTradeId"); - - b.HasIndex("TakeProfit1TradeId"); - - b.HasIndex("TakeProfit2TradeId"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "Identifier"); - - b.ToTable("Positions"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("LoopbackPeriod") - .HasColumnType("integer"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("Name"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "Name"); - - b.ToTable("Scenarios"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioIndicatorEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("IndicatorId") - .HasColumnType("integer"); - - b.Property("ScenarioId") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("IndicatorId"); - - b.HasIndex("ScenarioId"); - - b.HasIndex("ScenarioId", "IndicatorId") - .IsUnique(); - - b.ToTable("ScenarioIndicators"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SignalEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CandleJson") - .HasColumnType("text"); - - b.Property("Confidence") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Date") - .HasColumnType("timestamp with time zone"); - - b.Property("Direction") - .IsRequired() - .HasColumnType("text"); - - b.Property("Identifier") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("IndicatorName") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("SignalType") - .IsRequired() - .HasColumnType("text"); - - b.Property("Status") - .IsRequired() - .HasColumnType("text"); - - b.Property("Ticker") - .IsRequired() - .HasColumnType("text"); - - b.Property("Timeframe") - .IsRequired() - .HasColumnType("text"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("Date"); - - b.HasIndex("Identifier"); - - b.HasIndex("Status"); - - b.HasIndex("Ticker"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "Date"); - - b.HasIndex("Identifier", "Date", "UserName") - .IsUnique(); - - b.ToTable("Signals"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SpotlightOverviewEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DateTime") - .HasColumnType("timestamp with time zone"); - - b.Property("Identifier") - .HasColumnType("uuid"); - - b.Property("ScenarioCount") - .HasColumnType("integer"); - - b.Property("SpotlightsJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("DateTime"); - - b.HasIndex("Identifier") - .IsUnique(); - - b.HasIndex("ScenarioCount"); - - b.HasIndex("DateTime", "ScenarioCount"); - - b.ToTable("SpotlightOverviews"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SynthMinersLeaderboardEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Asset") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("CacheKey") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("IsBacktest") - .HasColumnType("boolean"); - - b.Property("MinersData") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("SignalDate") - .HasColumnType("timestamp with time zone"); - - b.Property("TimeIncrement") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("CacheKey") - .IsUnique(); - - b.HasIndex("CreatedAt"); - - b.ToTable("SynthMinersLeaderboards"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SynthPredictionEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Asset") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("CacheKey") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("IsBacktest") - .HasColumnType("boolean"); - - b.Property("MinerUid") - .HasColumnType("integer"); - - b.Property("PredictionData") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("SignalDate") - .HasColumnType("timestamp with time zone"); - - b.Property("TimeIncrement") - .HasColumnType("integer"); - - b.Property("TimeLength") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("CacheKey") - .IsUnique(); - - b.HasIndex("CreatedAt"); - - b.ToTable("SynthPredictions"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TopVolumeTickerEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Date") - .HasColumnType("timestamp with time zone"); - - b.Property("Exchange") - .HasColumnType("integer"); - - b.Property("Rank") - .HasColumnType("integer"); - - b.Property("Ticker") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Volume") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.HasKey("Id"); - - b.HasIndex("Date"); - - b.HasIndex("Exchange"); - - b.HasIndex("Rank"); - - b.HasIndex("Ticker"); - - b.HasIndex("Date", "Rank"); - - b.HasIndex("Exchange", "Date"); - - b.ToTable("TopVolumeTickers"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Date") - .HasColumnType("timestamp with time zone"); - - b.Property("Direction") - .IsRequired() - .HasColumnType("text"); - - b.Property("ExchangeOrderId") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Fee") - .HasColumnType("decimal(18,8)"); - - b.Property("Leverage") - .HasColumnType("decimal(18,8)"); - - b.Property("Message") - .HasColumnType("text"); - - b.Property("Price") - .HasColumnType("decimal(18,8)"); - - b.Property("Quantity") - .HasColumnType("decimal(18,8)"); - - b.Property("Status") - .IsRequired() - .HasColumnType("text"); - - b.Property("Ticker") - .IsRequired() - .HasColumnType("text"); - - b.Property("TradeType") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("Date"); - - b.HasIndex("ExchangeOrderId"); - - b.HasIndex("Status"); - - b.ToTable("Trades"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TraderEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Address") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("AverageLoss") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.Property("AverageWin") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("IsBestTrader") - .HasColumnType("boolean"); - - b.Property("Pnl") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.Property("Roi") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.Property("TradeCount") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Winrate") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("Address"); - - b.HasIndex("IsBestTrader"); - - b.HasIndex("Pnl"); - - b.HasIndex("Roi"); - - b.HasIndex("Winrate"); - - b.HasIndex("Address", "IsBestTrader") - .IsUnique(); - - b.HasIndex("IsBestTrader", "Roi"); - - b.HasIndex("IsBestTrader", "Winrate"); - - b.ToTable("Traders"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AgentName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("AvatarUrl") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("TelegramChannel") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.ToTable("Users"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.WorkerEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("DelayTicks") - .HasColumnType("bigint"); - - b.Property("ExecutionCount") - .HasColumnType("integer"); - - b.Property("IsActive") - .HasColumnType("boolean"); - - b.Property("LastRunTime") - .HasColumnType("timestamp with time zone"); - - b.Property("StartTime") - .HasColumnType("timestamp with time zone"); - - b.Property("WorkerType") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("WorkerType") - .IsUnique(); - - b.ToTable("Workers"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AccountEntity", b => - { - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.SetNull); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.GeneticRequestEntity", b => - { - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.SetNull); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => - { - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "OpenTrade") - .WithMany() - .HasForeignKey("OpenTradeId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "StopLossTrade") - .WithMany() - .HasForeignKey("StopLossTradeId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "TakeProfit1Trade") - .WithMany() - .HasForeignKey("TakeProfit1TradeId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "TakeProfit2Trade") - .WithMany() - .HasForeignKey("TakeProfit2TradeId") - .OnDelete(DeleteBehavior.SetNull); - - b.Navigation("OpenTrade"); - - b.Navigation("StopLossTrade"); - - b.Navigation("TakeProfit1Trade"); - - b.Navigation("TakeProfit2Trade"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioIndicatorEntity", b => - { - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", "Indicator") - .WithMany("ScenarioIndicators") - .HasForeignKey("IndicatorId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", "Scenario") - .WithMany("ScenarioIndicators") - .HasForeignKey("ScenarioId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Indicator"); - - b.Navigation("Scenario"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => - { - b.Navigation("ScenarioIndicators"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => - { - b.Navigation("ScenarioIndicators"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/Managing.Infrastructure.Database/Migrations/20250724164138_UpdateScoreMessageToText.cs b/src/Managing.Infrastructure.Database/Migrations/20250724164138_UpdateScoreMessageToText.cs deleted file mode 100644 index f1fc77c..0000000 --- a/src/Managing.Infrastructure.Database/Migrations/20250724164138_UpdateScoreMessageToText.cs +++ /dev/null @@ -1,38 +0,0 @@ -īģŋusing Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Managing.Infrastructure.Databases.Migrations -{ - /// - public partial class UpdateScoreMessageToText : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn( - name: "ScoreMessage", - table: "Backtests", - type: "text", - maxLength: 1000, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(1000)", - oldMaxLength: 1000); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn( - name: "ScoreMessage", - table: "Backtests", - type: "character varying(1000)", - maxLength: 1000, - nullable: false, - oldClrType: typeof(string), - oldType: "text", - oldMaxLength: 1000); - } - } -} diff --git a/src/Managing.Infrastructure.Database/Migrations/20250725014014_AddUserIdToBundleBacktestRequest.cs b/src/Managing.Infrastructure.Database/Migrations/20250725014014_AddUserIdToBundleBacktestRequest.cs deleted file mode 100644 index da200c1..0000000 --- a/src/Managing.Infrastructure.Database/Migrations/20250725014014_AddUserIdToBundleBacktestRequest.cs +++ /dev/null @@ -1,49 +0,0 @@ -īģŋusing Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Managing.Infrastructure.Databases.Migrations -{ - /// - public partial class AddUserIdToBundleBacktestRequest : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "UserId", - table: "BundleBacktestRequests", - type: "integer", - nullable: true); - - migrationBuilder.CreateIndex( - name: "IX_BundleBacktestRequests_UserId", - table: "BundleBacktestRequests", - column: "UserId"); - - migrationBuilder.AddForeignKey( - name: "FK_BundleBacktestRequests_Users_UserId", - table: "BundleBacktestRequests", - column: "UserId", - principalTable: "Users", - principalColumn: "Id", - onDelete: ReferentialAction.SetNull); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_BundleBacktestRequests_Users_UserId", - table: "BundleBacktestRequests"); - - migrationBuilder.DropIndex( - name: "IX_BundleBacktestRequests_UserId", - table: "BundleBacktestRequests"); - - migrationBuilder.DropColumn( - name: "UserId", - table: "BundleBacktestRequests"); - } - } -} diff --git a/src/Managing.Infrastructure.Database/Migrations/20250725092603_RemoveOptimizedMoneyManagementJsonFromBacktestEntity.Designer.cs b/src/Managing.Infrastructure.Database/Migrations/20250725092603_RemoveOptimizedMoneyManagementJsonFromBacktestEntity.Designer.cs deleted file mode 100644 index 4b73028..0000000 --- a/src/Managing.Infrastructure.Database/Migrations/20250725092603_RemoveOptimizedMoneyManagementJsonFromBacktestEntity.Designer.cs +++ /dev/null @@ -1,1360 +0,0 @@ -īģŋ// -using System; -using Managing.Infrastructure.Databases.PostgreSql; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Managing.Infrastructure.Databases.Migrations -{ - [DbContext(typeof(ManagingDbContext))] - [Migration("20250725092603_RemoveOptimizedMoneyManagementJsonFromBacktestEntity")] - partial class RemoveOptimizedMoneyManagementJsonFromBacktestEntity - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.11") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AccountEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Exchange") - .IsRequired() - .HasColumnType("text"); - - b.Property("Key") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Secret") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.Property("UserId") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("Key"); - - b.HasIndex("Name") - .IsUnique(); - - b.HasIndex("UserId"); - - b.ToTable("Accounts"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BacktestEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ConfigJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("EndDate") - .HasColumnType("timestamp with time zone"); - - b.Property("Fees") - .HasColumnType("decimal(18,8)"); - - b.Property("FinalPnl") - .HasColumnType("decimal(18,8)"); - - b.Property("GrowthPercentage") - .HasColumnType("decimal(18,8)"); - - b.Property("HodlPercentage") - .HasColumnType("decimal(18,8)"); - - b.Property("Identifier") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Metadata") - .HasColumnType("text"); - - b.Property("MoneyManagementJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("PositionsJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("RequestId") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Score") - .HasColumnType("double precision"); - - b.Property("ScoreMessage") - .IsRequired() - .HasMaxLength(1000) - .HasColumnType("text"); - - b.Property("SignalsJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("StartDate") - .HasColumnType("timestamp with time zone"); - - b.Property("StatisticsJson") - .HasColumnType("jsonb"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("WinRate") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("EndDate"); - - b.HasIndex("FinalPnl"); - - b.HasIndex("Identifier") - .IsUnique(); - - b.HasIndex("RequestId"); - - b.HasIndex("Score"); - - b.HasIndex("StartDate"); - - b.HasIndex("UserName"); - - b.HasIndex("WinRate"); - - b.HasIndex("RequestId", "Score"); - - b.HasIndex("UserName", "Score"); - - b.ToTable("Backtests"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotBackupEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreateDate") - .HasColumnType("timestamp with time zone"); - - b.Property("Data") - .IsRequired() - .HasColumnType("text"); - - b.Property("Identifier") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("LastStatus") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("CreateDate"); - - b.HasIndex("Identifier") - .IsUnique(); - - b.HasIndex("LastStatus"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "CreateDate"); - - b.ToTable("BotBackups"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BundleBacktestRequestEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("BacktestRequestsJson") - .IsRequired() - .HasColumnType("text"); - - b.Property("CompletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CompletedBacktests") - .HasColumnType("integer"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CurrentBacktest") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("ErrorMessage") - .HasColumnType("text"); - - b.Property("EstimatedTimeRemainingSeconds") - .HasColumnType("integer"); - - b.Property("FailedBacktests") - .HasColumnType("integer"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("ProgressInfo") - .HasColumnType("text"); - - b.Property("RequestId") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("ResultsJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("Status") - .IsRequired() - .HasColumnType("text"); - - b.Property("TotalBacktests") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("integer"); - - b.Property("UserName") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("CompletedAt"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("RequestId") - .IsUnique(); - - b.HasIndex("Status"); - - b.HasIndex("UserId"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "CreatedAt"); - - b.ToTable("BundleBacktestRequests"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.FeeEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Cost") - .HasColumnType("decimal(18,8)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Exchange") - .IsRequired() - .HasColumnType("text"); - - b.Property("LastUpdate") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("Exchange") - .IsUnique(); - - b.HasIndex("LastUpdate"); - - b.ToTable("Fees"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.FundingRateEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Date") - .HasColumnType("timestamp with time zone"); - - b.Property("Direction") - .HasColumnType("integer"); - - b.Property("Exchange") - .HasColumnType("integer"); - - b.Property("OpenInterest") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.Property("Rate") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.Property("Ticker") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("Date"); - - b.HasIndex("Direction"); - - b.HasIndex("Exchange"); - - b.HasIndex("Ticker"); - - b.HasIndex("Exchange", "Date"); - - b.HasIndex("Ticker", "Exchange"); - - b.HasIndex("Ticker", "Exchange", "Date") - .IsUnique(); - - b.ToTable("FundingRates"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.GeneticRequestEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Balance") - .HasColumnType("decimal(18,8)"); - - b.Property("BestChromosome") - .HasMaxLength(4000) - .HasColumnType("character varying(4000)"); - - b.Property("BestFitness") - .HasColumnType("double precision"); - - b.Property("BestFitnessSoFar") - .HasColumnType("double precision"); - - b.Property("BestIndividual") - .HasMaxLength(4000) - .HasColumnType("character varying(4000)"); - - b.Property("CompletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CrossoverMethod") - .IsRequired() - .HasColumnType("text"); - - b.Property("CurrentGeneration") - .HasColumnType("integer"); - - b.Property("EligibleIndicatorsJson") - .HasMaxLength(2000) - .HasColumnType("character varying(2000)"); - - b.Property("ElitismPercentage") - .HasColumnType("integer"); - - b.Property("EndDate") - .HasColumnType("timestamp with time zone"); - - b.Property("ErrorMessage") - .HasMaxLength(2000) - .HasColumnType("character varying(2000)"); - - b.Property("Generations") - .HasColumnType("integer"); - - b.Property("MaxTakeProfit") - .HasColumnType("double precision"); - - b.Property("MutationMethod") - .IsRequired() - .HasColumnType("text"); - - b.Property("MutationRate") - .HasColumnType("double precision"); - - b.Property("PopulationSize") - .HasColumnType("integer"); - - b.Property("ProgressInfo") - .HasMaxLength(4000) - .HasColumnType("character varying(4000)"); - - b.Property("RequestId") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("SelectionMethod") - .IsRequired() - .HasColumnType("text"); - - b.Property("StartDate") - .HasColumnType("timestamp with time zone"); - - b.Property("Status") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("Ticker") - .IsRequired() - .HasColumnType("text"); - - b.Property("Timeframe") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("RequestId") - .IsUnique(); - - b.HasIndex("Status"); - - b.HasIndex("UserId"); - - b.ToTable("GeneticRequests"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CyclePeriods") - .HasColumnType("integer"); - - b.Property("FastPeriods") - .HasColumnType("integer"); - - b.Property("MinimumHistory") - .HasColumnType("integer"); - - b.Property("Multiplier") - .HasColumnType("double precision"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Period") - .HasColumnType("integer"); - - b.Property("SignalPeriods") - .HasColumnType("integer"); - - b.Property("SignalType") - .IsRequired() - .HasColumnType("text"); - - b.Property("SlowPeriods") - .HasColumnType("integer"); - - b.Property("SmoothPeriods") - .HasColumnType("integer"); - - b.Property("StochPeriods") - .HasColumnType("integer"); - - b.Property("Timeframe") - .IsRequired() - .HasColumnType("text"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("Name"); - - b.HasIndex("Type"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "Name"); - - b.ToTable("Indicators"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.MoneyManagementEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Leverage") - .HasColumnType("decimal(18,8)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("StopLoss") - .HasColumnType("decimal(18,8)"); - - b.Property("TakeProfit") - .HasColumnType("decimal(18,8)"); - - b.Property("Timeframe") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("Name"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "Name"); - - b.ToTable("MoneyManagements"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AccountName") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Date") - .HasColumnType("timestamp with time zone"); - - b.Property("Identifier") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Initiator") - .IsRequired() - .HasColumnType("text"); - - b.Property("MoneyManagementJson") - .HasColumnType("text"); - - b.Property("OpenTradeId") - .HasColumnType("integer"); - - b.Property("OriginDirection") - .IsRequired() - .HasColumnType("text"); - - b.Property("ProfitAndLoss") - .HasColumnType("decimal(18,8)"); - - b.Property("SignalIdentifier") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Status") - .IsRequired() - .HasColumnType("text"); - - b.Property("StopLossTradeId") - .HasColumnType("integer"); - - b.Property("TakeProfit1TradeId") - .HasColumnType("integer"); - - b.Property("TakeProfit2TradeId") - .HasColumnType("integer"); - - b.Property("Ticker") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("Date"); - - b.HasIndex("Identifier") - .IsUnique(); - - b.HasIndex("Initiator"); - - b.HasIndex("OpenTradeId"); - - b.HasIndex("Status"); - - b.HasIndex("StopLossTradeId"); - - b.HasIndex("TakeProfit1TradeId"); - - b.HasIndex("TakeProfit2TradeId"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "Identifier"); - - b.ToTable("Positions"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("LoopbackPeriod") - .HasColumnType("integer"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("Name"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "Name"); - - b.ToTable("Scenarios"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioIndicatorEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("IndicatorId") - .HasColumnType("integer"); - - b.Property("ScenarioId") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("IndicatorId"); - - b.HasIndex("ScenarioId"); - - b.HasIndex("ScenarioId", "IndicatorId") - .IsUnique(); - - b.ToTable("ScenarioIndicators"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SignalEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CandleJson") - .HasColumnType("text"); - - b.Property("Confidence") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Date") - .HasColumnType("timestamp with time zone"); - - b.Property("Direction") - .IsRequired() - .HasColumnType("text"); - - b.Property("Identifier") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("IndicatorName") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("SignalType") - .IsRequired() - .HasColumnType("text"); - - b.Property("Status") - .IsRequired() - .HasColumnType("text"); - - b.Property("Ticker") - .IsRequired() - .HasColumnType("text"); - - b.Property("Timeframe") - .IsRequired() - .HasColumnType("text"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("Date"); - - b.HasIndex("Identifier"); - - b.HasIndex("Status"); - - b.HasIndex("Ticker"); - - b.HasIndex("UserName"); - - b.HasIndex("UserName", "Date"); - - b.HasIndex("Identifier", "Date", "UserName") - .IsUnique(); - - b.ToTable("Signals"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SpotlightOverviewEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DateTime") - .HasColumnType("timestamp with time zone"); - - b.Property("Identifier") - .HasColumnType("uuid"); - - b.Property("ScenarioCount") - .HasColumnType("integer"); - - b.Property("SpotlightsJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("DateTime"); - - b.HasIndex("Identifier") - .IsUnique(); - - b.HasIndex("ScenarioCount"); - - b.HasIndex("DateTime", "ScenarioCount"); - - b.ToTable("SpotlightOverviews"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SynthMinersLeaderboardEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Asset") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("CacheKey") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("IsBacktest") - .HasColumnType("boolean"); - - b.Property("MinersData") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("SignalDate") - .HasColumnType("timestamp with time zone"); - - b.Property("TimeIncrement") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("CacheKey") - .IsUnique(); - - b.HasIndex("CreatedAt"); - - b.ToTable("SynthMinersLeaderboards"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SynthPredictionEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Asset") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("CacheKey") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("IsBacktest") - .HasColumnType("boolean"); - - b.Property("MinerUid") - .HasColumnType("integer"); - - b.Property("PredictionData") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("SignalDate") - .HasColumnType("timestamp with time zone"); - - b.Property("TimeIncrement") - .HasColumnType("integer"); - - b.Property("TimeLength") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("CacheKey") - .IsUnique(); - - b.HasIndex("CreatedAt"); - - b.ToTable("SynthPredictions"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TopVolumeTickerEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Date") - .HasColumnType("timestamp with time zone"); - - b.Property("Exchange") - .HasColumnType("integer"); - - b.Property("Rank") - .HasColumnType("integer"); - - b.Property("Ticker") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Volume") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.HasKey("Id"); - - b.HasIndex("Date"); - - b.HasIndex("Exchange"); - - b.HasIndex("Rank"); - - b.HasIndex("Ticker"); - - b.HasIndex("Date", "Rank"); - - b.HasIndex("Exchange", "Date"); - - b.ToTable("TopVolumeTickers"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Date") - .HasColumnType("timestamp with time zone"); - - b.Property("Direction") - .IsRequired() - .HasColumnType("text"); - - b.Property("ExchangeOrderId") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("Fee") - .HasColumnType("decimal(18,8)"); - - b.Property("Leverage") - .HasColumnType("decimal(18,8)"); - - b.Property("Message") - .HasColumnType("text"); - - b.Property("Price") - .HasColumnType("decimal(18,8)"); - - b.Property("Quantity") - .HasColumnType("decimal(18,8)"); - - b.Property("Status") - .IsRequired() - .HasColumnType("text"); - - b.Property("Ticker") - .IsRequired() - .HasColumnType("text"); - - b.Property("TradeType") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("Date"); - - b.HasIndex("ExchangeOrderId"); - - b.HasIndex("Status"); - - b.ToTable("Trades"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TraderEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Address") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("AverageLoss") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.Property("AverageWin") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("IsBestTrader") - .HasColumnType("boolean"); - - b.Property("Pnl") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.Property("Roi") - .HasPrecision(18, 8) - .HasColumnType("decimal(18,8)"); - - b.Property("TradeCount") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Winrate") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("Address"); - - b.HasIndex("IsBestTrader"); - - b.HasIndex("Pnl"); - - b.HasIndex("Roi"); - - b.HasIndex("Winrate"); - - b.HasIndex("Address", "IsBestTrader") - .IsUnique(); - - b.HasIndex("IsBestTrader", "Roi"); - - b.HasIndex("IsBestTrader", "Winrate"); - - b.ToTable("Traders"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AgentName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("AvatarUrl") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("TelegramChannel") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.ToTable("Users"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.WorkerEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("DelayTicks") - .HasColumnType("bigint"); - - b.Property("ExecutionCount") - .HasColumnType("integer"); - - b.Property("IsActive") - .HasColumnType("boolean"); - - b.Property("LastRunTime") - .HasColumnType("timestamp with time zone"); - - b.Property("StartTime") - .HasColumnType("timestamp with time zone"); - - b.Property("WorkerType") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("WorkerType") - .IsUnique(); - - b.ToTable("Workers"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AccountEntity", b => - { - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.SetNull); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BundleBacktestRequestEntity", b => - { - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.SetNull); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.GeneticRequestEntity", b => - { - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.SetNull); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => - { - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "OpenTrade") - .WithMany() - .HasForeignKey("OpenTradeId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "StopLossTrade") - .WithMany() - .HasForeignKey("StopLossTradeId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "TakeProfit1Trade") - .WithMany() - .HasForeignKey("TakeProfit1TradeId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "TakeProfit2Trade") - .WithMany() - .HasForeignKey("TakeProfit2TradeId") - .OnDelete(DeleteBehavior.SetNull); - - b.Navigation("OpenTrade"); - - b.Navigation("StopLossTrade"); - - b.Navigation("TakeProfit1Trade"); - - b.Navigation("TakeProfit2Trade"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioIndicatorEntity", b => - { - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", "Indicator") - .WithMany("ScenarioIndicators") - .HasForeignKey("IndicatorId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", "Scenario") - .WithMany("ScenarioIndicators") - .HasForeignKey("ScenarioId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Indicator"); - - b.Navigation("Scenario"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => - { - b.Navigation("ScenarioIndicators"); - }); - - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => - { - b.Navigation("ScenarioIndicators"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/Managing.Infrastructure.Database/Migrations/20250725172635_AddUserIdToMoneyManagement.cs b/src/Managing.Infrastructure.Database/Migrations/20250725172635_AddUserIdToMoneyManagement.cs deleted file mode 100644 index 2e7d542..0000000 --- a/src/Managing.Infrastructure.Database/Migrations/20250725172635_AddUserIdToMoneyManagement.cs +++ /dev/null @@ -1,49 +0,0 @@ -īģŋusing Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Managing.Infrastructure.Databases.Migrations -{ - /// - public partial class AddUserIdToMoneyManagement : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "UserId", - table: "MoneyManagements", - type: "integer", - nullable: true); - - migrationBuilder.CreateIndex( - name: "IX_MoneyManagements_UserId", - table: "MoneyManagements", - column: "UserId"); - - migrationBuilder.AddForeignKey( - name: "FK_MoneyManagements_Users_UserId", - table: "MoneyManagements", - column: "UserId", - principalTable: "Users", - principalColumn: "Id", - onDelete: ReferentialAction.SetNull); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_MoneyManagements_Users_UserId", - table: "MoneyManagements"); - - migrationBuilder.DropIndex( - name: "IX_MoneyManagements_UserId", - table: "MoneyManagements"); - - migrationBuilder.DropColumn( - name: "UserId", - table: "MoneyManagements"); - } - } -} diff --git a/src/Managing.Infrastructure.Database/Migrations/20250725173315_AddUserIdToBotBackup.cs b/src/Managing.Infrastructure.Database/Migrations/20250725173315_AddUserIdToBotBackup.cs deleted file mode 100644 index 153d1dd..0000000 --- a/src/Managing.Infrastructure.Database/Migrations/20250725173315_AddUserIdToBotBackup.cs +++ /dev/null @@ -1,49 +0,0 @@ -īģŋusing Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Managing.Infrastructure.Databases.Migrations -{ - /// - public partial class AddUserIdToBotBackup : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "UserId", - table: "BotBackups", - type: "integer", - nullable: true); - - migrationBuilder.CreateIndex( - name: "IX_BotBackups_UserId", - table: "BotBackups", - column: "UserId"); - - migrationBuilder.AddForeignKey( - name: "FK_BotBackups_Users_UserId", - table: "BotBackups", - column: "UserId", - principalTable: "Users", - principalColumn: "Id", - onDelete: ReferentialAction.SetNull); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_BotBackups_Users_UserId", - table: "BotBackups"); - - migrationBuilder.DropIndex( - name: "IX_BotBackups_UserId", - table: "BotBackups"); - - migrationBuilder.DropColumn( - name: "UserId", - table: "BotBackups"); - } - } -} diff --git a/src/Managing.Infrastructure.Database/Migrations/20250725202808_RemoveFeeEntity.cs b/src/Managing.Infrastructure.Database/Migrations/20250725202808_RemoveFeeEntity.cs deleted file mode 100644 index c530c5c..0000000 --- a/src/Managing.Infrastructure.Database/Migrations/20250725202808_RemoveFeeEntity.cs +++ /dev/null @@ -1,51 +0,0 @@ -īģŋusing System; -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Managing.Infrastructure.Databases.Migrations -{ - /// - public partial class RemoveFeeEntity : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Fees"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Fees", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Cost = table.Column(type: "numeric(18,8)", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - Exchange = table.Column(type: "text", nullable: false), - LastUpdate = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Fees", x => x.Id); - }); - - migrationBuilder.CreateIndex( - name: "IX_Fees_Exchange", - table: "Fees", - column: "Exchange", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Fees_LastUpdate", - table: "Fees", - column: "LastUpdate"); - } - } -} diff --git a/src/Managing.Infrastructure.Database/Migrations/20250725202808_RemoveFeeEntity.Designer.cs b/src/Managing.Infrastructure.Database/Migrations/20250801100607_Init.Designer.cs similarity index 96% rename from src/Managing.Infrastructure.Database/Migrations/20250725202808_RemoveFeeEntity.Designer.cs rename to src/Managing.Infrastructure.Database/Migrations/20250801100607_Init.Designer.cs index 72b7e08..c732943 100644 --- a/src/Managing.Infrastructure.Database/Migrations/20250725202808_RemoveFeeEntity.Designer.cs +++ b/src/Managing.Infrastructure.Database/Migrations/20250801100607_Init.Designer.cs @@ -12,8 +12,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace Managing.Infrastructure.Databases.Migrations { [DbContext(typeof(ManagingDbContext))] - [Migration("20250725202808_RemoveFeeEntity")] - partial class RemoveFeeEntity + [Migration("20250801100607_Init")] + partial class Init { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -177,55 +177,68 @@ namespace Managing.Infrastructure.Databases.Migrations b.ToTable("Backtests"); }); - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotBackupEntity", b => + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotEntity", b => { - b.Property("Id") + b.Property("Identifier") .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + .HasMaxLength(255) + .HasColumnType("uuid"); b.Property("CreateDate") .HasColumnType("timestamp with time zone"); - b.Property("Data") - .IsRequired() - .HasColumnType("text"); + b.Property("Fees") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); - b.Property("Identifier") + b.Property("Name") .IsRequired() .HasMaxLength(255) .HasColumnType("character varying(255)"); - b.Property("LastStatus") + b.Property("Pnl") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("Roi") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("StartupTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("TradeLosses") + .HasColumnType("integer"); + + b.Property("TradeWins") .HasColumnType("integer"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); - b.Property("UserId") + b.Property("UserId") .HasColumnType("integer"); - b.Property("UserName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); + b.Property("Volume") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); - b.HasKey("Id"); + b.HasKey("Identifier"); b.HasIndex("CreateDate"); b.HasIndex("Identifier") .IsUnique(); - b.HasIndex("LastStatus"); + b.HasIndex("Status"); b.HasIndex("UserId"); - b.HasIndex("UserName"); - - b.HasIndex("UserName", "CreateDate"); - - b.ToTable("BotBackups"); + b.ToTable("Bots"); }); modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BundleBacktestRequestEntity", b => @@ -619,11 +632,9 @@ namespace Managing.Infrastructure.Databases.Migrations modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => { - b.Property("Id") + b.Property("Identifier") .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + .HasColumnType("uuid"); b.Property("AccountName") .IsRequired() @@ -636,11 +647,6 @@ namespace Managing.Infrastructure.Databases.Migrations b.Property("Date") .HasColumnType("timestamp with time zone"); - b.Property("Identifier") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - b.Property("Initiator") .IsRequired() .HasColumnType("text"); @@ -687,7 +693,7 @@ namespace Managing.Infrastructure.Databases.Migrations .HasMaxLength(255) .HasColumnType("character varying(255)"); - b.HasKey("Id"); + b.HasKey("Identifier"); b.HasIndex("CreatedAt"); @@ -1185,6 +1191,7 @@ namespace Managing.Infrastructure.Databases.Migrations NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("AgentName") + .IsRequired() .HasMaxLength(255) .HasColumnType("character varying(255)"); @@ -1251,12 +1258,13 @@ namespace Managing.Infrastructure.Databases.Migrations b.Navigation("User"); }); - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotBackupEntity", b => + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotEntity", b => { b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") .WithMany() .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.SetNull); + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); b.Navigation("User"); }); diff --git a/src/Managing.Infrastructure.Database/Migrations/20250723194312_InitialCreate.cs b/src/Managing.Infrastructure.Database/Migrations/20250801100607_Init.cs similarity index 86% rename from src/Managing.Infrastructure.Database/Migrations/20250723194312_InitialCreate.cs rename to src/Managing.Infrastructure.Database/Migrations/20250801100607_Init.cs index e216e6f..4638768 100644 --- a/src/Managing.Infrastructure.Database/Migrations/20250723194312_InitialCreate.cs +++ b/src/Managing.Infrastructure.Database/Migrations/20250801100607_Init.cs @@ -7,7 +7,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace Managing.Infrastructure.Databases.Migrations { /// - public partial class InitialCreate : Migration + public partial class Init : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) @@ -34,7 +34,7 @@ namespace Managing.Infrastructure.Databases.Migrations StatisticsJson = table.Column(type: "jsonb", nullable: true), Fees = table.Column(type: "numeric(18,8)", nullable: false), Score = table.Column(type: "double precision", nullable: false), - ScoreMessage = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: false), + ScoreMessage = table.Column(type: "text", maxLength: 1000, nullable: false), Metadata = table.Column(type: "text", nullable: true), CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) @@ -44,69 +44,6 @@ namespace Managing.Infrastructure.Databases.Migrations table.PrimaryKey("PK_Backtests", x => x.Id); }); - migrationBuilder.CreateTable( - name: "BotBackups", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Identifier = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), - UserName = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), - Data = table.Column(type: "jsonb", nullable: false), - LastStatus = table.Column(type: "integer", nullable: false), - CreateDate = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_BotBackups", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "BundleBacktestRequests", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - RequestId = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), - UserName = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - CompletedAt = table.Column(type: "timestamp with time zone", nullable: true), - Status = table.Column(type: "text", nullable: false), - BacktestRequestsJson = table.Column(type: "text", nullable: false), - TotalBacktests = table.Column(type: "integer", nullable: false), - CompletedBacktests = table.Column(type: "integer", nullable: false), - FailedBacktests = table.Column(type: "integer", nullable: false), - ErrorMessage = table.Column(type: "text", nullable: true), - ProgressInfo = table.Column(type: "text", nullable: true), - CurrentBacktest = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), - EstimatedTimeRemainingSeconds = table.Column(type: "integer", nullable: true), - Name = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), - ResultsJson = table.Column(type: "jsonb", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_BundleBacktestRequests", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Fees", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Cost = table.Column(type: "numeric(18,8)", nullable: false), - Exchange = table.Column(type: "text", nullable: false), - LastUpdate = table.Column(type: "timestamp with time zone", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Fees", x => x.Id); - }); - migrationBuilder.CreateTable( name: "FundingRates", columns: table => new @@ -155,26 +92,6 @@ namespace Managing.Infrastructure.Databases.Migrations table.PrimaryKey("PK_Indicators", x => x.Id); }); - migrationBuilder.CreateTable( - name: "MoneyManagements", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Name = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), - Timeframe = table.Column(type: "text", nullable: false), - StopLoss = table.Column(type: "numeric(18,8)", nullable: false), - TakeProfit = table.Column(type: "numeric(18,8)", nullable: false), - Leverage = table.Column(type: "numeric(18,8)", nullable: false), - UserName = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_MoneyManagements", x => x.Id); - }); - migrationBuilder.CreateTable( name: "Scenarios", columns: table => new @@ -236,6 +153,44 @@ namespace Managing.Infrastructure.Databases.Migrations table.PrimaryKey("PK_SpotlightOverviews", x => x.Id); }); + migrationBuilder.CreateTable( + name: "SynthMinersLeaderboards", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Asset = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + TimeIncrement = table.Column(type: "integer", nullable: false), + SignalDate = table.Column(type: "timestamp with time zone", nullable: true), + IsBacktest = table.Column(type: "boolean", nullable: false), + MinersData = table.Column(type: "jsonb", nullable: false), + CacheKey = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SynthMinersLeaderboards", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "SynthPredictions", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Asset = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + MinerUid = table.Column(type: "integer", nullable: false), + TimeIncrement = table.Column(type: "integer", nullable: false), + TimeLength = table.Column(type: "integer", nullable: false), + SignalDate = table.Column(type: "timestamp with time zone", nullable: true), + IsBacktest = table.Column(type: "boolean", nullable: false), + PredictionData = table.Column(type: "jsonb", nullable: false), + CacheKey = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SynthPredictions", x => x.Id); + }); + migrationBuilder.CreateTable( name: "TopVolumeTickers", columns: table => new @@ -309,7 +264,7 @@ namespace Managing.Infrastructure.Databases.Migrations Id = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), Name = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), - AgentName = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), + AgentName = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), AvatarUrl = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), TelegramChannel = table.Column(type: "character varying(255)", maxLength: 255, nullable: true) }, @@ -318,6 +273,24 @@ namespace Managing.Infrastructure.Databases.Migrations table.PrimaryKey("PK_Users", x => x.Id); }); + migrationBuilder.CreateTable( + name: "Workers", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + WorkerType = table.Column(type: "text", nullable: false), + StartTime = table.Column(type: "timestamp with time zone", nullable: false), + LastRunTime = table.Column(type: "timestamp with time zone", nullable: true), + ExecutionCount = table.Column(type: "integer", nullable: false), + DelayTicks = table.Column(type: "bigint", nullable: false), + IsActive = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Workers", x => x.Id); + }); + migrationBuilder.CreateTable( name: "ScenarioIndicators", columns: table => new @@ -349,9 +322,7 @@ namespace Managing.Infrastructure.Databases.Migrations name: "Positions", columns: table => new { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Identifier = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + Identifier = table.Column(type: "uuid", nullable: false), Date = table.Column(type: "timestamp with time zone", nullable: false), ProfitAndLoss = table.Column(type: "numeric(18,8)", nullable: false), OriginDirection = table.Column(type: "text", nullable: false), @@ -371,7 +342,7 @@ namespace Managing.Infrastructure.Databases.Migrations }, constraints: table => { - table.PrimaryKey("PK_Positions", x => x.Id); + table.PrimaryKey("PK_Positions", x => x.Identifier); table.ForeignKey( name: "FK_Positions_Trades_OpenTradeId", column: x => x.OpenTradeId, @@ -422,6 +393,70 @@ namespace Managing.Infrastructure.Databases.Migrations onDelete: ReferentialAction.SetNull); }); + migrationBuilder.CreateTable( + name: "Bots", + columns: table => new + { + Identifier = table.Column(type: "uuid", maxLength: 255, nullable: false), + Name = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + UserId = table.Column(type: "integer", nullable: false), + Status = table.Column(type: "text", nullable: false), + CreateDate = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), + StartupTime = table.Column(type: "timestamp with time zone", nullable: false), + TradeWins = table.Column(type: "integer", nullable: false), + TradeLosses = table.Column(type: "integer", nullable: false), + Pnl = table.Column(type: "numeric(18,8)", precision: 18, scale: 8, nullable: false), + Roi = table.Column(type: "numeric(18,8)", precision: 18, scale: 8, nullable: false), + Volume = table.Column(type: "numeric(18,8)", precision: 18, scale: 8, nullable: false), + Fees = table.Column(type: "numeric(18,8)", precision: 18, scale: 8, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Bots", x => x.Identifier); + table.ForeignKey( + name: "FK_Bots_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "BundleBacktestRequests", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + RequestId = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + UserName = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + UserId = table.Column(type: "integer", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + CompletedAt = table.Column(type: "timestamp with time zone", nullable: true), + Status = table.Column(type: "text", nullable: false), + BacktestRequestsJson = table.Column(type: "text", nullable: false), + TotalBacktests = table.Column(type: "integer", nullable: false), + CompletedBacktests = table.Column(type: "integer", nullable: false), + FailedBacktests = table.Column(type: "integer", nullable: false), + ErrorMessage = table.Column(type: "text", nullable: true), + ProgressInfo = table.Column(type: "text", nullable: true), + CurrentBacktest = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), + EstimatedTimeRemainingSeconds = table.Column(type: "integer", nullable: true), + Name = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + ResultsJson = table.Column(type: "jsonb", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BundleBacktestRequests", x => x.Id); + table.ForeignKey( + name: "FK_BundleBacktestRequests_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + migrationBuilder.CreateTable( name: "GeneticRequests", columns: table => new @@ -467,6 +502,33 @@ namespace Managing.Infrastructure.Databases.Migrations onDelete: ReferentialAction.SetNull); }); + migrationBuilder.CreateTable( + name: "MoneyManagements", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + Timeframe = table.Column(type: "text", nullable: false), + StopLoss = table.Column(type: "numeric(18,8)", nullable: false), + TakeProfit = table.Column(type: "numeric(18,8)", nullable: false), + Leverage = table.Column(type: "numeric(18,8)", nullable: false), + UserName = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), + UserId = table.Column(type: "integer", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_MoneyManagements", x => x.Id); + table.ForeignKey( + name: "FK_MoneyManagements_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + migrationBuilder.CreateIndex( name: "IX_Accounts_Key", table: "Accounts", @@ -540,30 +602,25 @@ namespace Managing.Infrastructure.Databases.Migrations column: "WinRate"); migrationBuilder.CreateIndex( - name: "IX_BotBackups_CreateDate", - table: "BotBackups", + name: "IX_Bots_CreateDate", + table: "Bots", column: "CreateDate"); migrationBuilder.CreateIndex( - name: "IX_BotBackups_Identifier", - table: "BotBackups", + name: "IX_Bots_Identifier", + table: "Bots", column: "Identifier", unique: true); migrationBuilder.CreateIndex( - name: "IX_BotBackups_LastStatus", - table: "BotBackups", - column: "LastStatus"); + name: "IX_Bots_Status", + table: "Bots", + column: "Status"); migrationBuilder.CreateIndex( - name: "IX_BotBackups_UserName", - table: "BotBackups", - column: "UserName"); - - migrationBuilder.CreateIndex( - name: "IX_BotBackups_UserName_CreateDate", - table: "BotBackups", - columns: new[] { "UserName", "CreateDate" }); + name: "IX_Bots_UserId", + table: "Bots", + column: "UserId"); migrationBuilder.CreateIndex( name: "IX_BundleBacktestRequests_CompletedAt", @@ -586,6 +643,11 @@ namespace Managing.Infrastructure.Databases.Migrations table: "BundleBacktestRequests", column: "Status"); + migrationBuilder.CreateIndex( + name: "IX_BundleBacktestRequests_UserId", + table: "BundleBacktestRequests", + column: "UserId"); + migrationBuilder.CreateIndex( name: "IX_BundleBacktestRequests_UserName", table: "BundleBacktestRequests", @@ -596,17 +658,6 @@ namespace Managing.Infrastructure.Databases.Migrations table: "BundleBacktestRequests", columns: new[] { "UserName", "CreatedAt" }); - migrationBuilder.CreateIndex( - name: "IX_Fees_Exchange", - table: "Fees", - column: "Exchange", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Fees_LastUpdate", - table: "Fees", - column: "LastUpdate"); - migrationBuilder.CreateIndex( name: "IX_FundingRates_Date", table: "FundingRates", @@ -694,6 +745,11 @@ namespace Managing.Infrastructure.Databases.Migrations table: "MoneyManagements", column: "Name"); + migrationBuilder.CreateIndex( + name: "IX_MoneyManagements_UserId", + table: "MoneyManagements", + column: "UserId"); + migrationBuilder.CreateIndex( name: "IX_MoneyManagements_UserName", table: "MoneyManagements", @@ -858,6 +914,28 @@ namespace Managing.Infrastructure.Databases.Migrations table: "SpotlightOverviews", column: "ScenarioCount"); + migrationBuilder.CreateIndex( + name: "IX_SynthMinersLeaderboards_CacheKey", + table: "SynthMinersLeaderboards", + column: "CacheKey", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_SynthMinersLeaderboards_CreatedAt", + table: "SynthMinersLeaderboards", + column: "CreatedAt"); + + migrationBuilder.CreateIndex( + name: "IX_SynthPredictions_CacheKey", + table: "SynthPredictions", + column: "CacheKey", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_SynthPredictions_CreatedAt", + table: "SynthPredictions", + column: "CreatedAt"); + migrationBuilder.CreateIndex( name: "IX_TopVolumeTickers_Date", table: "TopVolumeTickers", @@ -948,6 +1026,12 @@ namespace Managing.Infrastructure.Databases.Migrations name: "IX_Trades_Status", table: "Trades", column: "Status"); + + migrationBuilder.CreateIndex( + name: "IX_Workers_WorkerType", + table: "Workers", + column: "WorkerType", + unique: true); } /// @@ -960,14 +1044,11 @@ namespace Managing.Infrastructure.Databases.Migrations name: "Backtests"); migrationBuilder.DropTable( - name: "BotBackups"); + name: "Bots"); migrationBuilder.DropTable( name: "BundleBacktestRequests"); - migrationBuilder.DropTable( - name: "Fees"); - migrationBuilder.DropTable( name: "FundingRates"); @@ -989,12 +1070,21 @@ namespace Managing.Infrastructure.Databases.Migrations migrationBuilder.DropTable( name: "SpotlightOverviews"); + migrationBuilder.DropTable( + name: "SynthMinersLeaderboards"); + + migrationBuilder.DropTable( + name: "SynthPredictions"); + migrationBuilder.DropTable( name: "TopVolumeTickers"); migrationBuilder.DropTable( name: "Traders"); + migrationBuilder.DropTable( + name: "Workers"); + migrationBuilder.DropTable( name: "Users"); diff --git a/src/Managing.Infrastructure.Database/Migrations/20250725014014_AddUserIdToBundleBacktestRequest.Designer.cs b/src/Managing.Infrastructure.Database/Migrations/20250801111224_UpdateUserEntity.Designer.cs similarity index 95% rename from src/Managing.Infrastructure.Database/Migrations/20250725014014_AddUserIdToBundleBacktestRequest.Designer.cs rename to src/Managing.Infrastructure.Database/Migrations/20250801111224_UpdateUserEntity.Designer.cs index d5b1b72..b917f0a 100644 --- a/src/Managing.Infrastructure.Database/Migrations/20250725014014_AddUserIdToBundleBacktestRequest.Designer.cs +++ b/src/Managing.Infrastructure.Database/Migrations/20250801111224_UpdateUserEntity.Designer.cs @@ -12,8 +12,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace Managing.Infrastructure.Databases.Migrations { [DbContext(typeof(ManagingDbContext))] - [Migration("20250725014014_AddUserIdToBundleBacktestRequest")] - partial class AddUserIdToBundleBacktestRequest + [Migration("20250801111224_UpdateUserEntity")] + partial class UpdateUserEntity { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -111,10 +111,6 @@ namespace Managing.Infrastructure.Databases.Migrations .IsRequired() .HasColumnType("jsonb"); - b.Property("OptimizedMoneyManagementJson") - .IsRequired() - .HasColumnType("jsonb"); - b.Property("PositionsJson") .IsRequired() .HasColumnType("jsonb"); @@ -181,50 +177,68 @@ namespace Managing.Infrastructure.Databases.Migrations b.ToTable("Backtests"); }); - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotBackupEntity", b => + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotEntity", b => { - b.Property("Id") + b.Property("Identifier") .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + .HasMaxLength(255) + .HasColumnType("uuid"); b.Property("CreateDate") .HasColumnType("timestamp with time zone"); - b.Property("Data") - .IsRequired() - .HasColumnType("text"); + b.Property("Fees") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); - b.Property("Identifier") + b.Property("Name") .IsRequired() .HasMaxLength(255) .HasColumnType("character varying(255)"); - b.Property("LastStatus") + b.Property("Pnl") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("Roi") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("StartupTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("TradeLosses") + .HasColumnType("integer"); + + b.Property("TradeWins") .HasColumnType("integer"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); - b.Property("UserName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); + b.Property("UserId") + .HasColumnType("integer"); - b.HasKey("Id"); + b.Property("Volume") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.HasKey("Identifier"); b.HasIndex("CreateDate"); b.HasIndex("Identifier") .IsUnique(); - b.HasIndex("LastStatus"); + b.HasIndex("Status"); - b.HasIndex("UserName"); + b.HasIndex("UserId"); - b.HasIndex("UserName", "CreateDate"); - - b.ToTable("BotBackups"); + b.ToTable("Bots"); }); modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BundleBacktestRequestEntity", b => @@ -316,40 +330,6 @@ namespace Managing.Infrastructure.Databases.Migrations b.ToTable("BundleBacktestRequests"); }); - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.FeeEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Cost") - .HasColumnType("decimal(18,8)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Exchange") - .IsRequired() - .HasColumnType("text"); - - b.Property("LastUpdate") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("Exchange") - .IsUnique(); - - b.HasIndex("LastUpdate"); - - b.ToTable("Fees"); - }); - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.FundingRateEntity", b => { b.Property("Id") @@ -630,6 +610,9 @@ namespace Managing.Infrastructure.Databases.Migrations b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); + b.Property("UserId") + .HasColumnType("integer"); + b.Property("UserName") .HasMaxLength(255) .HasColumnType("character varying(255)"); @@ -638,6 +621,8 @@ namespace Managing.Infrastructure.Databases.Migrations b.HasIndex("Name"); + b.HasIndex("UserId"); + b.HasIndex("UserName"); b.HasIndex("UserName", "Name"); @@ -647,11 +632,9 @@ namespace Managing.Infrastructure.Databases.Migrations modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => { - b.Property("Id") + b.Property("Identifier") .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + .HasColumnType("uuid"); b.Property("AccountName") .IsRequired() @@ -664,11 +647,6 @@ namespace Managing.Infrastructure.Databases.Migrations b.Property("Date") .HasColumnType("timestamp with time zone"); - b.Property("Identifier") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - b.Property("Initiator") .IsRequired() .HasColumnType("text"); @@ -715,7 +693,7 @@ namespace Managing.Infrastructure.Databases.Migrations .HasMaxLength(255) .HasColumnType("character varying(255)"); - b.HasKey("Id"); + b.HasKey("Identifier"); b.HasIndex("CreatedAt"); @@ -1279,6 +1257,17 @@ namespace Managing.Infrastructure.Databases.Migrations b.Navigation("User"); }); + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BundleBacktestRequestEntity", b => { b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") @@ -1299,6 +1288,16 @@ namespace Managing.Infrastructure.Databases.Migrations b.Navigation("User"); }); + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.MoneyManagementEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => { b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "OpenTrade") diff --git a/src/Managing.Infrastructure.Database/Migrations/20250723221025_UpdateBotBackupDataToText.cs b/src/Managing.Infrastructure.Database/Migrations/20250801111224_UpdateUserEntity.cs similarity index 50% rename from src/Managing.Infrastructure.Database/Migrations/20250723221025_UpdateBotBackupDataToText.cs rename to src/Managing.Infrastructure.Database/Migrations/20250801111224_UpdateUserEntity.cs index f89ebfa..c16c1d7 100644 --- a/src/Managing.Infrastructure.Database/Migrations/20250723221025_UpdateBotBackupDataToText.cs +++ b/src/Managing.Infrastructure.Database/Migrations/20250801111224_UpdateUserEntity.cs @@ -5,30 +5,36 @@ namespace Managing.Infrastructure.Databases.Migrations { /// - public partial class UpdateBotBackupDataToText : Migration + public partial class UpdateUserEntity : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.AlterColumn( - name: "Data", - table: "BotBackups", - type: "text", - nullable: false, + name: "AgentName", + table: "Users", + type: "character varying(255)", + maxLength: 255, + nullable: true, oldClrType: typeof(string), - oldType: "jsonb"); + oldType: "character varying(255)", + oldMaxLength: 255); } /// protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.AlterColumn( - name: "Data", - table: "BotBackups", - type: "jsonb", + name: "AgentName", + table: "Users", + type: "character varying(255)", + maxLength: 255, nullable: false, + defaultValue: "", oldClrType: typeof(string), - oldType: "text"); + oldType: "character varying(255)", + oldMaxLength: 255, + oldNullable: true); } } } diff --git a/src/Managing.Infrastructure.Database/Migrations/20250725172635_AddUserIdToMoneyManagement.Designer.cs b/src/Managing.Infrastructure.Database/Migrations/20250803201734_AddTickerToBots.Designer.cs similarity index 95% rename from src/Managing.Infrastructure.Database/Migrations/20250725172635_AddUserIdToMoneyManagement.Designer.cs rename to src/Managing.Infrastructure.Database/Migrations/20250803201734_AddTickerToBots.Designer.cs index 3c957ad..6b0a031 100644 --- a/src/Managing.Infrastructure.Database/Migrations/20250725172635_AddUserIdToMoneyManagement.Designer.cs +++ b/src/Managing.Infrastructure.Database/Migrations/20250803201734_AddTickerToBots.Designer.cs @@ -12,8 +12,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace Managing.Infrastructure.Databases.Migrations { [DbContext(typeof(ManagingDbContext))] - [Migration("20250725172635_AddUserIdToMoneyManagement")] - partial class AddUserIdToMoneyManagement + [Migration("20250803201734_AddTickerToBots")] + partial class AddTickerToBots { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -177,50 +177,71 @@ namespace Managing.Infrastructure.Databases.Migrations b.ToTable("Backtests"); }); - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotBackupEntity", b => + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotEntity", b => { - b.Property("Id") + b.Property("Identifier") .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + .HasMaxLength(255) + .HasColumnType("uuid"); b.Property("CreateDate") .HasColumnType("timestamp with time zone"); - b.Property("Data") - .IsRequired() - .HasColumnType("text"); + b.Property("Fees") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); - b.Property("Identifier") + b.Property("Name") .IsRequired() .HasMaxLength(255) .HasColumnType("character varying(255)"); - b.Property("LastStatus") + b.Property("Pnl") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("Roi") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("StartupTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ticker") + .HasColumnType("integer"); + + b.Property("TradeLosses") + .HasColumnType("integer"); + + b.Property("TradeWins") .HasColumnType("integer"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); - b.Property("UserName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); + b.Property("UserId") + .HasColumnType("integer"); - b.HasKey("Id"); + b.Property("Volume") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.HasKey("Identifier"); b.HasIndex("CreateDate"); b.HasIndex("Identifier") .IsUnique(); - b.HasIndex("LastStatus"); + b.HasIndex("Status"); - b.HasIndex("UserName"); + b.HasIndex("UserId"); - b.HasIndex("UserName", "CreateDate"); - - b.ToTable("BotBackups"); + b.ToTable("Bots"); }); modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BundleBacktestRequestEntity", b => @@ -312,40 +333,6 @@ namespace Managing.Infrastructure.Databases.Migrations b.ToTable("BundleBacktestRequests"); }); - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.FeeEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Cost") - .HasColumnType("decimal(18,8)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Exchange") - .IsRequired() - .HasColumnType("text"); - - b.Property("LastUpdate") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("Exchange") - .IsUnique(); - - b.HasIndex("LastUpdate"); - - b.ToTable("Fees"); - }); - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.FundingRateEntity", b => { b.Property("Id") @@ -648,11 +635,9 @@ namespace Managing.Infrastructure.Databases.Migrations modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => { - b.Property("Id") + b.Property("Identifier") .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + .HasColumnType("uuid"); b.Property("AccountName") .IsRequired() @@ -665,11 +650,6 @@ namespace Managing.Infrastructure.Databases.Migrations b.Property("Date") .HasColumnType("timestamp with time zone"); - b.Property("Identifier") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - b.Property("Initiator") .IsRequired() .HasColumnType("text"); @@ -716,7 +696,7 @@ namespace Managing.Infrastructure.Databases.Migrations .HasMaxLength(255) .HasColumnType("character varying(255)"); - b.HasKey("Id"); + b.HasKey("Identifier"); b.HasIndex("CreatedAt"); @@ -1280,6 +1260,17 @@ namespace Managing.Infrastructure.Databases.Migrations b.Navigation("User"); }); + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BundleBacktestRequestEntity", b => { b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") diff --git a/src/Managing.Infrastructure.Database/Migrations/20250725092603_RemoveOptimizedMoneyManagementJsonFromBacktestEntity.cs b/src/Managing.Infrastructure.Database/Migrations/20250803201734_AddTickerToBots.cs similarity index 55% rename from src/Managing.Infrastructure.Database/Migrations/20250725092603_RemoveOptimizedMoneyManagementJsonFromBacktestEntity.cs rename to src/Managing.Infrastructure.Database/Migrations/20250803201734_AddTickerToBots.cs index 90bade7..3225d19 100644 --- a/src/Managing.Infrastructure.Database/Migrations/20250725092603_RemoveOptimizedMoneyManagementJsonFromBacktestEntity.cs +++ b/src/Managing.Infrastructure.Database/Migrations/20250803201734_AddTickerToBots.cs @@ -5,25 +5,25 @@ namespace Managing.Infrastructure.Databases.Migrations { /// - public partial class RemoveOptimizedMoneyManagementJsonFromBacktestEntity : Migration + public partial class AddTickerToBots : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) { - migrationBuilder.DropColumn( - name: "OptimizedMoneyManagementJson", - table: "Backtests"); + migrationBuilder.AddColumn( + name: "Ticker", + table: "Bots", + type: "integer", + nullable: false, + defaultValue: 0); } /// protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.AddColumn( - name: "OptimizedMoneyManagementJson", - table: "Backtests", - type: "jsonb", - nullable: false, - defaultValue: ""); + migrationBuilder.DropColumn( + name: "Ticker", + table: "Bots"); } } } diff --git a/src/Managing.Infrastructure.Database/Migrations/20250725173315_AddUserIdToBotBackup.Designer.cs b/src/Managing.Infrastructure.Database/Migrations/20250803204725_UpdateBotTicker.Designer.cs similarity index 95% rename from src/Managing.Infrastructure.Database/Migrations/20250725173315_AddUserIdToBotBackup.Designer.cs rename to src/Managing.Infrastructure.Database/Migrations/20250803204725_UpdateBotTicker.Designer.cs index 01d42bf..da20376 100644 --- a/src/Managing.Infrastructure.Database/Migrations/20250725173315_AddUserIdToBotBackup.Designer.cs +++ b/src/Managing.Infrastructure.Database/Migrations/20250803204725_UpdateBotTicker.Designer.cs @@ -12,8 +12,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace Managing.Infrastructure.Databases.Migrations { [DbContext(typeof(ManagingDbContext))] - [Migration("20250725173315_AddUserIdToBotBackup")] - partial class AddUserIdToBotBackup + [Migration("20250803204725_UpdateBotTicker")] + partial class UpdateBotTicker { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -177,55 +177,71 @@ namespace Managing.Infrastructure.Databases.Migrations b.ToTable("Backtests"); }); - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotBackupEntity", b => + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotEntity", b => { - b.Property("Id") + b.Property("Identifier") .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + .HasMaxLength(255) + .HasColumnType("uuid"); b.Property("CreateDate") .HasColumnType("timestamp with time zone"); - b.Property("Data") - .IsRequired() - .HasColumnType("text"); + b.Property("Fees") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); - b.Property("Identifier") + b.Property("Name") .IsRequired() .HasMaxLength(255) .HasColumnType("character varying(255)"); - b.Property("LastStatus") + b.Property("Pnl") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("Roi") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("StartupTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ticker") + .HasColumnType("integer"); + + b.Property("TradeLosses") + .HasColumnType("integer"); + + b.Property("TradeWins") .HasColumnType("integer"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); - b.Property("UserId") + b.Property("UserId") .HasColumnType("integer"); - b.Property("UserName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); + b.Property("Volume") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); - b.HasKey("Id"); + b.HasKey("Identifier"); b.HasIndex("CreateDate"); b.HasIndex("Identifier") .IsUnique(); - b.HasIndex("LastStatus"); + b.HasIndex("Status"); b.HasIndex("UserId"); - b.HasIndex("UserName"); - - b.HasIndex("UserName", "CreateDate"); - - b.ToTable("BotBackups"); + b.ToTable("Bots"); }); modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BundleBacktestRequestEntity", b => @@ -317,40 +333,6 @@ namespace Managing.Infrastructure.Databases.Migrations b.ToTable("BundleBacktestRequests"); }); - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.FeeEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Cost") - .HasColumnType("decimal(18,8)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Exchange") - .IsRequired() - .HasColumnType("text"); - - b.Property("LastUpdate") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("Exchange") - .IsUnique(); - - b.HasIndex("LastUpdate"); - - b.ToTable("Fees"); - }); - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.FundingRateEntity", b => { b.Property("Id") @@ -653,11 +635,9 @@ namespace Managing.Infrastructure.Databases.Migrations modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => { - b.Property("Id") + b.Property("Identifier") .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + .HasColumnType("uuid"); b.Property("AccountName") .IsRequired() @@ -670,11 +650,6 @@ namespace Managing.Infrastructure.Databases.Migrations b.Property("Date") .HasColumnType("timestamp with time zone"); - b.Property("Identifier") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - b.Property("Initiator") .IsRequired() .HasColumnType("text"); @@ -721,7 +696,7 @@ namespace Managing.Infrastructure.Databases.Migrations .HasMaxLength(255) .HasColumnType("character varying(255)"); - b.HasKey("Id"); + b.HasKey("Identifier"); b.HasIndex("CreatedAt"); @@ -1285,12 +1260,13 @@ namespace Managing.Infrastructure.Databases.Migrations b.Navigation("User"); }); - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotBackupEntity", b => + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotEntity", b => { b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") .WithMany() .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.SetNull); + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); b.Navigation("User"); }); diff --git a/src/Managing.Infrastructure.Database/Migrations/20250803204725_UpdateBotTicker.cs b/src/Managing.Infrastructure.Database/Migrations/20250803204725_UpdateBotTicker.cs new file mode 100644 index 0000000..46c6b49 --- /dev/null +++ b/src/Managing.Infrastructure.Database/Migrations/20250803204725_UpdateBotTicker.cs @@ -0,0 +1,22 @@ +īģŋusing Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Managing.Infrastructure.Databases.Migrations +{ + /// + public partial class UpdateBotTicker : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/src/Managing.Infrastructure.Database/Migrations/20250803231246_AddAgentSummaryEntity.Designer.cs b/src/Managing.Infrastructure.Database/Migrations/20250803231246_AddAgentSummaryEntity.Designer.cs new file mode 100644 index 0000000..f3e206a --- /dev/null +++ b/src/Managing.Infrastructure.Database/Migrations/20250803231246_AddAgentSummaryEntity.Designer.cs @@ -0,0 +1,1428 @@ +īģŋ// +using System; +using Managing.Infrastructure.Databases.PostgreSql; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Managing.Infrastructure.Databases.Migrations +{ + [DbContext(typeof(ManagingDbContext))] + [Migration("20250803231246_AddAgentSummaryEntity")] + partial class AddAgentSummaryEntity + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AccountEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Exchange") + .IsRequired() + .HasColumnType("text"); + + b.Property("Key") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Secret") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Key"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AgentSummaryEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AgentName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Losses") + .HasColumnType("integer"); + + b.Property("Runtime") + .HasColumnType("timestamp with time zone"); + + b.Property("TotalPnL") + .HasColumnType("decimal(18,8)"); + + b.Property("TotalROI") + .HasColumnType("decimal(18,8)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("Wins") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AgentName"); + + b.HasIndex("TotalPnL"); + + b.HasIndex("UpdatedAt"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("AgentSummaries"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BacktestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ConfigJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Fees") + .HasColumnType("decimal(18,8)"); + + b.Property("FinalPnl") + .HasColumnType("decimal(18,8)"); + + b.Property("GrowthPercentage") + .HasColumnType("decimal(18,8)"); + + b.Property("HodlPercentage") + .HasColumnType("decimal(18,8)"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Metadata") + .HasColumnType("text"); + + b.Property("MoneyManagementJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("PositionsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RequestId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Score") + .HasColumnType("double precision"); + + b.Property("ScoreMessage") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("text"); + + b.Property("SignalsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("StatisticsJson") + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("WinRate") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("EndDate"); + + b.HasIndex("FinalPnl"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("RequestId"); + + b.HasIndex("Score"); + + b.HasIndex("StartDate"); + + b.HasIndex("UserName"); + + b.HasIndex("WinRate"); + + b.HasIndex("RequestId", "Score"); + + b.HasIndex("UserName", "Score"); + + b.ToTable("Backtests"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotEntity", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasMaxLength(255) + .HasColumnType("uuid"); + + b.Property("CreateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Fees") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Pnl") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("Roi") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("StartupTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ticker") + .HasColumnType("integer"); + + b.Property("TradeLosses") + .HasColumnType("integer"); + + b.Property("TradeWins") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("Volume") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.HasKey("Identifier"); + + b.HasIndex("CreateDate"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.ToTable("Bots"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BundleBacktestRequestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BacktestRequestsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CompletedBacktests") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentBacktest") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ErrorMessage") + .HasColumnType("text"); + + b.Property("EstimatedTimeRemainingSeconds") + .HasColumnType("integer"); + + b.Property("FailedBacktests") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ProgressInfo") + .HasColumnType("text"); + + b.Property("RequestId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ResultsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("TotalBacktests") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("CompletedAt"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("RequestId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.HasIndex("UserName"); + + b.HasIndex("UserName", "CreatedAt"); + + b.ToTable("BundleBacktestRequests"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.FundingRateEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Direction") + .HasColumnType("integer"); + + b.Property("Exchange") + .HasColumnType("integer"); + + b.Property("OpenInterest") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("Rate") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("Ticker") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("Direction"); + + b.HasIndex("Exchange"); + + b.HasIndex("Ticker"); + + b.HasIndex("Exchange", "Date"); + + b.HasIndex("Ticker", "Exchange"); + + b.HasIndex("Ticker", "Exchange", "Date") + .IsUnique(); + + b.ToTable("FundingRates"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.GeneticRequestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Balance") + .HasColumnType("decimal(18,8)"); + + b.Property("BestChromosome") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("BestFitness") + .HasColumnType("double precision"); + + b.Property("BestFitnessSoFar") + .HasColumnType("double precision"); + + b.Property("BestIndividual") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CrossoverMethod") + .IsRequired() + .HasColumnType("text"); + + b.Property("CurrentGeneration") + .HasColumnType("integer"); + + b.Property("EligibleIndicatorsJson") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("ElitismPercentage") + .HasColumnType("integer"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Generations") + .HasColumnType("integer"); + + b.Property("MaxTakeProfit") + .HasColumnType("double precision"); + + b.Property("MutationMethod") + .IsRequired() + .HasColumnType("text"); + + b.Property("MutationRate") + .HasColumnType("double precision"); + + b.Property("PopulationSize") + .HasColumnType("integer"); + + b.Property("ProgressInfo") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("RequestId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SelectionMethod") + .IsRequired() + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("RequestId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.ToTable("GeneticRequests"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CyclePeriods") + .HasColumnType("integer"); + + b.Property("FastPeriods") + .HasColumnType("integer"); + + b.Property("MinimumHistory") + .HasColumnType("integer"); + + b.Property("Multiplier") + .HasColumnType("double precision"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Period") + .HasColumnType("integer"); + + b.Property("SignalPeriods") + .HasColumnType("integer"); + + b.Property("SignalType") + .IsRequired() + .HasColumnType("text"); + + b.Property("SlowPeriods") + .HasColumnType("integer"); + + b.Property("SmoothPeriods") + .HasColumnType("integer"); + + b.Property("StochPeriods") + .HasColumnType("integer"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("Name"); + + b.HasIndex("Type"); + + b.HasIndex("UserName"); + + b.HasIndex("UserName", "Name"); + + b.ToTable("Indicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.MoneyManagementEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Leverage") + .HasColumnType("decimal(18,8)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("StopLoss") + .HasColumnType("decimal(18,8)"); + + b.Property("TakeProfit") + .HasColumnType("decimal(18,8)"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("UserName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.HasIndex("UserId"); + + b.HasIndex("UserName"); + + b.HasIndex("UserName", "Name"); + + b.ToTable("MoneyManagements"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Initiator") + .IsRequired() + .HasColumnType("text"); + + b.Property("MoneyManagementJson") + .HasColumnType("text"); + + b.Property("OpenTradeId") + .HasColumnType("integer"); + + b.Property("OriginDirection") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProfitAndLoss") + .HasColumnType("decimal(18,8)"); + + b.Property("SignalIdentifier") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("StopLossTradeId") + .HasColumnType("integer"); + + b.Property("TakeProfit1TradeId") + .HasColumnType("integer"); + + b.Property("TakeProfit2TradeId") + .HasColumnType("integer"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Identifier"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("Date"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("Initiator"); + + b.HasIndex("OpenTradeId"); + + b.HasIndex("Status"); + + b.HasIndex("StopLossTradeId"); + + b.HasIndex("TakeProfit1TradeId"); + + b.HasIndex("TakeProfit2TradeId"); + + b.HasIndex("UserName"); + + b.HasIndex("UserName", "Identifier"); + + b.ToTable("Positions"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LoopbackPeriod") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("Name"); + + b.HasIndex("UserName"); + + b.HasIndex("UserName", "Name"); + + b.ToTable("Scenarios"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioIndicatorEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IndicatorId") + .HasColumnType("integer"); + + b.Property("ScenarioId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IndicatorId"); + + b.HasIndex("ScenarioId"); + + b.HasIndex("ScenarioId", "IndicatorId") + .IsUnique(); + + b.ToTable("ScenarioIndicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SignalEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandleJson") + .HasColumnType("text"); + + b.Property("Confidence") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Direction") + .IsRequired() + .HasColumnType("text"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IndicatorName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SignalType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("Date"); + + b.HasIndex("Identifier"); + + b.HasIndex("Status"); + + b.HasIndex("Ticker"); + + b.HasIndex("UserName"); + + b.HasIndex("UserName", "Date"); + + b.HasIndex("Identifier", "Date", "UserName") + .IsUnique(); + + b.ToTable("Signals"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SpotlightOverviewEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Identifier") + .HasColumnType("uuid"); + + b.Property("ScenarioCount") + .HasColumnType("integer"); + + b.Property("SpotlightsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DateTime"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("ScenarioCount"); + + b.HasIndex("DateTime", "ScenarioCount"); + + b.ToTable("SpotlightOverviews"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SynthMinersLeaderboardEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Asset") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CacheKey") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBacktest") + .HasColumnType("boolean"); + + b.Property("MinersData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("SignalDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeIncrement") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CacheKey") + .IsUnique(); + + b.HasIndex("CreatedAt"); + + b.ToTable("SynthMinersLeaderboards"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SynthPredictionEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Asset") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CacheKey") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBacktest") + .HasColumnType("boolean"); + + b.Property("MinerUid") + .HasColumnType("integer"); + + b.Property("PredictionData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("SignalDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeIncrement") + .HasColumnType("integer"); + + b.Property("TimeLength") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CacheKey") + .IsUnique(); + + b.HasIndex("CreatedAt"); + + b.ToTable("SynthPredictions"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TopVolumeTickerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Exchange") + .HasColumnType("integer"); + + b.Property("Rank") + .HasColumnType("integer"); + + b.Property("Ticker") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Volume") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("Exchange"); + + b.HasIndex("Rank"); + + b.HasIndex("Ticker"); + + b.HasIndex("Date", "Rank"); + + b.HasIndex("Exchange", "Date"); + + b.ToTable("TopVolumeTickers"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Direction") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExchangeOrderId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Fee") + .HasColumnType("decimal(18,8)"); + + b.Property("Leverage") + .HasColumnType("decimal(18,8)"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("decimal(18,8)"); + + b.Property("Quantity") + .HasColumnType("decimal(18,8)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("TradeType") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("Date"); + + b.HasIndex("ExchangeOrderId"); + + b.HasIndex("Status"); + + b.ToTable("Trades"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TraderEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AverageLoss") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("AverageWin") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBestTrader") + .HasColumnType("boolean"); + + b.Property("Pnl") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("Roi") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("TradeCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Winrate") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Address"); + + b.HasIndex("IsBestTrader"); + + b.HasIndex("Pnl"); + + b.HasIndex("Roi"); + + b.HasIndex("Winrate"); + + b.HasIndex("Address", "IsBestTrader") + .IsUnique(); + + b.HasIndex("IsBestTrader", "Roi"); + + b.HasIndex("IsBestTrader", "Winrate"); + + b.ToTable("Traders"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AgentName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AvatarUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("TelegramChannel") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.WorkerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DelayTicks") + .HasColumnType("bigint"); + + b.Property("ExecutionCount") + .HasColumnType("integer"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastRunTime") + .HasColumnType("timestamp with time zone"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("WorkerType") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("WorkerType") + .IsUnique(); + + b.ToTable("Workers"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AccountEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AgentSummaryEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BundleBacktestRequestEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.GeneticRequestEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.MoneyManagementEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "OpenTrade") + .WithMany() + .HasForeignKey("OpenTradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "StopLossTrade") + .WithMany() + .HasForeignKey("StopLossTradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "TakeProfit1Trade") + .WithMany() + .HasForeignKey("TakeProfit1TradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "TakeProfit2Trade") + .WithMany() + .HasForeignKey("TakeProfit2TradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("OpenTrade"); + + b.Navigation("StopLossTrade"); + + b.Navigation("TakeProfit1Trade"); + + b.Navigation("TakeProfit2Trade"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioIndicatorEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", "Indicator") + .WithMany("ScenarioIndicators") + .HasForeignKey("IndicatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", "Scenario") + .WithMany("ScenarioIndicators") + .HasForeignKey("ScenarioId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Indicator"); + + b.Navigation("Scenario"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => + { + b.Navigation("ScenarioIndicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => + { + b.Navigation("ScenarioIndicators"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Managing.Infrastructure.Database/Migrations/20250803231246_AddAgentSummaryEntity.cs b/src/Managing.Infrastructure.Database/Migrations/20250803231246_AddAgentSummaryEntity.cs new file mode 100644 index 0000000..2dc1799 --- /dev/null +++ b/src/Managing.Infrastructure.Database/Migrations/20250803231246_AddAgentSummaryEntity.cs @@ -0,0 +1,71 @@ +īģŋusing System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Managing.Infrastructure.Databases.Migrations +{ + /// + public partial class AddAgentSummaryEntity : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AgentSummaries", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + UserId = table.Column(type: "integer", nullable: false), + AgentName = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + TotalPnL = table.Column(type: "numeric(18,8)", nullable: false), + TotalROI = table.Column(type: "numeric(18,8)", nullable: false), + Wins = table.Column(type: "integer", nullable: false), + Losses = table.Column(type: "integer", nullable: false), + Runtime = table.Column(type: "timestamp with time zone", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AgentSummaries", x => x.Id); + table.ForeignKey( + name: "FK_AgentSummaries_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_AgentSummaries_AgentName", + table: "AgentSummaries", + column: "AgentName"); + + migrationBuilder.CreateIndex( + name: "IX_AgentSummaries_TotalPnL", + table: "AgentSummaries", + column: "TotalPnL"); + + migrationBuilder.CreateIndex( + name: "IX_AgentSummaries_UpdatedAt", + table: "AgentSummaries", + column: "UpdatedAt"); + + migrationBuilder.CreateIndex( + name: "IX_AgentSummaries_UserId", + table: "AgentSummaries", + column: "UserId", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AgentSummaries"); + } + } +} diff --git a/src/Managing.Infrastructure.Database/Migrations/20250804200654_AddMissingAgentSummaryColumns.Designer.cs b/src/Managing.Infrastructure.Database/Migrations/20250804200654_AddMissingAgentSummaryColumns.Designer.cs new file mode 100644 index 0000000..6ab8f23 --- /dev/null +++ b/src/Managing.Infrastructure.Database/Migrations/20250804200654_AddMissingAgentSummaryColumns.Designer.cs @@ -0,0 +1,1435 @@ +īģŋ// +using System; +using Managing.Infrastructure.Databases.PostgreSql; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Managing.Infrastructure.Databases.Migrations +{ + [DbContext(typeof(ManagingDbContext))] + [Migration("20250804200654_AddMissingAgentSummaryColumns")] + partial class AddMissingAgentSummaryColumns + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AccountEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Exchange") + .IsRequired() + .HasColumnType("text"); + + b.Property("Key") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Secret") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Key"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AgentSummaryEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActiveStrategiesCount") + .HasColumnType("integer"); + + b.Property("AgentName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Losses") + .HasColumnType("integer"); + + b.Property("Runtime") + .HasColumnType("timestamp with time zone"); + + b.Property("TotalPnL") + .HasColumnType("decimal(18,8)"); + + b.Property("TotalROI") + .HasColumnType("decimal(18,8)"); + + b.Property("TotalVolume") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("Wins") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AgentName"); + + b.HasIndex("TotalPnL"); + + b.HasIndex("UpdatedAt"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("AgentSummaries"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BacktestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ConfigJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Fees") + .HasColumnType("decimal(18,8)"); + + b.Property("FinalPnl") + .HasColumnType("decimal(18,8)"); + + b.Property("GrowthPercentage") + .HasColumnType("decimal(18,8)"); + + b.Property("HodlPercentage") + .HasColumnType("decimal(18,8)"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Metadata") + .HasColumnType("text"); + + b.Property("MoneyManagementJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("PositionsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RequestId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Score") + .HasColumnType("double precision"); + + b.Property("ScoreMessage") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("text"); + + b.Property("SignalsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("StatisticsJson") + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("WinRate") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("EndDate"); + + b.HasIndex("FinalPnl"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("RequestId"); + + b.HasIndex("Score"); + + b.HasIndex("StartDate"); + + b.HasIndex("UserName"); + + b.HasIndex("WinRate"); + + b.HasIndex("RequestId", "Score"); + + b.HasIndex("UserName", "Score"); + + b.ToTable("Backtests"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotEntity", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasMaxLength(255) + .HasColumnType("uuid"); + + b.Property("CreateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Fees") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Pnl") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("Roi") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("StartupTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ticker") + .HasColumnType("integer"); + + b.Property("TradeLosses") + .HasColumnType("integer"); + + b.Property("TradeWins") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("Volume") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.HasKey("Identifier"); + + b.HasIndex("CreateDate"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.ToTable("Bots"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BundleBacktestRequestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BacktestRequestsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CompletedBacktests") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentBacktest") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ErrorMessage") + .HasColumnType("text"); + + b.Property("EstimatedTimeRemainingSeconds") + .HasColumnType("integer"); + + b.Property("FailedBacktests") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ProgressInfo") + .HasColumnType("text"); + + b.Property("RequestId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ResultsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("TotalBacktests") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("CompletedAt"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("RequestId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.HasIndex("UserName"); + + b.HasIndex("UserName", "CreatedAt"); + + b.ToTable("BundleBacktestRequests"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.FundingRateEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Direction") + .HasColumnType("integer"); + + b.Property("Exchange") + .HasColumnType("integer"); + + b.Property("OpenInterest") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("Rate") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("Ticker") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("Direction"); + + b.HasIndex("Exchange"); + + b.HasIndex("Ticker"); + + b.HasIndex("Exchange", "Date"); + + b.HasIndex("Ticker", "Exchange"); + + b.HasIndex("Ticker", "Exchange", "Date") + .IsUnique(); + + b.ToTable("FundingRates"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.GeneticRequestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Balance") + .HasColumnType("decimal(18,8)"); + + b.Property("BestChromosome") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("BestFitness") + .HasColumnType("double precision"); + + b.Property("BestFitnessSoFar") + .HasColumnType("double precision"); + + b.Property("BestIndividual") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CrossoverMethod") + .IsRequired() + .HasColumnType("text"); + + b.Property("CurrentGeneration") + .HasColumnType("integer"); + + b.Property("EligibleIndicatorsJson") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("ElitismPercentage") + .HasColumnType("integer"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Generations") + .HasColumnType("integer"); + + b.Property("MaxTakeProfit") + .HasColumnType("double precision"); + + b.Property("MutationMethod") + .IsRequired() + .HasColumnType("text"); + + b.Property("MutationRate") + .HasColumnType("double precision"); + + b.Property("PopulationSize") + .HasColumnType("integer"); + + b.Property("ProgressInfo") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("RequestId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SelectionMethod") + .IsRequired() + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("RequestId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.ToTable("GeneticRequests"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CyclePeriods") + .HasColumnType("integer"); + + b.Property("FastPeriods") + .HasColumnType("integer"); + + b.Property("MinimumHistory") + .HasColumnType("integer"); + + b.Property("Multiplier") + .HasColumnType("double precision"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Period") + .HasColumnType("integer"); + + b.Property("SignalPeriods") + .HasColumnType("integer"); + + b.Property("SignalType") + .IsRequired() + .HasColumnType("text"); + + b.Property("SlowPeriods") + .HasColumnType("integer"); + + b.Property("SmoothPeriods") + .HasColumnType("integer"); + + b.Property("StochPeriods") + .HasColumnType("integer"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("Name"); + + b.HasIndex("Type"); + + b.HasIndex("UserName"); + + b.HasIndex("UserName", "Name"); + + b.ToTable("Indicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.MoneyManagementEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Leverage") + .HasColumnType("decimal(18,8)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("StopLoss") + .HasColumnType("decimal(18,8)"); + + b.Property("TakeProfit") + .HasColumnType("decimal(18,8)"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("UserName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.HasIndex("UserId"); + + b.HasIndex("UserName"); + + b.HasIndex("UserName", "Name"); + + b.ToTable("MoneyManagements"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Initiator") + .IsRequired() + .HasColumnType("text"); + + b.Property("MoneyManagementJson") + .HasColumnType("text"); + + b.Property("OpenTradeId") + .HasColumnType("integer"); + + b.Property("OriginDirection") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProfitAndLoss") + .HasColumnType("decimal(18,8)"); + + b.Property("SignalIdentifier") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("StopLossTradeId") + .HasColumnType("integer"); + + b.Property("TakeProfit1TradeId") + .HasColumnType("integer"); + + b.Property("TakeProfit2TradeId") + .HasColumnType("integer"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Identifier"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("Date"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("Initiator"); + + b.HasIndex("OpenTradeId"); + + b.HasIndex("Status"); + + b.HasIndex("StopLossTradeId"); + + b.HasIndex("TakeProfit1TradeId"); + + b.HasIndex("TakeProfit2TradeId"); + + b.HasIndex("UserName"); + + b.HasIndex("UserName", "Identifier"); + + b.ToTable("Positions"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LoopbackPeriod") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("Name"); + + b.HasIndex("UserName"); + + b.HasIndex("UserName", "Name"); + + b.ToTable("Scenarios"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioIndicatorEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IndicatorId") + .HasColumnType("integer"); + + b.Property("ScenarioId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IndicatorId"); + + b.HasIndex("ScenarioId"); + + b.HasIndex("ScenarioId", "IndicatorId") + .IsUnique(); + + b.ToTable("ScenarioIndicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SignalEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandleJson") + .HasColumnType("text"); + + b.Property("Confidence") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Direction") + .IsRequired() + .HasColumnType("text"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IndicatorName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SignalType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("Date"); + + b.HasIndex("Identifier"); + + b.HasIndex("Status"); + + b.HasIndex("Ticker"); + + b.HasIndex("UserName"); + + b.HasIndex("UserName", "Date"); + + b.HasIndex("Identifier", "Date", "UserName") + .IsUnique(); + + b.ToTable("Signals"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SpotlightOverviewEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Identifier") + .HasColumnType("uuid"); + + b.Property("ScenarioCount") + .HasColumnType("integer"); + + b.Property("SpotlightsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DateTime"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("ScenarioCount"); + + b.HasIndex("DateTime", "ScenarioCount"); + + b.ToTable("SpotlightOverviews"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SynthMinersLeaderboardEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Asset") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CacheKey") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBacktest") + .HasColumnType("boolean"); + + b.Property("MinersData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("SignalDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeIncrement") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CacheKey") + .IsUnique(); + + b.HasIndex("CreatedAt"); + + b.ToTable("SynthMinersLeaderboards"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SynthPredictionEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Asset") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CacheKey") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBacktest") + .HasColumnType("boolean"); + + b.Property("MinerUid") + .HasColumnType("integer"); + + b.Property("PredictionData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("SignalDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeIncrement") + .HasColumnType("integer"); + + b.Property("TimeLength") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CacheKey") + .IsUnique(); + + b.HasIndex("CreatedAt"); + + b.ToTable("SynthPredictions"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TopVolumeTickerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Exchange") + .HasColumnType("integer"); + + b.Property("Rank") + .HasColumnType("integer"); + + b.Property("Ticker") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Volume") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("Exchange"); + + b.HasIndex("Rank"); + + b.HasIndex("Ticker"); + + b.HasIndex("Date", "Rank"); + + b.HasIndex("Exchange", "Date"); + + b.ToTable("TopVolumeTickers"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Direction") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExchangeOrderId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Fee") + .HasColumnType("decimal(18,8)"); + + b.Property("Leverage") + .HasColumnType("decimal(18,8)"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("decimal(18,8)"); + + b.Property("Quantity") + .HasColumnType("decimal(18,8)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("TradeType") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("Date"); + + b.HasIndex("ExchangeOrderId"); + + b.HasIndex("Status"); + + b.ToTable("Trades"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TraderEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AverageLoss") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("AverageWin") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBestTrader") + .HasColumnType("boolean"); + + b.Property("Pnl") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("Roi") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("TradeCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Winrate") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Address"); + + b.HasIndex("IsBestTrader"); + + b.HasIndex("Pnl"); + + b.HasIndex("Roi"); + + b.HasIndex("Winrate"); + + b.HasIndex("Address", "IsBestTrader") + .IsUnique(); + + b.HasIndex("IsBestTrader", "Roi"); + + b.HasIndex("IsBestTrader", "Winrate"); + + b.ToTable("Traders"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AgentName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AvatarUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("TelegramChannel") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.WorkerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DelayTicks") + .HasColumnType("bigint"); + + b.Property("ExecutionCount") + .HasColumnType("integer"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastRunTime") + .HasColumnType("timestamp with time zone"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("WorkerType") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("WorkerType") + .IsUnique(); + + b.ToTable("Workers"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AccountEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AgentSummaryEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BundleBacktestRequestEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.GeneticRequestEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.MoneyManagementEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "OpenTrade") + .WithMany() + .HasForeignKey("OpenTradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "StopLossTrade") + .WithMany() + .HasForeignKey("StopLossTradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "TakeProfit1Trade") + .WithMany() + .HasForeignKey("TakeProfit1TradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "TakeProfit2Trade") + .WithMany() + .HasForeignKey("TakeProfit2TradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("OpenTrade"); + + b.Navigation("StopLossTrade"); + + b.Navigation("TakeProfit1Trade"); + + b.Navigation("TakeProfit2Trade"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioIndicatorEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", "Indicator") + .WithMany("ScenarioIndicators") + .HasForeignKey("IndicatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", "Scenario") + .WithMany("ScenarioIndicators") + .HasForeignKey("ScenarioId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Indicator"); + + b.Navigation("Scenario"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => + { + b.Navigation("ScenarioIndicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => + { + b.Navigation("ScenarioIndicators"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Managing.Infrastructure.Database/Migrations/20250804200654_AddMissingAgentSummaryColumns.cs b/src/Managing.Infrastructure.Database/Migrations/20250804200654_AddMissingAgentSummaryColumns.cs new file mode 100644 index 0000000..622f0a3 --- /dev/null +++ b/src/Managing.Infrastructure.Database/Migrations/20250804200654_AddMissingAgentSummaryColumns.cs @@ -0,0 +1,42 @@ +īģŋusing Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Managing.Infrastructure.Databases.Migrations +{ + /// + public partial class AddMissingAgentSummaryColumns : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ActiveStrategiesCount", + table: "AgentSummaries", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "TotalVolume", + table: "AgentSummaries", + type: "numeric(18,8)", + precision: 18, + scale: 8, + nullable: false, + defaultValue: 0m); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ActiveStrategiesCount", + table: "AgentSummaries"); + + migrationBuilder.DropColumn( + name: "TotalVolume", + table: "AgentSummaries"); + } + } +} diff --git a/src/Managing.Infrastructure.Database/Migrations/ManagingDbContextModelSnapshot.cs b/src/Managing.Infrastructure.Database/Migrations/ManagingDbContextModelSnapshot.cs index 9e56873..b3a3bbd 100644 --- a/src/Managing.Infrastructure.Database/Migrations/ManagingDbContextModelSnapshot.cs +++ b/src/Managing.Infrastructure.Database/Migrations/ManagingDbContextModelSnapshot.cs @@ -66,6 +66,64 @@ namespace Managing.Infrastructure.Databases.Migrations b.ToTable("Accounts"); }); + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AgentSummaryEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActiveStrategiesCount") + .HasColumnType("integer"); + + b.Property("AgentName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Losses") + .HasColumnType("integer"); + + b.Property("Runtime") + .HasColumnType("timestamp with time zone"); + + b.Property("TotalPnL") + .HasColumnType("decimal(18,8)"); + + b.Property("TotalROI") + .HasColumnType("decimal(18,8)"); + + b.Property("TotalVolume") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("Wins") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AgentName"); + + b.HasIndex("TotalPnL"); + + b.HasIndex("UpdatedAt"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("AgentSummaries"); + }); + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BacktestEntity", b => { b.Property("Id") @@ -174,55 +232,71 @@ namespace Managing.Infrastructure.Databases.Migrations b.ToTable("Backtests"); }); - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotBackupEntity", b => + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotEntity", b => { - b.Property("Id") + b.Property("Identifier") .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + .HasMaxLength(255) + .HasColumnType("uuid"); b.Property("CreateDate") .HasColumnType("timestamp with time zone"); - b.Property("Data") - .IsRequired() - .HasColumnType("text"); + b.Property("Fees") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); - b.Property("Identifier") + b.Property("Name") .IsRequired() .HasMaxLength(255) .HasColumnType("character varying(255)"); - b.Property("LastStatus") + b.Property("Pnl") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("Roi") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("StartupTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ticker") + .HasColumnType("integer"); + + b.Property("TradeLosses") + .HasColumnType("integer"); + + b.Property("TradeWins") .HasColumnType("integer"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); - b.Property("UserId") + b.Property("UserId") .HasColumnType("integer"); - b.Property("UserName") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); + b.Property("Volume") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); - b.HasKey("Id"); + b.HasKey("Identifier"); b.HasIndex("CreateDate"); b.HasIndex("Identifier") .IsUnique(); - b.HasIndex("LastStatus"); + b.HasIndex("Status"); b.HasIndex("UserId"); - b.HasIndex("UserName"); - - b.HasIndex("UserName", "CreateDate"); - - b.ToTable("BotBackups"); + b.ToTable("Bots"); }); modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BundleBacktestRequestEntity", b => @@ -616,11 +690,9 @@ namespace Managing.Infrastructure.Databases.Migrations modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => { - b.Property("Id") + b.Property("Identifier") .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + .HasColumnType("uuid"); b.Property("AccountName") .IsRequired() @@ -633,11 +705,6 @@ namespace Managing.Infrastructure.Databases.Migrations b.Property("Date") .HasColumnType("timestamp with time zone"); - b.Property("Identifier") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - b.Property("Initiator") .IsRequired() .HasColumnType("text"); @@ -684,7 +751,7 @@ namespace Managing.Infrastructure.Databases.Migrations .HasMaxLength(255) .HasColumnType("character varying(255)"); - b.HasKey("Id"); + b.HasKey("Identifier"); b.HasIndex("CreatedAt"); @@ -1248,12 +1315,24 @@ namespace Managing.Infrastructure.Databases.Migrations b.Navigation("User"); }); - modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotBackupEntity", b => + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AgentSummaryEntity", b => { b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") .WithMany() .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.SetNull); + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); b.Navigation("User"); }); diff --git a/src/Managing.Infrastructure.Database/PostgreSql/AgentSummaryRepository.cs b/src/Managing.Infrastructure.Database/PostgreSql/AgentSummaryRepository.cs new file mode 100644 index 0000000..9c396f7 --- /dev/null +++ b/src/Managing.Infrastructure.Database/PostgreSql/AgentSummaryRepository.cs @@ -0,0 +1,232 @@ +using Managing.Application.Abstractions.Repositories; +using Managing.Domain.Statistics; +using Managing.Infrastructure.Databases.PostgreSql; +using Managing.Infrastructure.Databases.PostgreSql.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using static Managing.Common.Enums; + +namespace Managing.Infrastructure.Database.PostgreSql; + +public class AgentSummaryRepository : IAgentSummaryRepository +{ + private readonly ManagingDbContext _context; + private readonly ILogger _logger; + + public AgentSummaryRepository(ManagingDbContext context, ILogger logger) + { + _context = context; + _logger = logger; + } + + public async Task GetByUserIdAsync(int userId) + { + var entity = await _context.AgentSummaries + .Include(a => a.User) + .FirstOrDefaultAsync(a => a.UserId == userId); + + return entity != null ? MapToDomain(entity) : null; + } + + public async Task GetByAgentNameAsync(string agentName) + { + var entity = await _context.AgentSummaries + .Include(a => a.User) + .FirstOrDefaultAsync(a => a.AgentName == agentName); + + return entity != null ? MapToDomain(entity) : null; + } + + public async Task> GetAllAsync() + { + var entities = await _context.AgentSummaries + .Include(a => a.User) + .ToListAsync(); + + return entities.Select(MapToDomain); + } + + public async Task InsertAsync(AgentSummary agentSummary) + { + var entity = MapToEntity(agentSummary); + entity.CreatedAt = DateTime.UtcNow; + entity.UpdatedAt = DateTime.UtcNow; + + await _context.AgentSummaries.AddAsync(entity); + await _context.SaveChangesAsync(); + + _logger.LogInformation("AgentSummary inserted for user {UserId} with agent name {AgentName}", + agentSummary.UserId, agentSummary.AgentName); + } + + public async Task UpdateAsync(AgentSummary agentSummary) + { + var entity = await _context.AgentSummaries + .FirstOrDefaultAsync(a => a.UserId == agentSummary.UserId); + + if (entity != null) + { + MapToEntity(agentSummary, entity); + entity.UpdatedAt = DateTime.UtcNow; + + // No need to call Update() since the entity is already being tracked + await _context.SaveChangesAsync(); + + _logger.LogInformation("AgentSummary updated for user {UserId} with agent name {AgentName}", + agentSummary.UserId, agentSummary.AgentName); + } + } + + public async Task SaveOrUpdateAsync(AgentSummary agentSummary) + { + // Ensure the User entity exists and is saved + if (agentSummary.User != null) + { + var existingUser = await _context.Users + .FirstOrDefaultAsync(u => u.Id == agentSummary.UserId); + + if (existingUser == null) + { + // User doesn't exist, save it first + var userEntity = PostgreSqlMappers.Map(agentSummary.User); + await _context.Users.AddAsync(userEntity); + await _context.SaveChangesAsync(); + + _logger.LogInformation("User created for AgentSummary with ID {UserId}", agentSummary.UserId); + } + } + + var existing = await _context.AgentSummaries + .FirstOrDefaultAsync(a => a.UserId == agentSummary.UserId); + + if (existing == null) + { + await InsertAsync(agentSummary); + } + else + { + // Update existing record - modify the tracked entity directly + MapToEntity(agentSummary, existing); + existing.UpdatedAt = DateTime.UtcNow; + + // No need to call Update() since the entity is already being tracked + await _context.SaveChangesAsync(); + + _logger.LogInformation("AgentSummary updated for user {UserId} with agent name {AgentName}", + agentSummary.UserId, agentSummary.AgentName); + } + } + + public async Task<(IEnumerable Results, int TotalCount)> GetPaginatedAsync( + int page, + int pageSize, + SortableFields sortBy, + string sortOrder, + IEnumerable? agentNames = null) + { + // Start with base query + var query = _context.AgentSummaries.Include(a => a.User).AsQueryable(); + + // Apply agent name filtering if specified + if (agentNames != null && agentNames.Any()) + { + query = query.Where(a => agentNames.Contains(a.AgentName)); + } + + // Get total count before applying pagination + var totalCount = await query.CountAsync(); + + // Apply sorting + var isDescending = sortOrder.ToLowerInvariant() == "desc"; + query = sortBy switch + { + SortableFields.TotalPnL => isDescending + ? query.OrderByDescending(a => a.TotalPnL) + : query.OrderBy(a => a.TotalPnL), + SortableFields.TotalROI => isDescending + ? query.OrderByDescending(a => a.TotalROI) + : query.OrderBy(a => a.TotalROI), + SortableFields.Wins => isDescending + ? query.OrderByDescending(a => a.Wins) + : query.OrderBy(a => a.Wins), + SortableFields.Losses => isDescending + ? query.OrderByDescending(a => a.Losses) + : query.OrderBy(a => a.Losses), + SortableFields.AgentName => isDescending + ? query.OrderByDescending(a => a.AgentName) + : query.OrderBy(a => a.AgentName), + SortableFields.CreatedAt => isDescending + ? query.OrderByDescending(a => a.CreatedAt) + : query.OrderBy(a => a.CreatedAt), + SortableFields.UpdatedAt => isDescending + ? query.OrderByDescending(a => a.UpdatedAt) + : query.OrderBy(a => a.UpdatedAt), + _ => isDescending + ? query.OrderByDescending(a => a.TotalPnL) // Default to TotalPnL desc + : query.OrderBy(a => a.TotalPnL) + }; + + // Apply pagination + var results = await query + .Skip((page - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); + + // Map to domain objects + var domainResults = results.Select(MapToDomain); + + return (domainResults, totalCount); + } + + private static AgentSummaryEntity MapToEntity(AgentSummary domain) + { + return new AgentSummaryEntity + { + Id = domain.Id, + UserId = domain.UserId, + AgentName = domain.AgentName, + TotalPnL = domain.TotalPnL, + TotalROI = domain.TotalROI, + Wins = domain.Wins, + Losses = domain.Losses, + Runtime = domain.Runtime, + CreatedAt = domain.CreatedAt, + UpdatedAt = domain.UpdatedAt, + ActiveStrategiesCount = domain.ActiveStrategiesCount, + TotalVolume = domain.TotalVolume + }; + } + + private static void MapToEntity(AgentSummary domain, AgentSummaryEntity entity) + { + entity.UserId = domain.UserId; + entity.AgentName = domain.AgentName; + entity.TotalPnL = domain.TotalPnL; + entity.TotalROI = domain.TotalROI; + entity.Wins = domain.Wins; + entity.Losses = domain.Losses; + entity.Runtime = domain.Runtime; + entity.ActiveStrategiesCount = domain.ActiveStrategiesCount; + entity.TotalVolume = domain.TotalVolume; + } + + private static AgentSummary MapToDomain(AgentSummaryEntity entity) + { + return new AgentSummary + { + Id = entity.Id, + UserId = entity.UserId, + AgentName = entity.AgentName, + TotalPnL = entity.TotalPnL, + TotalROI = entity.TotalROI, + Wins = entity.Wins, + Losses = entity.Losses, + Runtime = entity.Runtime, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt, + ActiveStrategiesCount = entity.ActiveStrategiesCount, + TotalVolume = entity.TotalVolume, + User = PostgreSqlMappers.Map(entity.User) + }; + } +} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Database/PostgreSql/Entities/AgentSummaryEntity.cs b/src/Managing.Infrastructure.Database/PostgreSql/Entities/AgentSummaryEntity.cs new file mode 100644 index 0000000..af9a258 --- /dev/null +++ b/src/Managing.Infrastructure.Database/PostgreSql/Entities/AgentSummaryEntity.cs @@ -0,0 +1,20 @@ +namespace Managing.Infrastructure.Databases.PostgreSql.Entities; + +public class AgentSummaryEntity +{ + public int Id { get; set; } + public int UserId { get; set; } + public string AgentName { get; set; } + public decimal TotalPnL { get; set; } + public decimal TotalROI { get; set; } + public int Wins { get; set; } + public int Losses { get; set; } + public DateTime? Runtime { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + public int ActiveStrategiesCount { get; set; } + public decimal TotalVolume { get; set; } + + // Navigation property + public UserEntity User { get; set; } +} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Database/PostgreSql/Entities/BotBackupEntity.cs b/src/Managing.Infrastructure.Database/PostgreSql/Entities/BotBackupEntity.cs deleted file mode 100644 index fc2fc01..0000000 --- a/src/Managing.Infrastructure.Database/PostgreSql/Entities/BotBackupEntity.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using static Managing.Common.Enums; - -namespace Managing.Infrastructure.Databases.PostgreSql.Entities; - -[Table("BotBackups")] -public class BotBackupEntity -{ - [Key] - public int Id { get; set; } - - [Required] - [MaxLength(255)] - public string Identifier { get; set; } - - [MaxLength(255)] - public string? UserName { get; set; } - - public int? UserId { get; set; } - - // Navigation properties - [ForeignKey("UserId")] - public UserEntity? User { get; set; } - - /// - /// Bot configuration and state data stored as JSON string - /// - [Column(TypeName = "text")] - public string Data { get; set; } - - public BotStatus LastStatus { get; set; } - - public DateTime CreateDate { get; set; } = DateTime.UtcNow; - public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; -} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Database/PostgreSql/Entities/BotEntity.cs b/src/Managing.Infrastructure.Database/PostgreSql/Entities/BotEntity.cs new file mode 100644 index 0000000..b9499dd --- /dev/null +++ b/src/Managing.Infrastructure.Database/PostgreSql/Entities/BotEntity.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using static Managing.Common.Enums; + +namespace Managing.Infrastructure.Databases.PostgreSql.Entities; + +[Table("Bots")] +public class BotEntity +{ + [Key] public Guid Identifier { get; set; } + + [Required] [MaxLength(255)] public required string Name { get; set; } + + public Ticker Ticker { get; set; } + + public int UserId { get; set; } + + [Required] [ForeignKey("UserId")] public required UserEntity User { get; set; } + + public BotStatus Status { get; set; } + public DateTime CreateDate { get; set; } + public DateTime UpdatedAt { get; set; } + public DateTime StartupTime { get; set; } + public int TradeWins { get; set; } + public int TradeLosses { get; set; } + public decimal Pnl { get; set; } + public decimal Roi { get; set; } + public decimal Volume { get; set; } + public decimal Fees { get; set; } +} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Database/PostgreSql/Entities/PositionEntity.cs b/src/Managing.Infrastructure.Database/PostgreSql/Entities/PositionEntity.cs index 42e4e70..5aac60e 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/Entities/PositionEntity.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/Entities/PositionEntity.cs @@ -7,55 +7,41 @@ namespace Managing.Infrastructure.Databases.PostgreSql.Entities; [Table("Positions")] public class PositionEntity { - [Key] - public int Id { get; set; } - - [Required] - [MaxLength(255)] - public string Identifier { get; set; } - + [Key] [Required] public Guid Identifier { get; set; } + public DateTime Date { get; set; } - - [Column(TypeName = "decimal(18,8)")] - public decimal ProfitAndLoss { get; set; } - + + [Column(TypeName = "decimal(18,8)")] public decimal ProfitAndLoss { get; set; } + public TradeDirection OriginDirection { get; set; } public PositionStatus Status { get; set; } public Ticker Ticker { get; set; } public PositionInitiator Initiator { get; set; } - - [MaxLength(255)] - public string SignalIdentifier { get; set; } - - [MaxLength(255)] - public string AccountName { get; set; } - - [MaxLength(255)] - public string? UserName { get; set; } - + + [MaxLength(255)] public string SignalIdentifier { get; set; } + + [MaxLength(255)] public string AccountName { get; set; } + + [MaxLength(255)] public string? UserName { get; set; } + // Foreign keys to trades public int? OpenTradeId { get; set; } public int? StopLossTradeId { get; set; } public int? TakeProfit1TradeId { get; set; } public int? TakeProfit2TradeId { get; set; } - + // Money management data stored as JSON - [Column(TypeName = "text")] - public string? MoneyManagementJson { get; set; } - + [Column(TypeName = "text")] public string? MoneyManagementJson { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; - + // Navigation properties - [ForeignKey("OpenTradeId")] - public virtual TradeEntity? OpenTrade { get; set; } - - [ForeignKey("StopLossTradeId")] - public virtual TradeEntity? StopLossTrade { get; set; } - - [ForeignKey("TakeProfit1TradeId")] - public virtual TradeEntity? TakeProfit1Trade { get; set; } - - [ForeignKey("TakeProfit2TradeId")] - public virtual TradeEntity? TakeProfit2Trade { get; set; } -} \ No newline at end of file + [ForeignKey("OpenTradeId")] public virtual TradeEntity? OpenTrade { get; set; } + + [ForeignKey("StopLossTradeId")] public virtual TradeEntity? StopLossTrade { get; set; } + + [ForeignKey("TakeProfit1TradeId")] public virtual TradeEntity? TakeProfit1Trade { get; set; } + + [ForeignKey("TakeProfit2TradeId")] public virtual TradeEntity? TakeProfit2Trade { get; set; } +} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Database/PostgreSql/Entities/UserEntity.cs b/src/Managing.Infrastructure.Database/PostgreSql/Entities/UserEntity.cs index 679e4de..4667cd3 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/Entities/UserEntity.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/Entities/UserEntity.cs @@ -1,10 +1,14 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + namespace Managing.Infrastructure.Databases.PostgreSql.Entities; +[Table("Users")] public class UserEntity { - public int Id { get; set; } - public string Name { get; set; } - public string? AgentName { get; set; } + [Key] public int Id { get; set; } + [Required] [MaxLength(255)] public required string Name { get; set; } + [MaxLength(255)] public string? AgentName { get; set; } public string? AvatarUrl { get; set; } public string? TelegramChannel { get; set; } -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Database/PostgreSql/ManagingDbContext.cs b/src/Managing.Infrastructure.Database/PostgreSql/ManagingDbContext.cs index 9d9d53b..b16ed91 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/ManagingDbContext.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/ManagingDbContext.cs @@ -29,9 +29,10 @@ public class ManagingDbContext : DbContext public DbSet SpotlightOverviews { get; set; } public DbSet Traders { get; set; } public DbSet FundingRates { get; set; } + public DbSet AgentSummaries { get; set; } // Bot entities - public DbSet BotBackups { get; set; } + public DbSet Bots { get; set; } // Settings entities public DbSet MoneyManagements { get; set; } @@ -46,6 +47,10 @@ public class ManagingDbContext : DbContext { base.OnModelCreating(modelBuilder); + // Configure schema for Orleans tables (if needed for future organization) + // Orleans tables will remain in the default schema for now + // This can be changed later if needed by configuring specific schemas + // Configure Account entity modelBuilder.Entity(entity => { @@ -280,8 +285,7 @@ public class ManagingDbContext : DbContext // Configure Position entity modelBuilder.Entity(entity => { - entity.HasKey(e => e.Id); - entity.Property(e => e.Identifier).IsRequired().HasMaxLength(255); + entity.HasKey(e => e.Identifier); entity.Property(e => e.ProfitAndLoss).HasColumnType("decimal(18,8)"); entity.Property(e => e.OriginDirection).IsRequired().HasConversion(); entity.Property(e => e.Status).IsRequired().HasConversion(); @@ -348,7 +352,6 @@ public class ManagingDbContext : DbContext }); - // Configure TopVolumeTicker entity modelBuilder.Entity(entity => { @@ -425,22 +428,26 @@ public class ManagingDbContext : DbContext }); // Configure BotBackup entity - modelBuilder.Entity(entity => + modelBuilder.Entity(entity => { - entity.HasKey(e => e.Id); + entity.HasKey(e => e.Identifier); entity.Property(e => e.Identifier).IsRequired().HasMaxLength(255); - entity.Property(e => e.UserName).HasMaxLength(255); - entity.Property(e => e.Data).IsRequired().HasColumnType("text"); + entity.Property(e => e.Name).IsRequired().HasMaxLength(255); + entity.Property(e => e.Status).IsRequired().HasConversion(); + entity.Property(e => e.CreateDate).IsRequired(); + entity.Property(e => e.StartupTime).IsRequired(); + entity.Property(e => e.TradeWins).IsRequired(); + entity.Property(e => e.TradeLosses).IsRequired(); + entity.Property(e => e.Pnl).HasPrecision(18, 8); + entity.Property(e => e.Roi).HasPrecision(18, 8); + entity.Property(e => e.Volume).HasPrecision(18, 8); + entity.Property(e => e.Fees).HasPrecision(18, 8); // Create indexes entity.HasIndex(e => e.Identifier).IsUnique(); - entity.HasIndex(e => e.UserName); - entity.HasIndex(e => e.LastStatus); + entity.HasIndex(e => e.Status); entity.HasIndex(e => e.CreateDate); - // Composite index for user bots - entity.HasIndex(e => new { e.UserName, e.CreateDate }); - // Configure relationship with User entity.HasOne(e => e.User) .WithMany() @@ -517,5 +524,105 @@ public class ManagingDbContext : DbContext entity.HasIndex(e => e.CacheKey).IsUnique(); entity.HasIndex(e => e.CreatedAt); }); + + // Configure AgentSummary entity + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.UserId).IsRequired(); + entity.Property(e => e.AgentName).IsRequired().HasMaxLength(255); + entity.Property(e => e.TotalPnL).HasColumnType("decimal(18,8)"); + entity.Property(e => e.TotalROI).HasColumnType("decimal(18,8)"); + entity.Property(e => e.Wins).IsRequired(); + entity.Property(e => e.Losses).IsRequired(); + entity.Property(e => e.Runtime); + entity.Property(e => e.CreatedAt).IsRequired(); + entity.Property(e => e.UpdatedAt).IsRequired(); + entity.Property(e => e.ActiveStrategiesCount).IsRequired(); + entity.Property(e => e.TotalVolume).HasPrecision(18, 8); + + // Create indexes for common queries + entity.HasIndex(e => e.UserId).IsUnique(); + entity.HasIndex(e => e.AgentName); + entity.HasIndex(e => e.TotalPnL); + entity.HasIndex(e => e.UpdatedAt); + + // Configure relationship with User + entity.HasOne(e => e.User) + .WithMany() + .HasForeignKey(e => e.UserId) + .OnDelete(DeleteBehavior.Cascade); + }); + + + } + + /// + /// Ensures Orleans tables are properly initialized in the database. + /// This method can be called during application startup to verify Orleans infrastructure. + /// + public async Task EnsureOrleansTablesExistAsync() + { + // Orleans tables are automatically created by the Orleans framework + // when using AdoNetClustering and AdoNetReminderService. + // This method serves as a verification point and can be extended + // for custom Orleans table management if needed. + + // For now, we just ensure the database is accessible + await Database.CanConnectAsync(); + } + + /// + /// Gets Orleans table statistics for monitoring purposes. + /// This helps track Orleans table sizes and performance. + /// + public async Task> GetOrleansTableStatsAsync() + { + var stats = new Dictionary(); + + // Orleans table names + var orleansTables = new[] + { + "orleansmembershiptable", + "orleansmembershipversiontable", + "orleansquery", + "orleansreminderstable", + "orleansstorage" + }; + + foreach (var tableName in orleansTables) + { + try + { + var count = await Database.SqlQueryRaw($"SELECT COUNT(*) FROM {tableName}").FirstOrDefaultAsync(); + stats[tableName] = count; + } + catch + { + // Table might not exist yet (normal during startup) + stats[tableName] = -1; + } + } + + return stats; + } + + /// + /// Database organization strategy: + /// - Application tables: Default schema (public) + /// - Orleans tables: Default schema (public) - managed by Orleans framework + /// - Future consideration: Move Orleans tables to 'orleans' schema if needed + /// + /// Benefits of current approach: + /// - Single database simplifies deployment and backup + /// - Orleans tables are automatically managed by the framework + /// - No additional configuration complexity + /// - Easier monitoring and maintenance + /// + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + + // Add any additional configuration here if needed } } \ No newline at end of file diff --git a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlBotRepository.cs b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlBotRepository.cs index 02e9143..dbc5a06 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlBotRepository.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlBotRepository.cs @@ -1,6 +1,7 @@ using Managing.Application.Abstractions.Repositories; using Managing.Domain.Bots; using Microsoft.EntityFrameworkCore; +using static Managing.Common.Enums; namespace Managing.Infrastructure.Databases.PostgreSql; @@ -13,7 +14,7 @@ public class PostgreSqlBotRepository : IBotRepository _context = context; } - public async Task InsertBotAsync(BotBackup bot) + public async Task InsertBotAsync(Bot bot) { bot.CreateDate = DateTime.UtcNow; var entity = PostgreSqlMappers.Map(bot); @@ -22,18 +23,24 @@ public class PostgreSqlBotRepository : IBotRepository { var userEntity = await _context.Users .AsNoTracking() - .FirstOrDefaultAsync(u => u.Name == bot.User.Name) + .FirstOrDefaultAsync(u => u.Id == bot.User.Id) .ConfigureAwait(false); - entity.UserId = userEntity?.Id; + + if (userEntity == null) + { + throw new InvalidOperationException($"User with id '{bot.User.Id}' not found"); + } + + entity.UserId = userEntity.Id; } - await _context.BotBackups.AddAsync(entity).ConfigureAwait(false); + await _context.Bots.AddAsync(entity).ConfigureAwait(false); await _context.SaveChangesAsync().ConfigureAwait(false); } - public async Task> GetBotsAsync() + public async Task> GetBotsAsync() { - var entities = await _context.BotBackups + var entities = await _context.Bots .AsNoTracking() .Include(m => m.User) .ToListAsync() @@ -42,9 +49,9 @@ public class PostgreSqlBotRepository : IBotRepository return PostgreSqlMappers.Map(entities); } - public async Task UpdateBackupBot(BotBackup bot) + public async Task UpdateBot(Bot bot) { - var existingEntity = await _context.BotBackups + var existingEntity = await _context.Bots .AsTracking() .FirstOrDefaultAsync(b => b.Identifier == bot.Identifier) .ConfigureAwait(false); @@ -54,18 +61,25 @@ public class PostgreSqlBotRepository : IBotRepository throw new InvalidOperationException($"Bot backup with identifier '{bot.Identifier}' not found"); } - // Update the entity properties - existingEntity.Data = bot.SerializeData(); // Use the serialized data string - existingEntity.LastStatus = bot.LastStatus; + // Update the existing entity properties directly instead of creating a new one + existingEntity.Name = bot.Name; + existingEntity.Ticker = bot.Ticker; + existingEntity.Status = bot.Status; + existingEntity.StartupTime = bot.StartupTime; + existingEntity.TradeWins = bot.TradeWins; + existingEntity.TradeLosses = bot.TradeLosses; + existingEntity.Pnl = bot.Pnl; + existingEntity.Roi = bot.Roi; + existingEntity.Volume = bot.Volume; + existingEntity.Fees = bot.Fees; existingEntity.UpdatedAt = DateTime.UtcNow; - existingEntity.UserName = bot.User?.Name; await _context.SaveChangesAsync().ConfigureAwait(false); } - public async Task DeleteBotBackup(string identifier) + public async Task DeleteBot(Guid identifier) { - var entity = await _context.BotBackups + var entity = await _context.Bots .AsTracking() .FirstOrDefaultAsync(b => b.Identifier == identifier) .ConfigureAwait(false); @@ -75,17 +89,142 @@ public class PostgreSqlBotRepository : IBotRepository throw new InvalidOperationException($"Bot backup with identifier '{identifier}' not found"); } - _context.BotBackups.Remove(entity); + _context.Bots.Remove(entity); await _context.SaveChangesAsync().ConfigureAwait(false); } - public async Task GetBotByIdentifierAsync(string identifier) + public async Task GetBotByIdentifierAsync(Guid identifier) { - var entity = await _context.BotBackups + var entity = await _context.Bots .AsNoTracking() .Include(m => m.User) .FirstOrDefaultAsync(b => b.Identifier == identifier) .ConfigureAwait(false); return PostgreSqlMappers.Map(entity); } + + public async Task> GetBotsByUserIdAsync(int id) + { + var entities = await _context.Bots + .AsNoTracking() + .Include(m => m.User) + .Where(b => b.UserId == id) + .ToListAsync() + .ConfigureAwait(false); + return PostgreSqlMappers.Map(entities); + } + + public async Task> GetBotsByStatusAsync(BotStatus status) + { + var entities = await _context.Bots + .AsNoTracking() + .Include(m => m.User) + .Where(b => b.Status == status) + .ToListAsync() + .ConfigureAwait(false); + return PostgreSqlMappers.Map(entities); + } + + public async Task GetBotByNameAsync(string name) + { + var entity = await _context.Bots + .AsNoTracking() + .Include(m => m.User) + .FirstOrDefaultAsync(b => b.Name == name) + .ConfigureAwait(false); + return PostgreSqlMappers.Map(entity); + } + + public async Task> GetBotsByIdsAsync(IEnumerable identifiers) + { + var entities = await _context.Bots + .AsNoTracking() + .Include(m => m.User) + .Where(b => identifiers.Contains(b.Identifier)) + .ToListAsync() + .ConfigureAwait(false); + + return PostgreSqlMappers.Map(entities); + } + + 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") + { + // Build the query with filters + var query = _context.Bots + .AsNoTracking() + .Include(m => m.User) + .AsQueryable(); + + // Apply filters + if (status.HasValue) + { + query = query.Where(b => b.Status == status.Value); + } + + if (!string.IsNullOrWhiteSpace(name)) + { + query = query.Where(b => EF.Functions.ILike(b.Name, $"%{name}%")); + } + + if (!string.IsNullOrWhiteSpace(ticker)) + { + query = query.Where(b => EF.Functions.ILike(b.Ticker.ToString(), $"%{ticker}%")); + } + + if (!string.IsNullOrWhiteSpace(agentName)) + { + query = query.Where(b => b.User != null && EF.Functions.ILike(b.User.AgentName, $"%{agentName}%")); + } + + // Get total count before applying pagination + var totalCount = await query.CountAsync().ConfigureAwait(false); + + // Apply sorting + query = sortBy.ToLower() switch + { + "name" => sortDirection.ToLower() == "asc" + ? query.OrderBy(b => b.Name) + : query.OrderByDescending(b => b.Name), + "ticker" => sortDirection.ToLower() == "asc" + ? query.OrderBy(b => b.Ticker) + : query.OrderByDescending(b => b.Ticker), + "status" => sortDirection.ToLower() == "asc" + ? query.OrderBy(b => b.Status) + : query.OrderByDescending(b => b.Status), + "startuptime" => sortDirection.ToLower() == "asc" + ? query.OrderBy(b => b.StartupTime) + : query.OrderByDescending(b => b.StartupTime), + "pnl" => sortDirection.ToLower() == "asc" + ? query.OrderBy(b => b.Pnl) + : query.OrderByDescending(b => b.Pnl), + "winrate" => sortDirection.ToLower() == "asc" + ? query.OrderBy(b => b.TradeWins / (b.TradeWins + b.TradeLosses)) + : query.OrderByDescending(b => b.TradeWins / (b.TradeWins + b.TradeLosses)), + "agentname" => sortDirection.ToLower() == "asc" + ? query.OrderBy(b => b.User.AgentName) + : query.OrderByDescending(b => b.User.AgentName), + _ => sortDirection.ToLower() == "asc" + ? query.OrderBy(b => b.CreateDate) + : query.OrderByDescending(b => b.CreateDate) // Default to CreateDate + }; + + // Apply pagination + var skip = (pageNumber - 1) * pageSize; + var entities = await query + .Skip(skip) + .Take(pageSize) + .ToListAsync() + .ConfigureAwait(false); + + var bots = PostgreSqlMappers.Map(entities); + return (bots, totalCount); + } } \ No newline at end of file diff --git a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlMappers.cs b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlMappers.cs index d5ea070..73bdb63 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlMappers.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlMappers.cs @@ -3,6 +3,7 @@ using Managing.Domain.Accounts; using Managing.Domain.Backtests; using Managing.Domain.Bots; using Managing.Domain.Candles; +using Managing.Domain.Indicators; using Managing.Domain.MoneyManagements; using Managing.Domain.Scenarios; using Managing.Domain.Statistics; @@ -13,7 +14,6 @@ using Managing.Domain.Workers; using Managing.Infrastructure.Databases.PostgreSql.Entities; using Newtonsoft.Json; using static Managing.Common.Enums; - using SystemJsonSerializer = System.Text.Json.JsonSerializer; namespace Managing.Infrastructure.Databases.PostgreSql; @@ -125,7 +125,8 @@ public static class PostgreSqlMappers Name = entity.Name, AgentName = entity.AgentName, AvatarUrl = entity.AvatarUrl, - TelegramChannel = entity.TelegramChannel + TelegramChannel = entity.TelegramChannel, + Id = entity.Id // Assuming Id is the primary key for UserEntity }; } @@ -183,7 +184,9 @@ public static class PostgreSqlMappers { try { - geneticRequest.EligibleIndicators = SystemJsonSerializer.Deserialize>(entity.EligibleIndicatorsJson) ?? new List(); + geneticRequest.EligibleIndicators = + SystemJsonSerializer.Deserialize>(entity.EligibleIndicatorsJson) ?? + new List(); } catch { @@ -263,10 +266,12 @@ public static class PostgreSqlMappers // Deserialize JSON fields using MongoMappers for compatibility var config = JsonConvert.DeserializeObject(entity.ConfigJson); - var positions = JsonConvert.DeserializeObject>(entity.PositionsJson) ?? new List(); - var signals = JsonConvert.DeserializeObject>(entity.SignalsJson) ?? new List(); - var statistics = !string.IsNullOrEmpty(entity.StatisticsJson) - ? JsonConvert.DeserializeObject(entity.StatisticsJson) + var positionsList = JsonConvert.DeserializeObject>(entity.PositionsJson) ?? new List(); + var positions = positionsList.ToDictionary(p => p.Identifier, p => p); + var signalsList = JsonConvert.DeserializeObject>(entity.SignalsJson) ?? new List(); + var signals = signalsList.ToDictionary(s => s.Identifier, s => s); + var statistics = !string.IsNullOrEmpty(entity.StatisticsJson) + ? JsonConvert.DeserializeObject(entity.StatisticsJson) : null; var backtest = new Backtest(config, positions, signals) @@ -303,8 +308,8 @@ public static class PostgreSqlMappers GrowthPercentage = backtest.GrowthPercentage, HodlPercentage = backtest.HodlPercentage, ConfigJson = JsonConvert.SerializeObject(backtest.Config), - PositionsJson = JsonConvert.SerializeObject(backtest.Positions), - SignalsJson = JsonConvert.SerializeObject(backtest.Signals), + PositionsJson = JsonConvert.SerializeObject(backtest.Positions.Values.ToList()), + SignalsJson = JsonConvert.SerializeObject(backtest.Signals.Values.ToList()), StartDate = backtest.StartDate, EndDate = backtest.EndDate, MoneyManagementJson = JsonConvert.SerializeObject(backtest.Config?.MoneyManagement), @@ -354,7 +359,8 @@ public static class PostgreSqlMappers { try { - bundleRequest.Results = JsonConvert.DeserializeObject>(entity.ResultsJson) ?? new List(); + bundleRequest.Results = JsonConvert.DeserializeObject>(entity.ResultsJson) ?? + new List(); } catch { @@ -426,7 +432,7 @@ public static class PostgreSqlMappers return new Scenario(entity.Name, entity.LoopbackPeriod) { User = entity.UserName != null ? new User { Name = entity.UserName } : null, - Indicators = new List() // Will be populated separately when needed + Indicators = new List() // Will be populated separately when needed }; } @@ -443,11 +449,11 @@ public static class PostgreSqlMappers } // Indicator mappings - public static Indicator Map(IndicatorEntity entity) + public static IndicatorBase Map(IndicatorEntity entity) { if (entity == null) return null; - return new Indicator(entity.Name, entity.Type) + return new IndicatorBase(entity.Name, entity.Type) { SignalType = entity.SignalType, MinimumHistory = entity.MinimumHistory, @@ -463,26 +469,26 @@ public static class PostgreSqlMappers }; } - public static IndicatorEntity Map(Indicator indicator) + public static IndicatorEntity Map(IndicatorBase indicatorBase) { - if (indicator == null) return null; + if (indicatorBase == null) return null; return new IndicatorEntity { - Name = indicator.Name, - Type = indicator.Type, + Name = indicatorBase.Name, + Type = indicatorBase.Type, Timeframe = Timeframe.FifteenMinutes, // Default timeframe - 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 + 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 }; } @@ -491,8 +497,8 @@ public static class PostgreSqlMappers { if (entity == null) return null; - var candle = !string.IsNullOrEmpty(entity.CandleJson) - ? JsonConvert.DeserializeObject(entity.CandleJson) + var candle = !string.IsNullOrEmpty(entity.CandleJson) + ? JsonConvert.DeserializeObject(entity.CandleJson) : null; return new Signal( @@ -541,7 +547,8 @@ public static class PostgreSqlMappers var moneyManagement = new MoneyManagement(); // Default money management if (!string.IsNullOrEmpty(entity.MoneyManagementJson)) { - moneyManagement = JsonConvert.DeserializeObject(entity.MoneyManagementJson) ?? new MoneyManagement(); + moneyManagement = JsonConvert.DeserializeObject(entity.MoneyManagementJson) ?? + new MoneyManagement(); } var position = new Position( @@ -590,7 +597,9 @@ public static class PostgreSqlMappers SignalIdentifier = position.SignalIdentifier, AccountName = position.AccountName, UserName = position.User?.Name, - MoneyManagementJson = position.MoneyManagement != null ? JsonConvert.SerializeObject(position.MoneyManagement) : null + MoneyManagementJson = position.MoneyManagement != null + ? JsonConvert.SerializeObject(position.MoneyManagement) + : null }; } @@ -636,16 +645,15 @@ public static class PostgreSqlMappers } - // Collection mappings public static IEnumerable Map(IEnumerable entities) { return entities?.Select(Map) ?? Enumerable.Empty(); } - public static IEnumerable Map(IEnumerable entities) + public static IEnumerable Map(IEnumerable entities) { - return entities?.Select(Map) ?? Enumerable.Empty(); + return entities?.Select(Map) ?? Enumerable.Empty(); } public static IEnumerable Map(IEnumerable entities) @@ -663,48 +671,57 @@ public static class PostgreSqlMappers #region Bot Mappings // BotBackup mappings - public static BotBackup Map(BotBackupEntity entity) + public static Bot Map(BotEntity entity) { if (entity == null) return null; - var botBackup = new BotBackup + var bot = new Bot { Identifier = entity.Identifier, User = entity.User != null ? Map(entity.User) : null, - LastStatus = entity.LastStatus, - CreateDate = entity.CreateDate + Status = entity.Status, + CreateDate = entity.CreateDate, + Name = entity.Name, + Ticker = entity.Ticker, + StartupTime = entity.StartupTime, + TradeWins = entity.TradeWins, + TradeLosses = entity.TradeLosses, + Pnl = entity.Pnl, + Roi = entity.Roi, + Volume = entity.Volume, + Fees = entity.Fees }; - // Deserialize the JSON data using the helper method - botBackup.DeserializeData(entity.Data); - - return botBackup; + return bot; } - public static BotBackupEntity Map(BotBackup botBackup) + public static BotEntity Map(Bot bot) { - if (botBackup == null) return null; + if (bot == null) return null; - return new BotBackupEntity + return new BotEntity { - Identifier = botBackup.Identifier, - UserName = botBackup.User?.Name, - User = botBackup.User != null ? Map(botBackup.User) : null, - Data = botBackup.SerializeData(), // Serialize the data using the helper method - LastStatus = botBackup.LastStatus, - CreateDate = botBackup.CreateDate, + Identifier = bot.Identifier, + UserId = bot.User.Id, + User = bot.User != null ? Map(bot.User) : null, + Status = bot.Status, + CreateDate = bot.CreateDate, + Name = bot.Name, + Ticker = bot.Ticker, + StartupTime = bot.StartupTime, + TradeWins = bot.TradeWins, + TradeLosses = bot.TradeLosses, + Pnl = bot.Pnl, + Roi = bot.Roi, + Volume = bot.Volume, + Fees = bot.Fees, UpdatedAt = DateTime.UtcNow }; } - public static IEnumerable Map(IEnumerable entities) + public static IEnumerable Map(IEnumerable entities) { - return entities?.Select(Map) ?? Enumerable.Empty(); - } - - public static IEnumerable Map(IEnumerable botBackups) - { - return botBackups?.Select(Map) ?? Enumerable.Empty(); + return entities?.Select(Map) ?? Enumerable.Empty(); } #endregion @@ -763,7 +780,8 @@ public static class PostgreSqlMappers { try { - overview.Spotlights = SystemJsonSerializer.Deserialize>(entity.SpotlightsJson) ?? new List(); + overview.Spotlights = SystemJsonSerializer.Deserialize>(entity.SpotlightsJson) ?? + new List(); } catch (JsonException) { @@ -913,4 +931,4 @@ public static class PostgreSqlMappers } #endregion -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlTradingRepository.cs b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlTradingRepository.cs index 87b325d..78ef396 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlTradingRepository.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlTradingRepository.cs @@ -86,8 +86,8 @@ public class PostgreSqlTradingRepository : ITradingRepository var existingScenario = await _context.Scenarios .AsNoTracking() .FirstOrDefaultAsync(s => s.Name == scenario.Name && - ((scenario.User == null && s.UserName == null) || - (scenario.User != null && s.UserName == scenario.User.Name))); + ((scenario.User == null && s.UserName == null) || + (scenario.User != null && s.UserName == scenario.User.Name))); if (existingScenario != null) { @@ -107,8 +107,8 @@ public class PostgreSqlTradingRepository : ITradingRepository var indicatorEntity = await _context.Indicators .AsNoTracking() .FirstOrDefaultAsync(i => i.Name == indicator.Name && - ((indicator.User == null && i.UserName == null) || - (indicator.User != null && i.UserName == indicator.User.Name))); + ((indicator.User == null && i.UserName == null) || + (indicator.User != null && i.UserName == indicator.User.Name))); if (indicatorEntity != null) { @@ -120,6 +120,7 @@ public class PostgreSqlTradingRepository : ITradingRepository _context.ScenarioIndicators.Add(junction); } } + await _context.SaveChangesAsync(); } } @@ -135,7 +136,7 @@ public class PostgreSqlTradingRepository : ITradingRepository entity.LoopbackPeriod = scenario.LoopbackPeriod ?? 1; entity.UserName = scenario.User?.Name; entity.UpdatedAt = DateTime.UtcNow; - + await _context.SaveChangesAsync(); } } @@ -149,7 +150,7 @@ public class PostgreSqlTradingRepository : ITradingRepository var indicator = _context.Indicators .AsTracking() .FirstOrDefault(i => i.Name == name); - + if (indicator != null) { _context.Indicators.Remove(indicator); @@ -164,7 +165,7 @@ public class PostgreSqlTradingRepository : ITradingRepository await _context.SaveChangesAsync(); } - public async Task> GetIndicatorsAsync() + public async Task> GetIndicatorsAsync() { var indicators = await _context.Indicators .AsNoTracking() @@ -174,7 +175,7 @@ public class PostgreSqlTradingRepository : ITradingRepository return PostgreSqlMappers.Map(indicators); } - public async Task> GetStrategiesAsync() + public async Task> GetStrategiesAsync() { var indicators = await _context.Indicators .AsNoTracking() @@ -183,7 +184,7 @@ public class PostgreSqlTradingRepository : ITradingRepository return PostgreSqlMappers.Map(indicators); } - public async Task GetStrategyByNameAsync(string name) + public async Task GetStrategyByNameAsync(string name) { var indicator = await _context.Indicators .AsNoTracking() @@ -193,48 +194,48 @@ public class PostgreSqlTradingRepository : ITradingRepository return PostgreSqlMappers.Map(indicator); } - public async Task InsertStrategyAsync(Indicator indicator) + public async Task InsertIndicatorAsync(IndicatorBase indicatorBase) { // Check if indicator already exists for the same user var existingIndicator = await _context.Indicators .AsNoTracking() - .FirstOrDefaultAsync(i => i.Name == indicator.Name && - ((indicator.User == null && i.UserName == null) || - (indicator.User != null && i.UserName == indicator.User.Name))); + .FirstOrDefaultAsync(i => i.Name == indicatorBase.Name && + ((indicatorBase.User == null && i.UserName == null) || + (indicatorBase.User != null && i.UserName == indicatorBase.User.Name))); if (existingIndicator != null) { throw new InvalidOperationException( - $"Indicator with name '{indicator.Name}' already exists for user '{indicator.User?.Name}'"); + $"Indicator with name '{indicatorBase.Name}' already exists for user '{indicatorBase.User?.Name}'"); } - var entity = PostgreSqlMappers.Map(indicator); + var entity = PostgreSqlMappers.Map(indicatorBase); _context.Indicators.Add(entity); await _context.SaveChangesAsync(); } - public async Task UpdateStrategyAsync(Indicator indicator) + public async Task UpdateStrategyAsync(IndicatorBase indicatorBase) { var entity = _context.Indicators .AsTracking() - .FirstOrDefault(i => i.Name == indicator.Name); + .FirstOrDefault(i => i.Name == indicatorBase.Name); if (entity != null) { - entity.Type = indicator.Type; - entity.SignalType = indicator.SignalType; - entity.MinimumHistory = indicator.MinimumHistory; - entity.Period = indicator.Period; - entity.FastPeriods = indicator.FastPeriods; - entity.SlowPeriods = indicator.SlowPeriods; - entity.SignalPeriods = indicator.SignalPeriods; - entity.Multiplier = indicator.Multiplier; - entity.SmoothPeriods = indicator.SmoothPeriods; - entity.StochPeriods = indicator.StochPeriods; - entity.CyclePeriods = indicator.CyclePeriods; - entity.UserName = indicator.User?.Name; + entity.Type = indicatorBase.Type; + entity.SignalType = indicatorBase.SignalType; + entity.MinimumHistory = indicatorBase.MinimumHistory; + entity.Period = indicatorBase.Period; + entity.FastPeriods = indicatorBase.FastPeriods; + entity.SlowPeriods = indicatorBase.SlowPeriods; + entity.SignalPeriods = indicatorBase.SignalPeriods; + entity.Multiplier = indicatorBase.Multiplier; + entity.SmoothPeriods = indicatorBase.SmoothPeriods; + entity.StochPeriods = indicatorBase.StochPeriods; + entity.CyclePeriods = indicatorBase.CyclePeriods; + entity.UserName = indicatorBase.User?.Name; entity.UpdatedAt = DateTime.UtcNow; - + await _context.SaveChangesAsync(); } } @@ -242,15 +243,9 @@ public class PostgreSqlTradingRepository : ITradingRepository #endregion - #region Position Methods - public Position GetPositionByIdentifier(string identifier) - { - return GetPositionByIdentifierAsync(identifier).Result; - } - - public async Task GetPositionByIdentifierAsync(string identifier) + public async Task GetPositionByIdentifierAsync(Guid identifier) { var position = await _context.Positions .AsNoTracking() @@ -310,8 +305,8 @@ public class PostgreSqlTradingRepository : ITradingRepository var existingPosition = await _context.Positions .AsNoTracking() .FirstOrDefaultAsync(p => p.Identifier == position.Identifier && - ((position.User == null && p.UserName == null) || - (position.User != null && p.UserName == position.User.Name))); + ((position.User == null && p.UserName == null) || + (position.User != null && p.UserName == position.User.Name))); if (existingPosition != null) { @@ -320,7 +315,7 @@ public class PostgreSqlTradingRepository : ITradingRepository } var entity = PostgreSqlMappers.Map(position); - + // Handle related trades if (position.Open != null) { @@ -370,11 +365,11 @@ public class PostgreSqlTradingRepository : ITradingRepository entity.ProfitAndLoss = position.ProfitAndLoss?.Realized ?? 0; entity.Status = position.Status; entity.SignalIdentifier = position.SignalIdentifier; - entity.MoneyManagementJson = position.MoneyManagement != null - ? JsonConvert.SerializeObject(position.MoneyManagement) + entity.MoneyManagementJson = position.MoneyManagement != null + ? JsonConvert.SerializeObject(position.MoneyManagement) : null; entity.UpdatedAt = DateTime.UtcNow; - + await _context.SaveChangesAsync(); } } @@ -393,7 +388,7 @@ public class PostgreSqlTradingRepository : ITradingRepository var signals = await _context.Signals .AsNoTracking() .Where(s => (user == null && s.UserName == null) || - (user != null && s.UserName == user.Name)) + (user != null && s.UserName == user.Name)) .ToListAsync() .ConfigureAwait(false); @@ -410,8 +405,8 @@ public class PostgreSqlTradingRepository : ITradingRepository var signal = await _context.Signals .AsNoTracking() .FirstOrDefaultAsync(s => s.Identifier == identifier && - ((user == null && s.UserName == null) || - (user != null && s.UserName == user.Name))) + ((user == null && s.UserName == null) || + (user != null && s.UserName == user.Name))) .ConfigureAwait(false); return PostgreSqlMappers.Map(signal); @@ -423,9 +418,9 @@ public class PostgreSqlTradingRepository : ITradingRepository var existingSignal = _context.Signals .AsNoTracking() .FirstOrDefault(s => s.Identifier == signal.Identifier && - s.Date == signal.Date && - ((s.UserName == null && signal.User == null) || - (s.UserName != null && signal.User != null && s.UserName == signal.User.Name))); + s.Date == signal.Date && + ((s.UserName == null && signal.User == null) || + (s.UserName != null && signal.User != null && s.UserName == signal.User.Name))); if (existingSignal != null) { @@ -438,7 +433,25 @@ public class PostgreSqlTradingRepository : ITradingRepository await _context.SaveChangesAsync(); } + public async Task GetStrategyByNameUserAsync(string name, User user) + { + var indicator = await _context.Indicators + .AsNoTracking() + .FirstOrDefaultAsync(i => i.Name == name && + ((user == null && i.UserName == null) || + (user != null && i.UserName == user.Name))); + return PostgreSqlMappers.Map(indicator); + } + + public async Task GetScenarioByNameUserAsync(string scenarioName, User user) + { + var scenario = await _context.Scenarios + .AsNoTracking() + .FirstOrDefaultAsync(s => s.Name == scenarioName && + ((user == null && s.UserName == null) || + (user != null && s.UserName == user.Name))); + return PostgreSqlMappers.Map(scenario); + } + #endregion - - -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Exchanges/Abstractions/IExchangeProcessor.cs b/src/Managing.Infrastructure.Exchanges/Abstractions/IExchangeProcessor.cs index c033f9a..ac1a7f9 100644 --- a/src/Managing.Infrastructure.Exchanges/Abstractions/IExchangeProcessor.cs +++ b/src/Managing.Infrastructure.Exchanges/Abstractions/IExchangeProcessor.cs @@ -33,7 +33,7 @@ public interface IExchangeProcessor 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); Orderbook GetOrderbook(Account account, Ticker ticker); Task> GetOrders(Account account, Ticker ticker); diff --git a/src/Managing.Infrastructure.Exchanges/ExchangeService.cs b/src/Managing.Infrastructure.Exchanges/ExchangeService.cs index 2169d1a..3d62078 100644 --- a/src/Managing.Infrastructure.Exchanges/ExchangeService.cs +++ b/src/Managing.Infrastructure.Exchanges/ExchangeService.cs @@ -200,7 +200,8 @@ namespace Managing.Infrastructure.Exchanges return await processor.GetTrades(account, ticker); } - public async Task> GetCandles(Account account, Ticker ticker, DateTime startDate, Timeframe timeframe, bool isFirstCall) + public async Task> GetCandles(Account account, Ticker ticker, DateTime startDate, + Timeframe timeframe, bool isFirstCall) { var processor = GetProcessor(account); // Only EvmProcessor supports isFirstCall @@ -208,22 +209,26 @@ namespace Managing.Infrastructure.Exchanges { return await evmProcessor.GetCandles(account, ticker, startDate, timeframe, isFirstCall); } + // Fallback to default behavior for other processors return await processor.GetCandles(account, ticker, startDate, timeframe); } - public async Task> GetCandlesInflux(TradingExchanges exchange, Ticker ticker, DateTime startDate, - Timeframe timeframe) + public async Task> GetCandlesInflux(TradingExchanges exchange, Ticker ticker, + DateTime startDate, + Timeframe timeframe, int? limit = null) { - var candlesFromRepo = await _candleRepository.GetCandles(exchange, ticker, timeframe, startDate); - return candlesFromRepo.ToList(); + var candlesFromRepo = await _candleRepository.GetCandles(exchange, ticker, timeframe, startDate, limit); + return candlesFromRepo; } - public async Task> GetCandlesInflux(TradingExchanges exchange, Ticker ticker, DateTime startDate, - Timeframe timeframe, DateTime endDate) + public async Task> GetCandlesInflux(TradingExchanges exchange, Ticker ticker, + DateTime startDate, + Timeframe timeframe, DateTime endDate, int? limit = null) { - var candlesFromRepo = await _candleRepository.GetCandles(exchange, ticker, timeframe, startDate, endDate); - return candlesFromRepo.ToList(); + var candlesFromRepo = + await _candleRepository.GetCandles(exchange, ticker, timeframe, startDate, endDate, limit); + return candlesFromRepo; } public async Task GetBalance(Account account, bool isForPaperTrading = false) @@ -249,10 +254,10 @@ namespace Managing.Infrastructure.Exchanges return await processor.GetPrice(account, ticker, date); } - public Candle GetCandle(Account account, Ticker ticker, DateTime date) + public async Task GetCandle(Account account, Ticker ticker, DateTime date) { var processor = GetProcessor(account); - return processor.GetCandle(account, ticker, date); + return await processor.GetCandle(account, ticker, date); } public async Task GetQuantityInPosition(Account account, Ticker ticker) diff --git a/src/Managing.Infrastructure.Exchanges/Exchanges/BaseProcessor.cs b/src/Managing.Infrastructure.Exchanges/Exchanges/BaseProcessor.cs index 2c9b7d8..2ac551d 100644 --- a/src/Managing.Infrastructure.Exchanges/Exchanges/BaseProcessor.cs +++ b/src/Managing.Infrastructure.Exchanges/Exchanges/BaseProcessor.cs @@ -13,7 +13,7 @@ namespace Managing.Infrastructure.Exchanges.Exchanges public abstract Task CancelOrder(Account account, Ticker ticker); public abstract TradingExchanges Exchange(); public abstract Task GetBalance(Account account, bool isForPaperTrading = false); - public abstract Candle GetCandle(Account account, Ticker ticker, DateTime date); + public abstract Task GetCandle(Account account, Ticker ticker, DateTime date); public abstract Task> GetCandles(Account account, Ticker ticker, DateTime startDate, Timeframe interval); public abstract decimal GetFee(Account account, bool isForPaperTrading = false); public abstract Task GetPrice(Account account, Ticker ticker, DateTime date); diff --git a/src/Managing.Infrastructure.Exchanges/Exchanges/EvmProcessor.cs b/src/Managing.Infrastructure.Exchanges/Exchanges/EvmProcessor.cs index b961268..4f4d7ad 100644 --- a/src/Managing.Infrastructure.Exchanges/Exchanges/EvmProcessor.cs +++ b/src/Managing.Infrastructure.Exchanges/Exchanges/EvmProcessor.cs @@ -62,9 +62,9 @@ public class EvmProcessor : BaseProcessor }); } - public override Candle GetCandle(Account account, Ticker ticker, DateTime date) + public override async Task GetCandle(Account account, Ticker ticker, DateTime date) { - return _evmManager.GetCandle(ticker).Result; + return await _evmManager.GetCandle(ticker); } public override async Task> GetCandles(Account account, Ticker ticker, DateTime startDate, diff --git a/src/Managing.Infrastructure.Messengers/Discord/DiscordService.cs b/src/Managing.Infrastructure.Messengers/Discord/DiscordService.cs index 6c520f5..3c6667f 100644 --- a/src/Managing.Infrastructure.Messengers/Discord/DiscordService.cs +++ b/src/Managing.Infrastructure.Messengers/Discord/DiscordService.cs @@ -303,7 +303,7 @@ namespace Managing.Infrastructure.Messengers.Discord var tradingService = (ITradingService)_services.GetService(typeof(ITradingService)); await component.RespondAsync("Alright, let met few seconds to close this position"); - var position = await tradingService.GetPositionByIdentifierAsync(parameters[1]); + var position = await tradingService.GetPositionByIdentifierAsync(Guid.Parse(parameters[1])); var command = new ClosePositionCommand(position); var result = await new ClosePositionCommandHandler(exchangeService, accountService, tradingService).Handle(command); diff --git a/src/Managing.Infrastructure.Tests/ExchangeServicesTests.cs b/src/Managing.Infrastructure.Tests/ExchangeServicesTests.cs index 7df8093..f780e5d 100644 --- a/src/Managing.Infrastructure.Tests/ExchangeServicesTests.cs +++ b/src/Managing.Infrastructure.Tests/ExchangeServicesTests.cs @@ -51,10 +51,10 @@ namespace Managing.Infrastructure.Tests [Theory] [InlineData(TradingExchanges.Evm, Ticker.ADA)] - public void Should_Return_Candle_For_Given_Ticker(TradingExchanges exchange, Ticker ticker) + public async Task Should_Return_Candle_For_Given_Ticker(TradingExchanges exchange, Ticker ticker) { var account = PrivateKeys.GetAccount(); - var candle = _exchangeService.GetCandle(account, ticker, DateTime.Now); + var candle = await _exchangeService.GetCandle(account, ticker, DateTime.Now); Assert.IsType(candle); Assert.InRange(candle.High, 0, 1000000); Assert.InRange(candle.Low, 0, 1000000); diff --git a/src/Managing.Infrastructure.Web3/Services/Gmx/GmxV2Mappers.cs b/src/Managing.Infrastructure.Web3/Services/Gmx/GmxV2Mappers.cs index 9003de5..8b77930 100644 --- a/src/Managing.Infrastructure.Web3/Services/Gmx/GmxV2Mappers.cs +++ b/src/Managing.Infrastructure.Web3/Services/Gmx/GmxV2Mappers.cs @@ -164,7 +164,7 @@ internal static class GmxV2Mappers { var direction = MiscExtensions.ParseEnum(gmxPosition.Direction); var ticker = MiscExtensions.ParseEnum(gmxPosition.Ticker); - var position = new Position("", "", + var position = new Position(Guid.NewGuid(), "", direction, ticker, new MoneyManagement(), diff --git a/src/Managing.WebApp/src/app/store/customScenario.tsx b/src/Managing.WebApp/src/app/store/customScenario.tsx index fd31402..d7c6c80 100644 --- a/src/Managing.WebApp/src/app/store/customScenario.tsx +++ b/src/Managing.WebApp/src/app/store/customScenario.tsx @@ -1,10 +1,10 @@ import {create} from 'zustand' -import type {Scenario} from '../../generated/ManagingApi' +import type {LightScenario} from '../../generated/ManagingApi' type CustomScenarioStore = { - setCustomScenario: (custom: Scenario | null) => void - scenario: Scenario | null + setCustomScenario: (custom: LightScenario | null) => void + scenario: LightScenario | null } export const useCustomScenario = create((set) => ({ diff --git a/src/Managing.WebApp/src/components/mollecules/CardText/CardText.tsx b/src/Managing.WebApp/src/components/mollecules/CardText/CardText.tsx index c3c0526..929c834 100644 --- a/src/Managing.WebApp/src/components/mollecules/CardText/CardText.tsx +++ b/src/Managing.WebApp/src/components/mollecules/CardText/CardText.tsx @@ -1,8 +1,8 @@ import ArrowDownIcon from '@heroicons/react/solid/ArrowDownIcon' import ArrowUpIcon from '@heroicons/react/solid/ArrowUpIcon' -import {Position, TradeDirection} from '../../../generated/ManagingApi' -import type {ICardPosition, ICardSignal, ICardText} from '../../../global/type' +import {LightSignal, Position, TradeDirection} from '../../../generated/ManagingApi' +import type {ICardPosition, ICardSignal, ICardText} from '../../../global/type.tsx' function getItemTextHeaderClass() { return 'text-xs opacity-50 ' @@ -38,7 +38,7 @@ export function CardPosition({ positions, positivePosition }: ICardPosition) { display="initial" >{' '} { - positions.filter((p) => p.originDirection == TradeDirection.Short) + positions.filter((p: Position) => p.originDirection == TradeDirection.Short) .length }{' '}

Signals

- {signals.filter((p) => p.direction == TradeDirection.Long).length}{' '} + {signals.filter((p: LightSignal) => p.direction == TradeDirection.Long).length}{' '} {' '} - {signals.filter((p) => p.direction == TradeDirection.Short).length}{' '} + {signals.filter((p: LightSignal) => p.direction == TradeDirection.Short).length}{' '} { const PrivyWalletButton = () => { const { login, logout, authenticated, user } = usePrivy() const { apiUrl } = useApiUrlStore() + const { getCookie } = useCookie() const api = new UserClient({}, apiUrl) + // Get JWT token from cookies + const jwtToken = getCookie('token') + // Fetch user information from the API const { data: userInfo } = useQuery({ queryKey: ['user'], queryFn: () => api.user_GetCurrentUser(), - enabled: authenticated, // Only fetch when authenticated + enabled: authenticated && !!jwtToken, // Only fetch when authenticated AND JWT token exists }) if (!authenticated) { diff --git a/src/Managing.WebApp/src/components/organism/ActiveBots/ActiveBots.tsx b/src/Managing.WebApp/src/components/organism/ActiveBots/ActiveBots.tsx index 16500e6..de5288a 100644 --- a/src/Managing.WebApp/src/components/organism/ActiveBots/ActiveBots.tsx +++ b/src/Managing.WebApp/src/components/organism/ActiveBots/ActiveBots.tsx @@ -1,212 +1,116 @@ -import {ArrowDownIcon, ArrowUpIcon, ChevronDownIcon, ChevronRightIcon, PlayIcon,} from '@heroicons/react/solid' -import React, {useEffect, useState} from 'react' - -import {Hub} from '../../../app/providers/Hubs' -import useApiUrlStore from '../../../app/store/apiStore' -import type {Account, TradingBotResponse} from '../../../generated/ManagingApi' -import {AccountClient, BotClient, TradeDirection, TradeStatus,} from '../../../generated/ManagingApi' -import {IndicatorsDisplay, SelectColumnFilter, Table} from '../../mollecules' +import {ChevronDownIcon, ChevronRightIcon,} from '@heroicons/react/solid' +import React from 'react' +import useBots from '../../../hooks/useBots' +import {SelectColumnFilter, Table} from '../../mollecules' import StatusBadge from '../StatusBadge/StatusBadge' import Summary from '../Trading/Summary' import BotRowDetails from './botRowDetails' export default function ActiveBots() { - const [bots, setBots] = useState([]) - const [accounts, setAccounts] = useState([]) - const { apiUrl } = useApiUrlStore() + const {data: bots = []} = useBots({}) - const columns = React.useMemo( - () => [ - { - Cell: ({ row }: any) => ( - // Use Cell to render an expander for each row. - // We can use the getToggleRowExpandedProps prop-getter - // to build the expander. - + const columns = React.useMemo( + () => [ + { + Cell: ({row}: any) => ( + // Use Cell to render an expander for each row. + // We can use the getToggleRowExpandedProps prop-getter + // to build the expander. + {row.isExpanded ? ( - + ) : ( - + )} - ), + ), - // Make sure it has an ID - Header: ({ getToggleAllRowsExpandedProps, isAllRowsExpanded }: any) => ( - + // Make sure it has an ID + Header: ({getToggleAllRowsExpandedProps, isAllRowsExpanded}: any) => ( + {isAllRowsExpanded ? 'v' : '>'} - ), - // Build our expander column - id: 'expander', - }, - { - Cell: ({ cell }: any) => ( - <> - - - ), - Header: 'Status', - accessor: 'status', - disableFilters: true, - disableSortBy: true, - search: false, - }, - { - accessor: 'isForWatchingOnly', - disableFilters: true, - disableSortBy: true, - search: false, - }, - { - Filter: SelectColumnFilter, - Header: 'Ticker', - accessor: 'config.ticker', - disableSortBy: true, - }, - { - Header: 'Name', - accessor: 'config.name', - }, - { - Filter: SelectColumnFilter, - Header: 'Timeframe', - accessor: 'config.timeframe', - disableSortBy: true, - }, - { - Header: 'Indicators', - accessor: 'config.scenario.indicators', - Cell: ({cell}: any) => { - const bot = cell.row.original as TradingBotResponse; - const indicators = bot.config?.scenario?.indicators || []; - - return ( - - ); - }, - disableFilters: true, - disableSortBy: true, - }, - { - Cell: ({ cell }: any) => ( - <> + ), + // Build our expander column + id: 'expander', + }, { - <> - { - cell.row.values.positions.filter( - (p: any) => p.originDirection == TradeDirection.Long - ).length - }{' '} - - {' | '} - { - cell.row.values.positions.filter( - (p: any) => p.originDirection == TradeDirection.Short - ).length - }{' '} - {' '} - { - cell.row.values.positions.filter( - (p: any) => p.status == TradeStatus.Filled - ).length - }{' '} - {' '} - - } - + Cell: ({cell}: any) => ( + <> + + + ), + Header: 'Status', + accessor: 'status', + disableFilters: true, + disableSortBy: true, + search: false, + }, + { + accessor: 'isForWatchingOnly', + disableFilters: true, + disableSortBy: true, + search: false, + }, + { + Filter: SelectColumnFilter, + Header: 'Ticker', + accessor: 'ticker', + disableSortBy: true, + }, + { + Header: 'Name', + accessor: 'name', + }, + { + Filter: SelectColumnFilter, + Header: 'Timeframe', + accessor: 'timeframe', + disableSortBy: true, + }, + { + Cell: ({cell}: any) => <>{cell.row.values.winRate} %, + Header: 'Winrate', + accessor: 'winRate', + disableFilters: true, + }, + { + Cell: ({cell}: any) => <>{cell.row.values.profitAndLoss} $, + Header: 'PNL', + accessor: 'profitAndLoss', + disableFilters: true, + }, + ], + [] + ) + + const renderRowSubComponent = React.useCallback( + ({row}: any) => ( + <> + + ), - Header: 'Positions', - accessor: 'positions', - disableFilters: true, - }, - { - Cell: ({ cell }) => <>{cell.row.values.winRate} %, - Header: 'Winrate', - accessor: 'winRate', - disableFilters: true, - }, - { - Cell: ({ cell }) => <>{cell.row.values.profitAndLoss} $, - Header: 'PNL', - accessor: 'profitAndLoss', - disableFilters: true, - }, - ], - [] - ) + [] + ) - useEffect(() => { - setupHubConnection().then(() => { - if (bots.length == 0) { - const client = new BotClient({}, apiUrl) - client.bot_GetActiveBots().then((data) => { - setBots(data) - }) - } - }) - const client = new AccountClient({}, apiUrl) - client.account_GetAccounts().then((data) => { - setAccounts(data) - }) - }, []) - - const setupHubConnection = async () => { - const hub = new Hub('bothub', apiUrl).hub - - hub.on('BotsSubscription', (data: TradingBotResponse[]) => { - // eslint-disable-next-line no-console - console.log( - 'bot List', - bots.map((bot: TradingBotResponse) => { - return bot.config.name - }) - ) - setBots(data) - }) - - return hub - } - - const renderRowSubComponent = React.useCallback( - ({ row }: any) => ( - <> - - - ), - [] - ) - - return ( - <> -

- -
-
- - - - ) + return ( + <> +
+ +
+
+
+ + + ) } diff --git a/src/Managing.WebApp/src/components/organism/Backtest/backtestModal.tsx b/src/Managing.WebApp/src/components/organism/Backtest/backtestModal.tsx index 91bd88e..e8ce73f 100644 --- a/src/Managing.WebApp/src/components/organism/Backtest/backtestModal.tsx +++ b/src/Managing.WebApp/src/components/organism/Backtest/backtestModal.tsx @@ -6,7 +6,6 @@ import useApiUrlStore from '../../../app/store/apiStore' import { AccountClient, BacktestClient, - BotType, DataClient, MoneyManagement, MoneyManagementClient, @@ -379,16 +378,6 @@ const BacktestModal: React.FC = ({ - - - - {/* Losing streak info */} diff --git a/src/Managing.WebApp/src/components/organism/Backtest/backtestRowDetails.tsx b/src/Managing.WebApp/src/components/organism/Backtest/backtestRowDetails.tsx index a1db898..d19db15 100644 --- a/src/Managing.WebApp/src/components/organism/Backtest/backtestRowDetails.tsx +++ b/src/Managing.WebApp/src/components/organism/Backtest/backtestRowDetails.tsx @@ -6,6 +6,7 @@ import { DataClient, GetCandlesWithIndicatorsRequest, IndicatorType, + LightSignal, Position, SignalType } from '../../../generated/ManagingApi' @@ -42,7 +43,7 @@ const BacktestRowDetails: React.FC = ({ if (backtest.candles && backtest.candles.length > 0) { return { candles: backtest.candles, - indicatorsValues: backtest.indicatorsValues || {} + indicatorsValues: {} // Default empty object since Backtest doesn't have indicatorsValues }; } @@ -83,17 +84,17 @@ const BacktestRowDetails: React.FC = ({ // Use the data from query or fallback to backtest data const candles = candlesData?.candles || currentBacktest.candles || []; - const indicatorsValues = candlesData?.indicatorsValues || currentBacktest.indicatorsValues || {}; + const indicatorsValues = candlesData?.indicatorsValues || {}; - // Only destructure these properties if we have full backtest data - const positions = fullBacktestData?.positions || []; - const walletBalances = fullBacktestData?.walletBalances || []; - const signals = fullBacktestData?.signals || []; - const statistics = fullBacktestData?.statistics; + // Convert positions and signals objects to arrays + const positionsArray: Position[] = Object.values(currentBacktest.positions || {}); + const signalsArray: LightSignal[] = Object.values(currentBacktest.signals || {}); + const walletBalances = currentBacktest.walletBalances || []; + const statistics = currentBacktest.statistics; const config = currentBacktest.config; // Helper function to calculate position open time in hours - const calculateOpenTimeInHours = (position: Position) => { + const calculateOpenTimeInHours = (position: Position): number => { const openDate = new Date(position.Open.date); let closeDate: Date | null = null; @@ -116,15 +117,15 @@ const BacktestRowDetails: React.FC = ({ }; // Calculate average open time for winning positions - const getAverageOpenTimeWinning = () => { - const winningPositions = positions.filter((p) => { + const getAverageOpenTimeWinning = (): string => { + const winningPositions = positionsArray.filter((p: Position) => { const realized = p.ProfitAndLoss?.realized ?? 0; return realized > 0; }); if (winningPositions.length === 0) return "0.00"; - const totalHours = winningPositions.reduce((sum, position) => { + const totalHours = winningPositions.reduce((sum: number, position: Position) => { return sum + calculateOpenTimeInHours(position); }, 0); @@ -133,15 +134,15 @@ const BacktestRowDetails: React.FC = ({ }; // Calculate average open time for losing positions - const getAverageOpenTimeLosing = () => { - const losingPositions = positions.filter((p) => { + const getAverageOpenTimeLosing = (): string => { + const losingPositions = positionsArray.filter((p: Position) => { const realized = p.ProfitAndLoss?.realized ?? 0; return realized <= 0; }); if (losingPositions.length === 0) return "0.00"; - const totalHours = losingPositions.reduce((sum, position) => { + const totalHours = losingPositions.reduce((sum: number, position: Position) => { return sum + calculateOpenTimeInHours(position); }, 0); @@ -150,25 +151,25 @@ const BacktestRowDetails: React.FC = ({ }; // Calculate maximum open time for winning positions - const getMaxOpenTimeWinning = () => { - const winningPositions = positions.filter((p) => { + const getMaxOpenTimeWinning = (): string => { + const winningPositions = positionsArray.filter((p: Position) => { const realized = p.ProfitAndLoss?.realized ?? 0; return realized > 0; }); if (winningPositions.length === 0) return "0.00"; - const openTimes = winningPositions.map(position => calculateOpenTimeInHours(position)); + const openTimes = winningPositions.map((position: Position) => calculateOpenTimeInHours(position)); const maxHours = Math.max(...openTimes); return maxHours.toFixed(2); }; // Calculate median opening time for all positions - const getMedianOpenTime = () => { - if (positions.length === 0) return "0.00"; + const getMedianOpenTime = (): string => { + if (positionsArray.length === 0) return "0.00"; - const openTimes = positions.map(position => calculateOpenTimeInHours(position)); - const sortedTimes = openTimes.sort((a, b) => a - b); + const openTimes = positionsArray.map((position: Position) => calculateOpenTimeInHours(position)); + const sortedTimes = openTimes.sort((a: number, b: number) => a - b); const mid = Math.floor(sortedTimes.length / 2); const median = sortedTimes.length % 2 === 0 @@ -179,10 +180,10 @@ const BacktestRowDetails: React.FC = ({ }; // Calculate total volume traded with leverage - const getTotalVolumeTraded = () => { + const getTotalVolumeTraded = (): number => { let totalVolume = 0; - positions.forEach((position) => { + positionsArray.forEach((position: Position) => { // Calculate volume for open trade const openLeverage = position.Open.leverage || 1; const openVolume = position.Open.quantity * position.Open.price * openLeverage; @@ -211,7 +212,7 @@ const BacktestRowDetails: React.FC = ({ }; // Calculate estimated UI fee (0.02% of total volume) - const getEstimatedUIFee = () => { + const getEstimatedUIFee = (): number => { const totalVolume = getTotalVolumeTraded(); const uiFeePercentage = 0.001; // 0.1% return totalVolume * uiFeePercentage; @@ -219,14 +220,14 @@ const BacktestRowDetails: React.FC = ({ // Calculate recommended cooldown based on positions that fail after a win const getCooldownRecommendations = () => { - if (positions?.length < 2 || !candles || candles?.length < 2) { + if (positionsArray.length < 2 || !candles || candles.length < 2) { return { percentile75: "0", average: "0", median: "0" }; } // Determine candle timeframe in milliseconds const candleTimeframeMs = new Date(candles[1].date).getTime() - new Date(candles[0].date).getTime(); - const sortedPositions = [...positions].sort((a, b) => { + const sortedPositions = [...positionsArray].sort((a: Position, b: Position) => { const dateA = new Date(a.Open.date).getTime(); const dateB = new Date(b.Open.date).getTime(); return dateA - dateB; @@ -271,12 +272,12 @@ const BacktestRowDetails: React.FC = ({ } // Calculate the 75th percentile - const sortedGaps = [...failAfterWinGaps].sort((a, b) => a - b); + const sortedGaps = [...failAfterWinGaps].sort((a: number, b: number) => a - b); const percentile75Index = Math.floor(sortedGaps.length * 0.75); const percentile75 = sortedGaps[percentile75Index] || 0; // Calculate the average - const sum = failAfterWinGaps.reduce((acc, gap) => acc + gap, 0); + const sum = failAfterWinGaps.reduce((acc: number, gap: number) => acc + gap, 0); const average = sum / failAfterWinGaps.length; // Calculate the median @@ -295,13 +296,13 @@ const BacktestRowDetails: React.FC = ({ const cooldownRecommendations = getCooldownRecommendations(); // Calculate average trades per day - const getAverageTradesPerDay = () => { - if (positions.length === 0) return "0.00"; + const getAverageTradesPerDay = (): string => { + if (positionsArray.length === 0) return "0.00"; // Get all trade dates and sort them - const tradeDates = positions.map(position => new Date(position.Open.date)).sort((a, b) => a.getTime() - b.getTime()); + const tradeDates = positionsArray.map((position: Position) => new Date(position.Open.date)).sort((a: Date, b: Date) => a.getTime() - b.getTime()); - if (tradeDates.length < 2) return positions.length.toString(); + if (tradeDates.length < 2) return positionsArray.length.toString(); // Calculate the date range in days const firstTradeDate = tradeDates[0]; @@ -309,7 +310,7 @@ const BacktestRowDetails: React.FC = ({ const diffInMs = lastTradeDate.getTime() - firstTradeDate.getTime(); const diffInDays = Math.max(1, diffInMs / (1000 * 60 * 60 * 24)); // Ensure at least 1 day - const averageTradesPerDay = positions.length / diffInDays; + const averageTradesPerDay = positionsArray.length / diffInDays; return averageTradesPerDay.toFixed(2); }; @@ -327,19 +328,19 @@ const BacktestRowDetails: React.FC = ({
{ + positions={positionsArray.filter((p: Position) => { const realized = p.ProfitAndLoss?.realized ?? 0 return realized > 0 ? p : null })} > { + positions={positionsArray.filter((p: Position) => { const realized = p.ProfitAndLoss?.realized ?? 0 return realized <= 0 ? p : null })} > - + = ({
diff --git a/src/Managing.WebApp/src/components/organism/Backtest/backtestTable.tsx b/src/Managing.WebApp/src/components/organism/Backtest/backtestTable.tsx index 7ce70b2..bb3103b 100644 --- a/src/Managing.WebApp/src/components/organism/Backtest/backtestTable.tsx +++ b/src/Managing.WebApp/src/components/organism/Backtest/backtestTable.tsx @@ -135,11 +135,12 @@ interface BacktestTableProps { displaySummary?: boolean onSortChange?: (sortBy: string, sortOrder: 'asc' | 'desc') => void currentSort?: { sortBy: string; sortOrder: 'asc' | 'desc' } + onBacktestDeleted?: () => void // Callback when a backtest is deleted } -const BacktestTable: React.FC = ({list, isFetching, onSortChange, currentSort}) => { +const BacktestTable: React.FC = ({list, isFetching, onSortChange, currentSort, onBacktestDeleted}) => { const [rows, setRows] = useState([]) const {apiUrl} = useApiUrlStore() const {removeBacktest} = useBacktestStore() @@ -213,6 +214,10 @@ const BacktestTable: React.FC = ({list, isFetching, onSortCh t.update('success', 'Backtest deleted') // Remove the deleted backtest from the store removeBacktest(id) + // Call the callback to invalidate queries + if (onBacktestDeleted) { + onBacktestDeleted() + } }) .catch((err) => { t.update('error', err) diff --git a/src/Managing.WebApp/src/components/organism/CustomScenario/CustomScenario.tsx b/src/Managing.WebApp/src/components/organism/CustomScenario/CustomScenario.tsx index cddde79..55ab5be 100644 --- a/src/Managing.WebApp/src/components/organism/CustomScenario/CustomScenario.tsx +++ b/src/Managing.WebApp/src/components/organism/CustomScenario/CustomScenario.tsx @@ -1,12 +1,12 @@ import React, {useEffect, useState} from 'react' -import type {Indicator, Scenario} from '../../../generated/ManagingApi' +import type {LightIndicator, LightScenario} from '../../../generated/ManagingApi' import {IndicatorType} from '../../../generated/ManagingApi' import FormInput from '../../mollecules/FormInput/FormInput' import {useCustomScenario} from '../../../app/store/customScenario' type ICustomScenario = { - onCreateScenario: (scenario: Scenario) => void + onCreateScenario: (scenario: LightScenario) => void showCustomScenario: boolean } @@ -18,7 +18,7 @@ const CustomScenario: React.FC = ({ const [name, setName] = useState(scenario?.name || 'Custom Scenario') const [loopbackPeriod, setLoopbackPeriod] = useState(scenario?.loopbackPeriod || 1) - const [indicators, setIndicators] = useState(scenario?.indicators || []) + const [indicators, setIndicators] = useState(scenario?.indicators || []) // Available indicator types with their required parameters const indicatorTypes = Object.values(IndicatorType).map(type => { @@ -124,7 +124,7 @@ const CustomScenario: React.FC = ({ }); const addIndicator = () => { - const newIndicator: Indicator = { + const newIndicator: LightIndicator = { name: `Indicator ${indicators.length + 1}`, type: indicatorTypes[0].type, period: 14, @@ -154,7 +154,7 @@ const CustomScenario: React.FC = ({ } const handleCreateScenario = () => { - const scenario: Scenario = { + const scenario: LightScenario = { name, indicators, loopbackPeriod, @@ -262,7 +262,7 @@ const CustomScenario: React.FC = ({ {getRequiredParams(indicator.type || indicatorTypes[0].type).map((param) => ( updateIndicator(index, param, param.includes('multiplier') ? parseFloat(e.target.value) : parseInt(e.target.value))} type='number' step={param.includes('multiplier') ? '0.1' : '1'} diff --git a/src/Managing.WebApp/src/components/organism/Trading/Summary.tsx b/src/Managing.WebApp/src/components/organism/Trading/Summary.tsx index 6242c01..dec6ed1 100644 --- a/src/Managing.WebApp/src/components/organism/Trading/Summary.tsx +++ b/src/Managing.WebApp/src/components/organism/Trading/Summary.tsx @@ -1,200 +1,124 @@ -import {ArrowDownIcon, ArrowUpIcon} from '@heroicons/react/solid' import React, {useEffect, useState} from 'react' import useApiUrlStore from '../../../app/store/apiStore' -import type {PlatformSummaryViewModel, TradingBot} from '../../../generated/ManagingApi' -import {DataClient, PositionStatus, TradeDirection} from '../../../generated/ManagingApi' -import type {IAccountBalanceProps} from '../../../global/type' +import type {PlatformSummaryViewModel, TradingBotResponse} from '../../../generated/ManagingApi' +import {DataClient} from '../../../generated/ManagingApi' // Time filter options matching backend const TIME_FILTERS = ['24H', '3D', '1W', '1M', '1Y', 'Total'] -function GetGlobalWinrate(bots: TradingBot[]) { - if (bots == null || bots == undefined || bots.length == 0) { - return 0 - } - - let totalPositions = 0 - let winningPosition = 0 - - bots.forEach((bot) => { - totalPositions += bot.positions.filter( - (p) => p.status != PositionStatus.New - ).length - winningPosition += bot.positions.filter((p) => { - const realized = p.profitAndLoss?.realized ?? 0 - return realized > 0 && - (p.status == PositionStatus.Finished || - p.status == PositionStatus.Flipped) - ? p - : null - }).length - }) - - if (totalPositions == 0) return 0 - - return (winningPosition * 100) / totalPositions -} - -function GetPositionCount( - bots: TradingBot[], - direction: TradeDirection, - status: PositionStatus -) { - let totalPositions = 0 - - if (bots == null || bots == undefined) { - return 0 - } - - bots.forEach((bot) => { - totalPositions += bot.positions.filter( - (p) => p.status == status && p.originDirection == direction - ).length - }) - - return totalPositions -} - -const Summary: React.FC = ({ bots }) => { - const [globalPnl, setGlobalPnl] = useState(0) - const [globalWinrate, setGlobalWinrate] = useState(0) - const [selectedTimeFilter, setSelectedTimeFilter] = useState('Total') - const [platformStats, setPlatformStats] = useState(null) - const [isLoading, setIsLoading] = useState(false) - - const [openLong, setLong] = useState(0) - const [openShort, setShort] = useState(0) - - const [closedLong, setClosedLong] = useState(0) - const [closedShort, setClosedShort] = useState(0) - - const { apiUrl } = useApiUrlStore() - - useEffect(() => { - if (bots) { - const pnl = bots.reduce((acc, bot) => { - return acc + bot.profitAndLoss - }, 0) - setGlobalPnl(pnl) - setGlobalWinrate(GetGlobalWinrate(bots)) - setLong( - GetPositionCount(bots, TradeDirection.Long, PositionStatus.Filled) - ) - setShort( - GetPositionCount(bots, TradeDirection.Short, PositionStatus.Filled) - ) - setClosedLong( - GetPositionCount(bots, TradeDirection.Long, PositionStatus.Finished) + - GetPositionCount(bots, TradeDirection.Long, PositionStatus.Flipped) - ) - setClosedShort( - GetPositionCount(bots, TradeDirection.Short, PositionStatus.Finished) + - GetPositionCount(bots, TradeDirection.Short, PositionStatus.Flipped) - ) - } - }, [bots]) - - // Fetch platform summary data - useEffect(() => { - const fetchPlatformStats = async () => { - setIsLoading(true) - try { - const dataClient = new DataClient({}, apiUrl) - const data = await dataClient.data_GetPlatformSummary(selectedTimeFilter) - setPlatformStats(data) - } catch (error) { - console.error('Error fetching platform stats:', error) - } finally { - setIsLoading(false) - } +function GetGlobalWinrate(bots: TradingBotResponse[]) { + if (bots == null || bots == undefined || bots.length == 0) { + return 0 } - fetchPlatformStats() - }, [apiUrl, selectedTimeFilter]) + return bots.reduce((acc, bot) => { + return acc + bot.winRate + }, 0) / bots.length +} - return ( -
-
-

Platform Overview

-
- {TIME_FILTERS.map((filter) => ( - - ))} -
-
+const Summary: React.FC<{ bots: TradingBotResponse[] }> = ({bots}) => { + const [globalPnl, setGlobalPnl] = useState(0) + const [globalWinrate, setGlobalWinrate] = useState(0) + const [selectedTimeFilter, setSelectedTimeFilter] = useState('Total') + const [platformStats, setPlatformStats] = useState(null) + const [isLoading, setIsLoading] = useState(false) -
-
-
Total Agents
-
{platformStats?.totalAgents ?? 0}
-
+ const {apiUrl} = useApiUrlStore() -
-
Active Strategies
-
{platformStats?.totalActiveStrategies ?? 0}
-
+ useEffect(() => { + if (bots) { + const pnl = bots.reduce((acc, bot) => { + return acc + bot.profitAndLoss + }, 0) + setGlobalPnl(pnl) + setGlobalWinrate(GetGlobalWinrate(bots)) -
-
Total Platform PnL
-
{(platformStats?.totalPlatformPnL ?? 0).toFixed(2)} $
-
+ } + }, [bots]) -
-
Volume (Total)
-
{(platformStats?.totalPlatformVolume ?? 0).toFixed(2)} $
-
+ // Fetch platform summary data + useEffect(() => { + const fetchPlatformStats = async () => { + setIsLoading(true) + try { + const dataClient = new DataClient({}, apiUrl) + const data = await dataClient.data_GetPlatformSummary(selectedTimeFilter) + setPlatformStats(data) + } catch (error) { + console.error('Error fetching platform stats:', error) + } finally { + setIsLoading(false) + } + } -
-
Volume (24h)
-
{(platformStats?.totalPlatformVolumeLast24h ?? 0).toFixed(2)} $
-
-
+ fetchPlatformStats() + }, [apiUrl, selectedTimeFilter]) -
-
-
Bots running
-
{bots.length}
-
+ return ( +
+
+

Platform Overview

+
+ {TIME_FILTERS.map((filter) => ( + + ))} +
+
-
-
Total Profit
-
{globalPnl.toFixed(4)} $
-
+
+
+
Total Agents
+
{platformStats?.totalAgents ?? 0}
+
-
-
Global Winrate
-
- {globalWinrate ? globalWinrate.toFixed(2) : 0} % -
-
+
+
Active Strategies
+
{platformStats?.totalActiveStrategies ?? 0}
+
-
-
Positions Openend
-
- {openLong} {' '} - {openShort}{' '} - {' '} -
+
+
Total Platform PnL
+
{(platformStats?.totalPlatformPnL ?? 0).toFixed(2)} $
+
+ +
+
Volume (Total)
+
{(platformStats?.totalPlatformVolume ?? 0).toFixed(2)} $
+
+ +
+
Volume (24h)
+
{(platformStats?.totalPlatformVolumeLast24h ?? 0).toFixed(2)} $
+
+
+ +
+
+
Bots running
+
{bots.length}
+
+ +
+
Total Profit
+
{globalPnl.toFixed(4)} $
+
+ +
+
Global Winrate
+
+ {globalWinrate ? globalWinrate.toFixed(2) : 0} % +
+
+
-
-
Positions Closed
-
- {closedLong}{' '} - {' '} - {closedShort}{' '} - {' '} -
-
-
-
- ) + ) } export default Summary diff --git a/src/Managing.WebApp/src/components/organism/UnifiedTradingModal/UnifiedTradingModal.tsx b/src/Managing.WebApp/src/components/organism/UnifiedTradingModal/UnifiedTradingModal.tsx index 815bd29..4673a9f 100644 --- a/src/Managing.WebApp/src/components/organism/UnifiedTradingModal/UnifiedTradingModal.tsx +++ b/src/Managing.WebApp/src/components/organism/UnifiedTradingModal/UnifiedTradingModal.tsx @@ -12,12 +12,12 @@ import { BotClient, DataClient, LightBacktestResponse, + LightScenario, MoneyManagement, MoneyManagementClient, RiskManagement, RiskToleranceLevel, RunBacktestRequest, - Scenario, ScenarioClient, ScenarioRequest, SignalType, @@ -41,6 +41,7 @@ const UnifiedTradingModal: React.FC = ({ setBacktests, backtest, existingBot, + onBacktestComplete, }) => { // Default dates for backtests const defaultStartDate = new Date(); @@ -128,7 +129,7 @@ const UnifiedTradingModal: React.FC = ({ const [selectedMoneyManagement, setSelectedMoneyManagement] = useState(); const [showCustomMoneyManagement, setShowCustomMoneyManagement] = useState(false); - const [customScenario, setCustomScenario] = useState(undefined); + const [customScenario, setCustomScenario] = useState(undefined); const [selectedScenario, setSelectedScenario] = useState(); const [showCustomScenario, setShowCustomScenario] = useState(false); @@ -334,10 +335,11 @@ const UnifiedTradingModal: React.FC = ({ } // Handle scenario - check if we have scenario data or just a name reference - if ((config as any).scenario) { + if (config.scenario) { setShowCustomScenario(true); - setCustomScenario((config as any).scenario); - setGlobalCustomScenario((config as any).scenario); // Also update global store for prefilling + // Use LightScenario directly since CustomScenario now supports it + setCustomScenario(config.scenario); + setGlobalCustomScenario(config.scenario); // Also update global store for prefilling setSelectedScenario('custom'); // Set dropdown to show "custom" } else if (config.scenarioName) { setValue('scenarioName', config.scenarioName); @@ -556,7 +558,7 @@ const UnifiedTradingModal: React.FC = ({ }; // Helper function to convert custom scenario to ScenarioRequest format - const convertScenarioToRequest = (scenario: Scenario): ScenarioRequest => { + const convertScenarioToRequest = (scenario: LightScenario): ScenarioRequest => { return { name: scenario.name || 'Custom Scenario', loopbackPeriod: scenario.loopbackPeriod || null, @@ -605,9 +607,13 @@ const UnifiedTradingModal: React.FC = ({ } }; - // Bot submission handler - const handleBotSubmission = async (form: IUnifiedTradingConfigInput) => { - const t = new Toast(mode === 'createBot' ? 'Creating bot...' : 'Updating bot...'); + // Unified bot submission handler + const handleBotSubmission = async (form: IUnifiedTradingConfigInput, saveOnly: boolean = false) => { + const t = new Toast( + mode === 'createBot' + ? (saveOnly ? 'Saving bot configuration...' : 'Creating bot...') + : 'Updating bot...' + ); try { // Create money management object @@ -657,8 +663,14 @@ const UnifiedTradingModal: React.FC = ({ const request: StartBotRequest = { config: tradingBotConfigRequest, }; - await botClient.bot_Start(request); - t.update('success', 'Bot created successfully!'); + + if (saveOnly) { + await botClient.bot_Save(request); + t.update('success', 'Bot configuration saved successfully!'); + } else { + await botClient.bot_Start(request); + t.update('success', 'Bot created successfully!'); + } } else { const request: UpdateBotConfigRequest = { identifier: existingBot!.identifier, @@ -715,6 +727,11 @@ const UnifiedTradingModal: React.FC = ({ t.update('success', `${ticker} Backtest Succeeded`); addBacktest(backtest as unknown as LightBacktestResponse); + + // Call the callback to notify parent component that backtest is completed + if (onBacktestComplete) { + onBacktestComplete(); + } } catch (err: any) { @@ -1595,9 +1612,24 @@ const UnifiedTradingModal: React.FC = ({ > Cancel - + {mode === 'createBot' ? ( + <> + + + + ) : ( + + )}
); diff --git a/src/Managing.WebApp/src/generated/ManagingApi.ts b/src/Managing.WebApp/src/generated/ManagingApi.ts index d064c03..06034b7 100644 --- a/src/Managing.WebApp/src/generated/ManagingApi.ts +++ b/src/Managing.WebApp/src/generated/ManagingApi.ts @@ -1161,9 +1161,50 @@ export class BotClient extends AuthorizedApiBase { return Promise.resolve(null as any); } - bot_Stop(identifier: string | null | undefined): Promise { + bot_Save(request: SaveBotRequest): Promise { + let url_ = this.baseUrl + "/Bot/Save"; + url_ = url_.replace(/[?&]$/, ""); + + const content_ = JSON.stringify(request); + + let options_: RequestInit = { + body: content_, + method: "POST", + headers: { + "Content-Type": "application/json", + "Accept": "application/json" + } + }; + + return this.transformOptions(options_).then(transformedOptions_ => { + return this.http.fetch(url_, transformedOptions_); + }).then((_response: Response) => { + return this.processBot_Save(_response); + }); + } + + protected processBot_Save(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 200) { + return response.text().then((_responseText) => { + let result200: any = null; + result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as string; + return result200; + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } + + bot_Stop(identifier: string | undefined): Promise { let url_ = this.baseUrl + "/Bot/Stop?"; - if (identifier !== undefined && identifier !== null) + if (identifier === null) + throw new Error("The parameter 'identifier' cannot be null."); + else if (identifier !== undefined) url_ += "identifier=" + encodeURIComponent("" + identifier) + "&"; url_ = url_.replace(/[?&]$/, ""); @@ -1181,13 +1222,13 @@ export class BotClient extends AuthorizedApiBase { }); } - protected processBot_Stop(response: Response): Promise { + protected processBot_Stop(response: Response): Promise { const status = response.status; let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; if (status === 200) { return response.text().then((_responseText) => { let result200: any = null; - result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as string; + result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as BotStatus; return result200; }); } else if (status !== 200 && status !== 204) { @@ -1195,12 +1236,14 @@ export class BotClient extends AuthorizedApiBase { return throwException("An unexpected server error occurred.", status, _responseText, _headers); }); } - return Promise.resolve(null as any); + return Promise.resolve(null as any); } - bot_Delete(identifier: string | null | undefined): Promise { + bot_Delete(identifier: string | undefined): Promise { let url_ = this.baseUrl + "/Bot/Delete?"; - if (identifier !== undefined && identifier !== null) + if (identifier === null) + throw new Error("The parameter 'identifier' cannot be null."); + else if (identifier !== undefined) url_ += "identifier=" + encodeURIComponent("" + identifier) + "&"; url_ = url_.replace(/[?&]$/, ""); @@ -1235,48 +1278,11 @@ export class BotClient extends AuthorizedApiBase { return Promise.resolve(null as any); } - bot_StopAll(): Promise { - let url_ = this.baseUrl + "/Bot/stop-all"; - url_ = url_.replace(/[?&]$/, ""); - - let options_: RequestInit = { - method: "POST", - headers: { - "Accept": "application/json" - } - }; - - return this.transformOptions(options_).then(transformedOptions_ => { - return this.http.fetch(url_, transformedOptions_); - }).then((_response: Response) => { - return this.processBot_StopAll(_response); - }); - } - - protected processBot_StopAll(response: Response): Promise { - const status = response.status; - let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; - if (status === 200) { - return response.text().then((_responseText) => { - let result200: any = null; - result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as string; - return result200; - }); - } else if (status !== 200 && status !== 204) { - return response.text().then((_responseText) => { - return throwException("An unexpected server error occurred.", status, _responseText, _headers); - }); - } - return Promise.resolve(null as any); - } - - bot_Restart(botType: BotType | undefined, identifier: string | null | undefined): Promise { + bot_Restart(identifier: string | undefined): Promise { let url_ = this.baseUrl + "/Bot/Restart?"; - if (botType === null) - throw new Error("The parameter 'botType' cannot be null."); - else if (botType !== undefined) - url_ += "botType=" + encodeURIComponent("" + botType) + "&"; - if (identifier !== undefined && identifier !== null) + if (identifier === null) + throw new Error("The parameter 'identifier' cannot be null."); + else if (identifier !== undefined) url_ += "identifier=" + encodeURIComponent("" + identifier) + "&"; url_ = url_.replace(/[?&]$/, ""); @@ -1311,78 +1317,6 @@ export class BotClient extends AuthorizedApiBase { return Promise.resolve(null as any); } - bot_RestartAll(): Promise { - let url_ = this.baseUrl + "/Bot/restart-all"; - url_ = url_.replace(/[?&]$/, ""); - - let options_: RequestInit = { - method: "POST", - headers: { - "Accept": "application/json" - } - }; - - return this.transformOptions(options_).then(transformedOptions_ => { - return this.http.fetch(url_, transformedOptions_); - }).then((_response: Response) => { - return this.processBot_RestartAll(_response); - }); - } - - protected processBot_RestartAll(response: Response): Promise { - const status = response.status; - let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; - if (status === 200) { - return response.text().then((_responseText) => { - let result200: any = null; - result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as string; - return result200; - }); - } else if (status !== 200 && status !== 204) { - return response.text().then((_responseText) => { - return throwException("An unexpected server error occurred.", status, _responseText, _headers); - }); - } - return Promise.resolve(null as any); - } - - bot_ToggleIsForWatching(identifier: string | null | undefined): Promise { - let url_ = this.baseUrl + "/Bot/ToggleIsForWatching?"; - if (identifier !== undefined && identifier !== null) - url_ += "identifier=" + encodeURIComponent("" + identifier) + "&"; - url_ = url_.replace(/[?&]$/, ""); - - let options_: RequestInit = { - method: "GET", - headers: { - "Accept": "application/json" - } - }; - - return this.transformOptions(options_).then(transformedOptions_ => { - return this.http.fetch(url_, transformedOptions_); - }).then((_response: Response) => { - return this.processBot_ToggleIsForWatching(_response); - }); - } - - protected processBot_ToggleIsForWatching(response: Response): Promise { - const status = response.status; - let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; - if (status === 200) { - return response.text().then((_responseText) => { - let result200: any = null; - result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as string; - return result200; - }); - } else if (status !== 200 && status !== 204) { - return response.text().then((_responseText) => { - return throwException("An unexpected server error occurred.", status, _responseText, _headers); - }); - } - return Promise.resolve(null as any); - } - bot_GetActiveBots(): Promise { let url_ = this.baseUrl + "/Bot"; url_ = url_.replace(/[?&]$/, ""); @@ -1457,6 +1391,134 @@ export class BotClient extends AuthorizedApiBase { return Promise.resolve(null as any); } + bot_GetBotsByStatus(status: BotStatus): Promise { + let url_ = this.baseUrl + "/Bot/ByStatus/{status}"; + if (status === undefined || status === null) + throw new Error("The parameter 'status' must be defined."); + url_ = url_.replace("{status}", encodeURIComponent("" + status)); + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "GET", + headers: { + "Accept": "application/json" + } + }; + + return this.transformOptions(options_).then(transformedOptions_ => { + return this.http.fetch(url_, transformedOptions_); + }).then((_response: Response) => { + return this.processBot_GetBotsByStatus(_response); + }); + } + + protected processBot_GetBotsByStatus(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 200) { + return response.text().then((_responseText) => { + let result200: any = null; + result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as TradingBotResponse[]; + return result200; + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } + + bot_GetMySavedBots(): Promise { + let url_ = this.baseUrl + "/Bot/GetMySavedBots"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "GET", + headers: { + "Accept": "application/json" + } + }; + + return this.transformOptions(options_).then(transformedOptions_ => { + return this.http.fetch(url_, transformedOptions_); + }).then((_response: Response) => { + return this.processBot_GetMySavedBots(_response); + }); + } + + protected processBot_GetMySavedBots(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 200) { + return response.text().then((_responseText) => { + let result200: any = null; + result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as TradingBotResponse[]; + return result200; + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } + + bot_GetBotsPaginated(pageNumber: number | undefined, pageSize: number | undefined, status: BotStatus | null | undefined, name: string | null | undefined, ticker: string | null | undefined, agentName: string | null | undefined, sortBy: string | null | undefined, sortDirection: string | null | undefined): Promise { + let url_ = this.baseUrl + "/Bot/Paginated?"; + if (pageNumber === null) + throw new Error("The parameter 'pageNumber' cannot be null."); + else if (pageNumber !== undefined) + url_ += "pageNumber=" + encodeURIComponent("" + pageNumber) + "&"; + if (pageSize === null) + throw new Error("The parameter 'pageSize' cannot be null."); + else if (pageSize !== undefined) + url_ += "pageSize=" + encodeURIComponent("" + pageSize) + "&"; + if (status !== undefined && status !== null) + url_ += "status=" + encodeURIComponent("" + status) + "&"; + if (name !== undefined && name !== null) + url_ += "name=" + encodeURIComponent("" + name) + "&"; + if (ticker !== undefined && ticker !== null) + url_ += "ticker=" + encodeURIComponent("" + ticker) + "&"; + if (agentName !== undefined && agentName !== null) + url_ += "agentName=" + encodeURIComponent("" + agentName) + "&"; + if (sortBy !== undefined && sortBy !== null) + url_ += "sortBy=" + encodeURIComponent("" + sortBy) + "&"; + if (sortDirection !== undefined && sortDirection !== null) + url_ += "sortDirection=" + encodeURIComponent("" + sortDirection) + "&"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "GET", + headers: { + "Accept": "application/json" + } + }; + + return this.transformOptions(options_).then(transformedOptions_ => { + return this.http.fetch(url_, transformedOptions_); + }).then((_response: Response) => { + return this.processBot_GetBotsPaginated(_response); + }); + } + + protected processBot_GetBotsPaginated(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 200) { + return response.text().then((_responseText) => { + let result200: any = null; + result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as PaginatedResponseOfTradingBotResponse; + return result200; + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } + bot_OpenPositionManually(request: OpenPositionManuallyRequest): Promise { let url_ = this.baseUrl + "/Bot/OpenPosition"; url_ = url_.replace(/[?&]$/, ""); @@ -1535,6 +1597,44 @@ export class BotClient extends AuthorizedApiBase { return Promise.resolve(null as any); } + bot_GetBotConfig(identifier: string): Promise { + let url_ = this.baseUrl + "/Bot/GetConfig/{identifier}"; + if (identifier === undefined || identifier === null) + throw new Error("The parameter 'identifier' must be defined."); + url_ = url_.replace("{identifier}", encodeURIComponent("" + identifier)); + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "GET", + headers: { + "Accept": "application/json" + } + }; + + return this.transformOptions(options_).then(transformedOptions_ => { + return this.http.fetch(url_, transformedOptions_); + }).then((_response: Response) => { + return this.processBot_GetBotConfig(_response); + }); + } + + protected processBot_GetBotConfig(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 200) { + return response.text().then((_responseText) => { + let result200: any = null; + result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as TradingBotConfig; + return result200; + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } + bot_UpdateBotConfig(request: UpdateBotConfigRequest): Promise { let url_ = this.baseUrl + "/Bot/UpdateConfig"; url_ = url_.replace(/[?&]$/, ""); @@ -1882,47 +1982,8 @@ export class DataClient extends AuthorizedApiBase { return Promise.resolve(null as any); } - data_GetAgentIndex(timeFilter: string | null | undefined): Promise { - let url_ = this.baseUrl + "/Data/GetAgentIndex?"; - if (timeFilter !== undefined && timeFilter !== null) - url_ += "timeFilter=" + encodeURIComponent("" + timeFilter) + "&"; - url_ = url_.replace(/[?&]$/, ""); - - let options_: RequestInit = { - method: "GET", - headers: { - "Accept": "application/json" - } - }; - - return this.transformOptions(options_).then(transformedOptions_ => { - return this.http.fetch(url_, transformedOptions_); - }).then((_response: Response) => { - return this.processData_GetAgentIndex(_response); - }); - } - - protected processData_GetAgentIndex(response: Response): Promise { - const status = response.status; - let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; - if (status === 200) { - return response.text().then((_responseText) => { - let result200: any = null; - result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as AgentIndexViewModel; - return result200; - }); - } else if (status !== 200 && status !== 204) { - return response.text().then((_responseText) => { - return throwException("An unexpected server error occurred.", status, _responseText, _headers); - }); - } - return Promise.resolve(null as any); - } - - data_GetAgentIndexPaginated(timeFilter: string | null | undefined, page: number | undefined, pageSize: number | undefined, sortBy: string | null | undefined, sortOrder: string | null | undefined, agentNames: string | null | undefined): Promise { + data_GetAgentIndexPaginated(page: number | undefined, pageSize: number | undefined, sortBy: SortableFields | undefined, sortOrder: string | null | undefined, agentNames: string | null | undefined): Promise { let url_ = this.baseUrl + "/Data/GetAgentIndexPaginated?"; - if (timeFilter !== undefined && timeFilter !== null) - url_ += "timeFilter=" + encodeURIComponent("" + timeFilter) + "&"; if (page === null) throw new Error("The parameter 'page' cannot be null."); else if (page !== undefined) @@ -1931,7 +1992,9 @@ export class DataClient extends AuthorizedApiBase { throw new Error("The parameter 'pageSize' cannot be null."); else if (pageSize !== undefined) url_ += "pageSize=" + encodeURIComponent("" + pageSize) + "&"; - if (sortBy !== undefined && sortBy !== null) + if (sortBy === null) + throw new Error("The parameter 'sortBy' cannot be null."); + else if (sortBy !== undefined) url_ += "sortBy=" + encodeURIComponent("" + sortBy) + "&"; if (sortOrder !== undefined && sortOrder !== null) url_ += "sortOrder=" + encodeURIComponent("" + sortOrder) + "&"; @@ -2983,9 +3046,11 @@ export class TradingClient extends AuthorizedApiBase { return Promise.resolve(null as any); } - trading_ClosePosition(identifier: string | null | undefined): Promise { + trading_ClosePosition(identifier: string | undefined): Promise { let url_ = this.baseUrl + "/Trading/ClosePosition?"; - if (identifier !== undefined && identifier !== null) + if (identifier === null) + throw new Error("The parameter 'identifier' cannot be null."); + else if (identifier !== undefined) url_ += "identifier=" + encodeURIComponent("" + identifier) + "&"; url_ = url_.replace(/[?&]$/, ""); @@ -3133,7 +3198,7 @@ export class UserClient extends AuthorizedApiBase { } user_CreateToken(login: LoginRequest): Promise { - let url_ = this.baseUrl + "/User"; + let url_ = this.baseUrl + "/User/create-token"; url_ = url_.replace(/[?&]$/, ""); const content_ = JSON.stringify(login); @@ -3550,6 +3615,7 @@ export enum AccountType { } export interface User { + id?: number; name?: string | null; accounts?: Account[] | null; agentName?: string | null; @@ -3736,8 +3802,8 @@ export interface Backtest { growthPercentage: number; hodlPercentage: number; config: TradingBotConfig; - positions: Position[]; - signals: LightSignal[]; + positions: { [key: string]: Position; }; + signals: { [key: string]: LightSignal; }; candles: Candle[]; startDate: Date; endDate: Date; @@ -3745,7 +3811,6 @@ export interface Backtest { fees: number; walletBalances: KeyValuePairOfDateTimeAndDecimal[]; user: User; - indicatorsValues: { [key in keyof typeof IndicatorType]?: IndicatorsResultBase; }; score: number; requestId?: string; metadata?: any | null; @@ -3948,10 +4013,7 @@ export enum PositionInitiator { CopyTrading = "CopyTrading", } -export interface ValueObject { -} - -export interface LightSignal extends ValueObject { +export interface LightSignal { status: SignalStatus; direction: TradeDirection; confidence: Confidence; @@ -4008,96 +4070,6 @@ export interface KeyValuePairOfDateTimeAndDecimal { value?: number; } -export interface IndicatorsResultBase { - ema?: EmaResult[] | null; - fastEma?: EmaResult[] | null; - slowEma?: EmaResult[] | null; - macd?: MacdResult[] | null; - rsi?: RsiResult[] | null; - stoch?: StochResult[] | null; - stochRsi?: StochRsiResult[] | null; - bollingerBands?: BollingerBandsResult[] | null; - chandelierShort?: ChandelierResult[] | null; - stc?: StcResult[] | null; - stdDev?: StdDevResult[] | null; - superTrend?: SuperTrendResult[] | null; - chandelierLong?: ChandelierResult[] | null; -} - -export interface ResultBase { - date?: Date; -} - -export interface EmaResult extends ResultBase { - ema?: number | null; - "skender.Stock.Indicators.IReusableResult.Value"?: number | null; -} - -export interface MacdResult extends ResultBase { - macd?: number | null; - signal?: number | null; - histogram?: number | null; - fastEma?: number | null; - slowEma?: number | null; - "skender.Stock.Indicators.IReusableResult.Value"?: number | null; -} - -export interface RsiResult extends ResultBase { - rsi?: number | null; - "skender.Stock.Indicators.IReusableResult.Value"?: number | null; -} - -/** Stochastic indicator results includes aliases for those who prefer the simpler K,D,J outputs. See documentation for more information. */ -export interface StochResult extends ResultBase { - oscillator?: number | null; - signal?: number | null; - percentJ?: number | null; - k?: number | null; - d?: number | null; - j?: number | null; - "skender.Stock.Indicators.IReusableResult.Value"?: number | null; -} - -export interface StochRsiResult extends ResultBase { - stochRsi?: number | null; - signal?: number | null; - "skender.Stock.Indicators.IReusableResult.Value"?: number | null; -} - -export interface BollingerBandsResult extends ResultBase { - sma?: number | null; - upperBand?: number | null; - lowerBand?: number | null; - percentB?: number | null; - zScore?: number | null; - width?: number | null; - "skender.Stock.Indicators.IReusableResult.Value"?: number | null; -} - -export interface ChandelierResult extends ResultBase { - chandelierExit?: number | null; - "skender.Stock.Indicators.IReusableResult.Value"?: number | null; -} - -export interface StcResult extends ResultBase { - stc?: number | null; - "skender.Stock.Indicators.IReusableResult.Value"?: number | null; -} - -export interface StdDevResult extends ResultBase { - stdDev?: number | null; - mean?: number | null; - zScore?: number | null; - stdDevSma?: number | null; - "skender.Stock.Indicators.IReusableResult.Value"?: number | null; -} - -export interface SuperTrendResult extends ResultBase { - superTrend?: number | null; - upperBand?: number | null; - lowerBand?: number | null; -} - export interface DeleteBacktestsRequest { backtestIds: string[]; } @@ -4332,34 +4304,48 @@ export interface StartBotRequest { config?: TradingBotConfigRequest | null; } -export enum BotType { - SimpleBot = "SimpleBot", - ScalpingBot = "ScalpingBot", - FlippingBot = "FlippingBot", +export interface SaveBotRequest extends StartBotRequest { +} + +export enum BotStatus { + None = "None", + Down = "Down", + Up = "Up", } export interface TradingBotResponse { status: string; - signals: LightSignal[]; - positions: Position[]; + signals: { [key: string]: LightSignal; }; + positions: { [key: string]: Position; }; candles: Candle[]; winRate: number; profitAndLoss: number; identifier: string; agentName: string; - config: TradingBotConfig; createDate: Date; startupTime: Date; + name: string; + ticker: Ticker; +} + +export interface PaginatedResponseOfTradingBotResponse { + items?: TradingBotResponse[] | null; + totalCount?: number; + pageNumber?: number; + pageSize?: number; + totalPages?: number; + hasPreviousPage?: boolean; + hasNextPage?: boolean; } export interface OpenPositionManuallyRequest { - identifier?: string | null; + identifier?: string; direction?: TradeDirection; } export interface ClosePositionRequest { - identifier?: string | null; - positionId?: string | null; + identifier?: string; + positionId?: string; } export interface UpdateBotConfigRequest { @@ -4388,14 +4374,13 @@ export interface Spotlight { export interface Scenario { name?: string | null; - indicators?: Indicator[] | null; + indicators?: IndicatorBase[] | null; loopbackPeriod?: number | null; user?: User | null; } -export interface Indicator { +export interface IndicatorBase { name?: string | null; - candles?: FixedSizeQueueOfCandle | null; type?: IndicatorType; signalType?: SignalType; minimumHistory?: number; @@ -4410,15 +4395,6 @@ export interface Indicator { user?: User | null; } -export interface Anonymous { - maxSize?: number; -} - -export interface FixedSizeQueueOfCandle extends Anonymous { - - [key: string]: any; -} - export interface TickerSignal { ticker: Ticker; fiveMinutes: LightSignal[]; @@ -4433,6 +4409,96 @@ export interface CandlesWithIndicatorsResponse { indicatorsValues?: { [key in keyof typeof IndicatorType]?: IndicatorsResultBase; } | null; } +export interface IndicatorsResultBase { + ema?: EmaResult[] | null; + fastEma?: EmaResult[] | null; + slowEma?: EmaResult[] | null; + macd?: MacdResult[] | null; + rsi?: RsiResult[] | null; + stoch?: StochResult[] | null; + stochRsi?: StochRsiResult[] | null; + bollingerBands?: BollingerBandsResult[] | null; + chandelierShort?: ChandelierResult[] | null; + stc?: StcResult[] | null; + stdDev?: StdDevResult[] | null; + superTrend?: SuperTrendResult[] | null; + chandelierLong?: ChandelierResult[] | null; +} + +export interface ResultBase { + date?: Date; +} + +export interface EmaResult extends ResultBase { + ema?: number | null; + "skender.Stock.Indicators.IReusableResult.Value"?: number | null; +} + +export interface MacdResult extends ResultBase { + macd?: number | null; + signal?: number | null; + histogram?: number | null; + fastEma?: number | null; + slowEma?: number | null; + "skender.Stock.Indicators.IReusableResult.Value"?: number | null; +} + +export interface RsiResult extends ResultBase { + rsi?: number | null; + "skender.Stock.Indicators.IReusableResult.Value"?: number | null; +} + +/** Stochastic indicator results includes aliases for those who prefer the simpler K,D,J outputs. See documentation for more information. */ +export interface StochResult extends ResultBase { + oscillator?: number | null; + signal?: number | null; + percentJ?: number | null; + k?: number | null; + d?: number | null; + j?: number | null; + "skender.Stock.Indicators.IReusableResult.Value"?: number | null; +} + +export interface StochRsiResult extends ResultBase { + stochRsi?: number | null; + signal?: number | null; + "skender.Stock.Indicators.IReusableResult.Value"?: number | null; +} + +export interface BollingerBandsResult extends ResultBase { + sma?: number | null; + upperBand?: number | null; + lowerBand?: number | null; + percentB?: number | null; + zScore?: number | null; + width?: number | null; + "skender.Stock.Indicators.IReusableResult.Value"?: number | null; +} + +export interface ChandelierResult extends ResultBase { + chandelierExit?: number | null; + "skender.Stock.Indicators.IReusableResult.Value"?: number | null; +} + +export interface StcResult extends ResultBase { + stc?: number | null; + "skender.Stock.Indicators.IReusableResult.Value"?: number | null; +} + +export interface StdDevResult extends ResultBase { + stdDev?: number | null; + mean?: number | null; + zScore?: number | null; + stdDevSma?: number | null; + "skender.Stock.Indicators.IReusableResult.Value"?: number | null; +} + +export interface SuperTrendResult extends ResultBase { + superTrend?: number | null; + upperBand?: number | null; + lowerBand?: number | null; +} + export interface GetCandlesWithIndicatorsRequest { ticker?: Ticker; startDate?: Date; @@ -4467,9 +4533,9 @@ export interface UserStrategyDetailsViewModel { volumeLast24H?: number; wins?: number; losses?: number; - positions?: Position[] | null; + positions?: { [key: string]: Position; } | null; identifier?: string | null; - scenarioName?: string | null; + walletBalances?: { [key: string]: number; } | null; } export interface PlatformSummaryViewModel { @@ -4481,25 +4547,6 @@ export interface PlatformSummaryViewModel { timeFilter?: string | null; } -export interface AgentIndexViewModel { - agentSummaries?: AgentSummaryViewModel[] | null; - timeFilter?: string | null; -} - -export interface AgentSummaryViewModel { - agentName?: string | null; - totalPnL?: number; - pnLLast24h?: number; - totalROI?: number; - roiLast24h?: number; - wins?: number; - losses?: number; - averageWinRate?: number; - activeStrategiesCount?: number; - totalVolume?: number; - volumeLast24h?: number; -} - export interface PaginatedAgentIndexResponse { agentSummaries?: AgentSummaryViewModel[] | null; totalCount?: number; @@ -4509,11 +4556,31 @@ export interface PaginatedAgentIndexResponse { hasNextPage?: boolean; hasPreviousPage?: boolean; timeFilter?: string | null; - sortBy?: string | null; + sortBy?: SortableFields; sortOrder?: string | null; filteredAgentNames?: string | null; } +export interface AgentSummaryViewModel { + agentName?: string | null; + totalPnL?: number; + totalROI?: number; + wins?: number; + losses?: number; + activeStrategiesCount?: number; + totalVolume?: number; +} + +export enum SortableFields { + TotalPnL = "TotalPnL", + TotalROI = "TotalROI", + Wins = "Wins", + Losses = "Losses", + AgentName = "AgentName", + CreatedAt = "CreatedAt", + UpdatedAt = "UpdatedAt", +} + export interface AgentBalanceHistory { agentName?: string | null; agentBalances?: AgentBalance[] | null; diff --git a/src/Managing.WebApp/src/generated/ManagingApiTypes.ts b/src/Managing.WebApp/src/generated/ManagingApiTypes.ts index e20fb70..d583dbc 100644 --- a/src/Managing.WebApp/src/generated/ManagingApiTypes.ts +++ b/src/Managing.WebApp/src/generated/ManagingApiTypes.ts @@ -38,6 +38,7 @@ export enum AccountType { } export interface User { + id?: number; name?: string | null; accounts?: Account[] | null; agentName?: string | null; @@ -224,8 +225,8 @@ export interface Backtest { growthPercentage: number; hodlPercentage: number; config: TradingBotConfig; - positions: Position[]; - signals: LightSignal[]; + positions: { [key: string]: Position; }; + signals: { [key: string]: LightSignal; }; candles: Candle[]; startDate: Date; endDate: Date; @@ -233,7 +234,6 @@ export interface Backtest { fees: number; walletBalances: KeyValuePairOfDateTimeAndDecimal[]; user: User; - indicatorsValues: { [key in keyof typeof IndicatorType]?: IndicatorsResultBase; }; score: number; requestId?: string; metadata?: any | null; @@ -436,10 +436,7 @@ export enum PositionInitiator { CopyTrading = "CopyTrading", } -export interface ValueObject { -} - -export interface LightSignal extends ValueObject { +export interface LightSignal { status: SignalStatus; direction: TradeDirection; confidence: Confidence; @@ -496,96 +493,6 @@ export interface KeyValuePairOfDateTimeAndDecimal { value?: number; } -export interface IndicatorsResultBase { - ema?: EmaResult[] | null; - fastEma?: EmaResult[] | null; - slowEma?: EmaResult[] | null; - macd?: MacdResult[] | null; - rsi?: RsiResult[] | null; - stoch?: StochResult[] | null; - stochRsi?: StochRsiResult[] | null; - bollingerBands?: BollingerBandsResult[] | null; - chandelierShort?: ChandelierResult[] | null; - stc?: StcResult[] | null; - stdDev?: StdDevResult[] | null; - superTrend?: SuperTrendResult[] | null; - chandelierLong?: ChandelierResult[] | null; -} - -export interface ResultBase { - date?: Date; -} - -export interface EmaResult extends ResultBase { - ema?: number | null; - "skender.Stock.Indicators.IReusableResult.Value"?: number | null; -} - -export interface MacdResult extends ResultBase { - macd?: number | null; - signal?: number | null; - histogram?: number | null; - fastEma?: number | null; - slowEma?: number | null; - "skender.Stock.Indicators.IReusableResult.Value"?: number | null; -} - -export interface RsiResult extends ResultBase { - rsi?: number | null; - "skender.Stock.Indicators.IReusableResult.Value"?: number | null; -} - -/** Stochastic indicator results includes aliases for those who prefer the simpler K,D,J outputs. See documentation for more information. */ -export interface StochResult extends ResultBase { - oscillator?: number | null; - signal?: number | null; - percentJ?: number | null; - k?: number | null; - d?: number | null; - j?: number | null; - "skender.Stock.Indicators.IReusableResult.Value"?: number | null; -} - -export interface StochRsiResult extends ResultBase { - stochRsi?: number | null; - signal?: number | null; - "skender.Stock.Indicators.IReusableResult.Value"?: number | null; -} - -export interface BollingerBandsResult extends ResultBase { - sma?: number | null; - upperBand?: number | null; - lowerBand?: number | null; - percentB?: number | null; - zScore?: number | null; - width?: number | null; - "skender.Stock.Indicators.IReusableResult.Value"?: number | null; -} - -export interface ChandelierResult extends ResultBase { - chandelierExit?: number | null; - "skender.Stock.Indicators.IReusableResult.Value"?: number | null; -} - -export interface StcResult extends ResultBase { - stc?: number | null; - "skender.Stock.Indicators.IReusableResult.Value"?: number | null; -} - -export interface StdDevResult extends ResultBase { - stdDev?: number | null; - mean?: number | null; - zScore?: number | null; - stdDevSma?: number | null; - "skender.Stock.Indicators.IReusableResult.Value"?: number | null; -} - -export interface SuperTrendResult extends ResultBase { - superTrend?: number | null; - upperBand?: number | null; - lowerBand?: number | null; -} - export interface DeleteBacktestsRequest { backtestIds: string[]; } @@ -820,34 +727,48 @@ export interface StartBotRequest { config?: TradingBotConfigRequest | null; } -export enum BotType { - SimpleBot = "SimpleBot", - ScalpingBot = "ScalpingBot", - FlippingBot = "FlippingBot", +export interface SaveBotRequest extends StartBotRequest { +} + +export enum BotStatus { + None = "None", + Down = "Down", + Up = "Up", } export interface TradingBotResponse { status: string; - signals: LightSignal[]; - positions: Position[]; + signals: { [key: string]: LightSignal; }; + positions: { [key: string]: Position; }; candles: Candle[]; winRate: number; profitAndLoss: number; identifier: string; agentName: string; - config: TradingBotConfig; createDate: Date; startupTime: Date; + name: string; + ticker: Ticker; +} + +export interface PaginatedResponseOfTradingBotResponse { + items?: TradingBotResponse[] | null; + totalCount?: number; + pageNumber?: number; + pageSize?: number; + totalPages?: number; + hasPreviousPage?: boolean; + hasNextPage?: boolean; } export interface OpenPositionManuallyRequest { - identifier?: string | null; + identifier?: string; direction?: TradeDirection; } export interface ClosePositionRequest { - identifier?: string | null; - positionId?: string | null; + identifier?: string; + positionId?: string; } export interface UpdateBotConfigRequest { @@ -876,14 +797,13 @@ export interface Spotlight { export interface Scenario { name?: string | null; - indicators?: Indicator[] | null; + indicators?: IndicatorBase[] | null; loopbackPeriod?: number | null; user?: User | null; } -export interface Indicator { +export interface IndicatorBase { name?: string | null; - candles?: FixedSizeQueueOfCandle | null; type?: IndicatorType; signalType?: SignalType; minimumHistory?: number; @@ -898,15 +818,6 @@ export interface Indicator { user?: User | null; } -export interface Anonymous { - maxSize?: number; -} - -export interface FixedSizeQueueOfCandle extends Anonymous { - - [key: string]: any; -} - export interface TickerSignal { ticker: Ticker; fiveMinutes: LightSignal[]; @@ -921,6 +832,96 @@ export interface CandlesWithIndicatorsResponse { indicatorsValues?: { [key in keyof typeof IndicatorType]?: IndicatorsResultBase; } | null; } +export interface IndicatorsResultBase { + ema?: EmaResult[] | null; + fastEma?: EmaResult[] | null; + slowEma?: EmaResult[] | null; + macd?: MacdResult[] | null; + rsi?: RsiResult[] | null; + stoch?: StochResult[] | null; + stochRsi?: StochRsiResult[] | null; + bollingerBands?: BollingerBandsResult[] | null; + chandelierShort?: ChandelierResult[] | null; + stc?: StcResult[] | null; + stdDev?: StdDevResult[] | null; + superTrend?: SuperTrendResult[] | null; + chandelierLong?: ChandelierResult[] | null; +} + +export interface ResultBase { + date?: Date; +} + +export interface EmaResult extends ResultBase { + ema?: number | null; + "skender.Stock.Indicators.IReusableResult.Value"?: number | null; +} + +export interface MacdResult extends ResultBase { + macd?: number | null; + signal?: number | null; + histogram?: number | null; + fastEma?: number | null; + slowEma?: number | null; + "skender.Stock.Indicators.IReusableResult.Value"?: number | null; +} + +export interface RsiResult extends ResultBase { + rsi?: number | null; + "skender.Stock.Indicators.IReusableResult.Value"?: number | null; +} + +/** Stochastic indicator results includes aliases for those who prefer the simpler K,D,J outputs. See documentation for more information. */ +export interface StochResult extends ResultBase { + oscillator?: number | null; + signal?: number | null; + percentJ?: number | null; + k?: number | null; + d?: number | null; + j?: number | null; + "skender.Stock.Indicators.IReusableResult.Value"?: number | null; +} + +export interface StochRsiResult extends ResultBase { + stochRsi?: number | null; + signal?: number | null; + "skender.Stock.Indicators.IReusableResult.Value"?: number | null; +} + +export interface BollingerBandsResult extends ResultBase { + sma?: number | null; + upperBand?: number | null; + lowerBand?: number | null; + percentB?: number | null; + zScore?: number | null; + width?: number | null; + "skender.Stock.Indicators.IReusableResult.Value"?: number | null; +} + +export interface ChandelierResult extends ResultBase { + chandelierExit?: number | null; + "skender.Stock.Indicators.IReusableResult.Value"?: number | null; +} + +export interface StcResult extends ResultBase { + stc?: number | null; + "skender.Stock.Indicators.IReusableResult.Value"?: number | null; +} + +export interface StdDevResult extends ResultBase { + stdDev?: number | null; + mean?: number | null; + zScore?: number | null; + stdDevSma?: number | null; + "skender.Stock.Indicators.IReusableResult.Value"?: number | null; +} + +export interface SuperTrendResult extends ResultBase { + superTrend?: number | null; + upperBand?: number | null; + lowerBand?: number | null; +} + export interface GetCandlesWithIndicatorsRequest { ticker?: Ticker; startDate?: Date; @@ -955,9 +956,9 @@ export interface UserStrategyDetailsViewModel { volumeLast24H?: number; wins?: number; losses?: number; - positions?: Position[] | null; + positions?: { [key: string]: Position; } | null; identifier?: string | null; - scenarioName?: string | null; + walletBalances?: { [key: string]: number; } | null; } export interface PlatformSummaryViewModel { @@ -969,25 +970,6 @@ export interface PlatformSummaryViewModel { timeFilter?: string | null; } -export interface AgentIndexViewModel { - agentSummaries?: AgentSummaryViewModel[] | null; - timeFilter?: string | null; -} - -export interface AgentSummaryViewModel { - agentName?: string | null; - totalPnL?: number; - pnLLast24h?: number; - totalROI?: number; - roiLast24h?: number; - wins?: number; - losses?: number; - averageWinRate?: number; - activeStrategiesCount?: number; - totalVolume?: number; - volumeLast24h?: number; -} - export interface PaginatedAgentIndexResponse { agentSummaries?: AgentSummaryViewModel[] | null; totalCount?: number; @@ -997,11 +979,31 @@ export interface PaginatedAgentIndexResponse { hasNextPage?: boolean; hasPreviousPage?: boolean; timeFilter?: string | null; - sortBy?: string | null; + sortBy?: SortableFields; sortOrder?: string | null; filteredAgentNames?: string | null; } +export interface AgentSummaryViewModel { + agentName?: string | null; + totalPnL?: number; + totalROI?: number; + wins?: number; + losses?: number; + activeStrategiesCount?: number; + totalVolume?: number; +} + +export enum SortableFields { + TotalPnL = "TotalPnL", + TotalROI = "TotalROI", + Wins = "Wins", + Losses = "Losses", + AgentName = "AgentName", + CreatedAt = "CreatedAt", + UpdatedAt = "UpdatedAt", +} + export interface AgentBalanceHistory { agentName?: string | null; agentBalances?: AgentBalance[] | null; diff --git a/src/Managing.WebApp/src/global/type.ts b/src/Managing.WebApp/src/global/type.ts index c819978..9f9689a 100644 --- a/src/Managing.WebApp/src/global/type.ts +++ b/src/Managing.WebApp/src/global/type.ts @@ -39,6 +39,7 @@ export interface UnifiedTradingModalProps { // For backtests setBacktests?: React.Dispatch> + onBacktestComplete?: () => void // Callback when backtest is completed // For bot creation/update backtest?: Backtest // Backtest object when creating from backtest diff --git a/src/Managing.WebApp/src/hooks/useBots.tsx b/src/Managing.WebApp/src/hooks/useBots.tsx new file mode 100644 index 0000000..e1f350c --- /dev/null +++ b/src/Managing.WebApp/src/hooks/useBots.tsx @@ -0,0 +1,32 @@ +import React from 'react' +import {useQuery} from '@tanstack/react-query' + +import useApiUrlStore from '../app/store/apiStore' +import {BotClient, BotStatus, type TradingBotResponse} from '../generated/ManagingApi' + +type UseBotsProps = { + status?: BotStatus + callback?: (data: TradingBotResponse[]) => void | undefined +} + +const useBots = ({status = BotStatus.None, callback}: UseBotsProps) => { + const {apiUrl} = useApiUrlStore() + const botClient = new BotClient({}, apiUrl) + + const query = useQuery({ + queryFn: () => botClient.bot_GetBotsByStatus(status), + queryKey: ['bots', status], + refetchInterval: 5000, // Refetch every 5 seconds for real-time updates + }) + + // Call callback when data changes + React.useEffect(() => { + if (query.data && callback) { + callback(query.data) + } + }, [query.data, callback]) + + return query +} + +export default useBots \ No newline at end of file diff --git a/src/Managing.WebApp/src/pages/backtestPage/backtestScanner.tsx b/src/Managing.WebApp/src/pages/backtestPage/backtestScanner.tsx index 9f63114..ad8d1e2 100644 --- a/src/Managing.WebApp/src/pages/backtestPage/backtestScanner.tsx +++ b/src/Managing.WebApp/src/pages/backtestPage/backtestScanner.tsx @@ -1,9 +1,9 @@ import {ColorSwatchIcon, TrashIcon} from '@heroicons/react/solid' +import {useQuery, useQueryClient} from '@tanstack/react-query' import React, {useEffect, useState} from 'react' import 'react-toastify/dist/ReactToastify.css' import useApiUrlStore from '../../app/store/apiStore' -import useBacktestStore from '../../app/store/backtestStore' import {Loader, Slider} from '../../components/atoms' import {Modal, Toast} from '../../components/mollecules' import {BacktestTable, UnifiedTradingModal} from '../../components/organism' @@ -21,43 +21,53 @@ const BacktestScanner: React.FC = () => { score: 50 }) const [currentPage, setCurrentPage] = useState(1) - const [totalBacktests, setTotalBacktests] = useState(0) - const [totalPages, setTotalPages] = useState(0) const [currentSort, setCurrentSort] = useState<{ sortBy: string; sortOrder: 'asc' | 'desc' }>({ sortBy: 'score', sortOrder: 'desc' }) - const [backtests, setBacktests] = useState([]) - const [isLoading, setIsLoading] = useState(false) const { apiUrl } = useApiUrlStore() - const { setBacktests: setBacktestsFromStore, setLoading } = useBacktestStore() + const queryClient = useQueryClient() const backtestClient = new BacktestClient({}, apiUrl) - // Fetch paginated/sorted backtests - const fetchBacktests = async (page = 1, sort = currentSort) => { - setIsLoading(true) - try { - const response = await backtestClient.backtest_GetBacktestsPaginated(page, PAGE_SIZE, sort.sortBy, sort.sortOrder) - setBacktests((response.backtests as LightBacktestResponse[]) || []) - setTotalBacktests(response.totalCount || 0) - setTotalPages(response.totalPages || 0) - } catch (err) { + // Use TanStack Query for fetching backtests + const { + data: backtestData, + isLoading, + error, + refetch + } = useQuery({ + queryKey: ['backtests', currentPage, currentSort.sortBy, currentSort.sortOrder], + queryFn: async () => { + const response = await backtestClient.backtest_GetBacktestsPaginated( + currentPage, + PAGE_SIZE, + currentSort.sortBy, + currentSort.sortOrder + ) + return { + backtests: (response.backtests as LightBacktestResponse[]) || [], + totalCount: response.totalCount || 0, + totalPages: response.totalPages || 0 + } + }, + staleTime: 30000, // Consider data fresh for 30 seconds + gcTime: 5 * 60 * 1000, // Keep in cache for 5 minutes (formerly cacheTime) + }) + + const backtests = backtestData?.backtests || [] + const totalBacktests = backtestData?.totalCount || 0 + const totalPages = backtestData?.totalPages || 0 + + // Note: We no longer need to sync with the store since we're using TanStack Query + // The store is kept for backward compatibility with other components + + // Handle errors + useEffect(() => { + if (error) { new Toast('Failed to load backtests', false) - } finally { - setIsLoading(false) } - } - - useEffect(() => { - fetchBacktests(currentPage, currentSort) - // eslint-disable-next-line - }, [currentPage, currentSort]) - - useEffect(() => { - setBacktestsFromStore(backtests as any) // Cast to any for backward compatibility - setLoading(isLoading) - }, [backtests, setBacktestsFromStore, setLoading, isLoading]) + }, [error]) useEffect(() => { if (backtests && showModalRemoveBacktest) { @@ -150,8 +160,8 @@ const BacktestScanner: React.FC = () => { await backtestClient.backtest_DeleteBacktests({ backtestIds }) notify.update('success', `${backTestToDelete.length} backtests deleted successfully`) - // Refetch backtests to update the list - fetchBacktests(currentPage, currentSort) + // Invalidate and refetch backtests to update the list + queryClient.invalidateQueries({ queryKey: ['backtests'] }) } catch (err: any) { notify.update('error', err?.message || 'An error occurred while deleting backtests') } @@ -202,6 +212,10 @@ const BacktestScanner: React.FC = () => { isFetching={isLoading} onSortChange={handleSortChange} currentSort={currentSort} + onBacktestDeleted={() => { + // Invalidate backtest queries when a backtest is deleted + queryClient.invalidateQueries({ queryKey: ['backtests'] }) + }} /> {/* Pagination controls */} {totalPages > 1 && ( @@ -220,6 +234,10 @@ const BacktestScanner: React.FC = () => { mode="backtest" showModal={showModal} closeModal={closeModal} + onBacktestComplete={() => { + // Invalidate backtest queries when a new backtest is completed + queryClient.invalidateQueries({ queryKey: ['backtests'] }) + }} /> {/****************************/} diff --git a/src/Managing.WebApp/src/pages/botsPage/botList.tsx b/src/Managing.WebApp/src/pages/botsPage/botList.tsx index 0e5bcbc..4be7118 100644 --- a/src/Managing.WebApp/src/pages/botsPage/botList.tsx +++ b/src/Managing.WebApp/src/pages/botsPage/botList.tsx @@ -8,15 +8,16 @@ import TradesModal from '../../components/mollecules/TradesModal/TradesModal' import {TradeChart, UnifiedTradingModal} from '../../components/organism' import { BotClient, - BotType, MoneyManagement, Position, + TradingBotConfig, TradingBotResponse, UserClient } from '../../generated/ManagingApi' import type {IBotList} from '../../global/type.tsx' import MoneyManagementModal from '../settingsPage/moneymanagement/moneyManagementModal' import {useQuery} from '@tanstack/react-query' +import useCookie from '../../hooks/useCookie' function baseBadgeClass(isOutlined = false) { let classes = 'text-xs badge badge-sm transition-all duration-200 hover:scale-105 ' @@ -38,10 +39,15 @@ const BotList: React.FC = ({ list }) => { const { apiUrl } = useApiUrlStore() const client = new BotClient({}, apiUrl) const userClient = new UserClient({}, apiUrl) + const { getCookie } = useCookie() + + // Get JWT token from cookies + const jwtToken = getCookie('token') const { data: currentUser } = useQuery({ queryFn: () => userClient.user_GetCurrentUser(), queryKey: ['currentUser'], + enabled: !!jwtToken, // Only fetch when JWT token exists }) const [showMoneyManagementModal, setShowMoneyManagementModal] = @@ -55,7 +61,7 @@ const BotList: React.FC = ({ list }) => { const [showBotConfigModal, setShowBotConfigModal] = useState(false) const [selectedBotForUpdate, setSelectedBotForUpdate] = useState<{ identifier: string - config: any + config: TradingBotConfig } | null>(null) // Helper function to check if current user owns the bot @@ -63,32 +69,6 @@ const BotList: React.FC = ({ list }) => { return currentUser?.agentName === botAgentName } - function getIsForWatchingBadge(isForWatchingOnly: boolean, identifier: string) { - const classes = - baseBadgeClass() + (isForWatchingOnly ? ' bg-accent' : ' bg-primary') - return ( - - ) - } - - function toggleIsForWatchingOnly(identifier: string) { - const t = new Toast('Switch watch only') - client.bot_ToggleIsForWatching(identifier).then(() => { - t.update('success', 'Bot updated') - }) - } function getDeleteBadge(identifier: string) { const classes = baseBadgeClass() + 'bg-error' return ( @@ -99,17 +79,13 @@ const BotList: React.FC = ({ list }) => { ) } - function getToggleBotStatusBadge( - status: string, - identifier: string, - botType: BotType - ) { + function getToggleBotStatusBadge(status: string, identifier: string) { const classes = baseBadgeClass() + (status == 'Up' ? ' bg-error' : ' bg-success') return (
-
- -
-
- -
{/* Tabs */} @@ -127,7 +80,7 @@ const Bots: React.FC = () => { diff --git a/src/Managing.WebApp/src/pages/dashboardPage/agentIndex.tsx b/src/Managing.WebApp/src/pages/dashboardPage/agentIndex.tsx index d2f4f12..fdda335 100644 --- a/src/Managing.WebApp/src/pages/dashboardPage/agentIndex.tsx +++ b/src/Managing.WebApp/src/pages/dashboardPage/agentIndex.tsx @@ -15,15 +15,12 @@ const TIME_FILTERS = [ const SORT_OPTIONS = [ { label: 'Total PnL', value: 'TotalPnL' }, - { label: '24H PnL', value: 'PnLLast24h' }, { label: 'Total ROI', value: 'TotalROI' }, - { label: '24H ROI', value: 'ROILast24h' }, { label: 'Wins', value: 'Wins' }, { label: 'Losses', value: 'Losses' }, - { label: 'Win Rate', value: 'AverageWinRate' }, - { label: 'Active Strategies', value: 'ActiveStrategiesCount' }, - { label: 'Total Volume', value: 'TotalVolume' }, - { label: '24H Volume', value: 'VolumeLast24h' }, + { label: 'Agent Name', value: 'AgentName' }, + { label: 'Created At', value: 'CreatedAt' }, + { label: 'Updated At', value: 'UpdatedAt' }, ] function AgentIndex({ index }: { index: number }) { @@ -40,6 +37,7 @@ function AgentIndex({ index }: { index: number }) { const [timeFilter, setTimeFilter] = useState('Total') const [sortBy, setSortBy] = useState('TotalPnL') const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc') + const [agentNameFilter, setAgentNameFilter] = useState('') const fetchData = async () => { setIsLoading(true) @@ -48,11 +46,11 @@ function AgentIndex({ index }: { index: number }) { try { const client = new DataClient({}, apiUrl) const response = await client.data_GetAgentIndexPaginated( - timeFilter, currentPage, pageSize, - sortBy, - sortOrder + sortBy as any, // Cast to enum type + sortOrder, + agentNameFilter || undefined, ) setData(response) } catch (err) { @@ -65,7 +63,7 @@ function AgentIndex({ index }: { index: number }) { useEffect(() => { fetchData() - }, [currentPage, pageSize, timeFilter, sortBy, sortOrder]) + }, [currentPage, pageSize, timeFilter, sortBy, sortOrder, agentNameFilter]) const handleSort = (columnId: string) => { if (sortBy === columnId) { @@ -76,6 +74,15 @@ function AgentIndex({ index }: { index: number }) { } } + const handlePageChange = (newPage: number) => { + setCurrentPage(newPage) + } + + const handleAgentNameFilterChange = (value: string) => { + setAgentNameFilter(value) + setCurrentPage(1) // Reset to first page when filtering + } + const columns = useMemo(() => [ { Header: 'Agent Name', @@ -93,15 +100,6 @@ function AgentIndex({ index }: { index: number }) { ), }, - { - Header: '24H PnL', - accessor: 'pnLLast24h', - Cell: ({ value }: { value: number }) => ( - = 0 ? 'text-green-500' : 'text-red-500'}> - {value >= 0 ? '+' : ''}${value.toLocaleString(undefined, { maximumFractionDigits: 2 })} - - ), - }, { Header: 'Total ROI', accessor: 'totalROI', @@ -111,15 +109,6 @@ function AgentIndex({ index }: { index: number }) { ), }, - { - Header: '24H ROI', - accessor: 'roiLast24h', - Cell: ({ value }: { value: number }) => ( - = 0 ? 'text-green-500' : 'text-red-500'}> - {value >= 0 ? '+' : ''}{value.toFixed(2)}% - - ), - }, { Header: 'Wins/Losses', accessor: 'wins', @@ -150,31 +139,28 @@ function AgentIndex({ index }: { index: number }) { ${value.toLocaleString(undefined, { maximumFractionDigits: 2 })} ), }, - { - Header: '24H Volume', - accessor: 'volumeLast24h', - Cell: ({ value }: { value: number }) => ( - ${value.toLocaleString(undefined, { maximumFractionDigits: 2 })} - ), - }, ], []) const tableData = useMemo(() => { if (!data?.agentSummaries) return [] - return data.agentSummaries.map(agent => ({ - ...agent, - // Ensure all numeric values are numbers for proper sorting - totalPnL: Number(agent.totalPnL) || 0, - pnLLast24h: Number(agent.pnLLast24h) || 0, - totalROI: Number(agent.totalROI) || 0, - roiLast24h: Number(agent.roiLast24h) || 0, - wins: Number(agent.wins) || 0, - losses: Number(agent.losses) || 0, - averageWinRate: Number(agent.averageWinRate) || 0, - activeStrategiesCount: Number(agent.activeStrategiesCount) || 0, - totalVolume: Number(agent.totalVolume) || 0, - volumeLast24h: Number(agent.volumeLast24h) || 0, - })) + return data.agentSummaries.map(agent => { + const wins = Number(agent.wins) || 0 + const losses = Number(agent.losses) || 0 + const totalTrades = wins + losses + const averageWinRate = totalTrades > 0 ? (wins * 100) / totalTrades : 0 + + return { + ...agent, + // Ensure all numeric values are numbers for proper sorting + totalPnL: Number(agent.totalPnL) || 0, + totalROI: Number(agent.totalROI) || 0, + wins, + losses, + averageWinRate, + activeStrategiesCount: Number(agent.activeStrategiesCount) || 0, + totalVolume: Number(agent.totalVolume) || 0, + } + }) }, [data?.agentSummaries]) return ( @@ -183,18 +169,14 @@ function AgentIndex({ index }: { index: number }) { {/* Filters and Controls */}
- - + + handleAgentNameFilterChange(e.target.value)} + />
@@ -229,7 +211,10 @@ function AgentIndex({ index }: { index: number }) {