* 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
This commit is contained in:
Oda
2025-07-30 11:03:30 +02:00
committed by GitHub
parent d281d7cd02
commit 3de8b5e00e
59 changed files with 2626 additions and 677 deletions

View File

@@ -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.

329
CLAUDE.md
View File

@@ -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 <MigrationName> --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<ExampleService> _logger;
public ExampleService(IExampleRepository repository, ILogger<ExampleService> logger)
{
_repository = repository;
_logger = logger;
}
public async Task<Example> 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 <Loader />;
return (
<div className="container mx-auto">
{/* Component content */}
</div>
);
}
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
## 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`)

255
Plan.md Normal file
View File

@@ -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<BotStatus> GetStatusAsync();
Task<TradingBotConfig> GetConfigurationAsync();
Task<bool> UpdateConfigurationAsync(TradingBotConfig newConfig);
Task<Position> OpenPositionManuallyAsync(TradeDirection direction);
Task ToggleIsForWatchOnlyAsync();
Task<TradingBotResponse> 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<IExchangeService, ExchangeService>();
services.AddSingleton<IAccountService, AccountService>();
services.AddSingleton<ITradingService, TradingService>();
services.AddSingleton<IMessengerService, MessengerService>();
services.AddSingleton<IBackupBotService, BackupBotService>();
});
// 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<ITradingBot> CreateTradingBotAsync(TradingBotConfig config, bool useGrain = true)
{
if (config.IsForBacktest || !useGrain)
{
// For backtesting: Create regular class instance
return new TradingBot(
_serviceProvider.GetService<ILogger<TradingBot>>(),
_serviceProvider.GetService<IServiceScopeFactory>(),
config
);
}
else
{
// For live trading: Use Orleans grain
var grain = _clusterClient.GetGrain<ITradingBotGrain>(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<LightSignal> Signals { get; set; }
[Id(2)] public List<Position> Positions { get; set; }
[Id(3)] public Dictionary<DateTime, decimal> 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<TradingBotGrainState>, 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<TradingBotGrainState>`
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

189
orleans-plan.md Normal file
View File

@@ -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<TradingBotGrainState> 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<TradingBotGrainState> 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<T> 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)

View File

@@ -29,7 +29,6 @@ public class BacktestController : BaseController
{
private readonly IHubContext<BacktestHub> _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<BacktestHub> 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
/// <summary>
/// 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.
/// </summary>
/// <param name="request">The backtest request containing configuration and parameters.</param>
/// <returns>The result of the backtest with complete configuration.</returns>
/// <returns>The lightweight result of the backtest with essential data.</returns>
[HttpPost]
[Route("Run")]
public async Task<ActionResult<Backtest>> Run([FromBody] RunBacktestRequest request)
public async Task<ActionResult<LightBacktest>> 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
}
/// <summary>
/// Notifies subscribers about the backtesting results via SignalR.
/// </summary>
/// <param name="backtesting">The backtest result to notify subscribers about.</param>
private async Task NotifyBacktesingSubscriberAsync(Backtest backtesting)
{
if (backtesting != null)
{
await _hubContext.Clients.All.SendAsync("BacktestsSubscription", backtesting);
}
}
public MoneyManagement Map(MoneyManagementRequest moneyManagementRequest)
{

View File

@@ -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,

View File

@@ -16,8 +16,10 @@
<PackageReference Include="AspNetCore.HealthChecks.Uris" Version="9.0.0"/>
<PackageReference Include="Essential.LoggerProvider.Elasticsearch" Version="1.3.2"/>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.5"/>
<PackageReference Include="Microsoft.Orleans.Core" Version="9.2.1"/>
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.20.1"/>
<PackageReference Include="NSwag.AspNetCore" Version="14.0.7"/>
<PackageReference Include="OrleansDashboard" Version="8.2.0"/>
<PackageReference Include="Sentry.AspNetCore" Version="5.5.1"/>
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1"/>
<PackageReference Include="Serilog.Enrichers.Environment" Version="2.3.0"/>

View File

@@ -186,6 +186,10 @@ builder.Services.AddSignalR().AddJsonProtocol();
builder.Services.AddScoped<IJwtUtils, JwtUtils>();
builder.Services.RegisterApiDependencies(builder.Configuration);
// Orleans Configuration
builder.Host.ConfigureOrleans(builder.Configuration, builder.Environment.IsProduction());
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApiDocument(document =>
{

View File

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

View File

@@ -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;
/// <summary>
/// Orleans grain interface for Backtest TradingBot operations.
/// This interface extends ITradingBotGrain with backtest-specific functionality.
/// </summary>
public interface IBacktestTradingBotGrain : IGrainWithGuidKey
{
/// <summary>
/// Runs a complete backtest following the exact pattern of GetBacktestingResult from Backtester.cs
/// </summary>
/// <param name="config">The trading bot configuration for this backtest</param>
/// <param name="candles">The candles to use for backtesting</param>
/// <param name="user">The user running the backtest (optional, required for saving)</param>
/// <param name="save">Whether to save the backtest results</param>
/// <param name="withCandles">Whether to include candles and indicators values in the response</param>
/// <param name="requestId">The request ID to associate with this backtest</param>
/// <param name="metadata">Additional metadata to associate with this backtest</param>
/// <returns>The complete backtest result</returns>
Task<LightBacktest> RunBacktestAsync(TradingBotConfig config, List<Candle> candles, User user = null, bool save = false, bool withCandles = false, string requestId = null, object metadata = null);
/// <summary>
/// Gets the current backtest progress
/// </summary>
/// <returns>Backtest progress information</returns>
Task<BacktestProgress> GetBacktestProgressAsync();
}
/// <summary>
/// Represents the progress of a backtest
/// </summary>
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; }
}

View File

@@ -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;
/// <summary>
/// Orleans grain interface for TradingBot operations.
/// This interface defines the distributed, async operations available for trading bots.
/// </summary>
public interface ITradingBotGrain : IGrainWithGuidKey
{
/// <summary>
/// Starts the trading bot asynchronously
/// </summary>
Task StartAsync();
/// <summary>
/// Stops the trading bot asynchronously
/// </summary>
Task StopAsync();
/// <summary>
/// Gets the current status of the trading bot
/// </summary>
Task<BotStatus> GetStatusAsync();
/// <summary>
/// Gets the current configuration of the trading bot
/// </summary>
Task<TradingBotConfig> GetConfigurationAsync();
/// <summary>
/// Updates the trading bot configuration
/// </summary>
/// <param name="newConfig">The new configuration to apply</param>
/// <returns>True if the configuration was successfully updated</returns>
Task<bool> UpdateConfigurationAsync(TradingBotConfig newConfig);
/// <summary>
/// Manually opens a position in the specified direction
/// </summary>
/// <param name="direction">The direction of the trade (Long/Short)</param>
/// <returns>The created Position object</returns>
Task<Position> OpenPositionManuallyAsync(TradeDirection direction);
/// <summary>
/// Toggles the bot between watch-only and trading mode
/// </summary>
Task ToggleIsForWatchOnlyAsync();
/// <summary>
/// Gets comprehensive bot data including positions, signals, and performance metrics
/// </summary>
Task<TradingBotResponse> GetBotDataAsync();
/// <summary>
/// Loads a bot backup into the grain state
/// </summary>
/// <param name="backup">The bot backup to load</param>
Task LoadBackupAsync(BotBackup backup);
/// <summary>
/// Forces a backup save of the current bot state
/// </summary>
Task SaveBackupAsync();
/// <summary>
/// Gets the current profit and loss for the bot
/// </summary>
Task<decimal> GetProfitAndLossAsync();
/// <summary>
/// Gets the current win rate percentage for the bot
/// </summary>
Task<int> GetWinRateAsync();
/// <summary>
/// Gets the bot's execution count (number of Run cycles completed)
/// </summary>
Task<long> GetExecutionCountAsync();
/// <summary>
/// Gets the bot's startup time
/// </summary>
Task<DateTime> GetStartupTimeAsync();
/// <summary>
/// Gets the bot's creation date
/// </summary>
Task<DateTime> GetCreateDateAsync();
}

View File

@@ -11,4 +11,8 @@
<ProjectReference Include="..\Managing.Domain\Managing.Domain.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Orleans.Core.Abstractions" Version="9.2.1" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,104 @@
using Managing.Domain.Bots;
using Managing.Domain.Trades;
using Orleans;
using static Managing.Common.Enums;
namespace Managing.Application.Abstractions.Models;
/// <summary>
/// Response model for trading bot data.
/// Used to return comprehensive bot information via Orleans grains.
/// </summary>
[GenerateSerializer]
public class TradingBotResponse
{
/// <summary>
/// Bot identifier
/// </summary>
[Id(0)]
public string Identifier { get; set; } = string.Empty;
/// <summary>
/// Bot display name
/// </summary>
[Id(1)]
public string Name { get; set; } = string.Empty;
/// <summary>
/// Current bot status
/// </summary>
[Id(2)]
public BotStatus Status { get; set; }
/// <summary>
/// Bot configuration
/// </summary>
[Id(3)]
public TradingBotConfig Config { get; set; }
/// <summary>
/// Trading positions
/// </summary>
[Id(4)]
public List<Position> Positions { get; set; } = new();
/// <summary>
/// Trading signals
/// </summary>
[Id(5)]
public List<LightSignal> Signals { get; set; } = new();
/// <summary>
/// Wallet balance history
/// </summary>
[Id(6)]
public Dictionary<DateTime, decimal> WalletBalances { get; set; } = new();
/// <summary>
/// Current profit and loss
/// </summary>
[Id(7)]
public decimal ProfitAndLoss { get; set; }
/// <summary>
/// Win rate percentage
/// </summary>
[Id(8)]
public int WinRate { get; set; }
/// <summary>
/// Execution count
/// </summary>
[Id(9)]
public long ExecutionCount { get; set; }
/// <summary>
/// Startup time
/// </summary>
[Id(10)]
public DateTime StartupTime { get; set; }
/// <summary>
/// Creation date
/// </summary>
[Id(11)]
public DateTime CreateDate { get; set; }
/// <summary>
/// Current balance
/// </summary>
[Id(12)]
public decimal CurrentBalance { get; set; }
/// <summary>
/// Number of active positions
/// </summary>
[Id(13)]
public int ActivePositionsCount { get; set; }
/// <summary>
/// Last execution time
/// </summary>
[Id(14)]
public DateTime LastExecution { get; set; }
}

View File

@@ -10,6 +10,7 @@ namespace Managing.Application.Abstractions.Services
/// <summary>
/// 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.
/// </summary>
/// <param name="config">The trading bot configuration (must include Scenario object or ScenarioName)</param>
/// <param name="startDate">The start date for the backtest</param>
@@ -19,8 +20,8 @@ namespace Managing.Application.Abstractions.Services
/// <param name="withCandles">Whether to include candles and indicators values in the response</param>
/// <param name="requestId">The request ID to associate with this backtest (optional)</param>
/// <param name="metadata">Additional metadata to associate with this backtest (optional)</param>
/// <returns>The backtest results</returns>
Task<Backtest> RunTradingBotBacktest(
/// <returns>The lightweight backtest results</returns>
Task<LightBacktest> RunTradingBotBacktest(
TradingBotConfig config,
DateTime startDate,
DateTime endDate,
@@ -33,6 +34,7 @@ namespace Managing.Application.Abstractions.Services
/// <summary>
/// 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.
/// </summary>
/// <param name="config">The trading bot configuration (must include Scenario object or ScenarioName)</param>
/// <param name="candles">The candles to use for backtesting</param>
@@ -40,8 +42,8 @@ namespace Managing.Application.Abstractions.Services
/// <param name="withCandles">Whether to include candles and indicators values in the response</param>
/// <param name="requestId">The request ID to associate with this backtest (optional)</param>
/// <param name="metadata">Additional metadata to associate with this backtest (optional)</param>
/// <returns>The backtest results</returns>
Task<Backtest> RunTradingBotBacktest(
/// <returns>The lightweight backtest results</returns>
Task<LightBacktest> RunTradingBotBacktest(
TradingBotConfig config,
List<Candle> candles,
User user = null,

View File

@@ -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<double>();
// 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,

View File

@@ -10,7 +10,7 @@
<ItemGroup>
<PackageReference Include="MathNet.Numerics" Version="5.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.5" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0"/>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.7" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="Microsoft.TestPlatform.AdapterUtilities" Version="17.9.0" />
<PackageReference Include="Moq" Version="4.20.70" />

View File

@@ -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<TradingBot> CreateTradingBotLogger()
public static ILogger<TradingBotBase> CreateTradingBotLogger()
{
ILoggerFactory loggerFactory = new Microsoft.Extensions.Logging.Abstractions.NullLoggerFactory();
ILoggerFactory loggerFactory = new NullLoggerFactory();
return loggerFactory.CreateLogger<TradingBot>();
return loggerFactory.CreateLogger<TradingBotBase>();
}
public static ILogger<Backtester> CreateBacktesterLogger()
{
ILoggerFactory loggerFactory = new Microsoft.Extensions.Logging.Abstractions.NullLoggerFactory();
ILoggerFactory loggerFactory = new NullLoggerFactory();
return loggerFactory.CreateLogger<Backtester>();
}
public static ILogger<CandleRepository> CreateCandleRepositoryLogger()
{
ILoggerFactory loggerFactory = new Microsoft.Extensions.Logging.Abstractions.NullLoggerFactory();
ILoggerFactory loggerFactory = new NullLoggerFactory();
return loggerFactory.CreateLogger<CandleRepository>();
}

View File

@@ -222,17 +222,14 @@ public class BundleBacktestWorker : BaseWorker<BundleBacktestWorker>
}
// 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<BundleBacktestWorker>
SmoothPeriods = indicatorRequest.SmoothPeriods,
StochPeriods = indicatorRequest.StochPeriods,
CyclePeriods = indicatorRequest.CyclePeriods,
User = null // No user context in worker
};
scenario.AddIndicator(indicator);
}

View File

@@ -7,7 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1"/>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.3" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
</ItemGroup>

View File

@@ -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<LightSignal>();
}
catch (Exception ex)
{

View File

@@ -29,12 +29,6 @@ public interface IBotService
/// <returns>ITradingBot instance configured for backtesting</returns>
Task<ITradingBot> CreateBacktestTradingBot(TradingBotConfig config);
// Legacy methods - these will use TradingBot internally but maintain backward compatibility
Task<ITradingBot> CreateScalpingBot(TradingBotConfig config);
Task<ITradingBot> CreateBacktestScalpingBot(TradingBotConfig config);
Task<ITradingBot> CreateFlippingBot(TradingBotConfig config);
Task<ITradingBot> CreateBacktestFlippingBot(TradingBotConfig config);
IBot CreateSimpleBot(string botName, Workflow workflow);
Task<string> StopBot(string botName);
Task<bool> DeleteBot(string botName);

View File

@@ -52,5 +52,6 @@ namespace Managing.Application.Abstractions
Task<bool> UpdateIndicatorByUser(User user, IndicatorType indicatorType, string name, int? period, int? fastPeriods,
int? slowPeriods, int? signalPeriods, double? multiplier, int? stochPeriods, int? smoothPeriods,
int? cyclePeriods);
Task<Scenario> GetScenarioByNameAndUserAsync(string scenarioName, User user);
}
}

View File

@@ -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<BacktestHub> _hubContext;
private readonly IGrainFactory _grainFactory;
public Backtester(
IExchangeService exchangeService,
@@ -42,7 +39,8 @@ namespace Managing.Application.Backtesting
IAccountService accountService,
IMessengerService messengerService,
IKaigenService kaigenService,
IHubContext<BacktestHub> hubContext)
IHubContext<BacktestHub> 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
/// <param name="withCandles">Whether to include candles and indicators values in the response</param>
/// <param name="requestId">The request ID to associate with this backtest (optional)</param>
/// <param name="metadata">Additional metadata to associate with this backtest (optional)</param>
/// <returns>The backtest results</returns>
public async Task<Backtest> RunTradingBotBacktest(
/// <returns>The lightweight backtest results</returns>
public async Task<LightBacktest> 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
/// <param name="candles">The candles to use for backtesting</param>
/// <param name="user">The user running the backtest (optional)</param>
/// <param name="withCandles">Whether to include candles and indicators values in the response</param>
/// <returns>The backtest results</returns>
public async Task<Backtest> RunTradingBotBacktest(
/// <param name="requestId">The request ID to associate with this backtest (optional)</param>
/// <param name="metadata">Additional metadata to associate with this backtest (optional)</param>
/// <returns>The lightweight backtest results</returns>
public async Task<LightBacktest> RunTradingBotBacktest(
TradingBotConfig config,
List<Candle> 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);
}
/// <summary>
/// Core backtesting logic - handles the actual backtest execution with pre-loaded candles
/// </summary>
private async Task<Backtest> RunBacktestWithCandles(
private async Task<LightBacktest> RunBacktestWithCandles(
TradingBotConfig config,
List<Candle> 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<IBacktestTradingBotGrain>(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<Account> GetAccountFromConfig(TradingBotConfig config)
@@ -237,128 +226,16 @@ namespace Managing.Application.Backtesting
return candles;
}
private async Task<Backtest> GetBacktestingResult(
TradingBotConfig config,
ITradingBot bot,
List<Candle> candles,
User user = null,
bool withCandles = false,
string requestId = null,
object metadata = null)
/// <summary>
/// Creates a clean copy of the trading bot config for Orleans serialization
/// Uses LightScenario and LightIndicator to avoid FixedSizeQueue serialization issues
/// </summary>
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<Candle>(candles);
// Only calculate indicators values if withCandles is true
Dictionary<IndicatorType, IndicatorsResultBase> 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<Candle>())
{
FinalPnl = finalPnl,
WinRate = winRate,
GrowthPercentage = growthPercentage,
HodlPercentage = hodlPercentage,
Fees = fees,
WalletBalances = bot.WalletBalances.ToList(),
Statistics = stats,
IndicatorsValues = withCandles
? AggregateValues(indicatorsValues, bot.IndicatorsValues)
: new Dictionary<IndicatorType, IndicatorsResultBase>(),
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<IndicatorType, IndicatorsResultBase> AggregateValues(
Dictionary<IndicatorType, IndicatorsResultBase> indicatorsValues,
Dictionary<IndicatorType, IndicatorsResultBase> 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<IndicatorType, IndicatorsResultBase>();
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<IndicatorType, IndicatorsResultBase> GetIndicatorsValues(List<Indicator> indicators,
List<Candle> candles)
{
var indicatorsValues = new Dictionary<IndicatorType, IndicatorsResultBase>();
var fixedCandles = new FixedSizeQueue<Candle>(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<bool> DeleteBacktestAsync(string id)
{

View File

@@ -12,14 +12,14 @@ namespace Managing.Application.Bots.Base
private readonly IExchangeService _exchangeService;
private readonly IMessengerService _messengerService;
private readonly IAccountService _accountService;
private readonly ILogger<TradingBot> _tradingBotLogger;
private readonly ILogger<TradingBotBase> _tradingBotLogger;
private readonly ITradingService _tradingService;
private readonly IBotService _botService;
private readonly IBackupBotService _backupBotService;
public BotFactory(
IExchangeService exchangeService,
ILogger<TradingBot> tradingBotLogger,
ILogger<TradingBotBase> tradingBotLogger,
IMessengerService messengerService,
IAccountService accountService,
ITradingService tradingService,

View File

@@ -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;
/// <summary>
/// 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.
/// </summary>
[StatelessWorker]
public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
{
private readonly ILogger<BacktestTradingBotGrain> _logger;
private readonly IServiceScopeFactory _scopeFactory;
private readonly IBacktestRepository _backtestRepository;
private bool _isDisposed = false;
public BacktestTradingBotGrain(
ILogger<BacktestTradingBotGrain> logger,
IServiceScopeFactory scopeFactory,
IBacktestRepository backtestRepository)
{
_logger = logger;
_scopeFactory = scopeFactory;
_backtestRepository = backtestRepository;
}
/// <summary>
/// Runs a complete backtest following the exact pattern of GetBacktestingResult from Backtester.cs
/// </summary>
/// <param name="config">The trading bot configuration for this backtest</param>
/// <param name="candles">The candles to use for backtesting</param>
/// <param name="withCandles">Whether to include candles and indicators values in the response</param>
/// <param name="requestId">The request ID to associate with this backtest</param>
/// <param name="metadata">Additional metadata to associate with this backtest</param>
/// <returns>The complete backtest result</returns>
public async Task<LightBacktest> RunBacktestAsync(
TradingBotConfig config,
List<Candle> 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<Candle>(candles);
// Only calculate indicators values if withCandles is true
Dictionary<IndicatorType, IndicatorsResultBase> 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<Candle>())
{
FinalPnl = finalPnl,
WinRate = winRate,
GrowthPercentage = growthPercentage,
HodlPercentage = hodlPercentage,
Fees = fees,
WalletBalances = tradingBot.WalletBalances.ToList(),
Statistics = stats,
IndicatorsValues = withCandles
? AggregateValues(indicatorsValues, tradingBot.IndicatorsValues)
: new Dictionary<IndicatorType, IndicatorsResultBase>(),
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);
}
/// <summary>
/// Converts a Backtest to LightBacktest for safe Orleans serialization
/// </summary>
/// <param name="backtest">The full backtest to convert</param>
/// <returns>A lightweight backtest suitable for Orleans serialization</returns>
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
};
}
/// <summary>
/// Creates a TradingBotBase instance using composition for backtesting
/// </summary>
private async Task<TradingBotBase> 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<ILogger<TradingBotBase>>();
var tradingBot = new TradingBotBase(logger, _scopeFactory, config);
// Set the user if available
if (user != null)
{
tradingBot.User = user;
}
return tradingBot;
}
/// <summary>
/// Sends notification if backtest meets criteria (following Backtester.cs pattern)
/// </summary>
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);
}
}
/// <summary>
/// Aggregates indicator values (following Backtester.cs pattern)
/// </summary>
private Dictionary<IndicatorType, IndicatorsResultBase> AggregateValues(
Dictionary<IndicatorType, IndicatorsResultBase> indicatorsValues,
Dictionary<IndicatorType, IndicatorsResultBase> botStrategiesValues)
{
var result = new Dictionary<IndicatorType, IndicatorsResultBase>();
foreach (var indicator in indicatorsValues)
{
result[indicator.Key] = indicator.Value;
}
return result;
}
/// <summary>
/// Gets indicators values (following Backtester.cs pattern)
/// </summary>
private Dictionary<IndicatorType, IndicatorsResultBase> GetIndicatorsValues(List<Indicator> indicators,
List<Candle> candles)
{
var indicatorsValues = new Dictionary<IndicatorType, IndicatorsResultBase>();
var fixedCandles = new FixedSizeQueue<Candle>(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<BacktestProgress> GetBacktestProgressAsync()
{
throw new NotImplementedException();
}
public Task StartAsync()
{
throw new NotImplementedException();
}
public Task StopAsync()
{
throw new NotImplementedException();
}
public Task<BotStatus> GetStatusAsync()
{
throw new NotImplementedException();
}
public Task<TradingBotConfig> GetConfigurationAsync()
{
throw new NotImplementedException();
}
public Task<Position> OpenPositionManuallyAsync(TradeDirection direction)
{
throw new NotImplementedException();
}
public Task ToggleIsForWatchOnlyAsync()
{
throw new NotImplementedException();
}
public Task<TradingBotResponse> GetBotDataAsync()
{
throw new NotImplementedException();
}
public Task LoadBackupAsync(BotBackup backup)
{
throw new NotImplementedException();
}
public Task SaveBackupAsync()
{
throw new NotImplementedException();
}
public Task<decimal> GetProfitAndLossAsync()
{
throw new NotImplementedException();
}
public Task<int> GetWinRateAsync()
{
throw new NotImplementedException();
}
public Task<long> GetExecutionCountAsync()
{
throw new NotImplementedException();
}
public Task<DateTime> GetStartupTimeAsync()
{
throw new NotImplementedException();
}
public Task<DateTime> GetCreateDateAsync()
{
throw new NotImplementedException();
}
}

View File

@@ -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;
/// <summary>
/// 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.
/// </summary>
public class LiveTradingBotGrain : Grain<TradingBotGrainState>, ITradingBotGrain
{
private readonly ILogger<LiveTradingBotGrain> _logger;
private readonly IServiceScopeFactory _scopeFactory;
private TradingBotBase? _tradingBot;
private IDisposable? _timer;
private bool _isDisposed = false;
public LiveTradingBotGrain(
ILogger<LiveTradingBotGrain> 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<BotStatus> GetStatusAsync()
{
return Task.FromResult(State.Status);
}
public Task<TradingBotConfig> GetConfigurationAsync()
{
return Task.FromResult(State.Config);
}
public async Task<bool> 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<Position> 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<TradingBotResponse> 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<decimal> 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<int> 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<long> GetExecutionCountAsync()
{
return Task.FromResult(State.ExecutionCount);
}
public Task<DateTime> GetStartupTimeAsync()
{
return Task.FromResult(State.StartupTime);
}
public Task<DateTime> GetCreateDateAsync()
{
return Task.FromResult(State.CreateDate);
}
/// <summary>
/// Creates a TradingBotBase instance using composition
/// </summary>
private async Task<TradingBotBase> 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<ILogger<TradingBotBase>>();
var tradingBot = new TradingBotBase(logger, _scopeFactory, State.Config);
// Set the user if available
if (State.User != null)
{
tradingBot.User = State.User;
}
return tradingBot;
}
/// <summary>
/// Starts the Orleans timer for periodic bot execution
/// </summary>
private void StartTimer()
{
if (_tradingBot == null) return;
var interval = _tradingBot.Interval;
_timer = RegisterTimer(
async _ => await ExecuteBotCycle(),
null,
TimeSpan.FromMilliseconds(interval),
TimeSpan.FromMilliseconds(interval));
}
/// <summary>
/// Executes one cycle of the trading bot
/// </summary>
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());
}
}
/// <summary>
/// Saves the current bot state to Orleans state storage
/// </summary>
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());
}
}
/// <summary>
/// Loads bot state from Orleans state storage
/// </summary>
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;
}
}
}

View File

@@ -9,12 +9,12 @@ namespace Managing.Application.Bots
{
public class SimpleBot : Bot
{
public readonly ILogger<TradingBot> Logger;
public readonly ILogger<TradingBotBase> Logger;
private readonly IBotService _botService;
private readonly IBackupBotService _backupBotService;
private Workflow _workflow;
public SimpleBot(string name, ILogger<TradingBot> logger, Workflow workflow, IBotService botService,
public SimpleBot(string name, ILogger<TradingBotBase> logger, Workflow workflow, IBotService botService,
IBackupBotService backupBotService) :
base(name)
{

View File

@@ -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<TradingBot> Logger;
public readonly ILogger<TradingBotBase> 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<TradingBot> logger,
public TradingBotBase(
ILogger<TradingBotBase> 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<IIndicator>();

View File

@@ -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;
/// <summary>
/// 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.
/// </summary>
[GenerateSerializer]
public class TradingBotGrainState
{
/// <summary>
/// The trading bot configuration
/// </summary>
[Id(0)]
public TradingBotConfig Config { get; set; } = new();
/// <summary>
/// Collection of trading signals generated by the bot
/// </summary>
[Id(1)]
public HashSet<LightSignal> Signals { get; set; } = new();
/// <summary>
/// List of trading positions opened by the bot
/// </summary>
[Id(2)]
public List<Position> Positions { get; set; } = new();
/// <summary>
/// Historical wallet balances tracked over time
/// </summary>
[Id(3)]
public Dictionary<DateTime, decimal> WalletBalances { get; set; } = new();
/// <summary>
/// Current status of the bot (Running, Stopped, etc.)
/// </summary>
[Id(4)]
public BotStatus Status { get; set; } = BotStatus.Down;
/// <summary>
/// When the bot was started
/// </summary>
[Id(5)]
public DateTime StartupTime { get; set; } = DateTime.UtcNow;
/// <summary>
/// When the bot was created
/// </summary>
[Id(6)]
public DateTime CreateDate { get; set; } = DateTime.UtcNow;
/// <summary>
/// The user who owns this bot
/// </summary>
[Id(7)]
public User User { get; set; }
/// <summary>
/// Bot execution counter
/// </summary>
[Id(8)]
public long ExecutionCount { get; set; } = 0;
/// <summary>
/// Bot identifier/name
/// </summary>
[Id(9)]
public string Identifier { get; set; } = string.Empty;
/// <summary>
/// Bot display name
/// </summary>
[Id(10)]
public string Name { get; set; } = string.Empty;
/// <summary>
/// Preload start date for candles
/// </summary>
[Id(11)]
public DateTime PreloadSince { get; set; } = DateTime.UtcNow;
/// <summary>
/// Number of preloaded candles
/// </summary>
[Id(12)]
public int PreloadedCandlesCount { get; set; } = 0;
/// <summary>
/// Timer interval for bot execution
/// </summary>
[Id(13)]
public int Interval { get; set; } = 60000; // Default 1 minute
/// <summary>
/// Maximum number of signals to keep in memory
/// </summary>
[Id(14)]
public int MaxSignals { get; set; } = 10;
/// <summary>
/// Indicates if the bot has been initialized
/// </summary>
[Id(15)]
public bool IsInitialized { get; set; } = false;
/// <summary>
/// Last time the bot state was persisted
/// </summary>
[Id(16)]
public DateTime LastBackupTime { get; set; } = DateTime.UtcNow;
}

View File

@@ -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<IBacktester, Backtest>(
var lightBacktest = ServiceScopeHelpers.WithScopedService<IBacktester, LightBacktest>(
_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;

View File

@@ -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<TradingBot> _tradingBotLogger;
private readonly ILogger<TradingBotBase> _tradingBotLogger;
private readonly ITradingService _tradingService;
private readonly IMoneyManagementService _moneyManagementService;
private readonly IUserService _userService;
private readonly IBackupBotService _backupBotService;
private readonly IServiceScopeFactory _scopeFactory;
private readonly IGrainFactory _grainFactory;
private ConcurrentDictionary<string, BotTaskWrapper> _botTasks =
new ConcurrentDictionary<string, BotTaskWrapper>();
public BotService(IBotRepository botRepository, IExchangeService exchangeService,
IMessengerService messengerService, IAccountService accountService, ILogger<TradingBot> tradingBotLogger,
IMessengerService messengerService, IAccountService accountService, ILogger<TradingBotBase> 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,12 +74,14 @@ namespace Managing.Application.ManageBot
_botTasks.AddOrUpdate(bot.Identifier, botTask, (key, existingVal) => botTask);
}
private async Task InitBot(ITradingBot bot, BotBackup backupBot)
{
try
{
var user = await _userService.GetUser(backupBot.User.Name);
bot.User = user;
// Config is already set correctly from backup data, so we only need to restore signals, positions, etc.
// Load backup data into the bot
bot.LoadBackup(backupBot);
// Only start the bot if the backup status is Up
@@ -92,6 +96,14 @@ namespace Managing.Application.ManageBot
bot.Stop();
}
}
catch (Exception ex)
{
_tradingBotLogger.LogError(ex, "Error initializing bot {Identifier} from backup", backupBot.Identifier);
// Ensure the bot is stopped if initialization fails
bot.Stop();
throw;
}
}
public List<ITradingBot> GetActiveBots()
{
@@ -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<bool> 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<ITradingBot> 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<ITradingBot> 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<ITradingBot> 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<ITradingBot> 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<ITradingBot> 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<ITradingBot> 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);
}
}
}

View File

@@ -46,7 +46,7 @@ public class LoadBackupBotCommandHandler : IRequestHandler<LoadBackupBotCommand,
// Try to get the active bot multiple times to ensure it's properly started
int attempts = 0;
const int maxAttempts = 5;
const int maxAttempts = 2;
while (attempts < maxAttempts)
{
@@ -58,7 +58,8 @@ public class LoadBackupBotCommandHandler : IRequestHandler<LoadBackupBotCommand,
if (backupBot.LastStatus == BotStatus.Down)
{
result[activeBot.Identifier] = BotStatus.Down;
_logger.LogInformation("Backup bot {Identifier} loaded but kept in Down status as it was originally Down.",
_logger.LogInformation(
"Backup bot {Identifier} loaded but kept in Down status as it was originally Down.",
backupBot.Identifier);
}
else
@@ -68,6 +69,7 @@ public class LoadBackupBotCommandHandler : IRequestHandler<LoadBackupBotCommand,
_logger.LogInformation("Backup bot {Identifier} started successfully.",
backupBot.Identifier);
}
break;
}

View File

@@ -21,6 +21,8 @@
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1" />
<PackageReference Include="Microsoft.Orleans.Client" Version="9.2.1" />
<PackageReference Include="Microsoft.Orleans.Core.Abstractions" Version="9.2.1" />
<PackageReference Include="Polly" Version="8.4.0" />
<PackageReference Include="Skender.Stock.Indicators" Version="2.5.0" />
</ItemGroup>

View File

@@ -302,5 +302,16 @@ namespace Managing.Application.Scenarios
return result;
}
public async Task<Scenario> 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;
}
}
}

View File

@@ -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<IExchangeService, ExchangeService>();
services.AddTransient<IAccountService, AccountService>();
services.AddTransient<ITradingService, TradingService>();
services.AddTransient<IMessengerService, MessengerService>();
services.AddTransient<IBackupBotService, BackupBotService>();
});
})
;
}
private static IServiceCollection AddApplication(this IServiceCollection services)
{
services.AddScoped<ITradingService, TradingService>();

View File

@@ -11,8 +11,14 @@
<PackageReference Include="MediatR" Version="12.2.0" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0"/>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0"/>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.7" />
<PackageReference Include="Microsoft.Orleans.Client" Version="9.2.1" />
<PackageReference Include="Microsoft.Orleans.Clustering.AdoNet" Version="9.2.1" />
<PackageReference Include="Microsoft.Orleans.Reminders.AdoNet" Version="9.2.1" />
<PackageReference Include="Microsoft.Orleans.Server" Version="9.2.1" />
<PackageReference Include="OrleansDashboard" Version="8.2.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -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<Balance> Balances { get; set; }
public bool IsPrivyWallet => Type == AccountType.Privy;

View File

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

View File

@@ -1,20 +1,26 @@
using Managing.Domain.Bots;
using Orleans;
namespace Managing.Domain.Backtests;
/// <summary>
/// Lightweight backtest class for Orleans serialization
/// Contains only the essential properties needed for backtest results
/// </summary>
[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;
}

View File

@@ -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; }
/// <summary>

View File

@@ -1,36 +1,44 @@
using Managing.Domain.Trades;
using Orleans;
namespace Managing.Domain.Bots;
[GenerateSerializer]
public class TradingBotBackup
{
/// <summary>
/// The complete trading bot configuration
/// </summary>
[Id(0)]
public TradingBotConfig Config { get; set; }
/// <summary>
/// Runtime state: Active signals for the bot
/// </summary>
[Id(1)]
public HashSet<LightSignal> Signals { get; set; }
/// <summary>
/// Runtime state: Open and closed positions for the bot
/// </summary>
[Id(2)]
public List<Position> Positions { get; set; }
/// <summary>
/// Runtime state: Historical wallet balances over time
/// </summary>
[Id(3)]
public Dictionary<DateTime, decimal> WalletBalances { get; set; }
/// <summary>
/// Runtime state: When the bot was started
/// </summary>
[Id(4)]
public DateTime StartupTime { get; set; }
/// <summary>
/// Runtime state: When the bot was created
/// </summary>
[Id(5)]
public DateTime CreateDate { get; set; }
}

View File

@@ -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; }
/// <summary>
@@ -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.
/// </summary>
[Id(11)]
public RiskManagement RiskManagement { get; set; } = new();
/// <summary>
/// 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.
/// </summary>
public Scenario Scenario { get; set; }
[Id(12)]
public LightScenario Scenario { get; set; }
/// <summary>
/// The scenario name to load from database. Only used when Scenario object is not provided.
/// </summary>
[Id(13)]
public string ScenarioName { get; set; }
/// <summary>
/// Maximum time in hours that a position can remain open before being automatically closed.
/// If null, time-based position closure is disabled.
/// </summary>
[Id(14)]
public decimal? MaxPositionTimeHours { get; set; }
/// <summary>
@@ -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.
/// </summary>
[Id(15)]
public bool CloseEarlyWhenProfitable { get; set; } = false;
/// <summary>
@@ -56,6 +85,7 @@ public class TradingBotConfig
/// If false, positions will be flipped regardless of profit status.
/// Default is true for safer trading.
/// </summary>
[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.
/// </summary>
[Id(17)]
public bool UseSynthApi { get; set; } = false;
/// <summary>
/// Whether to use Synth predictions for position sizing adjustments and risk assessment
/// </summary>
[Id(18)]
public bool UseForPositionSizing { get; set; } = true;
/// <summary>
/// Whether to use Synth predictions for signal filtering
/// </summary>
[Id(19)]
public bool UseForSignalFiltering { get; set; } = true;
/// <summary>
/// Whether to use Synth predictions for dynamic stop-loss/take-profit adjustments
/// </summary>
[Id(20)]
public bool UseForDynamicStopLoss { get; set; } = true;
}

View File

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

View File

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

View File

@@ -8,6 +8,7 @@
<ItemGroup>
<PackageReference Include="Exilion.TradingAtomics" Version="1.0.4"/>
<PackageReference Include="Microsoft.Orleans.Core.Abstractions" Version="9.2.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
<PackageReference Include="Skender.Stock.Indicators" Version="2.5.0"/>
</ItemGroup>

View File

@@ -1,12 +1,23 @@
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()

View File

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

View File

@@ -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
/// </summary>
[GenerateSerializer]
public class RiskManagement
{
/// <summary>
@@ -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%)
/// </summary>
[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%)
/// </summary>
[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)
/// </summary>
[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%
/// </summary>
[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%
/// </summary>
[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%
/// </summary>
[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)
/// </summary>
[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
/// </summary>
[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%
/// </summary>
[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%
/// </summary>
[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
/// </summary>
[Id(10)]
[Range(0.1, 1.0)]
[Required]
public decimal KellyFractionalMultiplier { get; set; } = 1.0m;
@@ -111,18 +124,21 @@ public class RiskManagement
/// <summary>
/// Risk tolerance level affecting overall risk calculations
/// </summary>
[Id(11)]
[Required]
public Enums.RiskToleranceLevel RiskTolerance { get; set; } = Enums.RiskToleranceLevel.Moderate;
/// <summary>
/// Whether to use Expected Utility Theory for decision making
/// </summary>
[Id(12)]
[Required]
public bool UseExpectedUtility { get; set; } = true;
/// <summary>
/// Whether to use Kelly Criterion for position sizing recommendations
/// </summary>
[Id(13)]
[Required]
public bool UseKellyCriterion { get; set; } = true;

View File

@@ -0,0 +1,57 @@
using Managing.Domain.Strategies;
using Orleans;
namespace Managing.Domain.Scenarios;
/// <summary>
/// Lightweight scenario class for Orleans serialization
/// Contains only the essential properties needed for backtesting
/// </summary>
[GenerateSerializer]
public class LightScenario
{
public LightScenario(string name, int? loopbackPeriod = 1)
{
Name = name;
Indicators = new List<LightIndicator>();
LoopbackPeriod = loopbackPeriod;
}
[Id(0)] public string Name { get; set; }
[Id(1)] public List<LightIndicator> Indicators { get; set; }
[Id(2)] public int? LoopbackPeriod { get; set; }
/// <summary>
/// Converts a full Scenario to a LightScenario
/// </summary>
public static LightScenario FromScenario(Scenario scenario)
{
var lightScenario = new LightScenario(scenario.Name, scenario.LoopbackPeriod)
{
Indicators = scenario.Indicators?.Select(LightIndicator.FromIndicator).ToList() ??
new List<LightIndicator>()
};
return lightScenario;
}
/// <summary>
/// Converts a LightScenario back to a full Scenario
/// </summary>
public Scenario ToScenario()
{
var scenario = new Scenario(Name, LoopbackPeriod)
{
Indicators = Indicators?.Select(li => li.ToIndicator()).ToList() ?? new List<Indicator>()
};
return scenario;
}
public void AddIndicator(LightIndicator indicator)
{
if (Indicators == null)
Indicators = new List<LightIndicator>();
Indicators.Add(indicator);
}
}

View File

@@ -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<Indicator> Indicators { get; set; }
[Id(2)]
public int? LoopbackPeriod { get; set; }
[Id(3)]
public User User { get; set; }
public void AddIndicator(Indicator indicator)

View File

@@ -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<Candle> Candles { get; set; }
public FixedSizeQueue<Candle> 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<LightSignal> Run()

View File

@@ -0,0 +1,84 @@
using Managing.Domain.Scenarios;
using Orleans;
using static Managing.Common.Enums;
namespace Managing.Domain.Strategies;
/// <summary>
/// Lightweight indicator class for Orleans serialization
/// Contains only the essential properties needed for backtesting
/// </summary>
[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; }
/// <summary>
/// Converts a full Indicator to a LightIndicator
/// </summary>
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
};
}
/// <summary>
/// Converts a LightIndicator back to a full Indicator
/// </summary>
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
};
}
}

View File

@@ -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<object> GetEqualityComponents()

View File

@@ -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()

View File

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

View File

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

View File

@@ -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<Account> Accounts { get; set; }
[Id(2)]
public string AgentName { get; set; }
[Id(3)]
public string AvatarUrl { get; set; }
[Id(4)]
public string TelegramChannel { get; set; }
}

View File

@@ -12,7 +12,7 @@
<PackageReference Include="FTX.Net" Version="1.0.16" />
<PackageReference Include="KrakenExchange.Net" Version="4.6.5" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
</ItemGroup>
<ItemGroup>

View File

@@ -7,9 +7,9 @@
<ItemGroup>
<PackageReference Include="Discord.Net" Version="3.15.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1"/>
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0"/>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.2"/>
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.3" />
</ItemGroup>
<ItemGroup>