Compare commits

...

10 Commits

Author SHA1 Message Date
20b0881084 Change orlean dashboard port
Some checks failed
Build & Deploy / build-and-deploy (push) Has been cancelled
.NET / build (push) Has been cancelled
2025-07-30 21:23:35 +07:00
84f3e91dc6 Try fixing orleans server runtime 2025-07-30 20:44:58 +07:00
1071730978 Fix solution build 2025-07-30 20:37:24 +07:00
Oda
3de8b5e00e Orlean (#32)
* Start building with orlean

* Add missing file

* Serialize grain state

* Remove grain and proxies

* update and add plan

* Update a bit

* Fix backtest grain

* Fix backtest grain

* Clean a bit
2025-07-30 16:03:30 +07:00
d281d7cd02 Clean repo 2025-07-29 05:29:10 +07:00
36cb672ce4 Update front and config 2025-07-29 03:02:33 +07:00
09e2c704ef Separate 2endpoints for data summary 2025-07-28 14:36:51 +07:00
38c7691615 Fix service scope 2025-07-27 21:33:50 +07:00
2ea911b3c2 prefilled did 2025-07-27 21:06:02 +07:00
4fe3c9bb51 Add postgres to db 2025-07-27 20:51:12 +07:00
105 changed files with 2822 additions and 5206 deletions

View File

@@ -11,9 +11,6 @@ You are a senior .NET backend developer and experimental quant with deep experti
## Quantitative Finance Core Principles ## Quantitative Finance Core Principles
- Prioritize numerical precision (use `decimal` for monetary calculations) - Prioritize numerical precision (use `decimal` for monetary calculations)
- Implement proven financial mathematics (e.g., Black-Scholes, Monte Carlo methods) - 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 Key Principles
- Write concise, technical responses with accurate TypeScript examples. - Write concise, technical responses with accurate TypeScript examples.
@@ -21,13 +18,11 @@ Key Principles
- Prefer iteration and modularization over duplication. - Prefer iteration and modularization over duplication.
- Use descriptive variable names with auxiliary verbs (e.g., isLoading). - Use descriptive variable names with auxiliary verbs (e.g., isLoading).
- Use lowercase with dashes for directories (e.g., components/auth-wizard). - 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. - Use the Receive an Object, Return an Object (RORO) pattern.
## Code Style and Structure ## Code Style and Structure
- Write concise, idiomatic C# code with accurate examples. - Write concise, idiomatic C# code with accurate examples.
- Follow .NET and ASP.NET Core conventions and best practices. - 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. - Prefer LINQ and lambda expressions for collection operations.
- Use descriptive variable and method names (e.g., 'IsUserSignedIn', 'CalculateTotal'). - Use descriptive variable and method names (e.g., 'IsUserSignedIn', 'CalculateTotal').
- Structure files according to .NET conventions (Controllers, Models, Services, etc.). - Structure files according to .NET conventions (Controllers, Models, Services, etc.).
@@ -41,7 +36,7 @@ Key Principles
## C# and .NET Usage ## C# and .NET Usage
- Use C# 10+ features when appropriate (e.g., record types, pattern matching, null-coalescing assignment). - Use C# 10+ features when appropriate (e.g., record types, pattern matching, null-coalescing assignment).
- Leverage built-in ASP.NET Core features and middleware. - 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 ## Syntax and Formatting
- Follow the C# Coding Conventions (https://docs.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions) - 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 ## API Design
- Follow RESTful API design principles. - Follow RESTful API design principles.
- Use attribute routing in controllers.
- Implement versioning for your API.
- Use action filters for cross-cutting concerns. - Use action filters for cross-cutting concerns.
## Performance Optimization ## Performance Optimization
@@ -67,11 +60,6 @@ Key Principles
- Use efficient LINQ queries and avoid N+1 query problems. - Use efficient LINQ queries and avoid N+1 query problems.
- Implement pagination for large data sets. - 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 ## Security
- Give me advice when you see that some data should be carefully handled - Give me advice when you see that some data should be carefully handled
@@ -81,7 +69,6 @@ Key Principles
React/Tailwind/DaisyUI React/Tailwind/DaisyUI
- Use functional components and TypeScript interfaces. - Use functional components and TypeScript interfaces.
- Use declarative JSX.
- Use function, not const, for components. - Use function, not const, for components.
- Use DaisyUI Tailwind Aria for components and styling. - Use DaisyUI Tailwind Aria for components and styling.
- Implement responsive design with Tailwind CSS. - 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 - Do not reference new react library if a component already exist in mollecules or atoms
- After finishing the editing, build the project - After finishing the editing, build the project
- you have to pass from controller -> application -> repository, do not inject repository inside controllers - 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.

204
CLAUDE.md Normal file
View File

@@ -0,0 +1,204 @@
# Managing Apps - Claude Code Guidelines
## Project Overview
This is a quantitative finance application with .NET backend and React TypeScript frontend, focusing on algorithmic trading, market indicators, and financial mathematics.
## Core Architecture Principles
### 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
# Backend
dotnet build
dotnet run --project src/Managing.Api
# Frontend
npm run build
npm run dev
# Regenerate API client (after backend changes)
cd src/Managing.Nswag && dotnet build
```
### 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
### Testing
- Write unit tests using xUnit for backend
- Use Mock or NSubstitute for mocking dependencies
- Implement integration tests for API endpoints
## 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
## Database Guidelines
- Use PostgreSQL for relational data
- Use InfluxDB for time-series data (candles, metrics)
- Use MongoDB for document storage
- Implement proper migrations
## 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
## 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
}
}
```
### Frontend Component Pattern
```typescript
interface ComponentProps {
isLoading: boolean;
data: SomeType[];
}
function Component({ isLoading, data }: ComponentProps): JSX.Element {
if (isLoading) return <Loader />;
return (
<div className="container mx-auto">
{/* Component content */}
</div>
);
}
export default Component;
```
## 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
```
## 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)

18
package-lock.json generated
View File

@@ -1,18 +0,0 @@
{
"name": "managing-apps",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"genetic-js": "^0.1.14"
}
},
"node_modules/genetic-js": {
"version": "0.1.14",
"resolved": "https://registry.npmjs.org/genetic-js/-/genetic-js-0.1.14.tgz",
"integrity": "sha512-HHm21naCEF1EVKTWPFzKX4ENB7Nn/my4kTy2POi4u/2gB0XPUOh8oDlhhESVCZVBge3b7nuLrZNZNAt4ObH19Q==",
"license": "BSD"
}
}
}

View File

@@ -1,5 +0,0 @@
{
"dependencies": {
"genetic-js": "^0.1.14"
}
}

View File

@@ -1 +0,0 @@

View File

@@ -1,4 +1,7 @@
{ {
"PostgreSql": {
"ConnectionString": "Host=managing-postgre.apps.managing.live;Port=5432;Database=managing;Username=postgres;Password=29032b13a5bc4d37"
},
"InfluxDb": { "InfluxDb": {
"Url": "http://srv-captain--influx-db:8086/", "Url": "http://srv-captain--influx-db:8086/",
"Organization": "managing-org", "Organization": "managing-org",

View File

@@ -29,7 +29,6 @@ public class BacktestController : BaseController
{ {
private readonly IHubContext<BacktestHub> _hubContext; private readonly IHubContext<BacktestHub> _hubContext;
private readonly IBacktester _backtester; private readonly IBacktester _backtester;
private readonly IScenarioService _scenarioService;
private readonly IAccountService _accountService; private readonly IAccountService _accountService;
private readonly IMoneyManagementService _moneyManagementService; private readonly IMoneyManagementService _moneyManagementService;
private readonly IGeneticService _geneticService; private readonly IGeneticService _geneticService;
@@ -47,7 +46,6 @@ public class BacktestController : BaseController
public BacktestController( public BacktestController(
IHubContext<BacktestHub> hubContext, IHubContext<BacktestHub> hubContext,
IBacktester backtester, IBacktester backtester,
IScenarioService scenarioService,
IAccountService accountService, IAccountService accountService,
IMoneyManagementService moneyManagementService, IMoneyManagementService moneyManagementService,
IGeneticService geneticService, IGeneticService geneticService,
@@ -55,7 +53,6 @@ public class BacktestController : BaseController
{ {
_hubContext = hubContext; _hubContext = hubContext;
_backtester = backtester; _backtester = backtester;
_scenarioService = scenarioService;
_accountService = accountService; _accountService = accountService;
_moneyManagementService = moneyManagementService; _moneyManagementService = moneyManagementService;
_geneticService = geneticService; _geneticService = geneticService;
@@ -245,7 +242,8 @@ public class BacktestController : BaseController
return BadRequest("Sort order must be 'asc' or 'desc'"); 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 totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
var response = new PaginatedBacktestsResponse var response = new PaginatedBacktestsResponse
@@ -279,14 +277,14 @@ public class BacktestController : BaseController
/// <summary> /// <summary>
/// Runs a backtest with the specified configuration. /// Runs a backtest with the specified configuration.
/// The returned backtest includes a complete TradingBotConfig that preserves all /// Returns a lightweight backtest result for efficient processing.
/// settings including nullable MaxPositionTimeHours for easy bot deployment. /// Use the returned ID to retrieve the full backtest data from the database.
/// </summary> /// </summary>
/// <param name="request">The backtest request containing configuration and parameters.</param> /// <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] [HttpPost]
[Route("Run")] [Route("Run")]
public async Task<ActionResult<Backtest>> Run([FromBody] RunBacktestRequest request) public async Task<ActionResult<LightBacktest>> Run([FromBody] RunBacktestRequest request)
{ {
if (request?.Config == null) if (request?.Config == null)
{ {
@@ -310,7 +308,7 @@ public class BacktestController : BaseController
try try
{ {
Backtest backtestResult = null; LightBacktest backtestResult = null;
var account = await _accountService.GetAccount(request.Config.AccountName, true, false); var account = await _accountService.GetAccount(request.Config.AccountName, true, false);
var user = await GetUser(); var user = await GetUser();
@@ -367,7 +365,9 @@ public class BacktestController : BaseController
MoneyManagement = moneyManagement, MoneyManagement = moneyManagement,
Ticker = request.Config.Ticker, Ticker = request.Config.Ticker,
ScenarioName = request.Config.ScenarioName, 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, Timeframe = request.Config.Timeframe,
IsForWatchingOnly = request.Config.IsForWatchingOnly, IsForWatchingOnly = request.Config.IsForWatchingOnly,
BotTradingBalance = request.Config.BotTradingBalance, BotTradingBalance = request.Config.BotTradingBalance,
@@ -395,7 +395,8 @@ public class BacktestController : BaseController
request.WithCandles, request.WithCandles,
null); // No requestId for regular backtests 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); 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) public MoneyManagement Map(MoneyManagementRequest moneyManagementRequest)
{ {

View File

@@ -220,7 +220,7 @@ public class BotController : BaseController
AccountName = request.Config.AccountName, AccountName = request.Config.AccountName,
MoneyManagement = moneyManagement, MoneyManagement = moneyManagement,
Ticker = request.Config.Ticker, 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 ScenarioName = request.Config.ScenarioName, // Fallback to scenario name if scenario object not provided
Timeframe = request.Config.Timeframe, Timeframe = request.Config.Timeframe,
IsForWatchingOnly = request.Config.IsForWatchingOnly, IsForWatchingOnly = request.Config.IsForWatchingOnly,
@@ -782,7 +782,7 @@ public class BotController : BaseController
AccountName = request.Config.AccountName, AccountName = request.Config.AccountName,
MoneyManagement = moneyManagement, MoneyManagement = moneyManagement,
Ticker = request.Config.Ticker, 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 ScenarioName = request.Config.ScenarioName, // Fallback to scenario name if scenario object not provided
Timeframe = request.Config.Timeframe, Timeframe = request.Config.Timeframe,
IsForWatchingOnly = request.Config.IsForWatchingOnly, IsForWatchingOnly = request.Config.IsForWatchingOnly,

View File

@@ -493,10 +493,10 @@ public class DataController : ControllerBase
} }
/// <summary> /// <summary>
/// Retrieves a summary of platform activity across all agents /// Retrieves a summary of platform activity across all agents (platform-level data only)
/// </summary> /// </summary>
/// <param name="timeFilter">Time filter to apply (24H, 3D, 1W, 1M, 1Y, Total)</param> /// <param name="timeFilter">Time filter to apply (24H, 3D, 1W, 1M, 1Y, Total)</param>
/// <returns>A summary of platform activity including per-agent statistics</returns> /// <returns>A summary of platform activity without individual agent details</returns>
[HttpGet("GetPlatformSummary")] [HttpGet("GetPlatformSummary")]
public async Task<ActionResult<PlatformSummaryViewModel>> GetPlatformSummary(string timeFilter = "Total") public async Task<ActionResult<PlatformSummaryViewModel>> GetPlatformSummary(string timeFilter = "Total")
{ {
@@ -533,6 +533,75 @@ public class DataController : ControllerBase
decimal totalPlatformVolume = 0; decimal totalPlatformVolume = 0;
decimal totalPlatformVolumeLast24h = 0; decimal totalPlatformVolumeLast24h = 0;
// Calculate totals from all agents
foreach (var agent in agentsWithStrategies)
{
var strategies = agent.Value;
if (strategies.Count == 0)
{
continue; // Skip agents with no strategies
}
// Combine all positions from all strategies
var allPositions = strategies.SelectMany<ITradingBot, Position>(s => s.Positions).ToList();
// Calculate agent metrics for platform totals
decimal totalPnL = TradingBox.GetPnLInTimeRange(allPositions, timeFilter);
decimal totalVolume = TradingBox.GetTotalVolumeTraded(allPositions);
decimal volumeLast24h = TradingBox.GetLast24HVolumeTraded(allPositions);
// Add to platform totals
totalPlatformPnL += totalPnL;
totalPlatformVolume += totalVolume;
totalPlatformVolumeLast24h += volumeLast24h;
}
// Set the platform totals
summary.TotalPlatformPnL = totalPlatformPnL;
summary.TotalPlatformVolume = totalPlatformVolume;
summary.TotalPlatformVolumeLast24h = totalPlatformVolumeLast24h;
// Cache the results for 5 minutes
_cacheService.SaveValue(cacheKey, summary, TimeSpan.FromMinutes(5));
return Ok(summary);
}
/// <summary>
/// Retrieves a list of agent summaries for the agent index page
/// </summary>
/// <param name="timeFilter">Time filter to apply (24H, 3D, 1W, 1M, 1Y, Total)</param>
/// <returns>A list of agent summaries sorted by performance</returns>
[HttpGet("GetAgentIndex")]
public async Task<ActionResult<AgentIndexViewModel>> GetAgentIndex(string timeFilter = "Total")
{
// Validate time filter
var validTimeFilters = new[] { "24H", "3D", "1W", "1M", "1Y", "Total" };
if (!validTimeFilters.Contains(timeFilter))
{
timeFilter = "Total"; // Default to Total if invalid
}
string cacheKey = $"AgentIndex_{timeFilter}";
// Check if the agent index is already cached
var cachedIndex = _cacheService.GetValue<AgentIndexViewModel>(cacheKey);
if (cachedIndex != null)
{
return Ok(cachedIndex);
}
// Get all agents and their strategies
var agentsWithStrategies = await _mediator.Send(new GetAllAgentsCommand(timeFilter));
// Create the agent index response
var agentIndex = new AgentIndexViewModel
{
TimeFilter = timeFilter
};
// Create summaries for each agent // Create summaries for each agent
foreach (var agent in agentsWithStrategies) foreach (var agent in agentsWithStrategies)
{ {
@@ -583,26 +652,16 @@ public class DataController : ControllerBase
VolumeLast24h = volumeLast24h VolumeLast24h = volumeLast24h
}; };
summary.AgentSummaries.Add(agentSummary); agentIndex.AgentSummaries.Add(agentSummary);
// Add to platform totals
totalPlatformPnL += totalPnL;
totalPlatformVolume += totalVolume;
totalPlatformVolumeLast24h += volumeLast24h;
} }
// Set the platform totals
summary.TotalPlatformPnL = totalPlatformPnL;
summary.TotalPlatformVolume = totalPlatformVolume;
summary.TotalPlatformVolumeLast24h = totalPlatformVolumeLast24h;
// Sort agent summaries by total PnL (highest first) // Sort agent summaries by total PnL (highest first)
summary.AgentSummaries = summary.AgentSummaries.OrderByDescending(a => a.TotalPnL).ToList(); agentIndex.AgentSummaries = agentIndex.AgentSummaries.OrderByDescending(a => a.TotalPnL).ToList();
// Cache the results for 5 minutes // Cache the results for 5 minutes
_cacheService.SaveValue(cacheKey, summary, TimeSpan.FromMinutes(5)); _cacheService.SaveValue(cacheKey, agentIndex, TimeSpan.FromMinutes(5));
return Ok(summary); return Ok(agentIndex);
} }
/// <summary> /// <summary>

View File

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

View File

@@ -62,7 +62,7 @@ namespace Managing.Api.Models.Responses
} }
/// <summary> /// <summary>
/// Platform-wide statistics including per-agent summaries /// Platform-wide statistics without individual agent details
/// </summary> /// </summary>
public class PlatformSummaryViewModel public class PlatformSummaryViewModel
{ {
@@ -92,7 +92,18 @@ namespace Managing.Api.Models.Responses
public decimal TotalPlatformVolumeLast24h { get; set; } public decimal TotalPlatformVolumeLast24h { get; set; }
/// <summary> /// <summary>
/// Summaries for each agent /// Time filter applied to the data
/// </summary>
public string TimeFilter { get; set; } = "Total";
}
/// <summary>
/// Response model containing a list of agent summaries for the agent index
/// </summary>
public class AgentIndexViewModel
{
/// <summary>
/// List of agent summaries sorted by performance
/// </summary> /// </summary>
public List<AgentSummaryViewModel> AgentSummaries { get; set; } = new List<AgentSummaryViewModel>(); public List<AgentSummaryViewModel> AgentSummaries { get; set; } = new List<AgentSummaryViewModel>();

View File

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

View File

@@ -39,5 +39,7 @@
"AllowedHosts": "*", "AllowedHosts": "*",
"WorkerBotManager": true, "WorkerBotManager": true,
"WorkerBalancesTracking": false, "WorkerBalancesTracking": false,
"WorkerNotifyBundleBacktest": true "WorkerNotifyBundleBacktest": false,
"KAIGEN_SECRET_KEY": "KaigenXCowchain",
"KAIGEN_CREDITS_ENABLED": false
} }

View File

@@ -12,7 +12,7 @@
"AppSecret": "63Chz2z5M8TgR5qc8dznSLRAGTHTyPU4cjdQobrBF1Cx5tszZpTuFgyrRd7hZ2k6HpwDz3GEwQZzsCqHb8Z311bF" "AppSecret": "63Chz2z5M8TgR5qc8dznSLRAGTHTyPU4cjdQobrBF1Cx5tszZpTuFgyrRd7hZ2k6HpwDz3GEwQZzsCqHb8Z311bF"
}, },
"Kaigen": { "Kaigen": {
"BaseUrl": "https://kaigen-back-development.up.railway.app/", "BaseUrl": "https://kaigen-back-development.up.railway.app",
"DebitEndpoint": "/api/credits/debit", "DebitEndpoint": "/api/credits/debit",
"RefundEndpoint": "/api/credits/refund" "RefundEndpoint": "/api/credits/refund"
}, },

View File

@@ -24,7 +24,7 @@
"BaseUrl": "http://localhost:4111" "BaseUrl": "http://localhost:4111"
}, },
"Kaigen": { "Kaigen": {
"BaseUrl": "https://api.kaigen.managing.live", "BaseUrl": "https://kaigen-back-development.up.railway.app",
"DebitEndpoint": "/api/credits/debit", "DebitEndpoint": "/api/credits/debit",
"RefundEndpoint": "/api/credits/refund" "RefundEndpoint": "/api/credits/refund"
}, },

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" /> <ProjectReference Include="..\Managing.Domain\Managing.Domain.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Orleans.Core.Abstractions" Version="9.2.1" />
</ItemGroup>
</Project> </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> /// <summary>
/// Runs a trading bot backtest with the specified configuration and date range. /// Runs a trading bot backtest with the specified configuration and date range.
/// Automatically handles different bot types based on config.BotType. /// Automatically handles different bot types based on config.BotType.
/// Returns a LightBacktest for efficient Orleans serialization.
/// </summary> /// </summary>
/// <param name="config">The trading bot configuration (must include Scenario object or ScenarioName)</param> /// <param name="config">The trading bot configuration (must include Scenario object or ScenarioName)</param>
/// <param name="startDate">The start date for the backtest</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="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="requestId">The request ID to associate with this backtest (optional)</param>
/// <param name="metadata">Additional metadata to associate with this backtest (optional)</param> /// <param name="metadata">Additional metadata to associate with this backtest (optional)</param>
/// <returns>The backtest results</returns> /// <returns>The lightweight backtest results</returns>
Task<Backtest> RunTradingBotBacktest( Task<LightBacktest> RunTradingBotBacktest(
TradingBotConfig config, TradingBotConfig config,
DateTime startDate, DateTime startDate,
DateTime endDate, DateTime endDate,
@@ -33,6 +34,7 @@ namespace Managing.Application.Abstractions.Services
/// <summary> /// <summary>
/// Runs a trading bot backtest with pre-loaded candles. /// Runs a trading bot backtest with pre-loaded candles.
/// Automatically handles different bot types based on config.BotType. /// Automatically handles different bot types based on config.BotType.
/// Returns a LightBacktest for efficient Orleans serialization.
/// </summary> /// </summary>
/// <param name="config">The trading bot configuration (must include Scenario object or ScenarioName)</param> /// <param name="config">The trading bot configuration (must include Scenario object or ScenarioName)</param>
/// <param name="candles">The candles to use for backtesting</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="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="requestId">The request ID to associate with this backtest (optional)</param>
/// <param name="metadata">Additional metadata to associate with this backtest (optional)</param> /// <param name="metadata">Additional metadata to associate with this backtest (optional)</param>
/// <returns>The backtest results</returns> /// <returns>The lightweight backtest results</returns>
Task<Backtest> RunTradingBotBacktest( Task<LightBacktest> RunTradingBotBacktest(
TradingBotConfig config, TradingBotConfig config,
List<Candle> candles, List<Candle> candles,
User user = null, User user = null,

View File

@@ -8,7 +8,6 @@ using Managing.Application.Bots.Base;
using Managing.Application.Hubs; using Managing.Application.Hubs;
using Managing.Application.ManageBot; using Managing.Application.ManageBot;
using Managing.Core; using Managing.Core;
using Managing.Domain.Backtests;
using Managing.Domain.Bots; using Managing.Domain.Bots;
using Managing.Domain.Candles; using Managing.Domain.Candles;
using Managing.Domain.MoneyManagements; using Managing.Domain.MoneyManagements;
@@ -51,7 +50,7 @@ namespace Managing.Application.Tests
_tradingService.Object, _tradingService.Object,
botService, backupBotService); botService, backupBotService);
_backtester = new Backtester(_exchangeService, _botFactory, backtestRepository, backtestLogger, _backtester = new Backtester(_exchangeService, _botFactory, backtestRepository, backtestLogger,
scenarioService, _accountService.Object, messengerService, kaigenService, hubContext); scenarioService, _accountService.Object, messengerService, kaigenService, hubContext, null);
_elapsedTimes = new List<double>(); _elapsedTimes = new List<double>();
// Initialize cross-platform file paths // Initialize cross-platform file paths
@@ -78,7 +77,7 @@ namespace Managing.Application.Tests
AccountName = _account.Name, AccountName = _account.Name,
MoneyManagement = MoneyManagement, MoneyManagement = MoneyManagement,
Ticker = ticker, Ticker = ticker,
Scenario = scenario, Scenario = LightScenario.FromScenario(scenario),
Timeframe = timeframe, Timeframe = timeframe,
IsForWatchingOnly = false, IsForWatchingOnly = false,
BotTradingBalance = 1000, BotTradingBalance = 1000,
@@ -128,7 +127,7 @@ namespace Managing.Application.Tests
AccountName = _account.Name, AccountName = _account.Name,
MoneyManagement = MoneyManagement, MoneyManagement = MoneyManagement,
Ticker = ticker, Ticker = ticker,
Scenario = scenario, Scenario = LightScenario.FromScenario(scenario),
Timeframe = timeframe, Timeframe = timeframe,
IsForWatchingOnly = false, IsForWatchingOnly = false,
BotTradingBalance = 1000, BotTradingBalance = 1000,
@@ -177,7 +176,7 @@ namespace Managing.Application.Tests
AccountName = _account.Name, AccountName = _account.Name,
MoneyManagement = moneyManagement, MoneyManagement = moneyManagement,
Ticker = ticker, Ticker = ticker,
Scenario = scenario, Scenario = LightScenario.FromScenario(scenario),
Timeframe = timeframe, Timeframe = timeframe,
IsForWatchingOnly = false, IsForWatchingOnly = false,
BotTradingBalance = 1000, BotTradingBalance = 1000,
@@ -194,7 +193,7 @@ namespace Managing.Application.Tests
// Act // Act
var backtestResult = await _backtester.RunTradingBotBacktest(config, DateTime.UtcNow.AddDays(-6), var backtestResult = await _backtester.RunTradingBotBacktest(config, DateTime.UtcNow.AddDays(-6),
DateTime.UtcNow, null, false, false); DateTime.UtcNow, null, false, false);
WriteCsvReport(backtestResult.GetStringReport()); // WriteCsvReport(backtestResult.GetStringReport());
// Assert // Assert
Assert.True(backtestResult.FinalPnl > 0); Assert.True(backtestResult.FinalPnl > 0);
@@ -234,10 +233,10 @@ namespace Managing.Application.Tests
if (candles == null || candles.Count == 0) if (candles == null || candles.Count == 0)
return; return;
Parallel.For(periodRange[0], periodRange[1], options, i => Parallel.For((long)periodRange[0], periodRange[1], options, i =>
{ {
var scenario = new Scenario("ScalpingScenario"); var scenario = new Scenario("ScalpingScenario");
var strategy = ScenarioHelpers.BuildIndicator(indicatorType, "RsiDiv", period: i); var strategy = ScenarioHelpers.BuildIndicator(indicatorType, "RsiDiv", period: (int)i);
scenario.AddIndicator(strategy); scenario.AddIndicator(strategy);
// -0.5 to -5 // -0.5 to -5
@@ -267,7 +266,7 @@ namespace Managing.Application.Tests
AccountName = _account.Name, AccountName = _account.Name,
MoneyManagement = moneyManagement, MoneyManagement = moneyManagement,
Ticker = ticker, Ticker = ticker,
Scenario = scenario, Scenario = LightScenario.FromScenario(scenario),
Timeframe = timeframe, Timeframe = timeframe,
IsForWatchingOnly = false, IsForWatchingOnly = false,
BotTradingBalance = 1000, BotTradingBalance = 1000,
@@ -285,7 +284,7 @@ namespace Managing.Application.Tests
AccountName = _account.Name, AccountName = _account.Name,
MoneyManagement = moneyManagement, MoneyManagement = moneyManagement,
Ticker = ticker, Ticker = ticker,
Scenario = scenario, Scenario = LightScenario.FromScenario(scenario),
Timeframe = timeframe, Timeframe = timeframe,
IsForWatchingOnly = false, IsForWatchingOnly = false,
BotTradingBalance = 1000, BotTradingBalance = 1000,
@@ -304,10 +303,10 @@ namespace Managing.Application.Tests
if (backtestResult.FinalPnl > 0 if (backtestResult.FinalPnl > 0
&& (backtestResult.GrowthPercentage - backtestResult.HodlPercentage) > 30 && (backtestResult.GrowthPercentage - backtestResult.HodlPercentage) > 30
&& backtestResult.Statistics.MaxDrawdown < 3) && backtestResult.Score < 3)
{ {
var currentResult = new Tuple<string, int, decimal, decimal, decimal, decimal>( var currentResult = new Tuple<string, int, decimal, decimal, decimal, decimal>(
ticker.ToString(), i, ticker.ToString(), (int)i,
backtestResult.FinalPnl, s, t, backtestResult.FinalPnl, s, t,
backtestResult.GrowthPercentage - backtestResult.HodlPercentage); backtestResult.GrowthPercentage - backtestResult.HodlPercentage);
result.Add(currentResult); result.Add(currentResult);
@@ -407,7 +406,7 @@ namespace Managing.Application.Tests
AccountName = _account.Name, AccountName = _account.Name,
MoneyManagement = moneyManagement, MoneyManagement = moneyManagement,
Ticker = ticker, Ticker = ticker,
Scenario = scenario, Scenario = LightScenario.FromScenario(scenario),
Timeframe = timeframe, Timeframe = timeframe,
IsForWatchingOnly = false, IsForWatchingOnly = false,
BotTradingBalance = 1000, BotTradingBalance = 1000,
@@ -425,7 +424,7 @@ namespace Managing.Application.Tests
AccountName = _account.Name, AccountName = _account.Name,
MoneyManagement = moneyManagement, MoneyManagement = moneyManagement,
Ticker = ticker, Ticker = ticker,
Scenario = scenario, Scenario = LightScenario.FromScenario(scenario),
Timeframe = timeframe, Timeframe = timeframe,
IsForWatchingOnly = false, IsForWatchingOnly = false,
BotTradingBalance = 1000, BotTradingBalance = 1000,
@@ -442,8 +441,7 @@ namespace Managing.Application.Tests
}; };
if (backtestResult.FinalPnl > 0 if (backtestResult.FinalPnl > 0
&& (backtestResult.GrowthPercentage - backtestResult.HodlPercentage) > 30 && (backtestResult.GrowthPercentage - backtestResult.HodlPercentage) > 30)
&& backtestResult.Statistics.MaxDrawdown < 3)
{ {
var currentResult = new Tuple<string, decimal, decimal, decimal, decimal>( var currentResult = new Tuple<string, decimal, decimal, decimal, decimal>(
ticker.ToString(), ticker.ToString(),
@@ -661,7 +659,7 @@ namespace Managing.Application.Tests
AccountName = _account.Name, AccountName = _account.Name,
MoneyManagement = standardMoneyManagement, MoneyManagement = standardMoneyManagement,
Ticker = ticker, Ticker = ticker,
Scenario = scenario, Scenario = LightScenario.FromScenario(scenario),
Timeframe = timeframe, Timeframe = timeframe,
IsForWatchingOnly = false, IsForWatchingOnly = false,
BotTradingBalance = 1000, BotTradingBalance = 1000,
@@ -679,24 +677,6 @@ namespace Managing.Application.Tests
timer.Stop(); timer.Stop();
var scoringParams = new BacktestScoringParams(
sharpeRatio: (double)(backtestResult.Statistics?.SharpeRatio ?? 0),
growthPercentage: (double)backtestResult.GrowthPercentage,
hodlPercentage: (double)backtestResult.HodlPercentage,
winRate: backtestResult.WinRate / 100.0, // Convert percentage to decimal
totalPnL: (double)backtestResult.FinalPnl,
fees: (double)backtestResult.Fees,
tradeCount: backtestResult.Positions?.Count ?? 0,
maxDrawdownRecoveryTime: backtestResult.Statistics?.MaxDrawdownRecoveryTime ?? TimeSpan.Zero,
maxDrawdown: backtestResult.Statistics?.MaxDrawdown ?? 0,
initialBalance: config.BotTradingBalance,
tradingBalance: config.BotTradingBalance,
startDate: backtestResult.StartDate,
endDate: backtestResult.EndDate,
timeframe: config.Timeframe,
moneyManagement: config.MoneyManagement
);
var scenarioResult = new ScenarioBacktestResult var scenarioResult = new ScenarioBacktestResult
{ {
ScenarioName = scenario.Name, ScenarioName = scenario.Name,
@@ -708,14 +688,13 @@ namespace Managing.Application.Tests
GrowthPercentage = backtestResult.GrowthPercentage, GrowthPercentage = backtestResult.GrowthPercentage,
HodlPercentage = backtestResult.HodlPercentage, HodlPercentage = backtestResult.HodlPercentage,
OutperformanceVsHodl = backtestResult.GrowthPercentage - backtestResult.HodlPercentage, OutperformanceVsHodl = backtestResult.GrowthPercentage - backtestResult.HodlPercentage,
MaxDrawdown = (double)(backtestResult.Statistics?.MaxDrawdown ?? 0), MaxDrawdown = (double)(backtestResult.MaxDrawdown ?? 0),
TotalTrades = backtestResult.Positions?.Count ?? 0, SharpeRatio = (double)(backtestResult.SharpeRatio ?? 0),
SharpeRatio = (double)(backtestResult.Statistics?.SharpeRatio ?? 0),
ExecutionTime = timer.Elapsed.TotalSeconds, ExecutionTime = timer.Elapsed.TotalSeconds,
StopLoss = standardMoneyManagement.StopLoss, StopLoss = standardMoneyManagement.StopLoss,
TakeProfit = standardMoneyManagement.TakeProfit, TakeProfit = standardMoneyManagement.TakeProfit,
Leverage = standardMoneyManagement.Leverage, Leverage = standardMoneyManagement.Leverage,
Score = BacktestScorer.CalculateTotalScore(scoringParams) Score = backtestResult.Score,
}; };
results.Add(scenarioResult); results.Add(scenarioResult);

View File

@@ -10,7 +10,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="MathNet.Numerics" Version="5.0.0" /> <PackageReference Include="MathNet.Numerics" Version="5.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.5" /> <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.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="Microsoft.TestPlatform.AdapterUtilities" Version="17.9.0" /> <PackageReference Include="Microsoft.TestPlatform.AdapterUtilities" Version="17.9.0" />
<PackageReference Include="Moq" Version="4.20.70" /> <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.Abstractions.Services;
using Managing.Application.Backtesting; using Managing.Application.Backtesting;
using Managing.Application.Bots; using Managing.Application.Bots;
@@ -8,16 +7,15 @@ using Managing.Infrastructure.Databases.InfluxDb;
using Managing.Infrastructure.Databases.InfluxDb.Models; using Managing.Infrastructure.Databases.InfluxDb.Models;
using Managing.Infrastructure.Evm; using Managing.Infrastructure.Evm;
using Managing.Infrastructure.Evm.Abstractions; using Managing.Infrastructure.Evm.Abstractions;
using Managing.Infrastructure.Evm.Models.Privy;
using Managing.Infrastructure.Evm.Services; using Managing.Infrastructure.Evm.Services;
using Managing.Infrastructure.Evm.Subgraphs; using Managing.Infrastructure.Evm.Subgraphs;
using Managing.Infrastructure.Exchanges; using Managing.Infrastructure.Exchanges;
using Managing.Infrastructure.Exchanges.Abstractions; using Managing.Infrastructure.Exchanges.Abstractions;
using Managing.Infrastructure.Exchanges.Exchanges; using Managing.Infrastructure.Exchanges.Exchanges;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Moq; using Moq;
using Nethereum.Web3;
using static Managing.Common.Enums; using static Managing.Common.Enums;
namespace Managing.Application.Tests namespace Managing.Application.Tests
@@ -26,7 +24,7 @@ namespace Managing.Application.Tests
{ {
public static IExchangeService GetExchangeService() 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 ChainlinkGmx = new ChainlinkGmx(SubgraphService.GetSubgraphClient(SubgraphProvider.ChainlinkGmx));
var Chainlink = new Chainlink(SubgraphService.GetSubgraphClient(SubgraphProvider.ChainlinkPrice)); var Chainlink = new Chainlink(SubgraphService.GetSubgraphClient(SubgraphProvider.ChainlinkPrice));
@@ -53,23 +51,23 @@ namespace Managing.Application.Tests
exchangeProcessors); 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() public static ILogger<Backtester> CreateBacktesterLogger()
{ {
ILoggerFactory loggerFactory = new Microsoft.Extensions.Logging.Abstractions.NullLoggerFactory(); ILoggerFactory loggerFactory = new NullLoggerFactory();
return loggerFactory.CreateLogger<Backtester>(); return loggerFactory.CreateLogger<Backtester>();
} }
public static ILogger<CandleRepository> CreateCandleRepositoryLogger() public static ILogger<CandleRepository> CreateCandleRepositoryLogger()
{ {
ILoggerFactory loggerFactory = new Microsoft.Extensions.Logging.Abstractions.NullLoggerFactory(); ILoggerFactory loggerFactory = new NullLoggerFactory();
return loggerFactory.CreateLogger<CandleRepository>(); return loggerFactory.CreateLogger<CandleRepository>();
} }

View File

@@ -222,17 +222,14 @@ public class BundleBacktestWorker : BaseWorker<BundleBacktestWorker>
} }
// Map Scenario // Map Scenario
Scenario scenario = null; LightScenario scenario = null;
if (runBacktestRequest.Config.Scenario != null) if (runBacktestRequest.Config.Scenario != null)
{ {
var sReq = runBacktestRequest.Config.Scenario; var sReq = runBacktestRequest.Config.Scenario;
scenario = new Scenario(sReq.Name, sReq.LoopbackPeriod) scenario = new LightScenario(sReq.Name, sReq.LoopbackPeriod);
{
User = null // No user context in worker
};
foreach (var indicatorRequest in sReq.Indicators) 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, SignalType = indicatorRequest.SignalType,
MinimumHistory = indicatorRequest.MinimumHistory, MinimumHistory = indicatorRequest.MinimumHistory,
@@ -244,7 +241,6 @@ public class BundleBacktestWorker : BaseWorker<BundleBacktestWorker>
SmoothPeriods = indicatorRequest.SmoothPeriods, SmoothPeriods = indicatorRequest.SmoothPeriods,
StochPeriods = indicatorRequest.StochPeriods, StochPeriods = indicatorRequest.StochPeriods,
CyclePeriods = indicatorRequest.CyclePeriods, CyclePeriods = indicatorRequest.CyclePeriods,
User = null // No user context in worker
}; };
scenario.AddIndicator(indicator); scenario.AddIndicator(indicator);
} }

View File

@@ -7,7 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <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="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
</ItemGroup> </ItemGroup>

View File

@@ -266,7 +266,8 @@ public class StatisticService : IStatisticService
await _statisticRepository.UpdateSpotlightOverviewAsync(overview); await _statisticRepository.UpdateSpotlightOverviewAsync(overview);
} }
private async Task<List<LightSignal>> GetSignals(Account account, Scenario scenario, Ticker ticker, Timeframe timeframe) private async Task<List<LightSignal>> GetSignals(Account account, Scenario scenario, Ticker ticker,
Timeframe timeframe)
{ {
try try
{ {
@@ -305,7 +306,11 @@ public class StatisticService : IStatisticService
false, false,
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) catch (Exception ex)
{ {

View File

@@ -1,12 +1,9 @@
using Managing.Domain.Bots; using Managing.Domain.Bots;
using Managing.Domain.Workflows;
namespace Managing.Application.Abstractions namespace Managing.Application.Abstractions
{ {
public interface IBotFactory public interface IBotFactory
{ {
IBot CreateSimpleBot(string botName, Workflow workflow);
/// <summary> /// <summary>
/// Creates a trading bot using the unified TradingBot class /// Creates a trading bot using the unified TradingBot class
/// </summary> /// </summary>

View File

@@ -29,12 +29,6 @@ public interface IBotService
/// <returns>ITradingBot instance configured for backtesting</returns> /// <returns>ITradingBot instance configured for backtesting</returns>
Task<ITradingBot> CreateBacktestTradingBot(TradingBotConfig config); 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); IBot CreateSimpleBot(string botName, Workflow workflow);
Task<string> StopBot(string botName); Task<string> StopBot(string botName);
Task<bool> DeleteBot(string botName); Task<bool> DeleteBot(string botName);

View File

@@ -23,7 +23,8 @@ namespace Managing.Application.Abstractions
Task<bool> UpdateScenario(string name, List<string> strategies, int? loopbackPeriod); Task<bool> UpdateScenario(string name, List<string> strategies, int? loopbackPeriod);
Task<bool> UpdateStrategy(IndicatorType indicatorType, string name, int? period, int? fastPeriods, int? slowPeriods, Task<bool> UpdateStrategy(IndicatorType indicatorType, string name, int? period, int? fastPeriods,
int? slowPeriods,
int? signalPeriods, double? multiplier, int? stochPeriods, int? smoothPeriods, int? cyclePeriods); int? signalPeriods, double? multiplier, int? stochPeriods, int? smoothPeriods, int? cyclePeriods);
Task<IEnumerable<Scenario>> GetScenariosByUserAsync(User user); Task<IEnumerable<Scenario>> GetScenariosByUserAsync(User user);
@@ -49,8 +50,11 @@ namespace Managing.Application.Abstractions
Task<bool> DeleteScenariosByUser(User user); Task<bool> DeleteScenariosByUser(User user);
Task<bool> UpdateScenarioByUser(User user, string name, List<string> strategies, int? loopbackPeriod); Task<bool> UpdateScenarioByUser(User user, string name, List<string> strategies, int? loopbackPeriod);
Task<bool> UpdateIndicatorByUser(User user, IndicatorType indicatorType, string name, int? period, int? fastPeriods, Task<bool> UpdateIndicatorByUser(User user, IndicatorType indicatorType, string name, int? period,
int? fastPeriods,
int? slowPeriods, int? signalPeriods, double? multiplier, int? stochPeriods, int? smoothPeriods, int? slowPeriods, int? signalPeriods, double? multiplier, int? stochPeriods, int? smoothPeriods,
int? cyclePeriods); int? cyclePeriods);
Task<Scenario> GetScenarioByNameAndUserAsync(string scenarioName, User user);
} }
} }

View File

@@ -1,19 +1,14 @@
using Managing.Application.Abstractions; using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Grains;
using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services; using Managing.Application.Abstractions.Services;
using Managing.Application.Bots;
using Managing.Application.Hubs; using Managing.Application.Hubs;
using Managing.Core.FixedSizedQueue;
using Managing.Domain.Accounts; using Managing.Domain.Accounts;
using Managing.Domain.Backtests; using Managing.Domain.Backtests;
using Managing.Domain.Bots; using Managing.Domain.Bots;
using Managing.Domain.Candles; using Managing.Domain.Candles;
using Managing.Domain.Scenarios; 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.Users;
using Managing.Domain.Workflows;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using static Managing.Common.Enums; using static Managing.Common.Enums;
@@ -32,6 +27,7 @@ namespace Managing.Application.Backtesting
private readonly IMessengerService _messengerService; private readonly IMessengerService _messengerService;
private readonly IKaigenService _kaigenService; private readonly IKaigenService _kaigenService;
private readonly IHubContext<BacktestHub> _hubContext; private readonly IHubContext<BacktestHub> _hubContext;
private readonly IGrainFactory _grainFactory;
public Backtester( public Backtester(
IExchangeService exchangeService, IExchangeService exchangeService,
@@ -42,7 +38,8 @@ namespace Managing.Application.Backtesting
IAccountService accountService, IAccountService accountService,
IMessengerService messengerService, IMessengerService messengerService,
IKaigenService kaigenService, IKaigenService kaigenService,
IHubContext<BacktestHub> hubContext) IHubContext<BacktestHub> hubContext,
IGrainFactory grainFactory)
{ {
_exchangeService = exchangeService; _exchangeService = exchangeService;
_botFactory = botFactory; _botFactory = botFactory;
@@ -53,19 +50,7 @@ namespace Managing.Application.Backtesting
_messengerService = messengerService; _messengerService = messengerService;
_kaigenService = kaigenService; _kaigenService = kaigenService;
_hubContext = hubContext; _hubContext = hubContext;
} _grainFactory = grainFactory;
public Backtest RunSimpleBotBacktest(Workflow workflow, bool save = false)
{
var simplebot = _botFactory.CreateSimpleBot("scenario", workflow);
Backtest result = null;
if (save && result != null)
{
// Simple bot backtest not implemented yet, would need user
// _backtestRepository.InsertBacktestForUser(null, result);
}
return result;
} }
/// <summary> /// <summary>
@@ -80,8 +65,8 @@ namespace Managing.Application.Backtesting
/// <param name="withCandles">Whether to include candles and indicators values in the response</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 (optional)</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> /// <param name="metadata">Additional metadata to associate with this backtest (optional)</param>
/// <returns>The backtest results</returns> /// <returns>The lightweight backtest results</returns>
public async Task<Backtest> RunTradingBotBacktest( public async Task<LightBacktestResponse> RunTradingBotBacktest(
TradingBotConfig config, TradingBotConfig config,
DateTime startDate, DateTime startDate,
DateTime endDate, DateTime endDate,
@@ -114,25 +99,7 @@ namespace Managing.Application.Backtesting
try try
{ {
var candles = GetCandles(config.Ticker, config.Timeframe, startDate, endDate); var candles = GetCandles(config.Ticker, config.Timeframe, startDate, endDate);
return await RunBacktestWithCandles(config, candles, user, save, withCandles, requestId, metadata);
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;
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -172,8 +139,10 @@ namespace Managing.Application.Backtesting
/// <param name="candles">The candles to use for backtesting</param> /// <param name="candles">The candles to use for backtesting</param>
/// <param name="user">The user running the backtest (optional)</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> /// <param name="withCandles">Whether to include candles and indicators values in the response</param>
/// <returns>The backtest results</returns> /// <param name="requestId">The request ID to associate with this backtest (optional)</param>
public async Task<Backtest> RunTradingBotBacktest( /// <param name="metadata">Additional metadata to associate with this backtest (optional)</param>
/// <returns>The lightweight backtest results</returns>
public async Task<LightBacktestResponse> RunTradingBotBacktest(
TradingBotConfig config, TradingBotConfig config,
List<Candle> candles, List<Candle> candles,
User user = null, User user = null,
@@ -181,43 +150,49 @@ namespace Managing.Application.Backtesting
string requestId = null, string requestId = null,
object metadata = null) object metadata = null)
{ {
return await RunBacktestWithCandles(config, candles, user, withCandles, requestId, metadata); return await RunBacktestWithCandles(config, candles, user, false, withCandles, requestId, metadata);
} }
/// <summary> /// <summary>
/// Core backtesting logic - handles the actual backtest execution with pre-loaded candles /// Core backtesting logic - handles the actual backtest execution with pre-loaded candles
/// </summary> /// </summary>
private async Task<Backtest> RunBacktestWithCandles( private async Task<LightBacktestResponse> RunBacktestWithCandles(
TradingBotConfig config, TradingBotConfig config,
List<Candle> candles, List<Candle> candles,
User user = null, User user = null,
bool save = false,
bool withCandles = false, bool withCandles = false,
string requestId = null, string requestId = null,
object metadata = 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 // Validate that scenario and indicators are properly loaded
// This is just a validation check to ensure everything loaded properly if (config.Scenario == null && string.IsNullOrEmpty(config.ScenarioName))
if (tradingBot is TradingBot bot && !bot.Indicators.Any())
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
$"No indicators were loaded for scenario '{config.ScenarioName ?? config.Scenario?.Name}'. " + "Backtest configuration must include either Scenario object or ScenarioName");
"This indicates a problem with scenario loading.");
} }
tradingBot.User = user; if (config.Scenario == null && !string.IsNullOrEmpty(config.ScenarioName))
tradingBot.Account = await GetAccountFromConfig(config);
var result =
await GetBacktestingResult(config, tradingBot, candles, user, withCandles, requestId, metadata);
if (user != null)
{ {
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) private async Task<Account> GetAccountFromConfig(TradingBotConfig config)
@@ -237,128 +212,16 @@ namespace Managing.Application.Backtesting
return candles; return candles;
} }
private async Task<Backtest> GetBacktestingResult(
TradingBotConfig config, /// <summary>
ITradingBot bot, /// Creates a clean copy of the trading bot config for Orleans serialization
List<Candle> candles, /// Uses LightScenario and LightIndicator to avoid FixedSizeQueue serialization issues
User user = null, /// </summary>
bool withCandles = false, private TradingBotConfig CreateCleanConfigForOrleans(TradingBotConfig originalConfig)
string requestId = null,
object metadata = null)
{ {
if (candles == null || candles.Count == 0) // 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
throw new Exception("No candle to backtest"); return originalConfig;
}
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;
} }
private async Task SendBacktestNotificationIfCriteriaMet(Backtest backtest) private async Task SendBacktestNotificationIfCriteriaMet(Backtest backtest)
@@ -376,56 +239,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) public async Task<bool> DeleteBacktestAsync(string id)
{ {

View File

@@ -2,7 +2,6 @@
using Managing.Application.Abstractions.Services; using Managing.Application.Abstractions.Services;
using Managing.Application.ManageBot; using Managing.Application.ManageBot;
using Managing.Domain.Bots; using Managing.Domain.Bots;
using Managing.Domain.Workflows;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Managing.Application.Bots.Base namespace Managing.Application.Bots.Base
@@ -12,14 +11,14 @@ namespace Managing.Application.Bots.Base
private readonly IExchangeService _exchangeService; private readonly IExchangeService _exchangeService;
private readonly IMessengerService _messengerService; private readonly IMessengerService _messengerService;
private readonly IAccountService _accountService; private readonly IAccountService _accountService;
private readonly ILogger<TradingBot> _tradingBotLogger; private readonly ILogger<TradingBotBase> _tradingBotLogger;
private readonly ITradingService _tradingService; private readonly ITradingService _tradingService;
private readonly IBotService _botService; private readonly IBotService _botService;
private readonly IBackupBotService _backupBotService; private readonly IBackupBotService _backupBotService;
public BotFactory( public BotFactory(
IExchangeService exchangeService, IExchangeService exchangeService,
ILogger<TradingBot> tradingBotLogger, ILogger<TradingBotBase> tradingBotLogger,
IMessengerService messengerService, IMessengerService messengerService,
IAccountService accountService, IAccountService accountService,
ITradingService tradingService, ITradingService tradingService,
@@ -35,11 +34,6 @@ namespace Managing.Application.Bots.Base
_backupBotService = backupBotService; _backupBotService = backupBotService;
} }
IBot IBotFactory.CreateSimpleBot(string botName, Workflow workflow)
{
return new SimpleBot(botName, _tradingBotLogger, workflow, _botService, _backupBotService);
}
public async Task<ITradingBot> CreateTradingBot(TradingBotConfig config) public async Task<ITradingBot> CreateTradingBot(TradingBotConfig config)
{ {
// Delegate to BotService which handles scenario loading properly // Delegate to BotService which handles scenario loading properly

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,496 @@
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 || _isDisposed)
{
return;
}
// Execute the bot's Run method
await _tradingBot.Run();
// Update execution count
State.ExecutionCount++;
await SaveBackupToState();
}
catch (ObjectDisposedException)
{
// Gracefully handle disposed service provider during shutdown
_logger.LogInformation("Service provider disposed during shutdown for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
return;
}
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 class SimpleBot : Bot
{ {
public readonly ILogger<TradingBot> Logger; public readonly ILogger<TradingBotBase> Logger;
private readonly IBotService _botService; private readonly IBotService _botService;
private readonly IBackupBotService _backupBotService; private readonly IBackupBotService _backupBotService;
private Workflow _workflow; 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) : IBackupBotService backupBotService) :
base(name) base(name)
{ {

View File

@@ -22,9 +22,9 @@ using static Managing.Common.Enums;
namespace Managing.Application.Bots; 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; private readonly IServiceScopeFactory _scopeFactory;
public TradingBotConfig Config { get; set; } 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 int _maxSignals = 10; // Maximum number of signals to keep in memory
public TradingBot( public TradingBotBase(
ILogger<TradingBot> logger, ILogger<TradingBotBase> logger,
IServiceScopeFactory scopeFactory, IServiceScopeFactory scopeFactory,
TradingBotConfig config TradingBotConfig config
) )
@@ -71,7 +71,9 @@ public class TradingBot : Bot, ITradingBot
// Load indicators if scenario is provided in config // Load indicators if scenario is provided in config
if (Config.Scenario != null) if (Config.Scenario != null)
{ {
LoadIndicators(Config.Scenario); // Convert LightScenario to full Scenario for indicator loading
var fullScenario = Config.Scenario.ToScenario();
LoadIndicators(fullScenario);
} }
else else
{ {
@@ -151,8 +153,6 @@ public class TradingBot : Bot, ITradingBot
} }
}); });
} }
} }
public async Task LoadAccount() public async Task LoadAccount()
@@ -185,8 +185,8 @@ public class TradingBot : Bot, ITradingBot
} }
else else
{ {
// Store the scenario in config and load indicators // Convert full Scenario to LightScenario for storage and load indicators
Config.Scenario = scenario; Config.Scenario = LightScenario.FromScenario(scenario);
LoadIndicators(ScenarioHelpers.GetIndicatorsFromScenario(scenario)); LoadIndicators(ScenarioHelpers.GetIndicatorsFromScenario(scenario));
Logger.LogInformation($"Loaded scenario '{scenario.Name}' with {Indicators.Count} indicators"); Logger.LogInformation($"Loaded scenario '{scenario.Name}' with {Indicators.Count} indicators");
@@ -1594,6 +1594,9 @@ public class TradingBot : Bot, ITradingBot
public override async Task SaveBackup() public override async Task SaveBackup()
{ {
if (Config.IsForBacktest)
return;
var data = new TradingBotBackup var data = new TradingBotBackup
{ {
Config = Config, Config = Config,
@@ -1908,7 +1911,9 @@ public class TradingBot : Bot, ITradingBot
{ {
if (newConfig.Scenario != null) 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 // Compare indicators after scenario change
var newIndicators = Indicators?.ToList() ?? new List<IIndicator>(); 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, UseForPositionSizing = false,
UseForSignalFiltering = false, UseForSignalFiltering = false,
UseForDynamicStopLoss = false, UseForDynamicStopLoss = false,
Scenario = scenario, Scenario = LightScenario.FromScenario(scenario),
MoneyManagement = mm, MoneyManagement = mm,
RiskManagement = new RiskManagement RiskManagement = new RiskManagement
{ {
@@ -915,7 +915,7 @@ public class TradingBotFitness : IFitness
var currentGeneration = _geneticAlgorithm?.GenerationsNumber ?? 0; var currentGeneration = _geneticAlgorithm?.GenerationsNumber ?? 0;
// Run backtest using scoped service to avoid DbContext concurrency issues // Run backtest using scoped service to avoid DbContext concurrency issues
var backtest = ServiceScopeHelpers.WithScopedService<IBacktester, Backtest>( var lightBacktest = ServiceScopeHelpers.WithScopedService<IBacktester, LightBacktest>(
_serviceScopeFactory, _serviceScopeFactory,
backtester => backtester.RunTradingBotBacktest( backtester => backtester.RunTradingBotBacktest(
config, config,
@@ -933,7 +933,7 @@ public class TradingBotFitness : IFitness
).Result; ).Result;
// Calculate multi-objective fitness based on backtest results // Calculate multi-objective fitness based on backtest results
var fitness = CalculateFitness(backtest, config); var fitness = CalculateFitness(lightBacktest, config);
return fitness; 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; return 0.1;
// Calculate base fitness from backtest score // Calculate base fitness from backtest score
var baseFitness = backtest.Score; var baseFitness = lightBacktest.Score;
// Return base fitness (no penalty for now) // Return base fitness (no penalty for now)
return baseFitness; return baseFitness;

View File

@@ -4,6 +4,7 @@ using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services; using Managing.Application.Abstractions.Services;
using Managing.Application.Bots; using Managing.Application.Bots;
using Managing.Domain.Bots; using Managing.Domain.Bots;
using Managing.Domain.Scenarios;
using Managing.Domain.Users; using Managing.Domain.Users;
using Managing.Domain.Workflows; using Managing.Domain.Workflows;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@@ -18,20 +19,21 @@ namespace Managing.Application.ManageBot
private readonly IExchangeService _exchangeService; private readonly IExchangeService _exchangeService;
private readonly IMessengerService _messengerService; private readonly IMessengerService _messengerService;
private readonly IAccountService _accountService; private readonly IAccountService _accountService;
private readonly ILogger<TradingBot> _tradingBotLogger; private readonly ILogger<TradingBotBase> _tradingBotLogger;
private readonly ITradingService _tradingService; private readonly ITradingService _tradingService;
private readonly IMoneyManagementService _moneyManagementService; private readonly IMoneyManagementService _moneyManagementService;
private readonly IUserService _userService; private readonly IUserService _userService;
private readonly IBackupBotService _backupBotService; private readonly IBackupBotService _backupBotService;
private readonly IServiceScopeFactory _scopeFactory; private readonly IServiceScopeFactory _scopeFactory;
private readonly IGrainFactory _grainFactory;
private ConcurrentDictionary<string, BotTaskWrapper> _botTasks = private ConcurrentDictionary<string, BotTaskWrapper> _botTasks =
new ConcurrentDictionary<string, BotTaskWrapper>(); new ConcurrentDictionary<string, BotTaskWrapper>();
public BotService(IBotRepository botRepository, IExchangeService exchangeService, 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, ITradingService tradingService, IMoneyManagementService moneyManagementService, IUserService userService,
IBackupBotService backupBotService, IServiceScopeFactory scopeFactory) IBackupBotService backupBotService, IServiceScopeFactory scopeFactory, IGrainFactory grainFactory)
{ {
_botRepository = botRepository; _botRepository = botRepository;
_exchangeService = exchangeService; _exchangeService = exchangeService;
@@ -43,26 +45,26 @@ namespace Managing.Application.ManageBot
_userService = userService; _userService = userService;
_backupBotService = backupBotService; _backupBotService = backupBotService;
_scopeFactory = scopeFactory; _scopeFactory = scopeFactory;
_grainFactory = grainFactory;
} }
public class BotTaskWrapper public class BotTaskWrapper
{ {
public Task Task { get; private set; } public Task Task { get; private set; }
public Type BotType { 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; Task = task;
BotType = botType; BotType = botType;
BotInstance = botInstance; // Set the bot instance BotInstance = botInstance;
} }
} }
public void AddSimpleBotToCache(IBot bot) public void AddSimpleBotToCache(IBot bot)
{ {
var botTask = var botTask = new BotTaskWrapper(Task.Run(() => bot.Start()), bot.GetType(), bot);
new BotTaskWrapper(Task.Run(() => bot.Start()), bot.GetType(), bot); // Pass bot as the instance
_botTasks.AddOrUpdate(bot.Identifier, botTask, (key, existingVal) => botTask); _botTasks.AddOrUpdate(bot.Identifier, botTask, (key, existingVal) => botTask);
} }
@@ -72,12 +74,14 @@ namespace Managing.Application.ManageBot
_botTasks.AddOrUpdate(bot.Identifier, botTask, (key, existingVal) => botTask); _botTasks.AddOrUpdate(bot.Identifier, botTask, (key, existingVal) => botTask);
} }
private async Task InitBot(ITradingBot bot, BotBackup backupBot) private async Task InitBot(ITradingBot bot, BotBackup backupBot)
{
try
{ {
var user = await _userService.GetUser(backupBot.User.Name); var user = await _userService.GetUser(backupBot.User.Name);
bot.User = user; 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); bot.LoadBackup(backupBot);
// Only start the bot if the backup status is Up // Only start the bot if the backup status is Up
@@ -92,6 +96,14 @@ namespace Managing.Application.ManageBot
bot.Stop(); 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() public List<ITradingBot> GetActiveBots()
{ {
@@ -137,7 +149,7 @@ namespace Managing.Application.ManageBot
var scenario = await _tradingService.GetScenarioByNameAsync(scalpingConfig.ScenarioName); var scenario = await _tradingService.GetScenarioByNameAsync(scalpingConfig.ScenarioName);
if (scenario != null) if (scenario != null)
{ {
scalpingConfig.Scenario = scenario; scalpingConfig.Scenario = LightScenario.FromScenario(scenario);
} }
else else
{ {
@@ -155,6 +167,10 @@ namespace Managing.Application.ManageBot
// Ensure critical properties are set correctly for restored bots // Ensure critical properties are set correctly for restored bots
scalpingConfig.IsForBacktest = false; 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); bot = await CreateTradingBot(scalpingConfig);
botTask = Task.Run(() => InitBot((ITradingBot)bot, backupBot)); botTask = Task.Run(() => InitBot((ITradingBot)bot, backupBot));
@@ -206,7 +222,7 @@ namespace Managing.Application.ManageBot
if (botWrapper.BotInstance is IBot bot) if (botWrapper.BotInstance is IBot bot)
{ {
await Task.Run(() => 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" + var stopMessage = $"🛑 **Bot Stopped**\n\n" +
$"🎯 **Agent:** {bot.User.AgentName}\n" + $"🎯 **Agent:** {bot.User.AgentName}\n" +
@@ -231,7 +247,7 @@ namespace Managing.Application.ManageBot
if (botWrapper.BotInstance is IBot bot) if (botWrapper.BotInstance is IBot bot)
{ {
await Task.Run(() => 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" + var deleteMessage = $"🗑️ **Bot Deleted**\n\n" +
$"🎯 **Agent:** {bot.User.AgentName}\n" + $"🎯 **Agent:** {bot.User.AgentName}\n" +
@@ -306,7 +322,7 @@ namespace Managing.Application.ManageBot
public async Task<bool> UpdateBotConfiguration(string identifier, TradingBotConfig newConfig) public async Task<bool> UpdateBotConfiguration(string identifier, TradingBotConfig newConfig)
{ {
if (_botTasks.TryGetValue(identifier, out var botTaskWrapper) && 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 // Ensure the scenario is properly loaded from database if needed
if (newConfig.Scenario == null && !string.IsNullOrEmpty(newConfig.ScenarioName)) if (newConfig.Scenario == null && !string.IsNullOrEmpty(newConfig.ScenarioName))
@@ -314,7 +330,7 @@ namespace Managing.Application.ManageBot
var scenario = await _tradingService.GetScenarioByNameAsync(newConfig.ScenarioName); var scenario = await _tradingService.GetScenarioByNameAsync(newConfig.ScenarioName);
if (scenario != null) if (scenario != null)
{ {
newConfig.Scenario = scenario; newConfig.Scenario = LightScenario.FromScenario(scenario);
} }
else else
{ {
@@ -370,7 +386,6 @@ namespace Managing.Application.ManageBot
return false; return false;
} }
public async Task<ITradingBot> CreateTradingBot(TradingBotConfig config) public async Task<ITradingBot> CreateTradingBot(TradingBotConfig config)
{ {
// Ensure the scenario is properly loaded from database if needed // 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); var scenario = await _tradingService.GetScenarioByNameAsync(config.ScenarioName);
if (scenario != null) if (scenario != null)
{ {
config.Scenario = scenario; config.Scenario = LightScenario.FromScenario(scenario);
} }
else else
{ {
@@ -392,7 +407,15 @@ namespace Managing.Application.ManageBot
throw new ArgumentException("Scenario object must be provided or ScenarioName must be valid"); 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) public async Task<ITradingBot> CreateBacktestTradingBot(TradingBotConfig config)
@@ -403,7 +426,7 @@ namespace Managing.Application.ManageBot
var scenario = await _tradingService.GetScenarioByNameAsync(config.ScenarioName); var scenario = await _tradingService.GetScenarioByNameAsync(config.ScenarioName);
if (scenario != null) if (scenario != null)
{ {
config.Scenario = scenario; config.Scenario = LightScenario.FromScenario(scenario);
} }
else else
{ {
@@ -417,109 +440,7 @@ namespace Managing.Application.ManageBot
} }
config.IsForBacktest = true; config.IsForBacktest = true;
return new TradingBot(_tradingBotLogger, _scopeFactory, config); return new TradingBotBase(_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);
} }
} }
} }

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

View File

@@ -21,6 +21,8 @@
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" 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.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="Polly" Version="8.4.0" />
<PackageReference Include="Skender.Stock.Indicators" Version="2.5.0" /> <PackageReference Include="Skender.Stock.Indicators" Version="2.5.0" />
</ItemGroup> </ItemGroup>

View File

@@ -146,7 +146,8 @@ namespace Managing.Application.Scenarios
return scenarios.Where(s => s.User?.Name == user.Name); return scenarios.Where(s => s.User?.Name == user.Name);
} }
public async Task<Scenario> CreateScenarioForUser(User user, string name, List<string> strategies, int? loopbackPeriod = 1) public async Task<Scenario> CreateScenarioForUser(User user, string name, List<string> strategies,
int? loopbackPeriod = 1)
{ {
var scenario = new Scenario(name, loopbackPeriod ?? 1) var scenario = new Scenario(name, loopbackPeriod ?? 1)
{ {
@@ -193,6 +194,7 @@ namespace Managing.Application.Scenarios
{ {
await _tradingService.DeleteScenarioAsync(scenario.Name); await _tradingService.DeleteScenarioAsync(scenario.Name);
} }
return true; return true;
} }
catch (Exception ex) catch (Exception ex)
@@ -211,6 +213,7 @@ namespace Managing.Application.Scenarios
{ {
await _tradingService.DeleteScenarioAsync(scenario.Name); await _tradingService.DeleteScenarioAsync(scenario.Name);
} }
return true; return true;
} }
catch (Exception ex) catch (Exception ex)
@@ -226,7 +229,8 @@ namespace Managing.Application.Scenarios
return scenario != null && scenario.User?.Name == user.Name ? scenario : null; return scenario != null && scenario.User?.Name == user.Name ? scenario : null;
} }
public async Task<Indicator> CreateIndicatorForUser(User user, IndicatorType type, string name, int? period = null, public async Task<Indicator> CreateIndicatorForUser(User user, IndicatorType type, string name,
int? period = null,
int? fastPeriods = null, int? slowPeriods = null, int? signalPeriods = null, int? fastPeriods = null, int? slowPeriods = null, int? signalPeriods = null,
double? multiplier = null, int? stochPeriods = null, int? smoothPeriods = null, double? multiplier = null, int? stochPeriods = null, int? smoothPeriods = null,
int? cyclePeriods = null) int? cyclePeriods = null)
@@ -253,6 +257,7 @@ namespace Managing.Application.Scenarios
{ {
await _tradingService.DeleteStrategyAsync(strategy.Name); await _tradingService.DeleteStrategyAsync(strategy.Name);
} }
return true; return true;
} }
catch (Exception ex) catch (Exception ex)
@@ -262,7 +267,8 @@ namespace Managing.Application.Scenarios
} }
} }
public async Task<bool> UpdateScenarioByUser(User user, string name, List<string> strategies, int? loopbackPeriod) public async Task<bool> UpdateScenarioByUser(User user, string name, List<string> strategies,
int? loopbackPeriod)
{ {
var scenario = await _tradingService.GetScenarioByNameAsync(name); var scenario = await _tradingService.GetScenarioByNameAsync(name);
if (scenario == null || scenario.User?.Name != user.Name) if (scenario == null || scenario.User?.Name != user.Name)
@@ -302,5 +308,16 @@ namespace Managing.Application.Scenarios
return result; 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,7 +42,10 @@ using MediatR;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Orleans.Configuration;
namespace Managing.Bootstrap; namespace Managing.Bootstrap;
@@ -60,9 +63,81 @@ public static class ApiBootstrap
.AddInfrastructure(configuration) .AddInfrastructure(configuration)
.AddWorkers(configuration) .AddWorkers(configuration)
.AddFluentValidation() .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));
// Only enable dashboard in development to avoid shutdown issues
if (!isProduction)
{
siloBuilder.UseDashboard(options =>
{
// Configure dashboard with proper shutdown handling
options.Port = 9999;
options.HostSelf = true;
options.CounterUpdateIntervalMs = 10000; // 10 seconds
options.HideTrace = true; // Hide trace to reduce dashboard overhead
});
}
siloBuilder.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>();
})
.Configure<ClusterOptions>(options =>
{
// Configure cluster options
options.ServiceId = "ManagingApp";
options.ClusterId = configuration["ASPNETCORE_ENVIRONMENT"] ?? "Development";
});
})
;
}
private static IServiceCollection AddApplication(this IServiceCollection services) private static IServiceCollection AddApplication(this IServiceCollection services)
{ {
services.AddScoped<ITradingService, TradingService>(); services.AddScoped<ITradingService, TradingService>();

View File

@@ -11,8 +11,14 @@
<PackageReference Include="MediatR" Version="12.2.0" /> <PackageReference Include="MediatR" Version="12.2.0" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.1.0" /> <PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.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.Configuration.Abstractions" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0"/> <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>
<ItemGroup> <ItemGroup>

View File

@@ -64,6 +64,7 @@ public static class WorkersBootstrap
services.AddScoped<IWorkerService, WorkerService>(); services.AddScoped<IWorkerService, WorkerService>();
services.AddScoped<ISynthPredictionService, SynthPredictionService>(); services.AddScoped<ISynthPredictionService, SynthPredictionService>();
services.AddScoped<ISynthApiClient, SynthApiClient>(); services.AddScoped<ISynthApiClient, SynthApiClient>();
services.AddScoped<IPricesService, PricesService>();
services.AddTransient<ICommandHandler<OpenPositionRequest, Position>, OpenPositionCommandHandler>(); services.AddTransient<ICommandHandler<OpenPositionRequest, Position>, OpenPositionCommandHandler>();
services.AddTransient<ICommandHandler<ClosePositionCommand, Position>, ClosePositionCommandHandler>(); services.AddTransient<ICommandHandler<ClosePositionCommand, Position>, ClosePositionCommandHandler>();

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; using static Managing.Common.Enums;
namespace Managing.Domain.Accounts; namespace Managing.Domain.Accounts;
[GenerateSerializer]
public class Account public class Account
{ {
[Id(0)]
[Required] public string Name { get; set; } [Required] public string Name { get; set; }
[Id(1)]
[Required] public TradingExchanges Exchange { get; set; } [Required] public TradingExchanges Exchange { get; set; }
[Id(2)]
[Required] public AccountType Type { get; set; } [Required] public AccountType Type { get; set; }
[Id(3)]
public string Key { get; set; } public string Key { get; set; }
[Id(4)]
public string Secret { get; set; } public string Secret { get; set; }
[Id(5)]
public User User { get; set; } public User User { get; set; }
[Id(6)]
public List<Balance> Balances { get; set; } public List<Balance> Balances { get; set; }
public bool IsPrivyWallet => Type == AccountType.Privy; public bool IsPrivyWallet => Type == AccountType.Privy;

View File

@@ -1,14 +1,29 @@
using Managing.Domain.Evm; using Managing.Domain.Evm;
using Orleans;
namespace Managing.Domain.Accounts; namespace Managing.Domain.Accounts;
[GenerateSerializer]
public class Balance public class Balance
{ {
[Id(0)]
public string TokenImage { get; set; } public string TokenImage { get; set; }
[Id(1)]
public string TokenName { get; set; } public string TokenName { get; set; }
[Id(2)]
public decimal Amount { get; set; } public decimal Amount { get; set; }
[Id(3)]
public decimal Price { get; set; } public decimal Price { get; set; }
[Id(4)]
public decimal Value { get; set; } public decimal Value { get; set; }
[Id(5)]
public string TokenAdress { get; set; } public string TokenAdress { get; set; }
[Id(6)]
public Chain Chain { get; set; } public Chain Chain { get; set; }
} }

View File

@@ -1,20 +1,26 @@
using Managing.Domain.Bots; using Managing.Domain.Bots;
using Orleans;
namespace Managing.Domain.Backtests; 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 class LightBacktest
{ {
public string Id { get; set; } = string.Empty; [Id(0)] public string Id { get; set; } = string.Empty;
public TradingBotConfig Config { get; set; } = new(); [Id(1)] public TradingBotConfig Config { get; set; } = new();
public decimal FinalPnl { get; set; } [Id(2)] public decimal FinalPnl { get; set; }
public int WinRate { get; set; } [Id(3)] public int WinRate { get; set; }
public decimal GrowthPercentage { get; set; } [Id(4)] public decimal GrowthPercentage { get; set; }
public decimal HodlPercentage { get; set; } [Id(5)] public decimal HodlPercentage { get; set; }
public DateTime StartDate { get; set; } [Id(6)] public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; } [Id(7)] public DateTime EndDate { get; set; }
public decimal? MaxDrawdown { get; set; } [Id(8)] public decimal? MaxDrawdown { get; set; }
public decimal Fees { get; set; } [Id(9)] public decimal Fees { get; set; }
public double? SharpeRatio { get; set; } [Id(10)] public double? SharpeRatio { get; set; }
public double Score { get; set; } [Id(11)] public double Score { get; set; }
public string ScoreMessage { get; set; } = string.Empty; [Id(12)] public string ScoreMessage { get; set; } = string.Empty;
} }

View File

@@ -1,15 +1,26 @@
using Managing.Domain.Users; using Managing.Domain.Users;
using Newtonsoft.Json; using Newtonsoft.Json;
using Orleans;
using static Managing.Common.Enums; using static Managing.Common.Enums;
namespace Managing.Domain.Bots; namespace Managing.Domain.Bots;
[GenerateSerializer]
public class BotBackup public class BotBackup
{ {
[Id(0)]
public string Identifier { get; set; } public string Identifier { get; set; }
[Id(1)]
public User User { get; set; } public User User { get; set; }
[Id(2)]
public TradingBotBackup Data { get; set; } public TradingBotBackup Data { get; set; }
[Id(3)]
public BotStatus LastStatus { get; set; } public BotStatus LastStatus { get; set; }
[Id(4)]
public DateTime CreateDate { get; set; } public DateTime CreateDate { get; set; }
/// <summary> /// <summary>

View File

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

View File

@@ -1,22 +1,45 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Managing.Domain.Risk; using Managing.Domain.Risk;
using Managing.Domain.Scenarios; using Managing.Domain.Scenarios;
using Orleans;
using static Managing.Common.Enums; using static Managing.Common.Enums;
namespace Managing.Domain.Bots; namespace Managing.Domain.Bots;
[GenerateSerializer]
public class TradingBotConfig public class TradingBotConfig
{ {
[Id(0)]
[Required] public string AccountName { get; set; } [Required] public string AccountName { get; set; }
[Id(1)]
[Required] public LightMoneyManagement MoneyManagement { get; set; } [Required] public LightMoneyManagement MoneyManagement { get; set; }
[Id(2)]
[Required] public Ticker Ticker { get; set; } [Required] public Ticker Ticker { get; set; }
[Id(3)]
[Required] public Timeframe Timeframe { get; set; } [Required] public Timeframe Timeframe { get; set; }
[Id(4)]
[Required] public bool IsForWatchingOnly { get; set; } [Required] public bool IsForWatchingOnly { get; set; }
[Id(5)]
[Required] public decimal BotTradingBalance { get; set; } [Required] public decimal BotTradingBalance { get; set; }
[Id(6)]
[Required] public bool IsForBacktest { get; set; } [Required] public bool IsForBacktest { get; set; }
[Id(7)]
[Required] public int CooldownPeriod { get; set; } [Required] public int CooldownPeriod { get; set; }
[Id(8)]
[Required] public int MaxLossStreak { get; set; } [Required] public int MaxLossStreak { get; set; }
[Id(9)]
[Required] public bool FlipPosition { get; set; } [Required] public bool FlipPosition { get; set; }
[Id(10)]
[Required] public string Name { get; set; } [Required] public string Name { get; set; }
/// <summary> /// <summary>
@@ -24,23 +47,28 @@ public class TradingBotConfig
/// Contains all configurable parameters for Expected Utility Theory, Kelly Criterion, and probability thresholds. /// Contains all configurable parameters for Expected Utility Theory, Kelly Criterion, and probability thresholds.
/// If null, default risk management settings will be used. /// If null, default risk management settings will be used.
/// </summary> /// </summary>
[Id(11)]
public RiskManagement RiskManagement { get; set; } = new(); public RiskManagement RiskManagement { get; set; } = new();
/// <summary> /// <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. /// This allows running backtests without requiring scenarios to be saved in the database.
/// Orleans-friendly version without FixedSizeQueue and User properties.
/// </summary> /// </summary>
public Scenario Scenario { get; set; } [Id(12)]
public LightScenario Scenario { get; set; }
/// <summary> /// <summary>
/// The scenario name to load from database. Only used when Scenario object is not provided. /// The scenario name to load from database. Only used when Scenario object is not provided.
/// </summary> /// </summary>
[Id(13)]
public string ScenarioName { get; set; } public string ScenarioName { get; set; }
/// <summary> /// <summary>
/// Maximum time in hours that a position can remain open before being automatically closed. /// Maximum time in hours that a position can remain open before being automatically closed.
/// If null, time-based position closure is disabled. /// If null, time-based position closure is disabled.
/// </summary> /// </summary>
[Id(14)]
public decimal? MaxPositionTimeHours { get; set; } public decimal? MaxPositionTimeHours { get; set; }
/// <summary> /// <summary>
@@ -49,6 +77,7 @@ public class TradingBotConfig
/// If false, the position will only be closed when MaxPositionTimeHours is reached. /// If false, the position will only be closed when MaxPositionTimeHours is reached.
/// Default is false to maintain existing behavior. /// Default is false to maintain existing behavior.
/// </summary> /// </summary>
[Id(15)]
public bool CloseEarlyWhenProfitable { get; set; } = false; public bool CloseEarlyWhenProfitable { get; set; } = false;
/// <summary> /// <summary>
@@ -56,6 +85,7 @@ public class TradingBotConfig
/// If false, positions will be flipped regardless of profit status. /// If false, positions will be flipped regardless of profit status.
/// Default is true for safer trading. /// Default is true for safer trading.
/// </summary> /// </summary>
[Id(16)]
[Required] [Required]
public bool FlipOnlyWhenInProfit { get; set; } = true; public bool FlipOnlyWhenInProfit { get; set; } = true;
@@ -65,20 +95,24 @@ public class TradingBotConfig
/// When false, the bot operates in traditional mode without Synth predictions. /// When false, the bot operates in traditional mode without Synth predictions.
/// The actual Synth configuration is managed centrally in SynthPredictionService. /// The actual Synth configuration is managed centrally in SynthPredictionService.
/// </summary> /// </summary>
[Id(17)]
public bool UseSynthApi { get; set; } = false; public bool UseSynthApi { get; set; } = false;
/// <summary> /// <summary>
/// Whether to use Synth predictions for position sizing adjustments and risk assessment /// Whether to use Synth predictions for position sizing adjustments and risk assessment
/// </summary> /// </summary>
[Id(18)]
public bool UseForPositionSizing { get; set; } = true; public bool UseForPositionSizing { get; set; } = true;
/// <summary> /// <summary>
/// Whether to use Synth predictions for signal filtering /// Whether to use Synth predictions for signal filtering
/// </summary> /// </summary>
[Id(19)]
public bool UseForSignalFiltering { get; set; } = true; public bool UseForSignalFiltering { get; set; } = true;
/// <summary> /// <summary>
/// Whether to use Synth predictions for dynamic stop-loss/take-profit adjustments /// Whether to use Synth predictions for dynamic stop-loss/take-profit adjustments
/// </summary> /// </summary>
[Id(20)]
public bool UseForDynamicStopLoss { get; set; } = true; public bool UseForDynamicStopLoss { get; set; } = true;
} }

View File

@@ -1,20 +1,41 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Managing.Common; using Managing.Common;
using Orleans;
using Skender.Stock.Indicators; using Skender.Stock.Indicators;
namespace Managing.Domain.Candles namespace Managing.Domain.Candles
{ {
[GenerateSerializer]
public class Candle : IQuote public class Candle : IQuote
{ {
[Id(0)]
[Required] public Enums.TradingExchanges Exchange { get; set; } [Required] public Enums.TradingExchanges Exchange { get; set; }
[Id(1)]
[Required] public string Ticker { get; set; } [Required] public string Ticker { get; set; }
[Id(2)]
[Required] public DateTime OpenTime { get; set; } [Required] public DateTime OpenTime { get; set; }
[Id(3)]
[Required] public DateTime Date { get; set; } [Required] public DateTime Date { get; set; }
[Id(4)]
[Required] public decimal Open { get; set; } [Required] public decimal Open { get; set; }
[Id(5)]
[Required] public decimal Close { get; set; } [Required] public decimal Close { get; set; }
[Id(6)]
[Required] public decimal High { get; set; } [Required] public decimal High { get; set; }
[Id(7)]
[Required] public decimal Low { get; set; } [Required] public decimal Low { get; set; }
[Id(8)]
[Required] public Enums.Timeframe Timeframe { get; set; } [Required] public Enums.Timeframe Timeframe { get; set; }
[Id(9)]
public decimal Volume { get; set; } 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 public class Chain
{ {
[Id(0)]
public string Id { get; set; } public string Id { get; set; }
[Id(1)]
public string RpcUrl { get; set; } public string RpcUrl { get; set; }
[Id(2)]
public string Name { get; set; } public string Name { get; set; }
[Id(3)]
public int ChainId { get; set; } public int ChainId { get; set; }
} }

View File

@@ -8,6 +8,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Exilion.TradingAtomics" Version="1.0.4"/> <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="Newtonsoft.Json" Version="13.0.3"/>
<PackageReference Include="Skender.Stock.Indicators" Version="2.5.0"/> <PackageReference Include="Skender.Stock.Indicators" Version="2.5.0"/>
</ItemGroup> </ItemGroup>

View File

@@ -1,12 +1,23 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Orleans;
using static Managing.Common.Enums; using static Managing.Common.Enums;
[GenerateSerializer]
public class LightMoneyManagement public class LightMoneyManagement
{ {
[Id(0)]
[Required] public string Name { get; set; } [Required] public string Name { get; set; }
[Id(1)]
[Required] public Timeframe Timeframe { get; set; } [Required] public Timeframe Timeframe { get; set; }
[Id(2)]
[Required] public decimal StopLoss { get; set; } [Required] public decimal StopLoss { get; set; }
[Id(3)]
[Required] public decimal TakeProfit { get; set; } [Required] public decimal TakeProfit { get; set; }
[Id(4)]
[Required] public decimal Leverage { get; set; } [Required] public decimal Leverage { get; set; }
public void FormatPercentage() public void FormatPercentage()

View File

@@ -1,9 +1,12 @@
using Managing.Domain.Users; using Managing.Domain.Users;
using Orleans;
namespace Managing.Domain.MoneyManagements namespace Managing.Domain.MoneyManagements
{ {
[GenerateSerializer]
public class MoneyManagement : LightMoneyManagement public class MoneyManagement : LightMoneyManagement
{ {
[Id(5)]
public User User { get; set; } public User User { get; set; }
} }
} }

View File

@@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Managing.Common; using Managing.Common;
using Orleans;
namespace Managing.Domain.Risk; namespace Managing.Domain.Risk;
@@ -7,6 +8,7 @@ namespace Managing.Domain.Risk;
/// Risk management configuration for trading bots /// Risk management configuration for trading bots
/// Contains all configurable risk parameters for probabilistic analysis and position sizing /// Contains all configurable risk parameters for probabilistic analysis and position sizing
/// </summary> /// </summary>
[GenerateSerializer]
public class RiskManagement public class RiskManagement
{ {
/// <summary> /// <summary>
@@ -14,6 +16,7 @@ public class RiskManagement
/// Signals with SL probability above this threshold may be filtered out /// Signals with SL probability above this threshold may be filtered out
/// Range: 0.05 (5%) to 0.50 (50%) /// Range: 0.05 (5%) to 0.50 (50%)
/// </summary> /// </summary>
[Id(0)]
[Range(0.05, 0.50)] [Range(0.05, 0.50)]
[Required] [Required]
public decimal AdverseProbabilityThreshold { get; set; } = 0.20m; public decimal AdverseProbabilityThreshold { get; set; } = 0.20m;
@@ -23,6 +26,7 @@ public class RiskManagement
/// Used for additional signal filtering and confidence assessment /// Used for additional signal filtering and confidence assessment
/// Range: 0.10 (10%) to 0.70 (70%) /// Range: 0.10 (10%) to 0.70 (70%)
/// </summary> /// </summary>
[Id(1)]
[Range(0.10, 0.70)] [Range(0.10, 0.70)]
[Required] [Required]
public decimal FavorableProbabilityThreshold { get; set; } = 0.30m; public decimal FavorableProbabilityThreshold { get; set; } = 0.30m;
@@ -32,6 +36,7 @@ public class RiskManagement
/// Higher values = more risk-averse behavior in utility calculations /// Higher values = more risk-averse behavior in utility calculations
/// Range: 0.1 (risk-seeking) to 5.0 (highly risk-averse) /// Range: 0.1 (risk-seeking) to 5.0 (highly risk-averse)
/// </summary> /// </summary>
[Id(2)]
[Range(0.1, 5.0)] [Range(0.1, 5.0)]
[Required] [Required]
public decimal RiskAversion { get; set; } = 1.0m; public decimal RiskAversion { get; set; } = 1.0m;
@@ -41,6 +46,7 @@ public class RiskManagement
/// Trades with Kelly fraction below this threshold are considered unfavorable /// Trades with Kelly fraction below this threshold are considered unfavorable
/// Range: 0.5% to 10% /// Range: 0.5% to 10%
/// </summary> /// </summary>
[Id(3)]
[Range(0.005, 0.10)] [Range(0.005, 0.10)]
[Required] [Required]
public decimal KellyMinimumThreshold { get; set; } = 0.01m; public decimal KellyMinimumThreshold { get; set; } = 0.01m;
@@ -50,6 +56,7 @@ public class RiskManagement
/// Prevents over-allocation even when Kelly suggests higher percentages /// Prevents over-allocation even when Kelly suggests higher percentages
/// Range: 5% to 50% /// Range: 5% to 50%
/// </summary> /// </summary>
[Id(4)]
[Range(0.05, 0.50)] [Range(0.05, 0.50)]
[Required] [Required]
public decimal KellyMaximumCap { get; set; } = 0.25m; public decimal KellyMaximumCap { get; set; } = 0.25m;
@@ -59,6 +66,7 @@ public class RiskManagement
/// Positions with higher liquidation risk may be blocked or reduced /// Positions with higher liquidation risk may be blocked or reduced
/// Range: 5% to 30% /// Range: 5% to 30%
/// </summary> /// </summary>
[Id(5)]
[Range(0.05, 0.30)] [Range(0.05, 0.30)]
[Required] [Required]
public decimal MaxLiquidationProbability { get; set; } = 0.10m; public decimal MaxLiquidationProbability { get; set; } = 0.10m;
@@ -68,6 +76,7 @@ public class RiskManagement
/// Longer horizons provide more stable predictions but less responsive signals /// Longer horizons provide more stable predictions but less responsive signals
/// Range: 1 hour to 168 hours (1 week) /// Range: 1 hour to 168 hours (1 week)
/// </summary> /// </summary>
[Id(6)]
[Range(1, 168)] [Range(1, 168)]
[Required] [Required]
public int SignalValidationTimeHorizonHours { get; set; } = 24; public int SignalValidationTimeHorizonHours { get; set; } = 24;
@@ -77,6 +86,7 @@ public class RiskManagement
/// Shorter horizons for more frequent risk updates on open positions /// Shorter horizons for more frequent risk updates on open positions
/// Range: 1 hour to 48 hours /// Range: 1 hour to 48 hours
/// </summary> /// </summary>
[Id(7)]
[Range(1, 48)] [Range(1, 48)]
[Required] [Required]
public int PositionMonitoringTimeHorizonHours { get; set; } = 6; public int PositionMonitoringTimeHorizonHours { get; set; } = 6;
@@ -86,6 +96,7 @@ public class RiskManagement
/// Positions exceeding this liquidation risk will trigger warnings /// Positions exceeding this liquidation risk will trigger warnings
/// Range: 10% to 40% /// Range: 10% to 40%
/// </summary> /// </summary>
[Id(8)]
[Range(0.10, 0.40)] [Range(0.10, 0.40)]
[Required] [Required]
public decimal PositionWarningThreshold { get; set; } = 0.20m; public decimal PositionWarningThreshold { get; set; } = 0.20m;
@@ -95,6 +106,7 @@ public class RiskManagement
/// Positions exceeding this liquidation risk will be automatically closed /// Positions exceeding this liquidation risk will be automatically closed
/// Range: 30% to 80% /// Range: 30% to 80%
/// </summary> /// </summary>
[Id(9)]
[Range(0.30, 0.80)] [Range(0.30, 0.80)]
[Required] [Required]
public decimal PositionAutoCloseThreshold { get; set; } = 0.50m; 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) /// Values less than 1.0 implement fractional Kelly (e.g., 0.5 = half-Kelly)
/// Range: 0.1 to 1.0 /// Range: 0.1 to 1.0
/// </summary> /// </summary>
[Id(10)]
[Range(0.1, 1.0)] [Range(0.1, 1.0)]
[Required] [Required]
public decimal KellyFractionalMultiplier { get; set; } = 1.0m; public decimal KellyFractionalMultiplier { get; set; } = 1.0m;
@@ -111,18 +124,21 @@ public class RiskManagement
/// <summary> /// <summary>
/// Risk tolerance level affecting overall risk calculations /// Risk tolerance level affecting overall risk calculations
/// </summary> /// </summary>
[Id(11)]
[Required] [Required]
public Enums.RiskToleranceLevel RiskTolerance { get; set; } = Enums.RiskToleranceLevel.Moderate; public Enums.RiskToleranceLevel RiskTolerance { get; set; } = Enums.RiskToleranceLevel.Moderate;
/// <summary> /// <summary>
/// Whether to use Expected Utility Theory for decision making /// Whether to use Expected Utility Theory for decision making
/// </summary> /// </summary>
[Id(12)]
[Required] [Required]
public bool UseExpectedUtility { get; set; } = true; public bool UseExpectedUtility { get; set; } = true;
/// <summary> /// <summary>
/// Whether to use Kelly Criterion for position sizing recommendations /// Whether to use Kelly Criterion for position sizing recommendations
/// </summary> /// </summary>
[Id(13)]
[Required] [Required]
public bool UseKellyCriterion { get; set; } = true; 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.Strategies;
using Managing.Domain.Users; using Managing.Domain.Users;
using Orleans;
namespace Managing.Domain.Scenarios namespace Managing.Domain.Scenarios
{ {
[GenerateSerializer]
public class Scenario public class Scenario
{ {
public Scenario(string name, int? loopbackPeriod = 1) public Scenario(string name, int? loopbackPeriod = 1)
@@ -12,9 +14,16 @@ namespace Managing.Domain.Scenarios
LoopbackPeriod = loopbackPeriod; LoopbackPeriod = loopbackPeriod;
} }
[Id(0)]
public string Name { get; set; } public string Name { get; set; }
[Id(1)]
public List<Indicator> Indicators { get; set; } public List<Indicator> Indicators { get; set; }
[Id(2)]
public int? LoopbackPeriod { get; set; } public int? LoopbackPeriod { get; set; }
[Id(3)]
public User User { get; set; } public User User { get; set; }
public void AddIndicator(Indicator indicator) public void AddIndicator(Indicator indicator)

View File

@@ -1,6 +1,4 @@
using System.Runtime.Serialization; using Managing.Core.FixedSizedQueue;
using System.Text.Json.Serialization;
using Managing.Core.FixedSizedQueue;
using Managing.Domain.Candles; using Managing.Domain.Candles;
using Managing.Domain.Scenarios; using Managing.Domain.Scenarios;
using Managing.Domain.Strategies.Base; using Managing.Domain.Strategies.Base;
@@ -20,18 +18,31 @@ namespace Managing.Domain.Strategies
} }
public string Name { get; set; } 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 IndicatorType Type { get; set; }
public SignalType SignalType { get; set; } public SignalType SignalType { get; set; }
public int MinimumHistory { get; set; } public int MinimumHistory { get; set; }
public int? Period { get; set; } public int? Period { get; set; }
public int? FastPeriods { get; set; } public int? FastPeriods { get; set; }
public int? SlowPeriods { get; set; } public int? SlowPeriods { get; set; }
public int? SignalPeriods { get; set; } public int? SignalPeriods { get; set; }
public double? Multiplier { get; set; } public double? Multiplier { get; set; }
public int? SmoothPeriods { get; set; } public int? SmoothPeriods { get; set; }
public int? StochPeriods { get; set; } public int? StochPeriods { get; set; }
public int? CyclePeriods { get; set; } public int? CyclePeriods { get; set; }
public User User { get; set; } public User User { get; set; }
public virtual List<LightSignal> Run() 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 System.Globalization;
using Managing.Core; using Managing.Core;
using Managing.Domain.Candles; using Managing.Domain.Candles;
using Orleans;
using static Managing.Common.Enums; using static Managing.Common.Enums;
[GenerateSerializer]
public class LightSignal : ValueObject public class LightSignal : ValueObject
{ {
public LightSignal(Ticker ticker, TradeDirection direction, Confidence confidence, Candle candle, DateTime date, 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}"; $"{indicatorName}-{indicatorType}-{direction}-{ticker}-{candle?.Close.ToString(CultureInfo.InvariantCulture)}-{date:yyyyMMdd-HHmmss}";
} }
[Id(0)]
[Required] public SignalStatus Status { get; set; } [Required] public SignalStatus Status { get; set; }
[Id(1)]
[Required] public TradeDirection Direction { get; } [Required] public TradeDirection Direction { get; }
[Id(2)]
[Required] public Confidence Confidence { get; set; } [Required] public Confidence Confidence { get; set; }
[Id(3)]
[Required] public Timeframe Timeframe { get; } [Required] public Timeframe Timeframe { get; }
[Id(4)]
[Required] public DateTime Date { get; private set; } [Required] public DateTime Date { get; private set; }
[Id(5)]
[Required] public Candle Candle { get; } [Required] public Candle Candle { get; }
[Id(6)]
[Required] public string Identifier { get; } [Required] public string Identifier { get; }
[Id(7)]
[Required] public Ticker Ticker { get; } [Required] public Ticker Ticker { get; }
[Id(8)]
[Required] public TradingExchanges Exchange { get; set; } [Required] public TradingExchanges Exchange { get; set; }
[Id(9)]
[Required] public IndicatorType IndicatorType { get; set; } [Required] public IndicatorType IndicatorType { get; set; }
[Id(10)]
[Required] public SignalType SignalType { get; set; } [Required] public SignalType SignalType { get; set; }
[Id(11)]
[Required] public string IndicatorName { get; set; } [Required] public string IndicatorName { get; set; }
protected override IEnumerable<object> GetEqualityComponents() protected override IEnumerable<object> GetEqualityComponents()

View File

@@ -1,10 +1,12 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Managing.Domain.Users; using Managing.Domain.Users;
using Orleans;
using static Managing.Common.Enums; using static Managing.Common.Enums;
namespace Managing.Domain.Trades namespace Managing.Domain.Trades
{ {
[GenerateSerializer]
public class Position public class Position
{ {
public Position(string identifier, string accountName, TradeDirection originDirection, Ticker ticker, public Position(string identifier, string accountName, TradeDirection originDirection, Ticker ticker,
@@ -21,28 +23,53 @@ namespace Managing.Domain.Trades
User = user; User = user;
} }
[Id(0)]
[Required] public string AccountName { get; set; } [Required] public string AccountName { get; set; }
[Id(1)]
[Required] public DateTime Date { get; set; } [Required] public DateTime Date { get; set; }
[Id(2)]
[Required] public TradeDirection OriginDirection { get; set; } [Required] public TradeDirection OriginDirection { get; set; }
[Id(3)]
[Required] public Ticker Ticker { get; set; } [Required] public Ticker Ticker { get; set; }
[Id(4)]
[Required] public LightMoneyManagement MoneyManagement { get; set; } [Required] public LightMoneyManagement MoneyManagement { get; set; }
[Id(5)]
[Required] [JsonPropertyName("Open")] public Trade Open { get; set; } [Required] [JsonPropertyName("Open")] public Trade Open { get; set; }
[Id(6)]
[Required] [Required]
[JsonPropertyName("StopLoss")] [JsonPropertyName("StopLoss")]
public Trade StopLoss { get; set; } public Trade StopLoss { get; set; }
[Id(7)]
[Required] [Required]
[JsonPropertyName("TakeProfit1")] [JsonPropertyName("TakeProfit1")]
public Trade TakeProfit1 { get; set; } public Trade TakeProfit1 { get; set; }
[Id(8)]
[JsonPropertyName("TakeProfit2")] public Trade TakeProfit2 { get; set; } [JsonPropertyName("TakeProfit2")] public Trade TakeProfit2 { get; set; }
[Id(9)]
[JsonPropertyName("ProfitAndLoss")] public ProfitAndLoss ProfitAndLoss { get; set; } [JsonPropertyName("ProfitAndLoss")] public ProfitAndLoss ProfitAndLoss { get; set; }
[Id(10)]
[Required] public PositionStatus Status { get; set; } [Required] public PositionStatus Status { get; set; }
[Id(11)]
public string SignalIdentifier { get; set; } public string SignalIdentifier { get; set; }
[Id(12)]
[Required] public string Identifier { get; set; } [Required] public string Identifier { get; set; }
[Id(13)]
[Required] public PositionInitiator Initiator { get; set; } [Required] public PositionInitiator Initiator { get; set; }
[Id(14)]
[Required] public User User { get; set; } [Required] public User User { get; set; }
public bool IsFinished() 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 namespace Managing.Domain.Trades
{ {
[GenerateSerializer]
public sealed class ProfitAndLoss public sealed class ProfitAndLoss
{ {
[Id(0)]
public decimal Realized { get; set; } public decimal Realized { get; set; }
[Id(1)]
public decimal Net { get; set; } public decimal Net { get; set; }
[Id(2)]
public decimal AverageOpenPrice { get; private set; } public decimal AverageOpenPrice { get; private set; }
private const decimal _multiplier = 100000; private const decimal _multiplier = 100000;

View File

@@ -1,8 +1,10 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Orleans;
using static Managing.Common.Enums; using static Managing.Common.Enums;
namespace Managing.Domain.Trades namespace Managing.Domain.Trades
{ {
[GenerateSerializer]
public class Trade public class Trade
{ {
public Trade(DateTime date, TradeDirection direction, TradeStatus status, TradeType tradeType, Ticker ticker, public Trade(DateTime date, TradeDirection direction, TradeStatus status, TradeType tradeType, Ticker ticker,
@@ -21,16 +23,37 @@ namespace Managing.Domain.Trades
Fee = 0; Fee = 0;
} }
[Id(0)]
[Required] public decimal Fee { get; set; } [Required] public decimal Fee { get; set; }
[Id(1)]
[Required] public DateTime Date { get; set; } [Required] public DateTime Date { get; set; }
[Id(2)]
[Required] public TradeDirection Direction { get; set; } [Required] public TradeDirection Direction { get; set; }
[Id(3)]
[Required] public TradeStatus Status { get; set; } [Required] public TradeStatus Status { get; set; }
[Id(4)]
[Required] public TradeType TradeType { get; set; } [Required] public TradeType TradeType { get; set; }
[Id(5)]
[Required] public Ticker Ticker { get; set; } [Required] public Ticker Ticker { get; set; }
[Id(6)]
[Required] public decimal Quantity { get; set; } [Required] public decimal Quantity { get; set; }
[Id(7)]
[Required] public decimal Price { get; set; } [Required] public decimal Price { get; set; }
[Id(8)]
[Required] public decimal Leverage { get; set; } [Required] public decimal Leverage { get; set; }
[Id(9)]
[Required] public string ExchangeOrderId { get; set; } [Required] public string ExchangeOrderId { get; set; }
[Id(10)]
[Required] public string Message { get; set; } [Required] public string Message { get; set; }
public void SetStatus(TradeStatus status) public void SetStatus(TradeStatus status)

View File

@@ -1,12 +1,23 @@
using Managing.Domain.Accounts; using Managing.Domain.Accounts;
using Orleans;
namespace Managing.Domain.Users; namespace Managing.Domain.Users;
[GenerateSerializer]
public class User public class User
{ {
[Id(0)]
public string Name { get; set; } public string Name { get; set; }
[Id(1)]
public List<Account> Accounts { get; set; } public List<Account> Accounts { get; set; }
[Id(2)]
public string AgentName { get; set; } public string AgentName { get; set; }
[Id(3)]
public string AvatarUrl { get; set; } public string AvatarUrl { get; set; }
[Id(4)]
public string TelegramChannel { get; set; } public string TelegramChannel { get; set; }
} }

View File

@@ -12,7 +12,7 @@
<PackageReference Include="FTX.Net" Version="1.0.16" /> <PackageReference Include="FTX.Net" Version="1.0.16" />
<PackageReference Include="KrakenExchange.Net" Version="4.6.5" /> <PackageReference Include="KrakenExchange.Net" Version="4.6.5" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" /> <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>
<ItemGroup> <ItemGroup>

View File

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

View File

@@ -92,7 +92,6 @@ public class KaigenService : IKaigenService
var requestPayload = new var requestPayload = new
{ {
requestId = requestId, requestId = requestId,
walletAddress = walletAddress,
debitAmount = debitAmount debitAmount = debitAmount
}; };
@@ -114,6 +113,13 @@ public class KaigenService : IKaigenService
} }
var result = await response.Content.ReadFromJsonAsync<KaigenResponse>(_jsonOptions); var result = await response.Content.ReadFromJsonAsync<KaigenResponse>(_jsonOptions);
if (result == null || !result.Success)
{
_logger.LogError("Debit request failed: {Message}", result?.Message ?? "Unknown error");
throw new Exception($"Debit request failed: {result?.Message}");
}
_logger.LogInformation( _logger.LogInformation(
"Successfully debited {Amount} credits for user {UserName} (wallet: {WalletAddress})", "Successfully debited {Amount} credits for user {UserName} (wallet: {WalletAddress})",
debitAmount, user.Name, walletAddress); debitAmount, user.Name, walletAddress);
@@ -145,7 +151,6 @@ public class KaigenService : IKaigenService
var requestPayload = new var requestPayload = new
{ {
requestId = requestId, requestId = requestId,
walletAddress = walletAddress
}; };
_logger.LogInformation( _logger.LogInformation(

View File

@@ -1,7 +0,0 @@
VITE_API_URL_LOCAL=http://localhost:5000
VITE_API_URL_SERVER=https://dev-managing-api.apps.managing.live
VITE_WORKER_URL_LOCAL=https://localhost:5002
VITE_WORKER_URL_SERVER=https://dev-managing-worker.apps.managing.live
ALCHEMY_ID=Bao7OirVe4bmYiDbPh0l8cs5gYb5D4_9
WALLET_CONNECT_PROJECT_ID=363bf09c10fec2293b21ee199b2ce8d5
VITE_PRIVY_APP_ID=cm7u09v0u002zrkuf2yjjr58p

View File

@@ -1,2 +0,0 @@
node_modules
dist

View File

@@ -1,98 +0,0 @@
{
"root": true,
"extends": [
"plugin:@typescript-eslint/recommended",
"plugin:jsx-a11y/recommended"
],
"parser": "@typescript-eslint/parser",
"plugins": [
"jsx-a11y",
"import",
"sort-keys-fix",
"react-hooks",
"@typescript-eslint",
"prettier"
],
"env": {
"browser": true,
"node": true,
"es6": true,
"jest": true
},
"globals": {
"JSX": "readonly"
},
"settings": {
"react": {
"version": "detect"
},
"import/parsers": {
"@typescript-eslint/parser": [".ts", ".tsx"]
},
"import/resolver": {
"node": {
"extensions": [".js", ".jsx", ".ts", ".tsx"]
},
"typescript": {
"alwaysTryTypes": true,
// always try to resolve types under `<root>@types` directory even it doesn't contain any source code, like `@types/unist`
"project": ["tsconfig.json"]
}
}
},
"rules": {
"no-alert": "error",
"no-console": "error",
"react-hooks/rules-of-hooks": "error",
"prettier/prettier": [
"warn",
{},
{
"properties": {
"usePrettierrc": true
}
}
],
"import/order": [
"warn",
{
"groups": [
"builtin",
"external",
"internal",
"parent",
"sibling",
"index",
"object"
],
"newlines-between": "always",
"alphabetize": {
"order": "asc",
"caseInsensitive": true
}
}
],
"import/named": "error",
"import/default": "error",
"import/export": "error",
"import/no-named-as-default": "warn",
"import/no-duplicates": "error",
"sort-keys-fix/sort-keys-fix": "warn",
"@import/no-named-as-default-member": "off",
"@typescript-eslint/consistent-type-imports": "warn",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-empty-function": "off"
},
"overrides": [
{
"files": ["*.js"],
"rules": {
"@typescript-eslint/explicit-module-boundary-types": ["off"],
"@typescript-eslint/no-var-requires": ["off"]
}
}
]
}

View File

@@ -1,3 +0,0 @@
.jest/* linguist-vendored
mocks/* linguist-vendored
mockServiceWorker.js linguist-vendored

View File

@@ -1,18 +0,0 @@
name: Build
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2-beta
with:
node-version: '18.1.0'
- run: yarn install
- run: yarn build

View File

@@ -1,18 +0,0 @@
name: Lint
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2-beta
with:
node-version: '18.1.0'
- run: yarn install
- run: yarn lint

View File

@@ -1,18 +0,0 @@
name: Test
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2-beta
with:
node-version: '18.1.0'
- run: yarn install
- run: yarn test

View File

@@ -1,18 +0,0 @@
name: Typecheck
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2-beta
with:
node-version: '18.1.0'
- run: yarn install
- run: yarn typecheck

View File

@@ -1,5 +0,0 @@
node_modules
.DS_Store
dist
dist-ssr
*.local

View File

@@ -1,5 +0,0 @@
.git
node_modules
.eslintignore
.gitignore
LICENSE

View File

@@ -1,4 +0,0 @@
{
"singleQuote": true,
"semi": false
}

View File

@@ -1,42 +0,0 @@
# Use an official Node.js image as the base
FROM node:18-alpine
# Set the working directory in the container
WORKDIR /app
# Set environment variable to skip Chromium download
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
# Install git and Python
RUN apk update && apk add --no-cache git python3 make g++
# Create a symlink for python3 as python
RUN ln -sf /usr/bin/python3 /usr/bin/python
# Copy package.json and package-lock.json to the container
# COPY package*.json ./
COPY /src/Managing.WebApp/package.json ./
# Install dependencies with the --legacy-peer-deps flag to bypass peer dependency conflicts
RUN npm install --legacy-peer-deps
RUN npm install -g tailwindcss postcss autoprefixer @tailwindcss/typography
# Copy the rest of the app's source code to the container
# COPY . .
COPY src/Managing.WebApp/ /app/
RUN node --max-old-space-size=8192 ./node_modules/.bin/vite build
# Build the app
RUN npm run build
# Use NGINX as the web server
FROM nginx:alpine
# Copy the built app to the NGINX web server directory
COPY --from=0 /app/build /usr/share/nginx/html
# Expose port 80 for the NGINX web server
EXPOSE 80
# Start the NGINX web server
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -1,44 +0,0 @@
# Use an official Node.js image as the base
FROM node:18-alpine
# Set the working directory in the container
WORKDIR /app
# Set environment variable to skip Chromium download
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
# Install git and Python
RUN apk update && apk add --no-cache git python3 make g++
# Create a symlink for python3 as python
RUN ln -sf /usr/bin/python3 /usr/bin/python
# Copy package.json and package-lock.json to the container
# COPY package*.json ./
COPY /src/Managing.Pinky/package.json ./
# Install dependencies with the --legacy-peer-deps flag to bypass peer dependency conflicts
RUN npm install --legacy-peer-deps
RUN npm install -g tailwindcss postcss autoprefixer @tailwindcss/typography
# Copy the rest of the app's source code to the container
# COPY . .
RUN ls -la
COPY src/Managing.Pinky/ .
RUN node --max-old-space-size=8192 ./node_modules/.bin/vite build
# Build the app
RUN npm run build
# Use NGINX as the web server
FROM nginx:alpine
# Copy the built app to the NGINX web server directory
# COPY --from=0 /app/build /usr/share/nginx/html
COPY --from=0 /app/dist /usr/share/nginx/html
# Expose port 80 for the NGINX web server
EXPOSE 80
# Start the NGINX web server
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) Managing
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,94 +0,0 @@
# vite-react-ts-extended [![Typecheck](https://github.com/laststance/vite-react-ts-extended/actions/workflows/typecheck.yml/badge.svg)](https://github.com/laststance/vite-react-ts-extended/actions/workflows/typecheck.yml) [![Test](https://github.com/laststance/vite-react-ts-extended/actions/workflows/test.yml/badge.svg)](https://github.com/laststance/vite-react-ts-extended/actions/workflows/test.yml) [![Build](https://github.com/laststance/vite-react-ts-extended/actions/workflows/build.yml/badge.svg)](https://github.com/laststance/vite-react-ts-extended/actions/workflows/build.yml) [![Lint](https://github.com/laststance/vite-react-ts-extended/actions/workflows/lint.yml/badge.svg)](https://github.com/laststance/vite-react-ts-extended/actions/workflows/lint.yml) [![Depfu](https://badges.depfu.com/badges/6c7775918ccc8647160750e168617a65/overview.svg)](https://depfu.com/github/laststance/vite-react-ts-extended?project_id=32682)
> My CRA alternative.
> Create plain and lightweight React+TS programming environment with familiar pre-setup tooling
> eslint/prettier, jest/TS/react-testing-library/msw, tailwindcss, CI.
## [Trying this Online!](https://codesandbox.io/s/vite-react-ts-extended-cbgyfz?file=/src/App.tsx)
<img src="https://digital3.nyc3.cdn.digitaloceanspaces.com/ext.png" />
This is the official [Vite](https://vitejs.dev/) template(`npm init vite@latest myapp -- --template react-ts`) and some extended setup.
- [eslint-typescript](https://github.com/typescript-eslint/typescript-eslint) and [Prettier](https://prettier.io/) integration. Rules are 100% my personal setup 💅
- [jest](https://jestjs.io/), [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/), [react-hooks-testing-library](https://github.com/testing-library/react-hooks-testing-library), [MSW](https://mswjs.io/)
- [tailwindcss](https://tailwindcss.com/)
- [Github Actions](https://github.com/features/actions)
All npm package are keeping least release version powered by [Depfu](https://depfu.com/).
# Installation
```
npx degit laststance/vite-react-ts-extended myapp
```
### yarn
```sh
cd myapp
yarn install
yarn validate # The installation was successful if no error occurs after running 'validate'.
yarn dev
```
### npm
```sh
cd myapp
npm install
npm run validate # The installation was successful if no error occurs after running 'validate'.
npm run dev
```
### Commands
```sh
yarn dev # start development server
yarn validate # run test,lint,build,typecheck concurrently
yarn test # run jest
yarn lint # run eslint
yarn lint:fix # run eslint with --fix option
yarn typecheck # run TypeScript compiler check
yarn build # build production bundle to 'dist' directly
yarn prettier # run prettier for json|yml|css|md|mdx files
yarn clean # remove 'node_modules' 'yarn.lock' 'dist' completely
yarn serve # launch server for production bundle in local
```
# Background
The evolution of the React framework is accelerating more than ever before.
[Next.js](https://nextjs.org/), [Remix](https://remix.run/), [RedwoodJS](https://redwoodjs.com/), [Gatsby](https://www.gatsbyjs.com/), [Blitz](https://blitzjs.com/) etc...
Ahthough I still need plain React programming starter some reason. (.e.g Demo, Experiment like Deep Dive React Core.)
So far, [create-react-app](https://github.com/facebook/create-react-app) **was** it.
In short, [create-react-app](https://github.com/facebook/create-react-app) development couldn't say active. Please read the [Issue](https://github.com/facebook/create-react-app/issues/11180) in details.
So I created an alternative to [create-react-app](https://github.com/facebook/create-react-app) for myself, based on [Vite](https://github.com/facebook/create-react-app).
This project contains my very opinionted setup,
but I hope it will be a useful tool for people who have similar needs to mine! 😀
# License
MIT
## Contributors ✨
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="http://ryota-murakami.github.io/"><img src="https://avatars1.githubusercontent.com/u/5501268?s=400&u=7bf6b1580b95930980af2588ef0057f3e9ec1ff8&v=4?s=100" width="100px;" alt=""/><br /><sub><b>ryota-murakami</b></sub></a><br /><a href="https://github.com/laststance/vite-react-ts-extended/laststance/vite-react-ts-extended/commits?author=ryota-murakami" title="Code">💻</a> <a href="https://github.com/laststance/vite-react-ts-extended/laststance/vite-react-ts-extended/commits?author=ryota-murakami" title="Documentation">📖</a> <a href="https://github.com/laststance/vite-react-ts-extended/laststance/vite-react-ts-extended/commits?author=ryota-murakami" title="Tests">⚠️</a></td>
</tr>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!

View File

@@ -1,493 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Happy Birthday My Love</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
body {
font-family: 'Press Start 2P', cursive;
background-color: #f5e6ff;
color: #5a3d7a;
overflow-x: hidden;
touch-action: manipulation;
}
.pixel-box {
border: 4px solid #5a3d7a;
box-shadow: 8px 8px 0 rgba(90, 61, 122, 0.2);
position: relative;
}
.pixel-box::after {
content: '';
position: absolute;
top: 2px;
left: 2px;
right: 2px;
bottom: 2px;
border: 2px solid #d9b3ff;
pointer-events: none;
}
.pixel-btn {
border: 3px solid #5a3d7a;
box-shadow: 4px 4px 0 rgba(90, 61, 122, 0.2);
transition: all 0.1s;
}
.pixel-btn:active {
transform: translate(2px, 2px);
box-shadow: 2px 2px 0 rgba(90, 61, 122, 0.2);
}
.pixel-divider {
height: 4px;
background-color: #5a3d7a;
position: relative;
}
.pixel-divider::after {
content: '';
position: absolute;
top: 2px;
left: 0;
right: 0;
height: 2px;
background-color: #d9b3ff;
}
.pixel-avatar {
border: 3px solid #5a3d7a;
}
.section {
min-height: 100vh;
padding: 2rem;
display: flex;
flex-direction: column;
justify-content: center;
}
.pixel-text {
text-shadow: 2px 2px 0 rgba(90, 61, 122, 0.2);
line-height: 1.6;
}
.pixel-scene {
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
/* Hide audio controls */
#bgMusic {
display: none;
}
/* Pixel heart animation */
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
.pixel-heart {
animation: pulse 1.5s infinite;
image-rendering: pixelated;
}
/* Password modal styles */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background-color: #fff;
padding: 2rem;
border-radius: 8px;
text-align: center;
animation: fadeIn 0.3s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: scale(0.8); }
to { opacity: 1; transform: scale(1); }
}
.content-hidden {
display: none;
}
.shake {
animation: shake 0.5s ease-in-out;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-10px); }
75% { transform: translateX(10px); }
}
</style>
</head>
<body class="relative">
<!-- Password Modal -->
<div id="passwordModal" class="modal-overlay">
<div class="modal-content pixel-box">
<h2 class="text-xl mb-6 text-purple-800 pixel-text">Enter Password</h2>
<div class="mb-4">
<input type="password" id="passwordInput"
class="w-full px-4 py-2 border-2 border-purple-300 rounded focus:outline-none focus:border-purple-500"
placeholder="Password">
</div>
<button id="submitPassword"
class="pixel-btn bg-purple-500 text-white px-6 py-2 rounded hover:bg-purple-600 transition-colors">
Enter
</button>
<p id="errorMessage" class="mt-4 text-red-500 text-sm hidden pixel-text">Incorrect password. Try again!</p>
</div>
</div>
<!-- Main Content -->
<div id="mainContent" class="content-hidden">
<!-- Background Music -->
<audio id="bgMusic" loop>
<source src="sound.mp3" type="audio/mpeg" autoplay>
</audio>
<!-- Music Toggle Button -->
<button id="musicToggle" class="fixed bottom-4 right-4 z-50 w-12 h-12 bg-purple-300 pixel-btn rounded-full flex items-center justify-center">
<svg id="musicIcon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#5a3d7a">
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>
</svg>
</button>
<!-- Navigation Dots -->
<div class="fixed right-4 top-1/2 transform -translate-y-1/2 z-40 flex flex-col space-y-3">
<a href="#section1" class="w-3 h-3 rounded-full bg-purple-300 border-2 border-purple-700 dot"></a>
<a href="#section2" class="w-3 h-3 rounded-full bg-purple-300 border-2 border-purple-700 dot"></a>
<a href="#section3" class="w-3 h-3 rounded-full bg-purple-300 border-2 border-purple-700 dot"></a>
<a href="#section4" class="w-3 h-3 rounded-full bg-purple-300 border-2 border-purple-700 dot"></a>
<a href="#section5" class="w-3 h-3 rounded-full bg-purple-300 border-2 border-purple-700 dot"></a>
<a href="#section6" class="w-3 h-3 rounded-full bg-purple-300 border-2 border-purple-700 dot"></a>
<a href="#section7" class="w-3 h-3 rounded-full bg-purple-300 border-2 border-purple-700 dot"></a>
<a href="#section8" class="w-3 h-3 rounded-full bg-purple-300 border-2 border-purple-700 dot"></a>
<a href="#section9" class="w-3 h-3 rounded-full bg-purple-300 border-2 border-purple-700 dot"></a>
</div>
<!-- Sections -->
<div id="section1" class="section bg-pink-100">
<div class="pixel-box bg-white p-6 mx-auto max-w-md">
<div class="flex justify-center mb-6">
<img src="https://img.freepik.com/premium-photo/high-rise-buildings-downtown-bangkok-night_5219-2322.jpg?w=996" alt="Rooftop pixel art" class="w-64 h-64 pixel-avatar">
</div>
<h1 class="text-xl md:text-2xl text-center mb-6 text-purple-800 pixel-text">Happy Birthday My Love!</h1>
<p class="text-xs md:text-sm mb-4 pixel-text">Our journey together began on a rooftop...</p>
<p class="text-xs md:text-sm pixel-text">Where we first met under the stars ✨</p>
<div class="flex justify-center mt-8">
<img src="https://cdn.pixabay.com/photo/2017/09/23/16/33/pixel-heart-2779422_1280.png" alt="Pixel heart" class="w-10 h-10 pixel-heart">
</div>
</div>
</div>
<div id="section2" class="section bg-purple-100">
<div class="pixel-box bg-white p-6 mx-auto max-w-md">
<div class="flex justify-center mb-6">
<img src="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQcLaTSzF2i_kDkosNQgoW8bgqg4rq-568TXw&s" alt="Park pixel art" class="w-64 h-64 pixel-avatar">
</div>
<h2 class="text-lg md:text-xl text-center mb-6 text-purple-800 pixel-text">Our First Date</h2>
<p class="text-xs md:text-sm mb-4 pixel-text">Walking in the park together</p>
<p class="text-xs md:text-sm pixel-text">I knew then you were special 💕</p>
<div class="flex justify-between mt-8">
<img src="pics/khaosok.JPG" alt="You" class="w-16 h-16 pixel-avatar">
<img src="pics/temple.jpg" alt="Her" class="w-16 h-16 pixel-avatar">
</div>
</div>
</div>
<div id="section3" class="section bg-pink-100">
<div class="pixel-box bg-white p-6 mx-auto max-w-md">
<div class="flex justify-center mb-6">
<img src="pics/cook.jpg" alt="Kitchen pixel art" class="w-64 h-64 pixel-avatar">
</div>
<h2 class="text-lg md:text-xl text-center mb-6 text-purple-800 pixel-text">Our First Cooking Night</h2>
<p class="text-xs md:text-sm mb-4 pixel-text">Making dinner together in your kitchen</p>
<p class="text-xs md:text-sm pixel-text">Simple moments that mean everything 🍳</p>
<div class="flex justify-center mt-8">
<img src="https://cdn.pixabay.com/photo/2017/09/23/16/33/pixel-heart-2779422_1280.png" alt="Pixel heart" class="w-10 h-10 pixel-heart">
</div>
</div>
</div>
<div id="section4" class="section bg-purple-100">
<div class="pixel-box bg-white p-6 mx-auto max-w-md">
<div class="flex justify-center mb-6">
<img src="https://www.airportels.asia/wp-content/uploads/2023/12/Shopping-Spots-in-Chinatown-scaled.webp" alt="Chinese restaurant pixel art" class="w-64 h-64 pixel-avatar">
</div>
<h2 class="text-lg md:text-xl text-center mb-6 text-purple-800 pixel-text">Chinese Restaurant</h2>
<p class="text-xs md:text-sm mb-4 pixel-text">"I like to try things"</p>
<p class="text-xs md:text-sm pixel-text">And trying life with you has been the best adventure 🥢</p>
<div class="flex justify-center mt-8">
<img src="https://cdn.pixabay.com/photo/2017/09/23/16/33/pixel-heart-2779422_1280.png" alt="Pixel heart" class="w-10 h-10 pixel-heart">
</div>
</div>
</div>
<div id="section5" class="section bg-pink-100">
<div class="pixel-box bg-white p-6 mx-auto max-w-md">
<div class="flex justify-center mb-6">
<img src="https://media-cdn.tripadvisor.com/media/attractions-splice-spp-674x446/07/00/a4/db.jpg" alt="Kao Sok pixel art" class="w-64 h-64 pixel-avatar">
</div>
<h2 class="text-lg md:text-xl text-center mb-6 text-purple-800 pixel-text">Kao Sok Adventure</h2>
<p class="text-xs md:text-sm mb-4 pixel-text">Our trip to Kao Sok with my best friend</p>
<p class="text-xs md:text-sm pixel-text">Sharing my world with you 🌿</p>
<div class="flex justify-center mt-8">
<img src="https://cdn.pixabay.com/photo/2017/09/23/16/33/pixel-heart-2779422_1280.png" alt="Pixel jungle" class="w-24 h-24">
</div>
</div>
</div>
<div id="section6" class="section bg-purple-100">
<div class="pixel-box bg-white p-6 mx-auto max-w-md">
<div class="flex justify-center mb-6">
<img src="https://dynamic-media-cdn.tripadvisor.com/media/photo-o/07/b9/58/b4/photo0jpg.jpg?w=1200&h=-1&s=1&cx=1000&cy=543&chk=v1_d6044045ef865bfe074a" alt="Kuala Lumpur pixel art" class="w-64 h-64 pixel-avatar">
</div>
<h2 class="text-lg md:text-xl text-center mb-6 text-purple-800 pixel-text">Kuala Lumpur</h2>
<p class="text-xs md:text-sm mb-4 pixel-text">Joining you during your work trip</p>
<p class="text-xs md:text-sm pixel-text">Because every moment with you is precious 🌆</p>
<div class="flex justify-center mt-8">
<img src="https://cdn.pixabay.com/photo/2017/09/23/16/33/pixel-heart-2779422_1280.png" alt="Pixel heart" class="w-10 h-10 pixel-heart">
</div>
</div>
</div>
<div id="section7" class="section bg-pink-100">
<div class="pixel-box bg-white p-6 mx-auto max-w-md">
<div class="flex justify-center mb-6">
<img src="https://img.static-af.com/transform/45cb9a13-b167-4842-8ea8-05d0cc7a4d04/" alt="France pixel art" class="w-64 h-64 pixel-avatar">
</div>
<h2 class="text-lg md:text-xl text-center mb-6 text-purple-800 pixel-text">Time Apart</h2>
<p class="text-xs md:text-sm mb-4 pixel-text">Me leaving for France</p>
<p class="text-xs md:text-sm mb-4 pixel-text">You facing work difficulties</p>
<p class="text-xs md:text-sm text-purple-600 pixel-text">Remember to take time for yourself</p>
<p class="text-xs md:text-sm text-purple-600 pixel-text">Your health and happiness matter most 💖</p>
</div>
</div>
<div id="section8" class="section bg-gradient-to-b from-purple-200 to-pink-200">
<div class="pixel-box bg-white p-6 mx-auto max-w-md">
<div class="flex justify-center mb-6">
<img src="pics/boat.jpg" alt="Birthday kiss pixel art" class="w-64 h-64 pixel-avatar">
</div>
<h2 class="text-lg md:text-xl text-center mb-6 text-purple-800 pixel-text">Happy Birthday!</h2>
<p class="text-xs md:text-sm mb-4 pixel-text">Back together in Bangkok</p>
<p class="text-xs md:text-sm mb-6 pixel-text">Celebrating you on your special day 🎂</p>
<div class="flex justify-center mb-6">
<img src="https://cdn.pixabay.com/photo/2017/09/23/16/33/pixel-heart-2779422_1280.png" alt="Pixel heart" class="w-16 h-16 pixel-heart">
</div>
<p class="text-xs md:text-sm text-center pixel-text">I love you more than words can say</p>
<p class="text-xs md:text-sm text-center text-purple-600 pixel-text mt-4">(Tap the heart below)</p>
<div class="flex justify-center mt-6">
<button id="finalHeart" class="pixel-btn bg-pink-200 p-4 rounded-full">
<img src="https://cdn.pixabay.com/photo/2017/09/23/16/33/pixel-heart-2779422_1280.png" alt="Pixel heart" class="w-12 h-12 pixel-heart">
</button>
</div>
<div id="kissAnimation" class="hidden mt-6 flex justify-center">
<img src="https://images-wixmp-ed30a86b8c4ca887773594c2.wixmp.com/f/e7781e25-3bf5-4185-aad7-ec93d0b5e1b0/d7fa2wq-cf60174b-9a41-44de-bb2f-605cde85dad5.gif?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1cm46YXBwOjdlMGQxODg5ODIyNjQzNzNhNWYwZDQxNWVhMGQyNmUwIiwiaXNzIjoidXJuOmFwcDo3ZTBkMTg4OTgyMjY0MzczYTVmMGQ0MTVlYTBkMjZlMCIsIm9iaiI6W1t7InBhdGgiOiJcL2ZcL2U3NzgxZTI1LTNiZjUtNDE4NS1hYWQ3LWVjOTNkMGI1ZTFiMFwvZDdmYTJ3cS1jZjYwMTc0Yi05YTQxLTQ0ZGUtYmIyZi02MDVjZGU4NWRhZDUuZ2lmIn1dXSwiYXVkIjpbInVybjpzZXJ2aWNlOmZpbGUuZG93bmxvYWQiXX0.vX_oTdMeJ4D5xWpqNQV4NOjiJW4ahpQWZ4Fyp4xX8x8" alt="Pixel kiss" class="w-32 h-32">
</div>
</div>
</div>
</div>
<script>
// Music toggle functionality
const musicToggle = document.getElementById('musicToggle');
const musicIcon = document.getElementById('musicIcon');
const bgMusic = document.getElementById('bgMusic');
let musicPlaying = true;
// Attempt to autoplay music (may be blocked by browser)
document.addEventListener('DOMContentLoaded', function() {
const playPromise = bgMusic.play();
if (playPromise !== undefined) {
playPromise.then(_ => {
musicPlaying = true;
musicIcon.innerHTML = '<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>';
})
.catch(error => {
musicPlaying = false;
musicIcon.innerHTML = '<path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/>';
});
}
});
musicToggle.addEventListener('click', function() {
if (musicPlaying) {
bgMusic.pause();
musicIcon.innerHTML = '<path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/>';
musicPlaying = false;
} else {
bgMusic.play();
musicIcon.innerHTML = '<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>';
musicPlaying = true;
}
});
// Final heart click animation
const finalHeart = document.getElementById('finalHeart');
const kissAnimation = document.getElementById('kissAnimation');
finalHeart.addEventListener('click', function() {
kissAnimation.classList.remove('hidden');
finalHeart.classList.add('hidden');
// Create floating hearts
for (let i = 0; i < 10; i++) {
createFloatingHeart();
}
});
function createFloatingHeart() {
const heart = document.createElement('div');
heart.innerHTML = '❤️';
heart.className = 'absolute text-xl animate-float';
heart.style.left = Math.random() * 100 + 'vw';
heart.style.top = '100vh';
heart.style.animationDuration = (Math.random() * 3 + 2) + 's';
document.body.appendChild(heart);
// Remove heart after animation
setTimeout(() => {
heart.remove();
}, 5000);
}
// Add animation style
const style = document.createElement('style');
style.innerHTML = `
@keyframes float {
0% {
transform: translateY(0) rotate(0deg);
opacity: 1;
}
100% {
transform: translateY(-100vh) rotate(360deg);
opacity: 0;
}
}
.animate-float {
animation: float linear forwards;
}
`;
document.head.appendChild(style);
// Update navigation dots based on scroll position
const dots = document.querySelectorAll('.dot');
const sections = document.querySelectorAll('.section');
function updateActiveDot() {
const scrollPosition = window.scrollY;
sections.forEach((section, index) => {
const sectionTop = section.offsetTop;
const sectionHeight = section.offsetHeight;
if (scrollPosition >= sectionTop - 50 && scrollPosition < sectionTop + sectionHeight - 50) {
dots.forEach(dot => dot.classList.remove('bg-purple-700'));
dots[index].classList.add('bg-purple-700');
}
});
}
window.addEventListener('scroll', updateActiveDot);
updateActiveDot(); // Initialize
// Password protection
document.addEventListener('DOMContentLoaded', function() {
const passwordModal = document.getElementById('passwordModal');
const mainContent = document.getElementById('mainContent');
const passwordInput = document.getElementById('passwordInput');
const submitButton = document.getElementById('submitPassword');
const errorMessage = document.getElementById('errorMessage');
const correctPassword = 'iloveyou';
const bgMusic = document.getElementById('bgMusic');
const musicToggle = document.getElementById('musicToggle');
const musicIcon = document.getElementById('musicIcon');
// Check if already authenticated
if (sessionStorage.getItem('authenticated') === 'true') {
passwordModal.style.display = 'none';
mainContent.classList.remove('content-hidden');
// Start playing music
bgMusic.play().catch(error => {
console.log('Auto-play prevented by browser');
musicPlaying = false;
updateMusicIcon(false);
});
}
function checkPassword() {
if (passwordInput.value === correctPassword) {
passwordModal.style.display = 'none';
mainContent.classList.remove('content-hidden');
sessionStorage.setItem('authenticated', 'true');
// Start playing music after password entry
bgMusic.play().catch(error => {
console.log('Auto-play prevented by browser');
musicPlaying = false;
updateMusicIcon(false);
});
} else {
errorMessage.classList.remove('hidden');
passwordInput.value = '';
passwordModal.querySelector('.modal-content').classList.add('shake');
setTimeout(() => {
passwordModal.querySelector('.modal-content').classList.remove('shake');
}, 500);
}
}
function updateMusicIcon(playing) {
if (playing) {
musicIcon.innerHTML = '<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>';
} else {
musicIcon.innerHTML = '<path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/>';
}
}
// Music toggle functionality
musicToggle.addEventListener('click', function() {
if (musicPlaying) {
bgMusic.pause();
musicPlaying = false;
} else {
bgMusic.play();
musicPlaying = true;
}
updateMusicIcon(musicPlaying);
});
submitButton.addEventListener('click', checkPassword);
passwordInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
checkPassword();
}
});
});
</script>
</body>
</html>

View File

@@ -1,32 +0,0 @@
#!/bin/bash
# Array of known potentially hanging packages
hanging_packages=("xmlhttprequest-ssl@latest" "engine.io-parser@latest")
# Timeout in seconds for each package installation attempt
install_with_timeout() {
package=$1
echo "Attempting to install $package with a timeout of $timeout_duration seconds."
# Start npm install in the background
npm install $package --verbose &> $package.log &
# Get PID of the npm process
pid=$!
# Wait for the npm process to finish or timeout
(sleep $timeout_duration && kill -0 $pid 2>/dev/null && kill -9 $pid && echo "Timeout reached for $package, process killed." && echo $package >> exclude.log) &
waiter_pid=$!
# Wait for the npm process to complete
wait $pid
# Kill the waiter process in case npm finished before the timeout
kill -0 $waiter_pid 2>/dev/null && kill -9 $waiter_pid
}
# Install potentially hanging packages first with a timeout
for package in "${hanging_packages[@]}"; do
install_with_timeout $package
done

View File

@@ -1,35 +0,0 @@
const config = {
collectCoverageFrom: ['<rootDir>/src/**/*.{js,jsx,ts,tsx}', '!src/**/*.d.ts'],
moduleDirectories: ['node_modules'],
moduleFileExtensions: ['js', 'mjs', 'jsx', 'ts', 'tsx', 'json'],
moduleNameMapper: {
'^.+\\.module\\.(css|sass|scss)$': 'identity-obj-proxy',
},
notify: true,
notifyMode: 'success-change',
resetMocks: true,
roots: ['<rootDir>'],
setupFilesAfterEnv: ['<rootDir>/jest/setupTests.ts'],
testEnvironment: 'jsdom',
testMatch: [
'<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}',
'<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}',
],
transform: {
'^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|json)$)':
'<rootDir>/jest/fileTransform.js',
'^.+\\.[jt]sx?$': 'esbuild-jest',
'^.+\\.css$': '<rootDir>/jest/cssTransform.js',
},
transformIgnorePatterns: [
'[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$',
'^.+\\.module\\.(css|sass|scss)$',
],
verbose: true,
watchPlugins: [
'jest-watch-typeahead/filename',
'jest-watch-typeahead/testname',
],
}
module.exports = config

File diff suppressed because it is too large Load Diff

View File

@@ -1,48 +0,0 @@
{
"name": "managing",
"version": "2.0.0",
"license": "MIT",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"serve": "serve -s dist -p 3000",
"test": "jest",
"lint": "eslint . --ext .ts,.tsx,.js,jsx",
"lint:fix": "eslint . --ext .ts,.tsx,.js,jsx --fix",
"typecheck": "tsc --noEmit",
"prettier": "prettier --write \"**/*.+(json|yml|css|md|mdx)\"",
"clean": "rimraf node_modules yarn.lock dist",
"validate": "./scripts/validate"
},
"dependencies": {
"@heroicons/react": "^1.0.6",
"@tailwindcss/typography": "^0.5.0",
"@tanstack/react-query": "^5.67.1",
"autoprefixer": "^10.4.7",
"classnames": "^2.3.1",
"jotai": "^1.6.7",
"latest-version": "^9.0.0"
},
"devDependencies": {
"@types/elliptic": "^6.4.18",
"@types/react": "^18.0.9",
"@types/react-dom": "^18.0.4",
"@types/react-grid-layout": "^1.3.2",
"@types/react-plotly.js": "^2.6.0",
"@types/react-slider": "^1.3.1",
"@types/react-table": "^7.7.12",
"@vitejs/plugin-react": "^1.3.2",
"autoprefixer": "^10.4.7",
"daisyui": "^3.5.1",
"postcss": "^8.4.13",
"prettier": "^2.6.1",
"prettier-plugin-tailwind-css": "^1.5.0",
"tailwindcss": "^3.0.23",
"typescript": "^5.7.3",
"vite": "^6.0.11"
},
"msw": {
"workerDirectory": ""
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 475 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 850 KiB

View File

@@ -1,6 +0,0 @@
module.exports = {
plugins: {
autoprefixer: {},
tailwindcss: {},
},
}

View File

@@ -1,4 +0,0 @@
module.exports = {
plugins: [require('prettier-plugin-tailwindcss')],
tailwindConfig: './tailwind.config.js',
}

Binary file not shown.

View File

@@ -1,12 +0,0 @@
module.exports = {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
daisyui: {
themes: ['black', 'coffee', 'cyberpunk', 'lofi', 'retro', 'kaigen'],
},
plugins: [require('@tailwindcss/typography'), require('daisyui')],
theme: {
container: {
center: true,
},
},
}

View File

@@ -1,21 +0,0 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["./src", "hardhat.config.js"],
"exclude": ["node_modules"]
}

View File

@@ -1,21 +0,0 @@
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
export default defineConfig({
build: {
minify: false,
sourcemap: false,
target: 'es2022',
},
optimizeDeps: {
esbuildOptions: {
target: 'es2022',
},
},
plugins: [react()],
publicDir: 'assets',
server: {
host: true,
open: true,
},
})

Some files were not shown because too many files have changed in this diff Show More