From 3de8b5e00eca24b5a6711f643a702bea70d3c213 Mon Sep 17 00:00:00 2001 From: Oda <102867384+CryptoOda@users.noreply.github.com> Date: Wed, 30 Jul 2025 11:03:30 +0200 Subject: [PATCH] Orlean (#32) * Start building with orlean * Add missing file * Serialize grain state * Remove grain and proxies * update and add plan * Update a bit * Fix backtest grain * Fix backtest grain * Clean a bit --- .cursor/rules/fullstack.mdc | 17 +- CLAUDE.md | 329 ++++++------ Plan.md | 255 +++++++++ orleans-plan.md | 189 +++++++ .../Controllers/BacktestController.cs | 34 +- src/Managing.Api/Controllers/BotController.cs | 4 +- src/Managing.Api/Managing.Api.csproj | 2 + src/Managing.Api/Program.cs | 4 + src/Managing.Api/appsettings.Oda.json | 4 +- .../Grains/IBacktestTradingBotGrain.cs | 45 ++ .../Grains/ITradingBotGrain.cs | 94 ++++ .../Managing.Application.Abstractions.csproj | 4 + .../Models/TradingBotResponse.cs | 104 ++++ .../Services/IBacktester.cs | 10 +- src/Managing.Application.Tests/BotsTests.cs | 6 +- .../Managing.Application.Tests.csproj | 34 +- .../TradingBaseTests.cs | 18 +- .../BundleBacktestWorker.cs | 10 +- .../Managing.Application.Workers.csproj | 10 +- .../StatisticService.cs | 5 +- .../Abstractions/IBotService.cs | 6 - .../Abstractions/IScenarioService.cs | 1 + .../Backtesting/Backtester.cs | 261 ++-------- .../Bots/Base/BotFactory.cs | 4 +- .../Bots/Grains/BacktestTradingBotGrain.cs | 403 ++++++++++++++ .../Bots/Grains/LiveTradingBotGrain.cs | 490 ++++++++++++++++++ src/Managing.Application/Bots/SimpleBot.cs | 4 +- .../Bots/{TradingBot.cs => TradingBotBase.cs} | 29 +- .../Bots/TradingBotGrainState.cs | 117 +++++ src/Managing.Application/GeneticService.cs | 12 +- .../ManageBot/BotService.cs | 185 ++----- .../ManageBot/LoadBackupBotCommandHandler.cs | 6 +- .../Managing.Application.csproj | 36 +- .../Scenarios/ScenarioService.cs | 11 + src/Managing.Bootstrap/ApiBootstrap.cs | 57 +- .../Managing.Bootstrap.csproj | 32 +- src/Managing.Domain/Accounts/Account.cs | 19 +- src/Managing.Domain/Accounts/Balance.cs | 15 + .../Backtests/LightBacktest.cs | 32 +- src/Managing.Domain/Bots/BotBackup.cs | 11 + src/Managing.Domain/Bots/TradingBotBackup.cs | 8 + src/Managing.Domain/Bots/TradingBotConfig.cs | 38 +- src/Managing.Domain/Candles/Candle.cs | 21 + src/Managing.Domain/Evm/Chain.cs | 12 +- src/Managing.Domain/Managing.Domain.csproj | 1 + .../MoneyManagements/LightMoneyManagement.cs | 21 +- .../MoneyManagements/MoneyManagement.cs | 3 + src/Managing.Domain/Risk/RiskManagement.cs | 16 + .../Scenarios/LightScenario.cs | 57 ++ src/Managing.Domain/Scenarios/Scenario.cs | 9 + src/Managing.Domain/Strategies/Indicator.cs | 19 +- .../Strategies/LightIndicator.cs | 84 +++ src/Managing.Domain/Strategies/LightSignal.cs | 25 + src/Managing.Domain/Trades/Position.cs | 27 + src/Managing.Domain/Trades/ProfitAndLoss.cs | 7 +- src/Managing.Domain/Trades/Trade.cs | 23 + src/Managing.Domain/Users/User.cs | 11 + .../Managing.Infrastructure.Exchanges.csproj | 2 +- .../Managing.Infrastructure.Messengers.csproj | 10 +- 59 files changed, 2626 insertions(+), 677 deletions(-) create mode 100644 Plan.md create mode 100644 orleans-plan.md create mode 100644 src/Managing.Application.Abstractions/Grains/IBacktestTradingBotGrain.cs create mode 100644 src/Managing.Application.Abstractions/Grains/ITradingBotGrain.cs create mode 100644 src/Managing.Application.Abstractions/Models/TradingBotResponse.cs create mode 100644 src/Managing.Application/Bots/Grains/BacktestTradingBotGrain.cs create mode 100644 src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs rename src/Managing.Application/Bots/{TradingBot.cs => TradingBotBase.cs} (99%) create mode 100644 src/Managing.Application/Bots/TradingBotGrainState.cs create mode 100644 src/Managing.Domain/Scenarios/LightScenario.cs create mode 100644 src/Managing.Domain/Strategies/LightIndicator.cs diff --git a/.cursor/rules/fullstack.mdc b/.cursor/rules/fullstack.mdc index 4125fb3..c98b51d 100644 --- a/.cursor/rules/fullstack.mdc +++ b/.cursor/rules/fullstack.mdc @@ -11,9 +11,6 @@ You are a senior .NET backend developer and experimental quant with deep experti ## Quantitative Finance Core Principles - Prioritize numerical precision (use `decimal` for monetary calculations) - Implement proven financial mathematics (e.g., Black-Scholes, Monte Carlo methods) -- Optimize time-series processing for tick data/OHLCV series -- Validate models with historical backtesting frameworks -- Maintain audit trails for financial calculations Key Principles - Write concise, technical responses with accurate TypeScript examples. @@ -21,13 +18,11 @@ Key Principles - Prefer iteration and modularization over duplication. - Use descriptive variable names with auxiliary verbs (e.g., isLoading). - Use lowercase with dashes for directories (e.g., components/auth-wizard). - - Favor named exports for components. - Use the Receive an Object, Return an Object (RORO) pattern. ## Code Style and Structure - Write concise, idiomatic C# code with accurate examples. - Follow .NET and ASP.NET Core conventions and best practices. - - Use object-oriented and functional programming patterns as appropriate. - Prefer LINQ and lambda expressions for collection operations. - Use descriptive variable and method names (e.g., 'IsUserSignedIn', 'CalculateTotal'). - Structure files according to .NET conventions (Controllers, Models, Services, etc.). @@ -41,7 +36,7 @@ Key Principles ## C# and .NET Usage - Use C# 10+ features when appropriate (e.g., record types, pattern matching, null-coalescing assignment). - Leverage built-in ASP.NET Core features and middleware. - - Use MongoDb and Influxdb effectively for database operations. + - Use Postgres and Influxdb effectively for database operations. ## Syntax and Formatting - Follow the C# Coding Conventions (https://docs.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions) @@ -57,8 +52,6 @@ Key Principles ## API Design - Follow RESTful API design principles. - - Use attribute routing in controllers. - - Implement versioning for your API. - Use action filters for cross-cutting concerns. ## Performance Optimization @@ -67,11 +60,6 @@ Key Principles - Use efficient LINQ queries and avoid N+1 query problems. - Implement pagination for large data sets. - ## Testing - - Write unit tests using xUnit. - - Use Mock or NSubstitute for mocking dependencies. - - Implement integration tests for API endpoints. - ## Security - Give me advice when you see that some data should be carefully handled @@ -81,7 +69,6 @@ Key Principles React/Tailwind/DaisyUI - Use functional components and TypeScript interfaces. - - Use declarative JSX. - Use function, not const, for components. - Use DaisyUI Tailwind Aria for components and styling. - Implement responsive design with Tailwind CSS. @@ -106,5 +93,5 @@ Key Principles - Do not reference new react library if a component already exist in mollecules or atoms - 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 - Follow the official Microsoft documentation and ASP.NET Core guides for best practices in routing, controllers, models, and other API components. diff --git a/CLAUDE.md b/CLAUDE.md index 4d79604..be3f9b9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,165 +1,204 @@ -# CLAUDE.md +# Managing Apps - Claude Code Guidelines -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +## Project Overview +This is a quantitative finance application with .NET backend and React TypeScript frontend, focusing on algorithmic trading, market indicators, and financial mathematics. -## Common Development Commands +## Core Architecture Principles -### Backend (.NET) +### Quantitative Finance Requirements +- **IMPORTANT**: Use `decimal` for all monetary calculations (never `double` or `float`) +- Implement proven financial mathematics (Black-Scholes, Monte Carlo, etc.) +- Optimize time-series processing for tick data/OHLCV series +- Validate models with historical backtesting frameworks +- Maintain audit trails for all financial calculations +- Prioritize numerical precision in all calculations + +### Backend (.NET/C#) Guidelines + +#### Code Style and Structure +- Write concise, idiomatic C# code following .NET conventions +- Use object-oriented and functional programming patterns appropriately +- Prefer LINQ and lambda expressions for collection operations +- Use descriptive variable and method names (e.g., `IsUserSignedIn`, `CalculateTotal`) +- Structure files according to .NET conventions (Controllers, Models, Services, etc.) + +#### Naming Conventions +- **PascalCase**: Class names, method names, public members +- **camelCase**: Local variables, private fields +- **UPPERCASE**: Constants +- **Prefix interfaces with "I"**: `IUserService`, `IAccountRepository` + +#### C# and .NET Usage +- Use C# 10+ features (record types, pattern matching, null-coalescing assignment) +- Leverage built-in ASP.NET Core features and middleware +- Use `var` for implicit typing when type is obvious +- Use MongoDb and Influxdb for database operations + +#### Architecture Layers (YOU MUST FOLLOW) +1. **Controller** → **Application** → **Repository** (NEVER inject repository in controllers) +2. Always implement methods you create +3. Check existing code before creating new objects/methods +4. Update all layers when necessary (database to frontend) + +#### Error Handling and Validation +- Use exceptions for exceptional cases, not control flow +- Implement proper error logging with .NET logging +- Use Data Annotations or Fluent Validation for model validation +- Return appropriate HTTP status codes and consistent error responses +- Services in `services/` directory must throw user-friendly errors for tanStackQuery + +#### API Design +- Follow RESTful API design principles +- Use attribute routing in controllers +- Implement versioning for APIs +- Use Swagger/OpenAPI for documentation + +#### Performance Optimization +- Use `async/await` for I/O-bound operations +- Implement caching strategies (IMemoryCache or distributed caching) +- Use efficient LINQ queries, avoid N+1 query problems +- Implement pagination for large datasets + +### Frontend (React/TypeScript) Guidelines + +#### Component Structure +- Use functional components with TypeScript interfaces +- Use declarative JSX +- Use `function`, not `const` for components +- Use DaisyUI Tailwind Aria for components and styling +- Implement responsive design with Tailwind CSS (mobile-first approach) + +#### File Organization +- Use lowercase with dashes for directories: `components/auth-wizard` +- Place static content and interfaces at file end +- Use content variables for static content outside render functions +- Favor named exports for components + +#### State Management +- Minimize `use client`, `useEffect`, and `setState` +- Favor React Server Components (RSC) +- Wrap client components in Suspense with fallback +- Use dynamic loading for non-critical components +- Use `useActionState` with react-hook-form for form validation + +#### Error Handling +- Model expected errors as return values (avoid try/catch for expected errors) +- Use error boundaries for unexpected errors (`error.tsx`, `global-error.tsx`) +- Use `useActionState` to manage errors and return them to client + +#### Component Library +- **DO NOT** reference new React libraries if components exist in `mollecules` or `atoms` +- Check existing components before creating new ones + +## Development Workflow + +### Build and Run Commands ```bash -# Build entire solution -dotnet build src/Managing.sln +# Backend +dotnet build +dotnet run --project src/Managing.Api -# Run main API (port 80/443) -dotnet run --project src/Managing.Api/Managing.Api.csproj +# Frontend +npm run build +npm run dev -# Run worker API (port 81/444) -dotnet run --project src/Managing.Api.Workers/Managing.Api.Workers.csproj - -# Run tests -dotnet test src/Managing.Application.Tests/ -dotnet test src/Managing.Infrastructure.Tests/ - -# Run specific test -dotnet test --filter "TestMethodName" - -# Database migrations -dotnet ef database update --project src/Managing.Infrastructure.Database/Managing.Infrastructure.Databases.csproj --context ManagingDbContext -dotnet ef migrations add --project src/Managing.Infrastructure.Database/Managing.Infrastructure.Databases.csproj --context ManagingDbContext - -# Regenerate API client (after API changes) +# Regenerate API client (after backend changes) cd src/Managing.Nswag && dotnet build ``` -### Frontend (React/TypeScript) -```bash -cd src/Managing.WebApp +### API Client Generation +1. **NEVER** update `ManagingApi.ts` manually +2. After backend endpoint changes: + - Run the Managing.Api project + - Execute: `cd src/Managing.Nswag && dotnet build` + - This regenerates `ManagingApi.ts` automatically -# Install dependencies -npm install +### Testing +- Write unit tests using xUnit for backend +- Use Mock or NSubstitute for mocking dependencies +- Implement integration tests for API endpoints -# Development server -npm run dev +## Security Guidelines +- **IMPORTANT**: Handle sensitive data carefully (API keys, private keys, etc.) +- Implement proper authentication and authorization +- Use secure communication protocols +- Validate all user inputs -# Build for production -npm run build +## Database Guidelines +- Use PostgreSQL for relational data +- Use InfluxDB for time-series data (candles, metrics) +- Use MongoDB for document storage +- Implement proper migrations -# Linting and type checking -npm run lint -npm run typecheck +## Orleans Integration (Co-Hosting) +- Use `IGrainFactory` instead of `IClusterClient` for co-hosting scenarios +- Orleans automatically provides `IGrainFactory` when using `UseOrleans()` +- Avoid circular dependency issues by not manually registering `IClusterClient` +- Use Orleans grains for high-availability trading bots -# Tests -npm run test +## Common Patterns + +### Backend Service Pattern +```csharp +public class ExampleService : IExampleService +{ + private readonly IExampleRepository _repository; + private readonly ILogger _logger; + + public ExampleService(IExampleRepository repository, ILogger logger) + { + _repository = repository; + _logger = logger; + } + + public async Task CreateExampleAsync(ExampleRequest request) + { + // Implementation + } +} ``` -### Full Stack Development -```bash -# Quick start with Docker -./scripts/build_and_run.sh +### Frontend Component Pattern +```typescript +interface ComponentProps { + isLoading: boolean; + data: SomeType[]; +} -# Alternative using Aspire -cd src && ./run-aspire.sh +function Component({ isLoading, data }: ComponentProps): JSX.Element { + if (isLoading) return ; + + return ( +
+ {/* Component content */} +
+ ); +} + +export default Component; ``` -## Architecture Overview +## File Structure Conventions +``` +src/ +├── Managing.Api/ # API Controllers +├── Managing.Application/ # Business Logic +├── Managing.Domain/ # Domain Models +├── Managing.Infrastructure/ # Data Access +└── Managing.WebApp/ # React Frontend + └── src/ + ├── components/ + │ ├── atoms/ # Basic components + │ ├── mollecules/ # Composite components + │ └── organism/ # Complex components + └── services/ # API calls +``` -### Clean Architecture Pattern -The codebase follows Clean Architecture with clear layer separation: -- **Domain** (`Managing.Domain`): Business entities and core rules -- **Application** (`Managing.Application*`): Use cases, commands, and business orchestration -- **Infrastructure** (`Managing.Infrastructure.*`): External integrations (databases, Web3, messaging) -- **Presentation** (`Managing.Api*`): REST APIs and controllers - -### Service Architecture -Multiple coordinated services handle different concerns: -- **Managing.Api**: Main trading operations, bot management, backtesting -- **Managing.Api.Workers**: Background workers for price data, genetic algorithms, statistics -- **Managing.Web3Proxy**: Node.js service for Web3/GMX blockchain interactions -- **Managing.WebApp**: React frontend with real-time SignalR updates - -### Data Storage Strategy (Polyglot Persistence) -- **PostgreSQL**: Transactional data (users, bots, positions, scenarios, backtests) -- **InfluxDB**: Time-series data (OHLCV candles, agent balances, performance metrics) -- **MongoDB**: Document storage for certain data types - -### Key Domain Concepts -- **Bot**: Abstract base with lifecycle management for trading automation -- **Position**: Trading positions with PnL tracking and trade history -- **Scenario**: Collections of technical indicators defining trading strategies -- **Indicator**: Technical analysis tools (RSI, MACD, EMA, SuperTrend, etc.) -- **MoneyManagement**: Risk parameters (stop loss, take profit, position sizing) -- **Account**: Multi-exchange trading accounts (CEX, GMX V2, Privy wallets) - -## Development Patterns - -### Adding New Features -1. **Domain First**: Create entities in `Managing.Domain` -2. **Application Layer**: Add services/command handlers in `Managing.Application` -3. **Infrastructure**: Implement repositories and external integrations -4. **API Layer**: Add controllers and endpoints -5. **Frontend**: Update React components and API client - -### Bot Development -- Inherit from `Bot` base class in `Managing.Domain/Bots/` -- Implement `SaveBackup()` and `LoadBackup()` for persistence -- Use dependency injection pattern for services -- Follow worker pattern for background execution - -### Adding Technical Indicators -1. Create indicator class implementing `IIndicator` -2. Add to `IndicatorType` enum in `Managing.Common` -3. Register in DI container via `ApiBootstrap` -4. Implement calculation logic in `TradingService` - -### Database Changes -1. Update entities in domain layer -2. Modify `ManagingDbContext` with new DbSets -3. Generate EF Core migration -4. Update repository interfaces and implementations -5. Consider InfluxDB for time-series data - -### Web3 Integration -- GMX V2 interactions go through `Managing.Web3Proxy` Node.js service -- Use `Managing.Infrastructure.Web3` for .NET integration -- Privy handles wallet management and authentication - -## Configuration & Environment - -### Key Configuration Files -- `src/Managing.Api/appsettings.*.json`: Main API configuration -- `src/Managing.Api.Workers/appsettings.*.json`: Worker configuration -- `src/Managing.WebApp/.env`: Frontend environment variables - -### Environment-Specific Deployments -- Development: `appsettings.Development.json` -- Sandbox: `appsettings.Sandbox.json` -- Production: `appsettings.Production.json` -- Docker: `appsettings.Oda-docker.json` - -## Important Development Guidelines - -### Quantitative Finance Principles -- Use `decimal` for all monetary calculations (never `float` or `double`) -- Implement proper audit trails for financial operations -- Validate trading strategies with historical backtesting -- Optimize time-series processing for performance - -### Code Architecture Rules -- Follow Controller → Application → Repository pattern (never inject repositories directly into controllers) -- Use CQRS pattern with command handlers for complex operations -- Implement proper error handling with user-friendly messages -- Maintain separation between domain logic and infrastructure concerns - -### API Development -- Follow RESTful conventions -- Use attribute routing in controllers -- Return appropriate HTTP status codes -- Implement proper validation using Data Annotations - -### Testing Strategy -- Unit tests focus on domain logic and indicators -- Integration tests for external service interactions -- Use data-driven testing with JSON fixtures for backtesting scenarios - -## Real-time Features -- SignalR hubs provide live updates for trading data, bot status, and market information -- Frontend uses reactive patterns with real-time price feeds and position updates \ No newline at end of file +## Important Reminders +- Always implement methods you create +- Check existing code before creating new functionality +- Update multiple layers when necessary +- Build project after finishing edits +- Follow the controller → application → repository pattern +- Use existing components in mollecules/atoms before adding new libraries +- Use `IGrainFactory` for Orleans co-hosting (not `IClusterClient`) \ No newline at end of file diff --git a/Plan.md b/Plan.md new file mode 100644 index 0000000..554ffc7 --- /dev/null +++ b/Plan.md @@ -0,0 +1,255 @@ +# Orleans Migration Plan for Managing Apps Trading Bot + +## Overview +Migrate the `TradingBot` class to Microsoft Orleans grains for improved performance, scalability, and high availability while maintaining backward compatibility with the `Backtester` class. + +## Current Architecture Analysis + +### TradingBot Key Characteristics +- Long-running stateful service with complex state (positions, signals, candles, indicators) +- Timer-based execution via `InitWorker(Run)` +- Dependency injection via `IServiceScopeFactory` +- Persistence via `SaveBackup()` and `LoadBackup()` +- SignalR integration for real-time updates + +### Backtester Requirements +- Creates TradingBot instances as regular classes (line 198: `_botFactory.CreateBacktestTradingBot`) +- Runs synchronous backtesting without Orleans overhead +- Needs direct object manipulation for performance + +## 1. Orleans Grain Design + +### A. Create ITradingBotGrain Interface +```csharp +// src/Managing.Application.Abstractions/Grains/ITradingBotGrain.cs +public interface ITradingBotGrain : IGrainWithStringKey +{ + Task StartAsync(); + Task StopAsync(); + Task GetStatusAsync(); + Task GetConfigurationAsync(); + Task UpdateConfigurationAsync(TradingBotConfig newConfig); + Task OpenPositionManuallyAsync(TradeDirection direction); + Task ToggleIsForWatchOnlyAsync(); + Task GetBotDataAsync(); + Task LoadBackupAsync(BotBackup backup); +} +``` + +### B. Modify TradingBot Class +```csharp +// src/Managing.Application/Bots/TradingBot.cs +public class TradingBot : Grain, ITradingBotGrain, ITradingBot +{ + // Keep existing implementation but add Orleans-specific methods + // Add grain lifecycle management + // Replace IServiceScopeFactory with Orleans DI +} +``` + +## 2. Program.cs Orleans Configuration + +Add to `src/Managing.Api/Program.cs` after line 188: + +```csharp +// Orleans Configuration +builder.Host.UseOrleans(siloBuilder => +{ + siloBuilder + .UseLocalhostClustering() // For local development + .ConfigureLogging(logging => logging.AddConsole()) + .UseDashboard(options => { options.Port = 8080; }) + .AddMemoryGrainStorageAsDefault() + .ConfigureServices(services => + { + // Register existing services for Orleans DI + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + }); + + // Production clustering configuration + if (builder.Environment.IsProduction()) + { + siloBuilder + .UseAdoNetClustering(options => + { + options.ConnectionString = postgreSqlConnectionString; + options.Invariant = "Npgsql"; + }) + .UseAdoNetReminderService(options => + { + options.ConnectionString = postgreSqlConnectionString; + options.Invariant = "Npgsql"; + }); + } +}); + +// Orleans Client Configuration (for accessing grains from controllers) +builder.Services.AddOrleansClient(clientBuilder => +{ + clientBuilder.UseLocalhostClustering(); + + if (builder.Environment.IsProduction()) + { + clientBuilder.UseAdoNetClustering(options => + { + options.ConnectionString = postgreSqlConnectionString; + options.Invariant = "Npgsql"; + }); + } +}); +``` + +## 3. Conditional Bot Instantiation Strategy + +### A. Enhanced BotFactory Pattern +```csharp +// src/Managing.Application/Bots/Base/BotFactory.cs +public class BotFactory : IBotFactory +{ + private readonly IClusterClient _clusterClient; + private readonly IServiceProvider _serviceProvider; + + public async Task CreateTradingBotAsync(TradingBotConfig config, bool useGrain = true) + { + if (config.IsForBacktest || !useGrain) + { + // For backtesting: Create regular class instance + return new TradingBot( + _serviceProvider.GetService>(), + _serviceProvider.GetService(), + config + ); + } + else + { + // For live trading: Use Orleans grain + var grain = _clusterClient.GetGrain(config.Name); + return new TradingBotGrainProxy(grain, config); + } + } +} +``` + +### B. TradingBotGrainProxy (Adapter Pattern) +```csharp +// src/Managing.Application/Bots/TradingBotGrainProxy.cs +public class TradingBotGrainProxy : ITradingBot +{ + private readonly ITradingBotGrain _grain; + private TradingBotConfig _config; + + public TradingBotGrainProxy(ITradingBotGrain grain, TradingBotConfig config) + { + _grain = grain; + _config = config; + } + + public async Task Start() => await _grain.StartAsync(); + public async Task Stop() => await _grain.StopAsync(); + + // Implement all ITradingBot methods by delegating to grain + // This maintains compatibility with existing bot management code +} +``` + +### C. Backtester Compatibility +In `Backtester.cs` (line 198), the factory call remains unchanged: +```csharp +// This will automatically create a regular class instance due to IsForBacktest = true +var tradingBot = await _botFactory.CreateBacktestTradingBot(config); +``` + +## 4. Orleans Grain State Management + +```csharp +// src/Managing.Application/Bots/TradingBotGrainState.cs +[GenerateSerializer] +public class TradingBotGrainState +{ + [Id(0)] public TradingBotConfig Config { get; set; } + [Id(1)] public HashSet Signals { get; set; } + [Id(2)] public List Positions { get; set; } + [Id(3)] public Dictionary WalletBalances { get; set; } + [Id(4)] public BotStatus Status { get; set; } + [Id(5)] public DateTime StartupTime { get; set; } + [Id(6)] public DateTime CreateDate { get; set; } +} + +// Updated TradingBot grain +public class TradingBot : Grain, ITradingBotGrain +{ + private IDisposable _timer; + + public override async Task OnActivateAsync(CancellationToken cancellationToken) + { + // Initialize grain state and start timer-based execution + if (State.Config != null && State.Status == BotStatus.Running) + { + await StartTimerAsync(); + } + } + + private async Task StartTimerAsync() + { + var interval = CandleExtensions.GetIntervalFromTimeframe(State.Config.Timeframe); + _timer = RegisterTimer(async _ => await Run(), null, TimeSpan.Zero, TimeSpan.FromMilliseconds(interval)); + } +} +``` + +## 5. Implementation Roadmap + +### Phase 1: Infrastructure Setup +1. **Add Orleans packages** (already done in Managing.Api.csproj) +2. **Configure Orleans in Program.cs** with clustering and persistence +3. **Create grain interfaces and state classes** + +### Phase 2: Core Migration +1. **Create ITradingBotGrain interface** with async methods +2. **Modify TradingBot class** to inherit from `Grain` +3. **Implement TradingBotGrainProxy** for compatibility +4. **Update BotFactory** with conditional instantiation logic + +### Phase 3: Service Integration +1. **Replace IServiceScopeFactory** with Orleans dependency injection +2. **Update timer management** to use Orleans grain timers +3. **Migrate state persistence** from SaveBackup/LoadBackup to Orleans state +4. **Update bot management services** to work with grains + +### Phase 4: Testing & Optimization +1. **Test backtesting compatibility** (should remain unchanged) +2. **Performance testing** with multiple concurrent bots +3. **High availability testing** with node failures +4. **Memory and resource optimization** + +## Key Benefits + +1. **High Availability**: Orleans automatic failover and grain migration +2. **Scalability**: Distributed bot execution across multiple nodes +3. **Performance**: Reduced serialization overhead, efficient state management +4. **Backward Compatibility**: Backtester continues using regular classes +5. **Simplified State Management**: Orleans handles persistence automatically + +## Migration Considerations + +1. **Async Conversion**: All bot operations become async +2. **State Serialization**: Ensure all state classes are serializable +3. **Timer Management**: Replace custom timers with Orleans grain timers +4. **Dependency Injection**: Adapt from ASP.NET Core DI to Orleans DI +5. **SignalR Integration**: Update to work with distributed grains + +## Current Status +- ✅ Orleans package added to Managing.Api.csproj +- ✅ Orleans configuration implemented in Program.cs +- ✅ ITradingBotGrain interface created +- ✅ TradingBotGrainState class created +- ✅ TradingBotGrain implementation completed +- ✅ TradingBotResponse model created +- ✅ TradingBotProxy adapter pattern implemented +- ✅ Original TradingBot class preserved for backtesting +- ✅ BotService conditional logic implemented for all creation methods +- ⏳ Testing Orleans integration \ No newline at end of file diff --git a/orleans-plan.md b/orleans-plan.md new file mode 100644 index 0000000..866e21e --- /dev/null +++ b/orleans-plan.md @@ -0,0 +1,189 @@ +Todo List +Phase 1: Keep TradingBotBase Unchanged (Composition Approach) ✅ COMPLETE +[✅] File: src/Managing.Application/Bots/TradingBotBase.cs +[✅] Keep class as concrete (not abstract) +[✅] No Orleans-specific methods needed +[✅] Preserve all existing functionality +[✅] Ensure it remains reusable for direct instantiation and Orleans composition + +Phase 2: Create Orleans Wrapper Grains (Composition) +[✅] File: src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs +[✅] Inherit from Grain and implement ITradingBotGrain +[✅] Use composition: private TradingBotBase _tradingBot +[✅] Implement Orleans lifecycle methods (OnActivateAsync, OnDeactivateAsync) +[✅] Delegate trading operations to _tradingBot instance +[✅] Handle Orleans timer management for bot execution +[✅] Implement state persistence between grain state and TradingBotBase +[✅] Add configuration validation for live trading +[✅] Implement all ITradingBotGrain methods as wrappers + +[✅] File: src/Managing.Application/Bots/Grains/BacktestTradingBotGrain.cs +[✅] Inherit from Grain and implement IBacktestTradingBotGrain +[✅] Use composition: private TradingBotBase _tradingBot +[✅] Implement Orleans lifecycle methods for backtest execution +[✅] Delegate trading operations to _tradingBot instance +[✅] Handle backtest-specific candle processing (no timer) +[✅] Implement state persistence for backtest scenarios +[✅] Add configuration validation for backtesting +[✅] Implement all ITradingBotGrain methods as wrappers +[✅] Add backtest-specific methods: RunBacktestAsync, GetBacktestProgressAsync (following GetBacktestingResult pattern) +[✅] Stateless design - no state persistence, fresh TradingBotBase instance per backtest +[✅] Simplified interface - Start/Stop are no-ops, other methods throw exceptions for backtest mode +[✅] StatelessWorker attribute - grain doesn't inherit from Grain but implements interface +[✅] Config passed as parameter - no state dependency, config passed to RunBacktestAsync method +[✅] **NEW: Orleans Serialization Support** +[✅] Return LightBacktest instead of Backtest for safe Orleans serialization +[✅] Add ConvertToLightBacktest method to map Backtest to LightBacktest +[✅] Handle type conversions (decimal? to double? for SharpeRatio, etc.) +[✅] Ensure all properties are Orleans-serializable + +[✅] File: src/Managing.Domain/Backtests/LightBacktest.cs +[✅] **NEW: Add Orleans Serialization Attributes** +[✅] Add [GenerateSerializer] attribute for Orleans serialization +[✅] Add [Id(n)] attributes to all properties for proper serialization +[✅] Add using Orleans; statement +[✅] Ensure all property types are Orleans-serializable +[✅] Match property types with LightBacktestResponse for consistency + +[✅] File: src/Managing.Application.Abstractions/Grains/IBacktestTradingBotGrain.cs +[✅] **NEW: Update Interface for LightBacktest** +[✅] Change RunBacktestAsync return type from Backtest to LightBacktest +[✅] Update method documentation to reflect LightBacktest usage +[✅] Ensure interface is Orleans-compatible + +[✅] File: src/Managing.Application/Backtesting/Backtester.cs +[✅] Inject IGrainFactory dependency +[✅] Update RunBacktestWithCandles to use Orleans grain instead of direct bot creation +[✅] Remove GetBacktestingResult method (logic moved to grain) +[✅] Remove helper methods (AggregateValues, GetIndicatorsValues) - moved to grain +[✅] Simplified backtesting flow - Backtester orchestrates, grain executes +[✅] Fixed Orleans serialization issue - CreateCleanConfigForOrleans method removes FixedSizeQueue objects +[✅] Created LightIndicator and LightScenario classes for Orleans serialization +[✅] Updated TradingBotConfig to use LightScenario instead of Scenario +[✅] Simplified serialization - no more FixedSizeQueue or User properties in Orleans data +[✅] Updated all application code to use LightScenario conversions +[✅] Main application builds successfully with Orleans integration +[✅] **NEW: Update for LightBacktest Integration** +[✅] Update interface to return LightBacktest instead of Backtest +[✅] Update RunTradingBotBacktest methods to return LightBacktest +[✅] Remove conversion methods (no longer needed) +[✅] Simplify Orleans grain calls to return LightBacktest directly +[✅] Update all dependent services to work with LightBacktest + +[✅] File: src/Managing.Application.Abstractions/Services/IBacktester.cs +[✅] **NEW: Update Interface for LightBacktest** +[✅] Change main backtest methods to return LightBacktest +[✅] Keep full Backtest methods for database retrieval +[✅] Update method documentation for LightBacktest usage +[✅] Ensure backward compatibility where needed + +[✅] File: src/Managing.Api/Controllers/BacktestController.cs +[✅] **NEW: Update Controller for LightBacktest** +[✅] Update Run method to return LightBacktest instead of Backtest +[✅] Update method documentation to explain LightBacktest usage +[✅] Remove unused notification method (handled in Orleans grain) +[✅] Update variable declarations and return statements +[✅] Ensure API responses are consistent with LightBacktest structure + +[✅] File: src/Managing.Application.Workers/StatisticService.cs +[✅] **NEW: Update for LightBacktest Compatibility** +[✅] Update GetSignals method to handle LightBacktest (no signals data) +[✅] Add warning log when signals data is not available +[✅] Return empty list for signals (full data available via database lookup) + +[✅] File: src/Managing.Application/GeneticService.cs +[✅] **NEW: Update for LightBacktest Compatibility** +[✅] Update TradingBotFitness.Evaluate to work with LightBacktest +[✅] Update CalculateFitness method to accept LightBacktest +[✅] Ensure genetic algorithm works with lightweight backtest data + +[ ] File: src/Managing.Application/Bots/Grains/TradingBotGrainProxy.cs +[ ] Fix remaining test compilation errors (6 scenario conversion errors in BotsTests.cs) +[ ] Create proxy class that implements ITradingBot interface +[ ] Wrap Orleans grain calls for seamless integration +[ ] Maintain compatibility with existing ITradingBot consumers +[ ] Handle async/await conversion between Orleans and synchronous calls + +Phase 3: Update BotService for Conditional Instantiation +[ ] File: src/Managing.Application/ManageBot/BotService.cs +[ ] Remove _botTasks dictionary (replaced by Orleans grain management) +[ ] Remove BotTaskWrapper class (no longer needed) +[ ] Inject IGrainFactory for Orleans grain creation +[ ] Update CreateTradingBot() with conditional logic: + [ ] If IsForBacktest: return new TradingBotBase() (direct instantiation) + [ ] If live trading: return new TradingBotGrainProxy(grain) (Orleans wrapper) +[ ] Update CreateBacktestTradingBot() with same conditional logic +[ ] Update all bot management methods to work with both direct and grain instances +[ ] Use Guid for grain identification + +Phase 4: Update Orleans Interface and State +[ ] File: src/Managing.Application.Abstractions/Grains/ITradingBotGrain.cs +[ ] Update to use IGrainWithGuidKey +[ ] Add InitializeAsync(TradingBotConfig config) method +[ ] Add RestartAsync() method +[ ] Add DeleteAsync() method +[ ] Add GetBotDataAsync() method +[ ] Ensure all methods are async and Orleans-compatible + +[ ] File: src/Managing.Application/Bots/TradingBotGrainState.cs +[ ] Ensure all properties are Orleans-serializable +[ ] Add methods for state synchronization with TradingBotBase +[ ] Implement backup/restore functionality +[ ] Add validation for state consistency + +Phase 5: Update Dependencies and Configuration +[ ] File: src/Managing.Bootstrap/ApiBootstrap.cs +[ ] Register Orleans grains (LiveTradingBotGrain, BacktestTradingBotGrain) +[ ] Keep existing bot service registrations for backward compatibility +[ ] Add grain factory registration +[ ] Configure Orleans clustering and persistence + +Phase 6: Testing and Validation +[ ] Test direct TradingBotBase instantiation (backtesting) +[ ] Test LiveTradingBotGrain functionality (live trading) +[ ] Test BacktestTradingBotGrain functionality (Orleans backtesting) +[ ] Test BotService conditional instantiation +[ ] Test Orleans reminder functionality +[ ] Test grain lifecycle management +[ ] Test state persistence and recovery +[ ] Test TradingBotGrainProxy compatibility +[✅] **NEW: Test LightBacktest Serialization** +[✅] Verify Orleans serialization works correctly +[✅] Test LightBacktest to Backtest conversion (if needed) +[✅] Verify API responses with LightBacktest data +[✅] Test genetic algorithm with LightBacktest + +Benefits of Composition Approach +✅ TradingBotBase remains concrete and reusable +✅ No Orleans-specific code in core trading logic +✅ Backward compatibility maintained +✅ Clean separation of concerns +✅ Easier testing and maintenance +✅ Follows SOLID principles +✅ Flexible architecture for future changes +✅ **NEW: Orleans Serialization Benefits** +✅ LightBacktest provides efficient serialization +✅ Reduced memory footprint for Orleans communication +✅ Safe type serialization with GenerateSerializer attributes +✅ Consistent data structure across Orleans grains and API responses + +Implementation Order +Phase 1: Keep TradingBotBase unchanged (preserve existing functionality) ✅ COMPLETE +Phase 2: Create Orleans wrapper grains (composition approach) ✅ COMPLETE +Phase 3: Update BotService for conditional instantiation +Phase 4: Update Orleans interface and state management +Phase 5: Update dependencies and configuration +Phase 6: Testing and validation + +Current Status +✅ Orleans infrastructure setup +✅ TradingBotBase contains all core logic (keep as-is) +✅ LiveTradingBot.cs exists (will be replaced by grain) +✅ Phase 1 Complete - TradingBotBase ready for composition approach +✅ Phase 2 Complete - Orleans wrapper grains created and working +✅ **NEW: LightBacktest Orleans Serialization Complete** +✅ BacktestTradingBotGrain returns LightBacktest for safe serialization +✅ All interfaces and services updated to use LightBacktest +✅ API controllers updated for LightBacktest responses +✅ Application builds successfully with Orleans integration +✅ Ready to start Phase 3 (update BotService for conditional instantiation) \ No newline at end of file diff --git a/src/Managing.Api/Controllers/BacktestController.cs b/src/Managing.Api/Controllers/BacktestController.cs index 892e7b7..62dd49c 100644 --- a/src/Managing.Api/Controllers/BacktestController.cs +++ b/src/Managing.Api/Controllers/BacktestController.cs @@ -29,7 +29,6 @@ public class BacktestController : BaseController { private readonly IHubContext _hubContext; private readonly IBacktester _backtester; - private readonly IScenarioService _scenarioService; private readonly IAccountService _accountService; private readonly IMoneyManagementService _moneyManagementService; private readonly IGeneticService _geneticService; @@ -47,7 +46,6 @@ public class BacktestController : BaseController public BacktestController( IHubContext hubContext, IBacktester backtester, - IScenarioService scenarioService, IAccountService accountService, IMoneyManagementService moneyManagementService, IGeneticService geneticService, @@ -55,7 +53,6 @@ public class BacktestController : BaseController { _hubContext = hubContext; _backtester = backtester; - _scenarioService = scenarioService; _accountService = accountService; _moneyManagementService = moneyManagementService; _geneticService = geneticService; @@ -245,7 +242,8 @@ public class BacktestController : BaseController return BadRequest("Sort order must be 'asc' or 'desc'"); } - var (backtests, totalCount) = await _backtester.GetBacktestsByUserPaginatedAsync(user, page, pageSize, sortBy, sortOrder); + var (backtests, totalCount) = + await _backtester.GetBacktestsByUserPaginatedAsync(user, page, pageSize, sortBy, sortOrder); var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize); var response = new PaginatedBacktestsResponse @@ -279,14 +277,14 @@ public class BacktestController : BaseController /// /// Runs a backtest with the specified configuration. - /// The returned backtest includes a complete TradingBotConfig that preserves all - /// settings including nullable MaxPositionTimeHours for easy bot deployment. + /// Returns a lightweight backtest result for efficient processing. + /// Use the returned ID to retrieve the full backtest data from the database. /// /// The backtest request containing configuration and parameters. - /// The result of the backtest with complete configuration. + /// The lightweight result of the backtest with essential data. [HttpPost] [Route("Run")] - public async Task> Run([FromBody] RunBacktestRequest request) + public async Task> Run([FromBody] RunBacktestRequest request) { if (request?.Config == null) { @@ -310,7 +308,7 @@ public class BacktestController : BaseController try { - Backtest backtestResult = null; + LightBacktest backtestResult = null; var account = await _accountService.GetAccount(request.Config.AccountName, true, false); var user = await GetUser(); @@ -367,7 +365,9 @@ public class BacktestController : BaseController MoneyManagement = moneyManagement, Ticker = request.Config.Ticker, ScenarioName = request.Config.ScenarioName, - Scenario = scenario, // Use the converted scenario object + Scenario = scenario != null + ? LightScenario.FromScenario(scenario) + : null, // Convert to LightScenario for Orleans Timeframe = request.Config.Timeframe, IsForWatchingOnly = request.Config.IsForWatchingOnly, BotTradingBalance = request.Config.BotTradingBalance, @@ -395,7 +395,8 @@ public class BacktestController : BaseController request.WithCandles, null); // No requestId for regular backtests - await NotifyBacktesingSubscriberAsync(backtestResult); + // Note: Notification is handled within the Orleans grain for LightBacktest + // The full Backtest data can be retrieved from the database using the ID if needed return Ok(backtestResult); } @@ -705,17 +706,6 @@ public class BacktestController : BaseController } - /// - /// Notifies subscribers about the backtesting results via SignalR. - /// - /// The backtest result to notify subscribers about. - private async Task NotifyBacktesingSubscriberAsync(Backtest backtesting) - { - if (backtesting != null) - { - await _hubContext.Clients.All.SendAsync("BacktestsSubscription", backtesting); - } - } public MoneyManagement Map(MoneyManagementRequest moneyManagementRequest) { diff --git a/src/Managing.Api/Controllers/BotController.cs b/src/Managing.Api/Controllers/BotController.cs index 82395ad..6cfa921 100644 --- a/src/Managing.Api/Controllers/BotController.cs +++ b/src/Managing.Api/Controllers/BotController.cs @@ -220,7 +220,7 @@ public class BotController : BaseController AccountName = request.Config.AccountName, MoneyManagement = moneyManagement, Ticker = request.Config.Ticker, - Scenario = scenario, // Use the converted scenario object + 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, @@ -782,7 +782,7 @@ public class BotController : BaseController AccountName = request.Config.AccountName, MoneyManagement = moneyManagement, Ticker = request.Config.Ticker, - Scenario = scenarioForUpdate, // Use the converted scenario object + Scenario = LightScenario.FromScenario(scenarioForUpdate), // 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, diff --git a/src/Managing.Api/Managing.Api.csproj b/src/Managing.Api/Managing.Api.csproj index 14e28c0..091b5da 100644 --- a/src/Managing.Api/Managing.Api.csproj +++ b/src/Managing.Api/Managing.Api.csproj @@ -16,8 +16,10 @@ + + diff --git a/src/Managing.Api/Program.cs b/src/Managing.Api/Program.cs index 1e458b6..859eb5d 100644 --- a/src/Managing.Api/Program.cs +++ b/src/Managing.Api/Program.cs @@ -186,6 +186,10 @@ builder.Services.AddSignalR().AddJsonProtocol(); builder.Services.AddScoped(); builder.Services.RegisterApiDependencies(builder.Configuration); + +// Orleans Configuration +builder.Host.ConfigureOrleans(builder.Configuration, builder.Environment.IsProduction()); + builder.Services.AddEndpointsApiExplorer(); builder.Services.AddOpenApiDocument(document => { diff --git a/src/Managing.Api/appsettings.Oda.json b/src/Managing.Api/appsettings.Oda.json index 9851253..c338725 100644 --- a/src/Managing.Api/appsettings.Oda.json +++ b/src/Managing.Api/appsettings.Oda.json @@ -39,7 +39,7 @@ "AllowedHosts": "*", "WorkerBotManager": true, "WorkerBalancesTracking": false, - "WorkerNotifyBundleBacktest": true, + "WorkerNotifyBundleBacktest": false, "KAIGEN_SECRET_KEY": "KaigenXCowchain", - "KAIGEN_CREDITS_ENABLED": true + "KAIGEN_CREDITS_ENABLED": false } \ No newline at end of file diff --git a/src/Managing.Application.Abstractions/Grains/IBacktestTradingBotGrain.cs b/src/Managing.Application.Abstractions/Grains/IBacktestTradingBotGrain.cs new file mode 100644 index 0000000..f89cb57 --- /dev/null +++ b/src/Managing.Application.Abstractions/Grains/IBacktestTradingBotGrain.cs @@ -0,0 +1,45 @@ +using Managing.Domain.Backtests; +using Managing.Domain.Bots; +using Managing.Domain.Candles; +using Managing.Domain.Users; +using Orleans; + +namespace Managing.Application.Abstractions.Grains; + +/// +/// Orleans grain interface for Backtest TradingBot operations. +/// This interface extends ITradingBotGrain with backtest-specific functionality. +/// +public interface IBacktestTradingBotGrain : IGrainWithGuidKey +{ + /// + /// Runs a complete backtest following the exact pattern of GetBacktestingResult from Backtester.cs + /// + /// The trading bot configuration for this backtest + /// The candles to use for backtesting + /// The user running the backtest (optional, required for saving) + /// Whether to save the backtest results + /// Whether to include candles and indicators values in the response + /// 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(); +} + +/// +/// 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/ITradingBotGrain.cs b/src/Managing.Application.Abstractions/Grains/ITradingBotGrain.cs new file mode 100644 index 0000000..16e802e --- /dev/null +++ b/src/Managing.Application.Abstractions/Grains/ITradingBotGrain.cs @@ -0,0 +1,94 @@ +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/Managing.Application.Abstractions.csproj b/src/Managing.Application.Abstractions/Managing.Application.Abstractions.csproj index 0578db3..8d999ea 100644 --- a/src/Managing.Application.Abstractions/Managing.Application.Abstractions.csproj +++ b/src/Managing.Application.Abstractions/Managing.Application.Abstractions.csproj @@ -11,4 +11,8 @@ + + + + diff --git a/src/Managing.Application.Abstractions/Models/TradingBotResponse.cs b/src/Managing.Application.Abstractions/Models/TradingBotResponse.cs new file mode 100644 index 0000000..6f3c92d --- /dev/null +++ b/src/Managing.Application.Abstractions/Models/TradingBotResponse.cs @@ -0,0 +1,104 @@ +using Managing.Domain.Bots; +using Managing.Domain.Trades; +using Orleans; +using static Managing.Common.Enums; + +namespace Managing.Application.Abstractions.Models; + +/// +/// Response model for trading bot data. +/// Used to return comprehensive bot information via Orleans grains. +/// +[GenerateSerializer] +public class TradingBotResponse +{ + /// + /// Bot identifier + /// + [Id(0)] + public string Identifier { get; set; } = string.Empty; + + /// + /// Bot display name + /// + [Id(1)] + public string Name { get; set; } = string.Empty; + + /// + /// Current bot status + /// + [Id(2)] + public BotStatus Status { get; set; } + + /// + /// Bot configuration + /// + [Id(3)] + public TradingBotConfig Config { get; set; } + + /// + /// Trading positions + /// + [Id(4)] + public List Positions { get; set; } = new(); + + /// + /// Trading signals + /// + [Id(5)] + public List Signals { get; set; } = new(); + + /// + /// Wallet balance history + /// + [Id(6)] + public Dictionary WalletBalances { get; set; } = new(); + + /// + /// Current profit and loss + /// + [Id(7)] + public decimal ProfitAndLoss { get; set; } + + /// + /// Win rate percentage + /// + [Id(8)] + public int WinRate { get; set; } + + /// + /// Execution count + /// + [Id(9)] + public long ExecutionCount { get; set; } + + /// + /// Startup time + /// + [Id(10)] + public DateTime StartupTime { get; set; } + + /// + /// Creation date + /// + [Id(11)] + public DateTime CreateDate { get; set; } + + /// + /// Current balance + /// + [Id(12)] + public decimal CurrentBalance { get; set; } + + /// + /// Number of active positions + /// + [Id(13)] + public int ActivePositionsCount { get; set; } + + /// + /// Last execution time + /// + [Id(14)] + public DateTime LastExecution { get; set; } +} \ 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 f22c0ef..9490ad2 100644 --- a/src/Managing.Application.Abstractions/Services/IBacktester.cs +++ b/src/Managing.Application.Abstractions/Services/IBacktester.cs @@ -10,6 +10,7 @@ namespace Managing.Application.Abstractions.Services /// /// Runs a trading bot backtest with the specified configuration and date range. /// Automatically handles different bot types based on config.BotType. + /// Returns a LightBacktest for efficient Orleans serialization. /// /// The trading bot configuration (must include Scenario object or ScenarioName) /// The start date for the backtest @@ -19,8 +20,8 @@ namespace Managing.Application.Abstractions.Services /// Whether to include candles and indicators values in the response /// The request ID to associate with this backtest (optional) /// Additional metadata to associate with this backtest (optional) - /// The backtest results - Task RunTradingBotBacktest( + /// The lightweight backtest results + Task RunTradingBotBacktest( TradingBotConfig config, DateTime startDate, DateTime endDate, @@ -33,6 +34,7 @@ namespace Managing.Application.Abstractions.Services /// /// Runs a trading bot backtest with pre-loaded candles. /// Automatically handles different bot types based on config.BotType. + /// Returns a LightBacktest for efficient Orleans serialization. /// /// The trading bot configuration (must include Scenario object or ScenarioName) /// The candles to use for backtesting @@ -40,8 +42,8 @@ namespace Managing.Application.Abstractions.Services /// Whether to include candles and indicators values in the response /// The request ID to associate with this backtest (optional) /// Additional metadata to associate with this backtest (optional) - /// The backtest results - Task RunTradingBotBacktest( + /// The lightweight backtest results + Task RunTradingBotBacktest( TradingBotConfig config, List candles, User user = null, diff --git a/src/Managing.Application.Tests/BotsTests.cs b/src/Managing.Application.Tests/BotsTests.cs index 0b44264..defe8cb 100644 --- a/src/Managing.Application.Tests/BotsTests.cs +++ b/src/Managing.Application.Tests/BotsTests.cs @@ -51,7 +51,7 @@ namespace Managing.Application.Tests _tradingService.Object, botService, backupBotService); _backtester = new Backtester(_exchangeService, _botFactory, backtestRepository, backtestLogger, - scenarioService, _accountService.Object, messengerService, kaigenService, hubContext); + scenarioService, _accountService.Object, messengerService, kaigenService, hubContext, null); _elapsedTimes = new List(); // Initialize cross-platform file paths @@ -78,7 +78,7 @@ namespace Managing.Application.Tests AccountName = _account.Name, MoneyManagement = MoneyManagement, Ticker = ticker, - Scenario = scenario, + Scenario = LightScenario.FromScenario(scenario), Timeframe = timeframe, IsForWatchingOnly = false, BotTradingBalance = 1000, @@ -128,7 +128,7 @@ namespace Managing.Application.Tests AccountName = _account.Name, MoneyManagement = MoneyManagement, Ticker = ticker, - Scenario = scenario, + Scenario = LightScenario.FromScenario(scenario), Timeframe = timeframe, IsForWatchingOnly = false, BotTradingBalance = 1000, diff --git a/src/Managing.Application.Tests/Managing.Application.Tests.csproj b/src/Managing.Application.Tests/Managing.Application.Tests.csproj index 7a19ab4..e4919c9 100644 --- a/src/Managing.Application.Tests/Managing.Application.Tests.csproj +++ b/src/Managing.Application.Tests/Managing.Application.Tests.csproj @@ -8,15 +8,15 @@ - - - - - - - - - + + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -24,17 +24,17 @@ - - - - - - - + + + + + + + - + diff --git a/src/Managing.Application.Tests/TradingBaseTests.cs b/src/Managing.Application.Tests/TradingBaseTests.cs index db79d9d..4f43683 100644 --- a/src/Managing.Application.Tests/TradingBaseTests.cs +++ b/src/Managing.Application.Tests/TradingBaseTests.cs @@ -1,5 +1,4 @@ -using Managing.Application.Abstractions; -using Managing.Application.Abstractions.Repositories; +using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Services; using Managing.Application.Backtesting; using Managing.Application.Bots; @@ -8,16 +7,15 @@ using Managing.Infrastructure.Databases.InfluxDb; using Managing.Infrastructure.Databases.InfluxDb.Models; using Managing.Infrastructure.Evm; using Managing.Infrastructure.Evm.Abstractions; -using Managing.Infrastructure.Evm.Models.Privy; using Managing.Infrastructure.Evm.Services; using Managing.Infrastructure.Evm.Subgraphs; using Managing.Infrastructure.Exchanges; using Managing.Infrastructure.Exchanges.Abstractions; using Managing.Infrastructure.Exchanges.Exchanges; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Moq; -using Nethereum.Web3; using static Managing.Common.Enums; namespace Managing.Application.Tests @@ -26,7 +24,7 @@ namespace Managing.Application.Tests { public static IExchangeService GetExchangeService() { - ILoggerFactory loggerFactory = new Microsoft.Extensions.Logging.Abstractions.NullLoggerFactory(); + ILoggerFactory loggerFactory = new NullLoggerFactory(); var ChainlinkGmx = new ChainlinkGmx(SubgraphService.GetSubgraphClient(SubgraphProvider.ChainlinkGmx)); var Chainlink = new Chainlink(SubgraphService.GetSubgraphClient(SubgraphProvider.ChainlinkPrice)); @@ -53,23 +51,23 @@ namespace Managing.Application.Tests exchangeProcessors); } - public static ILogger CreateTradingBotLogger() + public static ILogger CreateTradingBotLogger() { - ILoggerFactory loggerFactory = new Microsoft.Extensions.Logging.Abstractions.NullLoggerFactory(); + ILoggerFactory loggerFactory = new NullLoggerFactory(); - return loggerFactory.CreateLogger(); + return loggerFactory.CreateLogger(); } public static ILogger CreateBacktesterLogger() { - ILoggerFactory loggerFactory = new Microsoft.Extensions.Logging.Abstractions.NullLoggerFactory(); + ILoggerFactory loggerFactory = new NullLoggerFactory(); return loggerFactory.CreateLogger(); } public static ILogger CreateCandleRepositoryLogger() { - ILoggerFactory loggerFactory = new Microsoft.Extensions.Logging.Abstractions.NullLoggerFactory(); + ILoggerFactory loggerFactory = new NullLoggerFactory(); return loggerFactory.CreateLogger(); } diff --git a/src/Managing.Application.Workers/BundleBacktestWorker.cs b/src/Managing.Application.Workers/BundleBacktestWorker.cs index 457f7d4..575fa3f 100644 --- a/src/Managing.Application.Workers/BundleBacktestWorker.cs +++ b/src/Managing.Application.Workers/BundleBacktestWorker.cs @@ -222,17 +222,14 @@ public class BundleBacktestWorker : BaseWorker } // Map Scenario - Scenario scenario = null; + LightScenario scenario = null; if (runBacktestRequest.Config.Scenario != null) { var sReq = runBacktestRequest.Config.Scenario; - scenario = new Scenario(sReq.Name, sReq.LoopbackPeriod) - { - User = null // No user context in worker - }; + scenario = new LightScenario(sReq.Name, sReq.LoopbackPeriod); foreach (var indicatorRequest in sReq.Indicators) { - var indicator = new Indicator(indicatorRequest.Name, indicatorRequest.Type) + var indicator = new LightIndicator(indicatorRequest.Name, indicatorRequest.Type) { SignalType = indicatorRequest.SignalType, MinimumHistory = indicatorRequest.MinimumHistory, @@ -244,7 +241,6 @@ public class BundleBacktestWorker : BaseWorker SmoothPeriods = indicatorRequest.SmoothPeriods, StochPeriods = indicatorRequest.StochPeriods, CyclePeriods = indicatorRequest.CyclePeriods, - User = null // No user context in worker }; scenario.AddIndicator(indicator); } diff --git a/src/Managing.Application.Workers/Managing.Application.Workers.csproj b/src/Managing.Application.Workers/Managing.Application.Workers.csproj index 29a259b..ea8d26d 100644 --- a/src/Managing.Application.Workers/Managing.Application.Workers.csproj +++ b/src/Managing.Application.Workers/Managing.Application.Workers.csproj @@ -7,14 +7,14 @@ - - - + + + - - + + diff --git a/src/Managing.Application.Workers/StatisticService.cs b/src/Managing.Application.Workers/StatisticService.cs index 2724d81..bb8e649 100644 --- a/src/Managing.Application.Workers/StatisticService.cs +++ b/src/Managing.Application.Workers/StatisticService.cs @@ -305,7 +305,10 @@ public class StatisticService : IStatisticService false, false); - return backtest.Signals; + // Note: LightBacktest doesn't contain signals data, so we return an empty list + // The full signals data would need to be retrieved from the database using the backtest ID + _logger.LogWarning("GetSignals called but LightBacktest doesn't contain signals data. Returning empty list."); + return new List(); } catch (Exception ex) { diff --git a/src/Managing.Application/Abstractions/IBotService.cs b/src/Managing.Application/Abstractions/IBotService.cs index c64f51c..0960948 100644 --- a/src/Managing.Application/Abstractions/IBotService.cs +++ b/src/Managing.Application/Abstractions/IBotService.cs @@ -29,12 +29,6 @@ public interface IBotService /// ITradingBot instance configured for backtesting Task CreateBacktestTradingBot(TradingBotConfig config); - // Legacy methods - these will use TradingBot internally but maintain backward compatibility - Task CreateScalpingBot(TradingBotConfig config); - Task CreateBacktestScalpingBot(TradingBotConfig config); - Task CreateFlippingBot(TradingBotConfig config); - Task CreateBacktestFlippingBot(TradingBotConfig config); - IBot CreateSimpleBot(string botName, Workflow workflow); Task StopBot(string botName); Task DeleteBot(string botName); diff --git a/src/Managing.Application/Abstractions/IScenarioService.cs b/src/Managing.Application/Abstractions/IScenarioService.cs index c9708f5..73d5fd9 100644 --- a/src/Managing.Application/Abstractions/IScenarioService.cs +++ b/src/Managing.Application/Abstractions/IScenarioService.cs @@ -52,5 +52,6 @@ namespace Managing.Application.Abstractions Task UpdateIndicatorByUser(User user, IndicatorType indicatorType, string name, int? period, int? fastPeriods, int? slowPeriods, int? signalPeriods, double? multiplier, int? stochPeriods, int? smoothPeriods, int? cyclePeriods); + Task GetScenarioByNameAndUserAsync(string scenarioName, User user); } } \ No newline at end of file diff --git a/src/Managing.Application/Backtesting/Backtester.cs b/src/Managing.Application/Backtesting/Backtester.cs index d0ec0b4..d46dd1f 100644 --- a/src/Managing.Application/Backtesting/Backtester.cs +++ b/src/Managing.Application/Backtesting/Backtester.cs @@ -1,17 +1,13 @@ 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.Application.Hubs; -using Managing.Core.FixedSizedQueue; using Managing.Domain.Accounts; using Managing.Domain.Backtests; using Managing.Domain.Bots; using Managing.Domain.Candles; using Managing.Domain.Scenarios; -using Managing.Domain.Shared.Helpers; -using Managing.Domain.Strategies; -using Managing.Domain.Strategies.Base; using Managing.Domain.Users; using Managing.Domain.Workflows; using Microsoft.AspNetCore.SignalR; @@ -32,6 +28,7 @@ namespace Managing.Application.Backtesting private readonly IMessengerService _messengerService; private readonly IKaigenService _kaigenService; private readonly IHubContext _hubContext; + private readonly IGrainFactory _grainFactory; public Backtester( IExchangeService exchangeService, @@ -42,7 +39,8 @@ namespace Managing.Application.Backtesting IAccountService accountService, IMessengerService messengerService, IKaigenService kaigenService, - IHubContext hubContext) + IHubContext hubContext, + IGrainFactory grainFactory) { _exchangeService = exchangeService; _botFactory = botFactory; @@ -53,6 +51,7 @@ namespace Managing.Application.Backtesting _messengerService = messengerService; _kaigenService = kaigenService; _hubContext = hubContext; + _grainFactory = grainFactory; } public Backtest RunSimpleBotBacktest(Workflow workflow, bool save = false) @@ -80,8 +79,8 @@ namespace Managing.Application.Backtesting /// Whether to include candles and indicators values in the response /// The request ID to associate with this backtest (optional) /// Additional metadata to associate with this backtest (optional) - /// The backtest results - public async Task RunTradingBotBacktest( + /// The lightweight backtest results + public async Task RunTradingBotBacktest( TradingBotConfig config, DateTime startDate, DateTime endDate, @@ -114,25 +113,7 @@ namespace Managing.Application.Backtesting try { var candles = GetCandles(config.Ticker, config.Timeframe, startDate, endDate); - - var result = await RunBacktestWithCandles(config, candles, user, withCandles, requestId, metadata); - - // Set start and end dates - result.StartDate = startDate; - result.EndDate = endDate; - - // Ensure RequestId is set - required for PostgreSQL NOT NULL constraint - if (string.IsNullOrEmpty(result.RequestId)) - { - result.RequestId = Guid.NewGuid().ToString(); - } - - if (save && user != null) - { - _backtestRepository.InsertBacktestForUser(user, result); - } - - return result; + return await RunBacktestWithCandles(config, candles, user, save, withCandles, requestId, metadata); } catch (Exception ex) { @@ -172,8 +153,10 @@ namespace Managing.Application.Backtesting /// The candles to use for backtesting /// The user running the backtest (optional) /// Whether to include candles and indicators values in the response - /// The backtest results - public async Task RunTradingBotBacktest( + /// The request ID to associate with this backtest (optional) + /// Additional metadata to associate with this backtest (optional) + /// The lightweight backtest results + public async Task RunTradingBotBacktest( TradingBotConfig config, List candles, User user = null, @@ -181,43 +164,49 @@ namespace Managing.Application.Backtesting string requestId = null, object metadata = null) { - return await RunBacktestWithCandles(config, candles, user, withCandles, requestId, metadata); + return await RunBacktestWithCandles(config, candles, user, false, withCandles, requestId, metadata); } /// /// Core backtesting logic - handles the actual backtest execution with pre-loaded candles /// - private async Task RunBacktestWithCandles( + private async Task RunBacktestWithCandles( TradingBotConfig config, List candles, User user = null, + bool save = false, bool withCandles = false, string requestId = null, object metadata = null) { - var tradingBot = await _botFactory.CreateBacktestTradingBot(config); + // Ensure this is a backtest configuration + if (!config.IsForBacktest) + { + throw new InvalidOperationException("Backtest configuration must have IsForBacktest set to true"); + } - // Scenario and indicators should already be loaded in constructor by BotService - // This is just a validation check to ensure everything loaded properly - if (tradingBot is TradingBot bot && !bot.Indicators.Any()) + // Validate that scenario and indicators are properly loaded + if (config.Scenario == null && string.IsNullOrEmpty(config.ScenarioName)) { throw new InvalidOperationException( - $"No indicators were loaded for scenario '{config.ScenarioName ?? config.Scenario?.Name}'. " + - "This indicates a problem with scenario loading."); + "Backtest configuration must include either Scenario object or ScenarioName"); } - tradingBot.User = user; - tradingBot.Account = await GetAccountFromConfig(config); - - var result = - await GetBacktestingResult(config, tradingBot, candles, user, withCandles, requestId, metadata); - - if (user != null) + if (config.Scenario == null && !string.IsNullOrEmpty(config.ScenarioName)) { - result.User = user; + var fullScenario = await _scenarioService.GetScenarioByNameAndUserAsync(config.ScenarioName, user); + config.Scenario = LightScenario.FromScenario(fullScenario); } - return result; + // Create a clean copy of the config to avoid Orleans serialization issues + var cleanConfig = CreateCleanConfigForOrleans(config); + + // Create Orleans grain for backtesting + var backtestGrain = _grainFactory.GetGrain(Guid.NewGuid()); + + // Run the backtest using the Orleans grain and return LightBacktest directly + return await backtestGrain.RunBacktestAsync(cleanConfig, candles, user, save, withCandles, requestId, + metadata); } private async Task GetAccountFromConfig(TradingBotConfig config) @@ -237,128 +226,16 @@ namespace Managing.Application.Backtesting return candles; } - private async Task GetBacktestingResult( - TradingBotConfig config, - ITradingBot bot, - List candles, - User user = null, - bool withCandles = false, - string requestId = null, - object metadata = null) + + /// + /// Creates a clean copy of the trading bot config for Orleans serialization + /// Uses LightScenario and LightIndicator to avoid FixedSizeQueue serialization issues + /// + private TradingBotConfig CreateCleanConfigForOrleans(TradingBotConfig originalConfig) { - if (candles == null || candles.Count == 0) - { - throw new Exception("No candle to backtest"); - } - - var totalCandles = candles.Count; - var currentCandle = 0; - var lastLoggedPercentage = 0; - - _logger.LogInformation("Starting backtest with {TotalCandles} candles for {Ticker} on {Timeframe}", - totalCandles, config.Ticker, config.Timeframe); - - bot.WalletBalances.Add(candles.FirstOrDefault()!.Date, config.BotTradingBalance); - - foreach (var candle in candles) - { - bot.OptimizedCandles.Enqueue(candle); - bot.Candles.Add(candle); - await bot.Run(); - - currentCandle++; - - // Check if wallet balance fell below 10 USDC and break if so - var currentWalletBalance = bot.WalletBalances.Values.LastOrDefault(); - if (currentWalletBalance < 10m) - { - _logger.LogWarning( - "Backtest stopped early: Wallet balance fell below 10 USDC (Current: {CurrentBalance:F2} USDC) at candle {CurrentCandle}/{TotalCandles} from {CandleDate}", - 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..."); - - bot.Candles = new HashSet(candles); - - // Only calculate indicators values if withCandles is true - Dictionary indicatorsValues = null; - if (withCandles) - { - indicatorsValues = GetIndicatorsValues(bot.Config.Scenario.Indicators, candles); - } - - var finalPnl = bot.GetProfitAndLoss(); - var winRate = bot.GetWinRate(); - var stats = TradingHelpers.GetStatistics(bot.WalletBalances); - var growthPercentage = - TradingHelpers.GetGrowthFromInitalBalance(bot.WalletBalances.FirstOrDefault().Value, finalPnl); - var hodlPercentage = TradingHelpers.GetHodlPercentage(candles[0], candles.Last()); - - var fees = bot.GetTotalFees(); - var scoringParams = new BacktestScoringParams( - sharpeRatio: (double)stats.SharpeRatio, - growthPercentage: (double)growthPercentage, - hodlPercentage: (double)hodlPercentage, - winRate: winRate, - totalPnL: (double)finalPnl, - fees: (double)fees, - tradeCount: bot.Positions.Count, - maxDrawdownRecoveryTime: stats.MaxDrawdownRecoveryTime, - maxDrawdown: stats.MaxDrawdown, - initialBalance: bot.WalletBalances.FirstOrDefault().Value, - tradingBalance: config.BotTradingBalance, - startDate: candles[0].Date, - endDate: candles.Last().Date, - timeframe: config.Timeframe, - moneyManagement: config.MoneyManagement - ); - - var scoringResult = BacktestScorer.CalculateDetailedScore(scoringParams); - - // Create backtest result with conditional candles and indicators values - var result = new Backtest(config, bot.Positions, bot.Signals.ToList(), - withCandles ? candles : new List()) - { - FinalPnl = finalPnl, - WinRate = winRate, - GrowthPercentage = growthPercentage, - HodlPercentage = hodlPercentage, - Fees = fees, - WalletBalances = bot.WalletBalances.ToList(), - Statistics = stats, - IndicatorsValues = withCandles - ? AggregateValues(indicatorsValues, bot.IndicatorsValues) - : new Dictionary(), - Score = scoringResult.Score, - ScoreMessage = scoringResult.GenerateSummaryMessage(), - Id = Guid.NewGuid().ToString(), - RequestId = requestId, - Metadata = metadata, - StartDate = candles.FirstOrDefault()!.OpenTime, - EndDate = candles.LastOrDefault()!.OpenTime, - }; - - // Send notification if backtest meets criteria - await SendBacktestNotificationIfCriteriaMet(result); - - return result; + // Since we're now using LightScenario in TradingBotConfig, we can just return the original config + // The conversion to LightScenario is already done when loading the scenario + return originalConfig; } private async Task SendBacktestNotificationIfCriteriaMet(Backtest backtest) @@ -376,56 +253,6 @@ namespace Managing.Application.Backtesting } } - private Dictionary AggregateValues( - Dictionary indicatorsValues, - Dictionary botStrategiesValues) - { - // Foreach strategy type, only retrieve the values where the strategy is not present already in the bot - // Then, add the values to the bot values - - var result = new Dictionary(); - foreach (var indicator in indicatorsValues) - { - // if (!botStrategiesValues.ContainsKey(strategy.Key)) - // { - // result[strategy.Key] = strategy.Value; - // }else - // { - // result[strategy.Key] = botStrategiesValues[strategy.Key]; - // } - - result[indicator.Key] = indicator.Value; - } - - return result; - } - - private Dictionary GetIndicatorsValues(List indicators, - List 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(); - } - catch (Exception e) - { - Console.WriteLine(e); - } - } - - return indicatorsValues; - } public async Task DeleteBacktestAsync(string id) { diff --git a/src/Managing.Application/Bots/Base/BotFactory.cs b/src/Managing.Application/Bots/Base/BotFactory.cs index 9ef577c..615534c 100644 --- a/src/Managing.Application/Bots/Base/BotFactory.cs +++ b/src/Managing.Application/Bots/Base/BotFactory.cs @@ -12,14 +12,14 @@ namespace Managing.Application.Bots.Base private readonly IExchangeService _exchangeService; private readonly IMessengerService _messengerService; private readonly IAccountService _accountService; - private readonly ILogger _tradingBotLogger; + private readonly ILogger _tradingBotLogger; private readonly ITradingService _tradingService; private readonly IBotService _botService; private readonly IBackupBotService _backupBotService; public BotFactory( IExchangeService exchangeService, - ILogger tradingBotLogger, + ILogger tradingBotLogger, IMessengerService messengerService, IAccountService accountService, ITradingService tradingService, diff --git a/src/Managing.Application/Bots/Grains/BacktestTradingBotGrain.cs b/src/Managing.Application/Bots/Grains/BacktestTradingBotGrain.cs new file mode 100644 index 0000000..8165cf3 --- /dev/null +++ b/src/Managing.Application/Bots/Grains/BacktestTradingBotGrain.cs @@ -0,0 +1,403 @@ +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; +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; +using Orleans.Concurrency; +using static Managing.Common.Enums; + +namespace Managing.Application.Bots.Grains; + +/// +/// Orleans grain for backtest trading bot operations. +/// Uses composition with TradingBotBase to maintain separation of concerns. +/// This grain is stateless and follows the exact pattern of GetBacktestingResult from Backtester.cs. +/// +[StatelessWorker] +public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain +{ + private readonly ILogger _logger; + private readonly IServiceScopeFactory _scopeFactory; + private readonly IBacktestRepository _backtestRepository; + private bool _isDisposed = false; + + public BacktestTradingBotGrain( + ILogger logger, + IServiceScopeFactory scopeFactory, + IBacktestRepository backtestRepository) + { + _logger = logger; + _scopeFactory = scopeFactory; + _backtestRepository = backtestRepository; + } + + /// + /// Runs a complete backtest following the exact pattern of GetBacktestingResult from Backtester.cs + /// + /// The trading bot configuration for this backtest + /// The candles to use for backtesting + /// Whether to include candles and indicators values in the response + /// The request ID to associate with this backtest + /// Additional metadata to associate with this backtest + /// The complete backtest result + public async Task RunBacktestAsync( + TradingBotConfig config, + List candles, + User user = null, + bool save = false, + bool withCandles = false, + string requestId = null, + object metadata = null) + { + if (candles == null || candles.Count == 0) + { + throw new Exception("No candle to backtest"); + } + + // Create a fresh TradingBotBase instance for this backtest + var tradingBot = await CreateTradingBotInstance(config); + tradingBot.Start(); + + var totalCandles = candles.Count; + var currentCandle = 0; + var lastLoggedPercentage = 0; + + _logger.LogInformation("Starting backtest with {TotalCandles} candles for {Ticker} on {Timeframe}", + totalCandles, config.Ticker, config.Timeframe); + + // Initialize wallet balance with first candle + tradingBot.WalletBalances.Clear(); + tradingBot.WalletBalances.Add(candles.FirstOrDefault()!.Date, config.BotTradingBalance); + + // Process all candles following the exact pattern from GetBacktestingResult + foreach (var candle in candles) + { + tradingBot.OptimizedCandles.Enqueue(candle); + tradingBot.Candles.Add(candle); + await tradingBot.Run(); + + currentCandle++; + + // Check if wallet balance fell below 10 USDC and break if so + var currentWalletBalance = tradingBot.WalletBalances.Values.LastOrDefault(); + if (currentWalletBalance < 10m) + { + _logger.LogWarning( + "Backtest stopped early: Wallet balance fell below 10 USDC (Current: {CurrentBalance:F2} USDC) at candle {CurrentCandle}/{TotalCandles} from {CandleDate}", + 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 fees = tradingBot.GetTotalFees(); + var scoringParams = new BacktestScoringParams( + sharpeRatio: (double)stats.SharpeRatio, + growthPercentage: (double)growthPercentage, + hodlPercentage: (double)hodlPercentage, + winRate: winRate, + totalPnL: (double)finalPnl, + fees: (double)fees, + tradeCount: tradingBot.Positions.Count, + maxDrawdownRecoveryTime: stats.MaxDrawdownRecoveryTime, + maxDrawdown: stats.MaxDrawdown, + initialBalance: tradingBot.WalletBalances.FirstOrDefault().Value, + tradingBalance: config.BotTradingBalance, + startDate: candles[0].Date, + endDate: candles.Last().Date, + timeframe: config.Timeframe, + moneyManagement: config.MoneyManagement + ); + + var scoringResult = BacktestScorer.CalculateDetailedScore(scoringParams); + + // Generate requestId if not provided + 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()) + { + FinalPnl = finalPnl, + WinRate = winRate, + GrowthPercentage = growthPercentage, + HodlPercentage = hodlPercentage, + 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(), + RequestId = finalRequestId, + Metadata = metadata, + StartDate = candles.FirstOrDefault()!.OpenTime, + EndDate = candles.LastOrDefault()!.OpenTime, + }; + + if (save && user != null) + { + _backtestRepository.InsertBacktestForUser(user, result); + } + + // 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); + } + + /// + /// Converts a Backtest to LightBacktest for safe Orleans serialization + /// + /// The full backtest to convert + /// A lightweight backtest suitable for Orleans serialization + private LightBacktest ConvertToLightBacktest(Backtest backtest) + { + return new LightBacktest + { + Id = backtest.Id, + Config = backtest.Config, + FinalPnl = backtest.FinalPnl, + WinRate = backtest.WinRate, + GrowthPercentage = backtest.GrowthPercentage, + HodlPercentage = backtest.HodlPercentage, + StartDate = backtest.StartDate, + EndDate = backtest.EndDate, + MaxDrawdown = backtest.Statistics?.MaxDrawdown, + Fees = backtest.Fees, + SharpeRatio = (double?)backtest.Statistics?.SharpeRatio, + Score = backtest.Score, + ScoreMessage = backtest.ScoreMessage + }; + } + + /// + /// Creates a TradingBotBase instance using composition for backtesting + /// + private async Task CreateTradingBotInstance(TradingBotConfig config, User user = null) + { + // Validate configuration for backtesting + if (config == null) + { + throw new InvalidOperationException("Bot configuration is not initialized"); + } + + if (!config.IsForBacktest) + { + throw new InvalidOperationException("BacktestTradingBotGrain can only be used for backtesting"); + } + + // 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; + } + + /// + /// Sends notification if backtest meets criteria (following Backtester.cs pattern) + /// + private async Task SendBacktestNotificationIfCriteriaMet(Backtest backtest) + { + try + { + if (backtest.Score > 60) + { + // Note: In a real implementation, you would inject IMessengerService + // For now, we'll just log the notification + _logger.LogInformation("Backtest {BacktestId} scored {Score} - notification criteria met", + backtest.Id, backtest.Score); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to send backtest notification for backtest {Id}", backtest.Id); + } + } + + /// + /// Aggregates indicator values (following Backtester.cs pattern) + /// + private Dictionary AggregateValues( + Dictionary indicatorsValues, + Dictionary botStrategiesValues) + { + var result = new Dictionary(); + foreach (var indicator in indicatorsValues) + { + result[indicator.Key] = indicator.Value; + } + + return result; + } + + /// + /// Gets indicators values (following Backtester.cs pattern) + /// + private Dictionary GetIndicatorsValues(List indicators, + List 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(); + } + catch (Exception e) + { + _logger.LogError(e, "Error building indicator {IndicatorType}", indicator.Type); + } + } + + return indicatorsValues; + } + + public void Dispose() + { + if (!_isDisposed) + { + _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/LiveTradingBotGrain.cs b/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs new file mode 100644 index 0000000..2d313b4 --- /dev/null +++ b/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs @@ -0,0 +1,490 @@ +using Managing.Application.Abstractions.Grains; +using Managing.Application.Abstractions.Models; +using Managing.Domain.Bots; +using Managing.Domain.Trades; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using static Managing.Common.Enums; + +namespace Managing.Application.Bots.Grains; + +/// +/// Orleans grain for live trading bot operations. +/// 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 +{ + private readonly ILogger _logger; + private readonly IServiceScopeFactory _scopeFactory; + private TradingBotBase? _tradingBot; + private IDisposable? _timer; + private bool _isDisposed = false; + + public LiveTradingBotGrain( + ILogger logger, + IServiceScopeFactory scopeFactory) + { + _logger = logger; + _scopeFactory = scopeFactory; + } + + 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(); + } + } + + public override async Task OnDeactivateAsync(DeactivationReason reason, CancellationToken cancellationToken) + { + _logger.LogInformation("LiveTradingBotGrain {GrainId} deactivating. Reason: {Reason}", + this.GetPrimaryKey(), reason.Description); + + // Stop the timer and trading bot + await StopAsync(); + + await base.OnDeactivateAsync(reason, cancellationToken); + } + + public async Task StartAsync() + { + 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(); + + _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(); + throw; + } + } + + public async Task StopAsync() + { + try + { + // Stop the timer + _timer?.Dispose(); + _timer = null; + + // 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(); + + _logger.LogInformation("LiveTradingBotGrain {GrainId} stopped successfully", this.GetPrimaryKey()); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to stop LiveTradingBotGrain {GrainId}", this.GetPrimaryKey()); + throw; + } + } + + public Task GetStatusAsync() + { + return Task.FromResult(State.Status); + } + + public Task GetConfigurationAsync() + { + return Task.FromResult(State.Config); + } + + public async Task UpdateConfigurationAsync(TradingBotConfig newConfig) + { + try + { + if (_tradingBot == null) + { + throw new InvalidOperationException("Bot is not running"); + } + + // Ensure this is not a backtest configuration + if (newConfig.IsForBacktest) + { + throw new InvalidOperationException("LiveTradingBotGrain cannot be used for backtesting"); + } + + // 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; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to update configuration for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey()); + return false; + } + } + + public async Task OpenPositionManuallyAsync(TradeDirection direction) + { + try + { + if (_tradingBot == null) + { + throw new InvalidOperationException("Bot is not running"); + } + + return await _tradingBot.OpenPositionManually(direction); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to open manual position for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey()); + throw; + } + } + + public async Task ToggleIsForWatchOnlyAsync() + { + try + { + if (_tradingBot == null) + { + 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) + { + throw new InvalidOperationException("Bot is not running"); + } + + return new TradingBotResponse + { + Identifier = State.Identifier, + Name = State.Name, + Status = State.Status, + Config = State.Config, + Positions = _tradingBot.Positions, + Signals = _tradingBot.Signals.ToList(), + WalletBalances = _tradingBot.WalletBalances, + ProfitAndLoss = _tradingBot.GetProfitAndLoss(), + WinRate = _tradingBot.GetWinRate(), + ExecutionCount = _tradingBot.ExecutionCount, + StartupTime = State.StartupTime, + CreateDate = State.CreateDate + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to get bot data for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey()); + throw; + } + } + + public async Task LoadBackupAsync(BotBackup backup) + { + try + { + if (_tradingBot == null) + { + throw new InvalidOperationException("Bot is not running"); + } + + _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(); + + _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; + } + } + + 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() + { + if (_tradingBot == null) return; + + var interval = _tradingBot.Interval; + _timer = RegisterTimer( + async _ => await ExecuteBotCycle(), + null, + TimeSpan.FromMilliseconds(interval), + TimeSpan.FromMilliseconds(interval)); + } + + /// + /// Executes one cycle of the trading bot + /// + private async Task ExecuteBotCycle() + { + try + { + if (_tradingBot == null || State.Status != BotStatus.Up) + { + return; + } + + // Execute the bot's Run method + await _tradingBot.Run(); + + // Update execution count + State.ExecutionCount++; + + await SaveBackupToState(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during bot execution cycle for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey()); + } + } + + /// + /// Saves the current bot state to Orleans state storage + /// + private async Task SaveBackupToState() + { + 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(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to save state for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey()); + } + } + + /// + /// Loads bot state from Orleans state storage + /// + private async Task LoadBackupFromState() + { + 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; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to load state for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey()); + } + } + + public void Dispose() + { + if (!_isDisposed) + { + _timer?.Dispose(); + _isDisposed = true; + } + } +} \ No newline at end of file diff --git a/src/Managing.Application/Bots/SimpleBot.cs b/src/Managing.Application/Bots/SimpleBot.cs index 11d2b07..4ff1c8c 100644 --- a/src/Managing.Application/Bots/SimpleBot.cs +++ b/src/Managing.Application/Bots/SimpleBot.cs @@ -9,12 +9,12 @@ namespace Managing.Application.Bots { public class SimpleBot : Bot { - public readonly ILogger Logger; + 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, + public SimpleBot(string name, ILogger logger, Workflow workflow, IBotService botService, IBackupBotService backupBotService) : base(name) { diff --git a/src/Managing.Application/Bots/TradingBot.cs b/src/Managing.Application/Bots/TradingBotBase.cs similarity index 99% rename from src/Managing.Application/Bots/TradingBot.cs rename to src/Managing.Application/Bots/TradingBotBase.cs index e6f1605..0d379eb 100644 --- a/src/Managing.Application/Bots/TradingBot.cs +++ b/src/Managing.Application/Bots/TradingBotBase.cs @@ -22,9 +22,9 @@ using static Managing.Common.Enums; namespace Managing.Application.Bots; -public class TradingBot : Bot, ITradingBot +public class TradingBotBase : Bot, ITradingBot { - public readonly ILogger Logger; + public readonly ILogger Logger; private readonly IServiceScopeFactory _scopeFactory; public TradingBotConfig Config { get; set; } @@ -41,8 +41,8 @@ public class TradingBot : Bot, ITradingBot public int _maxSignals = 10; // Maximum number of signals to keep in memory - public TradingBot( - ILogger logger, + public TradingBotBase( + ILogger logger, IServiceScopeFactory scopeFactory, TradingBotConfig config ) @@ -71,7 +71,9 @@ public class TradingBot : Bot, ITradingBot // Load indicators if scenario is provided in config if (Config.Scenario != null) { - LoadIndicators(Config.Scenario); + // Convert LightScenario to full Scenario for indicator loading + var fullScenario = Config.Scenario.ToScenario(); + LoadIndicators(fullScenario); } else { @@ -151,8 +153,6 @@ public class TradingBot : Bot, ITradingBot } }); } - - } public async Task LoadAccount() @@ -185,8 +185,8 @@ public class TradingBot : Bot, ITradingBot } else { - // Store the scenario in config and load indicators - Config.Scenario = scenario; + // 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"); @@ -1594,6 +1594,9 @@ public class TradingBot : Bot, ITradingBot public override async Task SaveBackup() { + if (Config.IsForBacktest) + return; + var data = new TradingBotBackup { Config = Config, @@ -1908,7 +1911,9 @@ public class TradingBot : Bot, ITradingBot { if (newConfig.Scenario != null) { - LoadScenario(newConfig.Scenario); + // 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(); @@ -2068,14 +2073,14 @@ public class TradingBot : Bot, ITradingBot } var isInCooldown = positionClosingDate >= cooldownCandle.Date; - + 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; - + Logger.LogWarning( $"⏳ **Cooldown Period Active**\n" + $"Cannot open new positions\n" + diff --git a/src/Managing.Application/Bots/TradingBotGrainState.cs b/src/Managing.Application/Bots/TradingBotGrainState.cs new file mode 100644 index 0000000..50f9905 --- /dev/null +++ b/src/Managing.Application/Bots/TradingBotGrainState.cs @@ -0,0 +1,117 @@ +using Managing.Domain.Bots; +using Managing.Domain.Trades; +using Managing.Domain.Users; +using static Managing.Common.Enums; + +namespace Managing.Application.Bots; + +/// +/// Orleans grain state for TradingBot. +/// This class represents the persistent state of a trading bot grain. +/// All properties must be serializable for Orleans state management. +/// +[GenerateSerializer] +public class TradingBotGrainState +{ + /// + /// The trading bot configuration + /// + [Id(0)] + public TradingBotConfig Config { get; set; } = new(); + + /// + /// Collection of trading signals generated by the bot + /// + [Id(1)] + public HashSet Signals { get; set; } = new(); + + /// + /// List of trading positions opened by the bot + /// + [Id(2)] + public List Positions { get; set; } = new(); + + /// + /// Historical wallet balances tracked over time + /// + [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 + /// + [Id(5)] + public DateTime StartupTime { get; set; } = DateTime.UtcNow; + + /// + /// When the bot was created + /// + [Id(6)] + public DateTime CreateDate { get; set; } = DateTime.UtcNow; + + /// + /// The user who owns this bot + /// + [Id(7)] + public User User { get; set; } + + /// + /// Bot execution counter + /// + [Id(8)] + public long ExecutionCount { get; set; } = 0; + + /// + /// Bot identifier/name + /// + [Id(9)] + public string Identifier { get; set; } = string.Empty; + + /// + /// Bot display name + /// + [Id(10)] + public string Name { get; set; } = string.Empty; + + /// + /// Preload start date for candles + /// + [Id(11)] + public DateTime PreloadSince { get; set; } = DateTime.UtcNow; + + /// + /// Number of preloaded candles + /// + [Id(12)] + public int PreloadedCandlesCount { get; set; } = 0; + + /// + /// Timer interval for bot execution + /// + [Id(13)] + public int Interval { get; set; } = 60000; // Default 1 minute + + /// + /// Maximum number of signals to keep in memory + /// + [Id(14)] + public int MaxSignals { get; set; } = 10; + + /// + /// Indicates if the bot has been initialized + /// + [Id(15)] + public bool IsInitialized { get; set; } = false; + + /// + /// Last time the bot state was persisted + /// + [Id(16)] + public DateTime LastBackupTime { get; set; } = DateTime.UtcNow; +} \ No newline at end of file diff --git a/src/Managing.Application/GeneticService.cs b/src/Managing.Application/GeneticService.cs index e167df5..292d845 100644 --- a/src/Managing.Application/GeneticService.cs +++ b/src/Managing.Application/GeneticService.cs @@ -776,7 +776,7 @@ public class TradingBotChromosome : ChromosomeBase UseForPositionSizing = false, UseForSignalFiltering = false, UseForDynamicStopLoss = false, - Scenario = scenario, + Scenario = LightScenario.FromScenario(scenario), MoneyManagement = mm, RiskManagement = new RiskManagement { @@ -915,7 +915,7 @@ public class TradingBotFitness : IFitness var currentGeneration = _geneticAlgorithm?.GenerationsNumber ?? 0; // Run backtest using scoped service to avoid DbContext concurrency issues - var backtest = ServiceScopeHelpers.WithScopedService( + var lightBacktest = ServiceScopeHelpers.WithScopedService( _serviceScopeFactory, backtester => backtester.RunTradingBotBacktest( config, @@ -933,7 +933,7 @@ public class TradingBotFitness : IFitness ).Result; // Calculate multi-objective fitness based on backtest results - var fitness = CalculateFitness(backtest, config); + var fitness = CalculateFitness(lightBacktest, config); return fitness; } @@ -945,13 +945,13 @@ public class TradingBotFitness : IFitness } } - private double CalculateFitness(Backtest backtest, TradingBotConfig config) + private double CalculateFitness(LightBacktest lightBacktest, TradingBotConfig config) { - if (backtest == null || backtest.Statistics == null) + if (lightBacktest == null) return 0.1; // Calculate base fitness from backtest score - var baseFitness = backtest.Score; + var baseFitness = lightBacktest.Score; // Return base fitness (no penalty for now) return baseFitness; diff --git a/src/Managing.Application/ManageBot/BotService.cs b/src/Managing.Application/ManageBot/BotService.cs index acf0dbc..6802ec9 100644 --- a/src/Managing.Application/ManageBot/BotService.cs +++ b/src/Managing.Application/ManageBot/BotService.cs @@ -4,6 +4,7 @@ using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Services; using Managing.Application.Bots; using Managing.Domain.Bots; +using Managing.Domain.Scenarios; using Managing.Domain.Users; using Managing.Domain.Workflows; using Microsoft.Extensions.DependencyInjection; @@ -18,20 +19,21 @@ namespace Managing.Application.ManageBot private readonly IExchangeService _exchangeService; private readonly IMessengerService _messengerService; private readonly IAccountService _accountService; - private readonly ILogger _tradingBotLogger; + 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 ConcurrentDictionary _botTasks = new ConcurrentDictionary(); public BotService(IBotRepository botRepository, IExchangeService exchangeService, - IMessengerService messengerService, IAccountService accountService, ILogger tradingBotLogger, + IMessengerService messengerService, IAccountService accountService, ILogger tradingBotLogger, ITradingService tradingService, IMoneyManagementService moneyManagementService, IUserService userService, - IBackupBotService backupBotService, IServiceScopeFactory scopeFactory) + IBackupBotService backupBotService, IServiceScopeFactory scopeFactory, IGrainFactory grainFactory) { _botRepository = botRepository; _exchangeService = exchangeService; @@ -43,26 +45,26 @@ namespace Managing.Application.ManageBot _userService = userService; _backupBotService = backupBotService; _scopeFactory = scopeFactory; + _grainFactory = grainFactory; } public class BotTaskWrapper { public Task Task { get; private set; } public Type BotType { get; private set; } - public object BotInstance { get; private set; } // Add this line + public object BotInstance { get; private set; } - public BotTaskWrapper(Task task, Type botType, object botInstance) // Update constructor + public BotTaskWrapper(Task task, Type botType, object botInstance) { Task = task; BotType = botType; - BotInstance = botInstance; // Set the bot instance + BotInstance = botInstance; } } public void AddSimpleBotToCache(IBot bot) { - var botTask = - new BotTaskWrapper(Task.Run(() => bot.Start()), bot.GetType(), bot); // Pass bot as the instance + var botTask = new BotTaskWrapper(Task.Run(() => bot.Start()), bot.GetType(), bot); _botTasks.AddOrUpdate(bot.Identifier, botTask, (key, existingVal) => botTask); } @@ -72,24 +74,34 @@ namespace Managing.Application.ManageBot _botTasks.AddOrUpdate(bot.Identifier, botTask, (key, existingVal) => botTask); } - private async Task InitBot(ITradingBot bot, BotBackup backupBot) { - var user = await _userService.GetUser(backupBot.User.Name); - bot.User = user; - // Config is already set correctly from backup data, so we only need to restore signals, positions, etc. - bot.LoadBackup(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()); + // 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(); + } } - else + catch (Exception ex) { - // Keep the bot in Down status if it was originally Down + _tradingBotLogger.LogError(ex, "Error initializing bot {Identifier} from backup", backupBot.Identifier); + // Ensure the bot is stopped if initialization fails bot.Stop(); + throw; } } @@ -137,7 +149,7 @@ namespace Managing.Application.ManageBot var scenario = await _tradingService.GetScenarioByNameAsync(scalpingConfig.ScenarioName); if (scenario != null) { - scalpingConfig.Scenario = scenario; + scalpingConfig.Scenario = LightScenario.FromScenario(scenario); } else { @@ -155,6 +167,10 @@ namespace Managing.Application.ManageBot // 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)); @@ -206,7 +222,7 @@ namespace Managing.Application.ManageBot if (botWrapper.BotInstance is IBot bot) { await Task.Run(() => - bot.Stop()); // Assuming Stop is an asynchronous process wrapped in Task.Run for synchronous methods + bot.Stop()); var stopMessage = $"🛑 **Bot Stopped**\n\n" + $"🎯 **Agent:** {bot.User.AgentName}\n" + @@ -231,7 +247,7 @@ namespace Managing.Application.ManageBot if (botWrapper.BotInstance is IBot bot) { await Task.Run(() => - bot.Stop()); // Assuming Stop is an asynchronous process wrapped in Task.Run for synchronous methods + bot.Stop()); var deleteMessage = $"🗑️ **Bot Deleted**\n\n" + $"🎯 **Agent:** {bot.User.AgentName}\n" + @@ -306,7 +322,7 @@ namespace Managing.Application.ManageBot public async Task UpdateBotConfiguration(string identifier, TradingBotConfig newConfig) { if (_botTasks.TryGetValue(identifier, out var botTaskWrapper) && - botTaskWrapper.BotInstance is TradingBot tradingBot) + botTaskWrapper.BotInstance is TradingBotBase tradingBot) { // Ensure the scenario is properly loaded from database if needed if (newConfig.Scenario == null && !string.IsNullOrEmpty(newConfig.ScenarioName)) @@ -314,7 +330,7 @@ namespace Managing.Application.ManageBot var scenario = await _tradingService.GetScenarioByNameAsync(newConfig.ScenarioName); if (scenario != null) { - newConfig.Scenario = scenario; + newConfig.Scenario = LightScenario.FromScenario(scenario); } else { @@ -370,7 +386,6 @@ namespace Managing.Application.ManageBot return false; } - public async Task CreateTradingBot(TradingBotConfig config) { // Ensure the scenario is properly loaded from database if needed @@ -379,7 +394,7 @@ namespace Managing.Application.ManageBot var scenario = await _tradingService.GetScenarioByNameAsync(config.ScenarioName); if (scenario != null) { - config.Scenario = scenario; + config.Scenario = LightScenario.FromScenario(scenario); } else { @@ -392,7 +407,15 @@ namespace Managing.Application.ManageBot throw new ArgumentException("Scenario object must be provided or ScenarioName must be valid"); } - return new TradingBot(_tradingBotLogger, _scopeFactory, config); + // 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); } public async Task CreateBacktestTradingBot(TradingBotConfig config) @@ -403,7 +426,7 @@ namespace Managing.Application.ManageBot var scenario = await _tradingService.GetScenarioByNameAsync(config.ScenarioName); if (scenario != null) { - config.Scenario = scenario; + config.Scenario = LightScenario.FromScenario(scenario); } else { @@ -417,109 +440,7 @@ namespace Managing.Application.ManageBot } config.IsForBacktest = true; - return new TradingBot(_tradingBotLogger, _scopeFactory, config); - } - - public async Task CreateScalpingBot(TradingBotConfig config) - { - // 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 = 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"); - } - - config.FlipPosition = false; - return new TradingBot(_tradingBotLogger, _scopeFactory, config); - } - - public async Task CreateBacktestScalpingBot(TradingBotConfig config) - { - // 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 = 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"); - } - - config.IsForBacktest = true; - config.FlipPosition = false; - return new TradingBot(_tradingBotLogger, _scopeFactory, config); - } - - public async Task CreateFlippingBot(TradingBotConfig config) - { - // 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 = 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"); - } - - config.FlipPosition = true; - return new TradingBot(_tradingBotLogger, _scopeFactory, config); - } - - public async Task CreateBacktestFlippingBot(TradingBotConfig config) - { - // 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 = 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"); - } - - config.IsForBacktest = true; - config.FlipPosition = true; - return new TradingBot(_tradingBotLogger, _scopeFactory, config); + return new TradingBotBase(_tradingBotLogger, _scopeFactory, config); } } } \ No newline at end of file diff --git a/src/Managing.Application/ManageBot/LoadBackupBotCommandHandler.cs b/src/Managing.Application/ManageBot/LoadBackupBotCommandHandler.cs index 832f08a..c870375 100644 --- a/src/Managing.Application/ManageBot/LoadBackupBotCommandHandler.cs +++ b/src/Managing.Application/ManageBot/LoadBackupBotCommandHandler.cs @@ -46,7 +46,7 @@ public class LoadBackupBotCommandHandler : IRequestHandler - - - + + + - - - - - - - - - - + + + + + + + + + + + + - - - - + + + + diff --git a/src/Managing.Application/Scenarios/ScenarioService.cs b/src/Managing.Application/Scenarios/ScenarioService.cs index 417c247..2f42b12 100644 --- a/src/Managing.Application/Scenarios/ScenarioService.cs +++ b/src/Managing.Application/Scenarios/ScenarioService.cs @@ -302,5 +302,16 @@ namespace Managing.Application.Scenarios return result; } + + public async Task GetScenarioByNameAndUserAsync(string scenarioName, User user) + { + var scenario = await _tradingService.GetScenarioByNameAsync(scenarioName); + if (scenario == null) + { + throw new InvalidOperationException($"Scenario {scenarioName} not found for user {user.Name}"); + } + + return scenario; + } } } \ No newline at end of file diff --git a/src/Managing.Bootstrap/ApiBootstrap.cs b/src/Managing.Bootstrap/ApiBootstrap.cs index 096893c..a2beeee 100644 --- a/src/Managing.Bootstrap/ApiBootstrap.cs +++ b/src/Managing.Bootstrap/ApiBootstrap.cs @@ -42,6 +42,8 @@ using MediatR; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace Managing.Bootstrap; @@ -60,9 +62,62 @@ public static class ApiBootstrap .AddInfrastructure(configuration) .AddWorkers(configuration) .AddFluentValidation() - .AddMediatR(); + .AddMediatR() + ; } + // Note: IClusterClient is automatically available in co-hosting scenarios + // through IGrainFactory. Services should inject IGrainFactory instead of IClusterClient + // to avoid circular dependency issues during DI container construction. + + public static IHostBuilder ConfigureOrleans(this IHostBuilder hostBuilder, IConfiguration configuration, + bool isProduction) + { + var postgreSqlConnectionString = configuration.GetSection("Databases:PostgreSql")["ConnectionString"]; + + 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(); + } + + siloBuilder + .ConfigureLogging(logging => logging.SetMinimumLevel(LogLevel.Information)) + .UseDashboard(options => { }) + .AddMemoryGrainStorageAsDefault() + .ConfigureServices(services => + { + // Register existing services for Orleans DI + // These will be available to grains through dependency injection + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + }); + }) + ; + } + + private static IServiceCollection AddApplication(this IServiceCollection services) { services.AddScoped(); diff --git a/src/Managing.Bootstrap/Managing.Bootstrap.csproj b/src/Managing.Bootstrap/Managing.Bootstrap.csproj index 9c5f515..62b37ea 100644 --- a/src/Managing.Bootstrap/Managing.Bootstrap.csproj +++ b/src/Managing.Bootstrap/Managing.Bootstrap.csproj @@ -7,22 +7,28 @@ - - - - - - + + + + + + + + + + + + - - - - - - - + + + + + + + diff --git a/src/Managing.Domain/Accounts/Account.cs b/src/Managing.Domain/Accounts/Account.cs index 1b89e6b..fe467a6 100644 --- a/src/Managing.Domain/Accounts/Account.cs +++ b/src/Managing.Domain/Accounts/Account.cs @@ -1,17 +1,32 @@ -using Managing.Domain.Users; -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; +using Managing.Domain.Users; +using Orleans; using static Managing.Common.Enums; namespace Managing.Domain.Accounts; +[GenerateSerializer] public class Account { + [Id(0)] [Required] public string Name { get; set; } + + [Id(1)] [Required] public TradingExchanges Exchange { get; set; } + + [Id(2)] [Required] public AccountType Type { get; set; } + + [Id(3)] public string Key { get; set; } + + [Id(4)] public string Secret { get; set; } + + [Id(5)] public User User { get; set; } + + [Id(6)] public List Balances { get; set; } public bool IsPrivyWallet => Type == AccountType.Privy; diff --git a/src/Managing.Domain/Accounts/Balance.cs b/src/Managing.Domain/Accounts/Balance.cs index e066ee0..45f60cf 100644 --- a/src/Managing.Domain/Accounts/Balance.cs +++ b/src/Managing.Domain/Accounts/Balance.cs @@ -1,14 +1,29 @@ using Managing.Domain.Evm; +using Orleans; namespace Managing.Domain.Accounts; +[GenerateSerializer] public class Balance { + [Id(0)] public string TokenImage { get; set; } + + [Id(1)] public string TokenName { get; set; } + + [Id(2)] public decimal Amount { get; set; } + + [Id(3)] public decimal Price { get; set; } + + [Id(4)] public decimal Value { get; set; } + + [Id(5)] public string TokenAdress { get; set; } + + [Id(6)] public Chain Chain { get; set; } } \ No newline at end of file diff --git a/src/Managing.Domain/Backtests/LightBacktest.cs b/src/Managing.Domain/Backtests/LightBacktest.cs index cb26407..8e3d997 100644 --- a/src/Managing.Domain/Backtests/LightBacktest.cs +++ b/src/Managing.Domain/Backtests/LightBacktest.cs @@ -1,20 +1,26 @@ using Managing.Domain.Bots; +using Orleans; namespace Managing.Domain.Backtests; +/// +/// Lightweight backtest class for Orleans serialization +/// Contains only the essential properties needed for backtest results +/// +[GenerateSerializer] public class LightBacktest { - public string Id { get; set; } = string.Empty; - public TradingBotConfig Config { get; set; } = new(); - public decimal FinalPnl { get; set; } - public int WinRate { get; set; } - public decimal GrowthPercentage { get; set; } - public decimal HodlPercentage { get; set; } - public DateTime StartDate { get; set; } - public DateTime EndDate { get; set; } - public decimal? MaxDrawdown { get; set; } - public decimal Fees { get; set; } - public double? SharpeRatio { get; set; } - public double Score { get; set; } - public string ScoreMessage { get; set; } = string.Empty; + [Id(0)] public string Id { get; set; } = string.Empty; + [Id(1)] public TradingBotConfig Config { get; set; } = new(); + [Id(2)] public decimal FinalPnl { get; set; } + [Id(3)] public int WinRate { get; set; } + [Id(4)] public decimal GrowthPercentage { get; set; } + [Id(5)] public decimal HodlPercentage { get; set; } + [Id(6)] public DateTime StartDate { get; set; } + [Id(7)] public DateTime EndDate { get; set; } + [Id(8)] public decimal? MaxDrawdown { get; set; } + [Id(9)] public decimal Fees { get; set; } + [Id(10)] public double? SharpeRatio { get; set; } + [Id(11)] public double Score { get; set; } + [Id(12)] public string ScoreMessage { get; set; } = string.Empty; } \ No newline at end of file diff --git a/src/Managing.Domain/Bots/BotBackup.cs b/src/Managing.Domain/Bots/BotBackup.cs index 792d2ae..de35272 100644 --- a/src/Managing.Domain/Bots/BotBackup.cs +++ b/src/Managing.Domain/Bots/BotBackup.cs @@ -1,15 +1,26 @@ 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; } /// diff --git a/src/Managing.Domain/Bots/TradingBotBackup.cs b/src/Managing.Domain/Bots/TradingBotBackup.cs index ac5c538..871b701 100644 --- a/src/Managing.Domain/Bots/TradingBotBackup.cs +++ b/src/Managing.Domain/Bots/TradingBotBackup.cs @@ -1,36 +1,44 @@ using Managing.Domain.Trades; +using Orleans; namespace Managing.Domain.Bots; +[GenerateSerializer] public class TradingBotBackup { /// /// The complete trading bot configuration /// + [Id(0)] public TradingBotConfig Config { get; set; } /// /// Runtime state: Active signals for the bot /// + [Id(1)] public HashSet Signals { get; set; } /// /// Runtime state: Open and closed positions for the bot /// + [Id(2)] public List Positions { get; set; } /// /// Runtime state: Historical wallet balances over time /// + [Id(3)] public Dictionary WalletBalances { get; set; } /// /// Runtime state: When the bot was started /// + [Id(4)] public DateTime StartupTime { get; set; } /// /// Runtime state: When the bot was created /// + [Id(5)] public DateTime CreateDate { get; set; } } \ No newline at end of file diff --git a/src/Managing.Domain/Bots/TradingBotConfig.cs b/src/Managing.Domain/Bots/TradingBotConfig.cs index 1af42ef..ad0fc4a 100644 --- a/src/Managing.Domain/Bots/TradingBotConfig.cs +++ b/src/Managing.Domain/Bots/TradingBotConfig.cs @@ -1,22 +1,45 @@ using System.ComponentModel.DataAnnotations; using Managing.Domain.Risk; using Managing.Domain.Scenarios; +using Orleans; using static Managing.Common.Enums; namespace Managing.Domain.Bots; +[GenerateSerializer] public class TradingBotConfig { + [Id(0)] [Required] public string AccountName { get; set; } + + [Id(1)] [Required] public LightMoneyManagement MoneyManagement { get; set; } + + [Id(2)] [Required] public Ticker Ticker { get; set; } + + [Id(3)] [Required] public Timeframe Timeframe { get; set; } + + [Id(4)] [Required] public bool IsForWatchingOnly { get; set; } + + [Id(5)] [Required] public decimal BotTradingBalance { get; set; } + + [Id(6)] [Required] public bool IsForBacktest { get; set; } + + [Id(7)] [Required] public int CooldownPeriod { get; set; } + + [Id(8)] [Required] public int MaxLossStreak { get; set; } + + [Id(9)] [Required] public bool FlipPosition { get; set; } + + [Id(10)] [Required] public string Name { get; set; } /// @@ -24,23 +47,28 @@ public class TradingBotConfig /// Contains all configurable parameters for Expected Utility Theory, Kelly Criterion, and probability thresholds. /// If null, default risk management settings will be used. /// + [Id(11)] public RiskManagement RiskManagement { get; set; } = new(); /// - /// The scenario object containing all strategies. When provided, this takes precedence over ScenarioName. + /// The lightweight scenario object containing all strategies. When provided, this takes precedence over ScenarioName. /// This allows running backtests without requiring scenarios to be saved in the database. + /// Orleans-friendly version without FixedSizeQueue and User properties. /// - public Scenario Scenario { get; set; } + [Id(12)] + public LightScenario Scenario { get; set; } /// /// The scenario name to load from database. Only used when Scenario object is not provided. /// + [Id(13)] public string ScenarioName { get; set; } /// /// Maximum time in hours that a position can remain open before being automatically closed. /// If null, time-based position closure is disabled. /// + [Id(14)] public decimal? MaxPositionTimeHours { get; set; } /// @@ -49,6 +77,7 @@ public class TradingBotConfig /// If false, the position will only be closed when MaxPositionTimeHours is reached. /// Default is false to maintain existing behavior. /// + [Id(15)] public bool CloseEarlyWhenProfitable { get; set; } = false; /// @@ -56,6 +85,7 @@ public class TradingBotConfig /// If false, positions will be flipped regardless of profit status. /// Default is true for safer trading. /// + [Id(16)] [Required] public bool FlipOnlyWhenInProfit { get; set; } = true; @@ -65,20 +95,24 @@ public class TradingBotConfig /// When false, the bot operates in traditional mode without Synth predictions. /// The actual Synth configuration is managed centrally in SynthPredictionService. /// + [Id(17)] public bool UseSynthApi { get; set; } = false; /// /// Whether to use Synth predictions for position sizing adjustments and risk assessment /// + [Id(18)] public bool UseForPositionSizing { get; set; } = true; /// /// Whether to use Synth predictions for signal filtering /// + [Id(19)] public bool UseForSignalFiltering { get; set; } = true; /// /// Whether to use Synth predictions for dynamic stop-loss/take-profit adjustments /// + [Id(20)] public bool UseForDynamicStopLoss { get; set; } = true; } \ No newline at end of file diff --git a/src/Managing.Domain/Candles/Candle.cs b/src/Managing.Domain/Candles/Candle.cs index 47dbe5b..d316704 100644 --- a/src/Managing.Domain/Candles/Candle.cs +++ b/src/Managing.Domain/Candles/Candle.cs @@ -1,20 +1,41 @@ using System.ComponentModel.DataAnnotations; using Managing.Common; +using Orleans; using Skender.Stock.Indicators; namespace Managing.Domain.Candles { + [GenerateSerializer] public class Candle : IQuote { + [Id(0)] [Required] public Enums.TradingExchanges Exchange { get; set; } + + [Id(1)] [Required] public string Ticker { get; set; } + + [Id(2)] [Required] public DateTime OpenTime { get; set; } + + [Id(3)] [Required] public DateTime Date { get; set; } + + [Id(4)] [Required] public decimal Open { get; set; } + + [Id(5)] [Required] public decimal Close { get; set; } + + [Id(6)] [Required] public decimal High { get; set; } + + [Id(7)] [Required] public decimal Low { get; set; } + + [Id(8)] [Required] public Enums.Timeframe Timeframe { get; set; } + + [Id(9)] public decimal Volume { get; set; } } } \ No newline at end of file diff --git a/src/Managing.Domain/Evm/Chain.cs b/src/Managing.Domain/Evm/Chain.cs index ee3c540..a9fd340 100644 --- a/src/Managing.Domain/Evm/Chain.cs +++ b/src/Managing.Domain/Evm/Chain.cs @@ -1,9 +1,19 @@ -namespace Managing.Domain.Evm; +using Orleans; +namespace Managing.Domain.Evm; + +[GenerateSerializer] public class Chain { + [Id(0)] public string Id { get; set; } + + [Id(1)] public string RpcUrl { get; set; } + + [Id(2)] public string Name { get; set; } + + [Id(3)] public int ChainId { get; set; } } \ No newline at end of file diff --git a/src/Managing.Domain/Managing.Domain.csproj b/src/Managing.Domain/Managing.Domain.csproj index 8d0bdf1..67b12cd 100644 --- a/src/Managing.Domain/Managing.Domain.csproj +++ b/src/Managing.Domain/Managing.Domain.csproj @@ -8,6 +8,7 @@ + diff --git a/src/Managing.Domain/MoneyManagements/LightMoneyManagement.cs b/src/Managing.Domain/MoneyManagements/LightMoneyManagement.cs index 35ce875..f221512 100644 --- a/src/Managing.Domain/MoneyManagements/LightMoneyManagement.cs +++ b/src/Managing.Domain/MoneyManagements/LightMoneyManagement.cs @@ -1,17 +1,28 @@ using System.ComponentModel.DataAnnotations; +using Orleans; using static Managing.Common.Enums; +[GenerateSerializer] public class LightMoneyManagement { + [Id(0)] [Required] public string Name { get; set; } + + [Id(1)] [Required] public Timeframe Timeframe { get; set; } + + [Id(2)] [Required] public decimal StopLoss { get; set; } + + [Id(3)] [Required] public decimal TakeProfit { get; set; } + + [Id(4)] [Required] public decimal Leverage { get; set; } - public void FormatPercentage() - { - StopLoss /= 100; - TakeProfit /= 100; - } + public void FormatPercentage() + { + StopLoss /= 100; + TakeProfit /= 100; + } } \ No newline at end of file diff --git a/src/Managing.Domain/MoneyManagements/MoneyManagement.cs b/src/Managing.Domain/MoneyManagements/MoneyManagement.cs index b1ddc00..4ab8800 100644 --- a/src/Managing.Domain/MoneyManagements/MoneyManagement.cs +++ b/src/Managing.Domain/MoneyManagements/MoneyManagement.cs @@ -1,9 +1,12 @@ using Managing.Domain.Users; +using Orleans; namespace Managing.Domain.MoneyManagements { + [GenerateSerializer] public class MoneyManagement : LightMoneyManagement { + [Id(5)] public User User { get; set; } } } diff --git a/src/Managing.Domain/Risk/RiskManagement.cs b/src/Managing.Domain/Risk/RiskManagement.cs index 84424fd..3d83361 100644 --- a/src/Managing.Domain/Risk/RiskManagement.cs +++ b/src/Managing.Domain/Risk/RiskManagement.cs @@ -1,5 +1,6 @@ using System.ComponentModel.DataAnnotations; using Managing.Common; +using Orleans; namespace Managing.Domain.Risk; @@ -7,6 +8,7 @@ namespace Managing.Domain.Risk; /// Risk management configuration for trading bots /// Contains all configurable risk parameters for probabilistic analysis and position sizing /// +[GenerateSerializer] public class RiskManagement { /// @@ -14,6 +16,7 @@ public class RiskManagement /// Signals with SL probability above this threshold may be filtered out /// Range: 0.05 (5%) to 0.50 (50%) /// + [Id(0)] [Range(0.05, 0.50)] [Required] public decimal AdverseProbabilityThreshold { get; set; } = 0.20m; @@ -23,6 +26,7 @@ public class RiskManagement /// Used for additional signal filtering and confidence assessment /// Range: 0.10 (10%) to 0.70 (70%) /// + [Id(1)] [Range(0.10, 0.70)] [Required] public decimal FavorableProbabilityThreshold { get; set; } = 0.30m; @@ -32,6 +36,7 @@ public class RiskManagement /// Higher values = more risk-averse behavior in utility calculations /// Range: 0.1 (risk-seeking) to 5.0 (highly risk-averse) /// + [Id(2)] [Range(0.1, 5.0)] [Required] public decimal RiskAversion { get; set; } = 1.0m; @@ -41,6 +46,7 @@ public class RiskManagement /// Trades with Kelly fraction below this threshold are considered unfavorable /// Range: 0.5% to 10% /// + [Id(3)] [Range(0.005, 0.10)] [Required] public decimal KellyMinimumThreshold { get; set; } = 0.01m; @@ -50,6 +56,7 @@ public class RiskManagement /// Prevents over-allocation even when Kelly suggests higher percentages /// Range: 5% to 50% /// + [Id(4)] [Range(0.05, 0.50)] [Required] public decimal KellyMaximumCap { get; set; } = 0.25m; @@ -59,6 +66,7 @@ public class RiskManagement /// Positions with higher liquidation risk may be blocked or reduced /// Range: 5% to 30% /// + [Id(5)] [Range(0.05, 0.30)] [Required] public decimal MaxLiquidationProbability { get; set; } = 0.10m; @@ -68,6 +76,7 @@ public class RiskManagement /// Longer horizons provide more stable predictions but less responsive signals /// Range: 1 hour to 168 hours (1 week) /// + [Id(6)] [Range(1, 168)] [Required] public int SignalValidationTimeHorizonHours { get; set; } = 24; @@ -77,6 +86,7 @@ public class RiskManagement /// Shorter horizons for more frequent risk updates on open positions /// Range: 1 hour to 48 hours /// + [Id(7)] [Range(1, 48)] [Required] public int PositionMonitoringTimeHorizonHours { get; set; } = 6; @@ -86,6 +96,7 @@ public class RiskManagement /// Positions exceeding this liquidation risk will trigger warnings /// Range: 10% to 40% /// + [Id(8)] [Range(0.10, 0.40)] [Required] public decimal PositionWarningThreshold { get; set; } = 0.20m; @@ -95,6 +106,7 @@ public class RiskManagement /// Positions exceeding this liquidation risk will be automatically closed /// Range: 30% to 80% /// + [Id(9)] [Range(0.30, 0.80)] [Required] public decimal PositionAutoCloseThreshold { get; set; } = 0.50m; @@ -104,6 +116,7 @@ public class RiskManagement /// Values less than 1.0 implement fractional Kelly (e.g., 0.5 = half-Kelly) /// Range: 0.1 to 1.0 /// + [Id(10)] [Range(0.1, 1.0)] [Required] public decimal KellyFractionalMultiplier { get; set; } = 1.0m; @@ -111,18 +124,21 @@ public class RiskManagement /// /// Risk tolerance level affecting overall risk calculations /// + [Id(11)] [Required] public Enums.RiskToleranceLevel RiskTolerance { get; set; } = Enums.RiskToleranceLevel.Moderate; /// /// Whether to use Expected Utility Theory for decision making /// + [Id(12)] [Required] public bool UseExpectedUtility { get; set; } = true; /// /// Whether to use Kelly Criterion for position sizing recommendations /// + [Id(13)] [Required] public bool UseKellyCriterion { get; set; } = true; diff --git a/src/Managing.Domain/Scenarios/LightScenario.cs b/src/Managing.Domain/Scenarios/LightScenario.cs new file mode 100644 index 0000000..6ef3cce --- /dev/null +++ b/src/Managing.Domain/Scenarios/LightScenario.cs @@ -0,0 +1,57 @@ +using Managing.Domain.Strategies; +using Orleans; + +namespace Managing.Domain.Scenarios; + +/// +/// Lightweight scenario class for Orleans serialization +/// Contains only the essential properties needed for backtesting +/// +[GenerateSerializer] +public class LightScenario +{ + public LightScenario(string name, int? loopbackPeriod = 1) + { + Name = name; + 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; } + + /// + /// Converts a full Scenario to a LightScenario + /// + public static LightScenario FromScenario(Scenario scenario) + { + var lightScenario = new LightScenario(scenario.Name, scenario.LoopbackPeriod) + { + Indicators = scenario.Indicators?.Select(LightIndicator.FromIndicator).ToList() ?? + new List() + }; + return lightScenario; + } + + /// + /// Converts a LightScenario back to a full Scenario + /// + public Scenario ToScenario() + { + var scenario = new Scenario(Name, LoopbackPeriod) + { + Indicators = Indicators?.Select(li => li.ToIndicator()).ToList() ?? new List() + }; + 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 d15b475..ae974eb 100644 --- a/src/Managing.Domain/Scenarios/Scenario.cs +++ b/src/Managing.Domain/Scenarios/Scenario.cs @@ -1,8 +1,10 @@ using Managing.Domain.Strategies; using Managing.Domain.Users; +using Orleans; namespace Managing.Domain.Scenarios { + [GenerateSerializer] public class Scenario { public Scenario(string name, int? loopbackPeriod = 1) @@ -12,9 +14,16 @@ namespace Managing.Domain.Scenarios 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; } public void AddIndicator(Indicator indicator) diff --git a/src/Managing.Domain/Strategies/Indicator.cs b/src/Managing.Domain/Strategies/Indicator.cs index 5347e1d..0fb7210 100644 --- a/src/Managing.Domain/Strategies/Indicator.cs +++ b/src/Managing.Domain/Strategies/Indicator.cs @@ -1,6 +1,4 @@ -using System.Runtime.Serialization; -using System.Text.Json.Serialization; -using Managing.Core.FixedSizedQueue; +using Managing.Core.FixedSizedQueue; using Managing.Domain.Candles; using Managing.Domain.Scenarios; using Managing.Domain.Strategies.Base; @@ -20,18 +18,31 @@ namespace Managing.Domain.Strategies } public string Name { get; set; } - [JsonIgnore] [IgnoreDataMember] public FixedSizeQueue Candles { 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() diff --git a/src/Managing.Domain/Strategies/LightIndicator.cs b/src/Managing.Domain/Strategies/LightIndicator.cs new file mode 100644 index 0000000..843c224 --- /dev/null +++ b/src/Managing.Domain/Strategies/LightIndicator.cs @@ -0,0 +1,84 @@ +using Managing.Domain.Scenarios; +using Orleans; +using static Managing.Common.Enums; + +namespace Managing.Domain.Strategies; + +/// +/// Lightweight indicator class for Orleans serialization +/// Contains only the essential properties needed for backtesting +/// +[GenerateSerializer] +public class LightIndicator +{ + public LightIndicator(string name, IndicatorType type) + { + Name = name; + Type = type; + SignalType = ScenarioHelpers.GetSignalType(type); + } + + [Id(0)] public string Name { get; set; } + + [Id(1)] public IndicatorType Type { get; set; } + + [Id(2)] public SignalType SignalType { get; set; } + + [Id(3)] public int MinimumHistory { get; set; } + + [Id(4)] public int? Period { get; set; } + + [Id(5)] public int? FastPeriods { get; set; } + + [Id(6)] public int? SlowPeriods { get; set; } + + [Id(7)] public int? SignalPeriods { get; set; } + + [Id(8)] public double? Multiplier { get; set; } + + [Id(9)] public int? SmoothPeriods { get; set; } + + [Id(10)] public int? StochPeriods { get; set; } + + [Id(11)] public int? CyclePeriods { get; set; } + + /// + /// Converts a full Indicator to a LightIndicator + /// + public static LightIndicator FromIndicator(Indicator indicator) + { + return new LightIndicator(indicator.Name, 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 + }; + } + + /// + /// Converts a LightIndicator back to a full Indicator + /// + public Indicator ToIndicator() + { + return new Indicator(Name, Type) + { + SignalType = SignalType, + MinimumHistory = MinimumHistory, + Period = Period, + FastPeriods = FastPeriods, + SlowPeriods = SlowPeriods, + SignalPeriods = SignalPeriods, + Multiplier = Multiplier, + SmoothPeriods = SmoothPeriods, + StochPeriods = StochPeriods, + CyclePeriods = CyclePeriods + }; + } +} \ No newline at end of file diff --git a/src/Managing.Domain/Strategies/LightSignal.cs b/src/Managing.Domain/Strategies/LightSignal.cs index d238fcd..0b75d8f 100644 --- a/src/Managing.Domain/Strategies/LightSignal.cs +++ b/src/Managing.Domain/Strategies/LightSignal.cs @@ -2,8 +2,10 @@ 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, @@ -24,17 +26,40 @@ public class LightSignal : ValueObject $"{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() diff --git a/src/Managing.Domain/Trades/Position.cs b/src/Managing.Domain/Trades/Position.cs index cc7f855..da1ca82 100644 --- a/src/Managing.Domain/Trades/Position.cs +++ b/src/Managing.Domain/Trades/Position.cs @@ -1,10 +1,12 @@ using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; using Managing.Domain.Users; +using Orleans; using static Managing.Common.Enums; namespace Managing.Domain.Trades { + [GenerateSerializer] public class Position { public Position(string identifier, string accountName, TradeDirection originDirection, Ticker ticker, @@ -21,28 +23,53 @@ 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(5)] [Required] [JsonPropertyName("Open")] public Trade Open { get; set; } + [Id(6)] [Required] [JsonPropertyName("StopLoss")] public Trade StopLoss { get; set; } + [Id(7)] [Required] [JsonPropertyName("TakeProfit1")] public Trade TakeProfit1 { get; set; } + [Id(8)] [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; } public bool IsFinished() diff --git a/src/Managing.Domain/Trades/ProfitAndLoss.cs b/src/Managing.Domain/Trades/ProfitAndLoss.cs index ac98eb9..0ac40f2 100644 --- a/src/Managing.Domain/Trades/ProfitAndLoss.cs +++ b/src/Managing.Domain/Trades/ProfitAndLoss.cs @@ -1,13 +1,18 @@ -using static Managing.Common.Enums; +using Orleans; +using static Managing.Common.Enums; namespace Managing.Domain.Trades { + [GenerateSerializer] public sealed class ProfitAndLoss { + [Id(0)] public decimal Realized { get; set; } + [Id(1)] public decimal Net { get; set; } + [Id(2)] public decimal AverageOpenPrice { get; private set; } private const decimal _multiplier = 100000; diff --git a/src/Managing.Domain/Trades/Trade.cs b/src/Managing.Domain/Trades/Trade.cs index ebed50f..8c0a5a5 100644 --- a/src/Managing.Domain/Trades/Trade.cs +++ b/src/Managing.Domain/Trades/Trade.cs @@ -1,8 +1,10 @@ using System.ComponentModel.DataAnnotations; +using Orleans; using static Managing.Common.Enums; namespace Managing.Domain.Trades { + [GenerateSerializer] public class Trade { public Trade(DateTime date, TradeDirection direction, TradeStatus status, TradeType tradeType, Ticker ticker, @@ -21,16 +23,37 @@ namespace Managing.Domain.Trades Fee = 0; } + [Id(0)] [Required] public decimal Fee { get; set; } + + [Id(1)] [Required] public DateTime Date { get; set; } + + [Id(2)] [Required] public TradeDirection Direction { get; set; } + + [Id(3)] [Required] public TradeStatus Status { get; set; } + + [Id(4)] [Required] public TradeType TradeType { get; set; } + + [Id(5)] [Required] public Ticker Ticker { get; set; } + + [Id(6)] [Required] public decimal Quantity { get; set; } + + [Id(7)] [Required] public decimal Price { get; set; } + + [Id(8)] [Required] public decimal Leverage { get; set; } + + [Id(9)] [Required] public string ExchangeOrderId { get; set; } + + [Id(10)] [Required] public string Message { get; set; } public void SetStatus(TradeStatus status) diff --git a/src/Managing.Domain/Users/User.cs b/src/Managing.Domain/Users/User.cs index aeb9bfd..f87f9ce 100644 --- a/src/Managing.Domain/Users/User.cs +++ b/src/Managing.Domain/Users/User.cs @@ -1,12 +1,23 @@ using Managing.Domain.Accounts; +using Orleans; namespace Managing.Domain.Users; +[GenerateSerializer] public class User { + [Id(0)] public string Name { get; set; } + + [Id(1)] public List Accounts { get; set; } + + [Id(2)] public string AgentName { get; set; } + + [Id(3)] public string AvatarUrl { get; set; } + + [Id(4)] public string TelegramChannel { get; set; } } \ No newline at end of file diff --git a/src/Managing.Infrastructure.Exchanges/Managing.Infrastructure.Exchanges.csproj b/src/Managing.Infrastructure.Exchanges/Managing.Infrastructure.Exchanges.csproj index 775f0b5..f3d153e 100644 --- a/src/Managing.Infrastructure.Exchanges/Managing.Infrastructure.Exchanges.csproj +++ b/src/Managing.Infrastructure.Exchanges/Managing.Infrastructure.Exchanges.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/Managing.Infrastructure.Messengers/Managing.Infrastructure.Messengers.csproj b/src/Managing.Infrastructure.Messengers/Managing.Infrastructure.Messengers.csproj index 82f93c3..d4f8b41 100644 --- a/src/Managing.Infrastructure.Messengers/Managing.Infrastructure.Messengers.csproj +++ b/src/Managing.Infrastructure.Messengers/Managing.Infrastructure.Messengers.csproj @@ -6,14 +6,14 @@ - - - - + + + + - +