Compare commits

..

81 Commits

Author SHA1 Message Date
955c358138 Improve per on price update
Some checks failed
Build & Deploy / build-and-deploy (push) Has been cancelled
.NET / build (push) Has been cancelled
2025-08-16 17:02:31 +07:00
750f6cebbb rollback contracts 2025-08-16 07:37:47 +07:00
14f5cb0971 Update logs 2025-08-16 06:32:25 +07:00
7271889bdf Fix orleans local 2025-08-16 06:21:26 +07:00
3dbd2e91ea fix github build 2025-08-16 06:10:41 +07:00
4ff2ccdae3 Add Admin roles 2025-08-16 06:06:02 +07:00
7923b38a26 update orleans 2025-08-16 05:30:12 +07:00
2861a7f469 fix a bit orleans 2025-08-16 05:23:28 +07:00
6df6061d66 Update silo 2025-08-16 05:17:04 +07:00
eeb2923646 Update silo/cluster config 2025-08-16 05:09:04 +07:00
d2975be0f5 Merge workers into API 2025-08-16 04:55:33 +07:00
9841219e8b Remove workflow
Some checks failed
Build & Deploy / build-and-deploy (push) Has been cancelled
.NET / build (push) Has been cancelled
2025-08-16 01:33:15 +07:00
ae4d5b8abe Fix sln build 2025-08-16 00:59:43 +07:00
6ade009901 Update DSN sentry 2025-08-16 00:54:37 +07:00
137444a045 Add best agent by pnl 2025-08-15 22:35:29 +07:00
e73af1dd3a Fix address whitelist check 2025-08-15 22:25:59 +07:00
994fd5d9a6 Remove console.log 2025-08-15 22:13:25 +07:00
8315c36f30 Update proxy 2025-08-15 22:09:34 +07:00
ece75b1973 fix agent summary update 2025-08-15 21:49:27 +07:00
513f880243 Update api client 2025-08-15 21:27:32 +07:00
b4f6dc871b Add volume history to platform summary 2025-08-15 21:27:07 +07:00
4292e9e02f fix save agent summary 2025-08-15 21:09:26 +07:00
289fd25dc3 Add agent balance fetch from proxy 2025-08-15 20:52:37 +07:00
b178f15beb Add new endpoint to retrieve balance 2025-08-15 20:18:02 +07:00
cd93dede4e Add agentbalance 2025-08-15 19:35:01 +07:00
f58d1cea3b Add deployment mode 2025-08-15 17:01:19 +07:00
0966eace58 Disable orleans reminder for deploy and add whitelisted addresses 2025-08-15 16:48:23 +07:00
54bf914e95 fix no candle when closing position 2025-08-15 09:13:26 +07:00
8eefab4597 Fix concurrent on userStrategies 2025-08-15 08:56:32 +07:00
b4a4656b3b Update the position count and initiator 2025-08-15 08:47:48 +07:00
7528405845 update stats data 2025-08-15 07:42:26 +07:00
0a4a4e1398 Update plateform summary 2025-08-15 06:54:09 +07:00
e6c3ec139a Add event 2025-08-15 01:23:39 +07:00
2622da05e6 Update open interest 2025-08-14 23:53:45 +07:00
580ce4d9c9 Fix run time 2025-08-14 23:28:21 +07:00
8d37b04d3f Update front and fix back 2025-08-14 20:17:13 +07:00
4a45d6c970 Add platform grain 2025-08-14 19:44:33 +07:00
345d76e06f Update plateform summary 2025-08-14 18:59:37 +07:00
cfb04e9dc9 Fix concurrent 2025-08-14 18:31:44 +07:00
0a2b7aa335 fix concurrent 2025-08-14 18:11:22 +07:00
6a2e4e81b1 Update status to match UI 2025-08-14 18:08:31 +07:00
e4049045c3 Fix concurrency 2025-08-14 17:49:05 +07:00
aacb92018f Update cache for userStrategy details 2025-08-14 17:42:07 +07:00
9d0c7cf834 Fix bots restart/stop 2025-08-13 22:22:22 +07:00
46a6cdcd87 Fix manual position open 2025-08-07 14:47:36 +07:00
b1c1c8725d Update strategies agent return 2025-08-06 19:47:13 +07:00
a0bd2e2100 Update strategy details models reponse 2025-08-06 17:03:19 +07:00
93502ca7cc Remove cache for UserStrategies 2025-08-06 16:31:16 +07:00
93a6f9fd9e Stop all bot for a user 2025-08-06 16:03:42 +07:00
b70018ba15 Update summary on agentName change 2025-08-06 14:57:58 +07:00
5dcb5c318e Update docker file 2025-08-05 22:43:17 +07:00
ea85d8d519 Update dockerfile 2025-08-05 22:39:44 +07:00
36529ae403 Fix db and fix endpoints 2025-08-05 22:30:18 +07:00
2dcbcc3ef2 Clear a bit more 2025-08-05 19:34:42 +07:00
0c8c3de807 clean a bit more 2025-08-05 19:32:24 +07:00
3d3f71ac7a Move workers 2025-08-05 17:53:19 +07:00
7d92031059 Clean namings and namespace 2025-08-05 17:45:44 +07:00
843239d187 Fix mediator 2025-08-05 17:31:10 +07:00
6c63b80f4a Fix get online agentnames 2025-08-05 05:09:50 +07:00
eaf18189e4 Allow Anonymous on data controller 2025-08-05 05:00:06 +07:00
4d63b9e970 Fix jwt token 2025-08-05 04:51:24 +07:00
2f1abb3f05 Add new migration 2025-08-05 04:34:54 +07:00
05d44d0c25 Update index 2025-08-05 04:18:02 +07:00
434f61f2de Clean migration 2025-08-05 04:13:02 +07:00
Oda
082ae8714b Trading bot grain (#33)
* Trading bot Grain

* Fix a bit more of the trading bot

* Advance on the tradingbot grain

* Fix build

* Fix db script

* Fix user login

* Fix a bit backtest

* Fix cooldown and backtest

* start fixing bot start

* Fix startup

* Setup local db

* Fix build and update candles and scenario

* Add bot registry

* Add reminder

* Updateing the grains

* fix bootstraping

* Save stats on tick

* Save bot data every tick

* Fix serialization

* fix save bot stats

* Fix get candles

* use dict instead of list for position

* Switch hashset to dict

* Fix a bit

* Fix bot launch and bot view

* add migrations

* Remove the tolist

* Add agent grain

* Save agent summary

* clean

* Add save bot

* Update get bots

* Add get bots

* Fix stop/restart

* fix Update config

* Update scanner table on new backtest saved

* Fix backtestRowDetails.tsx

* Fix agentIndex

* Update agentIndex

* Fix more things

* Update user cache

* Fix

* Fix account load/start/restart/run
2025-08-05 04:07:06 +07:00
cd378587aa Add wallet balances to the userStrategy 2025-07-31 21:34:05 +07:00
5fabfbfadd fix backtest credit 2025-07-31 20:58:37 +07:00
857ca348ba Add agentNames to the endpoint index 2025-07-31 16:51:26 +07:00
6cd28a4edb Return only online agent name 2025-07-31 15:55:03 +07:00
c454e87d7a Add new endpoint for the agent status 2025-07-30 22:36:49 +07:00
4b0da0e864 Add agent index with pagination 2025-07-30 22:27:01 +07:00
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
416 changed files with 19598 additions and 21823 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,6 @@ 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
- when dividing, make sure variable is not zero
Follow the official Microsoft documentation and ASP.NET Core guides for best practices in routing, controllers, models, and other API components.

View File

@@ -24,7 +24,3 @@ jobs:
run: dotnet restore ./src/Managing.Api/Managing.Api.csproj run: dotnet restore ./src/Managing.Api/Managing.Api.csproj
- name: Build API - name: Build API
run: dotnet build --no-restore ./src/Managing.Api/Managing.Api.csproj run: dotnet build --no-restore ./src/Managing.Api/Managing.Api.csproj
- name: Restore Worker dependencies
run: dotnet restore ./src/Managing.Api.Workers/Managing.Api.Workers.csproj
- name: Build Worker
run: dotnet build --no-restore ./src/Managing.Api.Workers/Managing.Api.Workers.csproj

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

View File

@@ -0,0 +1,114 @@
# Worker Consolidation Summary
## Overview
Successfully consolidated the separate Managing.Api.Workers project into the main Managing.Api project as background services. This eliminates Orleans conflicts and simplifies deployment while maintaining all worker functionality.
## Changes Made
### 1. ✅ Updated ApiBootstrap.cs
- **File**: `src/Managing.Bootstrap/ApiBootstrap.cs`
- **Changes**: Added all worker services from WorkersBootstrap to the main AddWorkers method
- **Workers Added**:
- PricesFifteenMinutesWorker
- PricesOneHourWorker
- PricesFourHoursWorker
- PricesOneDayWorker
- PricesFiveMinutesWorker
- SpotlightWorker
- TraderWatcher
- LeaderboardWorker
- FundingRatesWatcher
- GeneticAlgorithmWorker
- BundleBacktestWorker
- BalanceTrackingWorker
- NotifyBundleBacktestWorker
### 2. ✅ Configuration Files Updated
- **File**: `src/Managing.Api/appsettings.json`
- **File**: `src/Managing.Api/appsettings.Oda-docker.json`
- **Changes**: Added worker configuration flags to control which workers run
- **Default Values**: All workers disabled by default (set to `false`)
### 3. ✅ Deployment Scripts Updated
- **Files**:
- `scripts/build_and_run.sh`
- `scripts/docker-deploy-local.cmd`
- `scripts/docker-redeploy-oda.cmd`
- `scripts/docker-deploy-sandbox.cmd`
- **Changes**: Removed worker-specific build and deployment commands
### 4. ✅ Docker Compose Files Updated
- **Files**:
- `src/Managing.Docker/docker-compose.yml`
- `src/Managing.Docker/docker-compose.local.yml`
- **Changes**: Removed managing.api.workers service definitions
### 5. ✅ Workers Project Deprecated
- **File**: `src/Managing.Api.Workers/Program.cs`
- **Changes**: Added deprecation notice and removed Orleans configuration
- **Note**: Project kept for reference but should not be deployed
## Benefits Achieved
### ✅ Orleans Conflicts Resolved
- **Before**: Two Orleans clusters competing for same ports (11111/30000)
- **After**: Single Orleans cluster in main API
- **Impact**: No more port conflicts or cluster identity conflicts
### ✅ Simplified Architecture
- **Before**: Two separate applications to deploy and monitor
- **After**: Single application with all functionality
- **Impact**: Easier deployment, monitoring, and debugging
### ✅ Resource Efficiency
- **Before**: Duplicate service registrations and database connections
- **After**: Shared resources and connection pools
- **Impact**: Better performance and resource utilization
### ✅ Configuration Management
- **Before**: Separate configuration files for workers
- **After**: Centralized configuration with worker flags
- **Impact**: Easier to manage and control worker execution
## How to Enable/Disable Workers
Workers are controlled via configuration flags in `appsettings.json`:
```json
{
"WorkerPricesFifteenMinutes": false,
"WorkerPricesOneHour": false,
"WorkerPricesFourHours": false,
"WorkerPricesOneDay": false,
"WorkerPricesFiveMinutes": false,
"WorkerSpotlight": false,
"WorkerTraderWatcher": false,
"WorkerLeaderboard": false,
"WorkerFundingRatesWatcher": false,
"WorkerGeneticAlgorithm": false,
"WorkerBundleBacktest": false,
"WorkerBalancesTracking": false,
"WorkerNotifyBundleBacktest": false
}
```
Set any worker to `true` to enable it in that environment.
## Testing
### ✅ Build Verification
- Main API project builds successfully
- All worker dependencies resolved
- No compilation errors
### Next Steps for Full Verification
1. **Runtime Testing**: Start the main API and verify workers load correctly
2. **Worker Functionality**: Test that enabled workers execute as expected
3. **Orleans Integration**: Verify workers can access Orleans grains properly
4. **Configuration Testing**: Test enabling/disabling workers via config
## Migration Complete
The worker consolidation is now complete. The Managing.Api project now contains all functionality previously split between the API and Workers projects, providing a more maintainable and efficient architecture.
**Deployment**: Use only the main API deployment scripts. The Workers project should not be deployed.

View File

@@ -107,22 +107,6 @@
- [x] Add button to display money management use by the bot - [x] Add button to display money management use by the bot
- [ ] POST POWNER - On the modarl, When simple bot selected, show only a select for the workflow - [ ] POST POWNER - On the modarl, When simple bot selected, show only a select for the workflow
## Workflow
- [x] List all workflow saved in
- [x] Use https://reactflow.dev/ to display a workflow (map flow to nodes and children to edges)
- [x] On update Nodes : https://codesandbox.io/s/dank-waterfall-8jfcf4?file=/src/App.js
- [x] Save workflow
- [ ] Reset workflow
- [ ] Add flows : Close Position, SendMessage
- [ ] On Flow.tsx : Display inputs/outputs names on the node
- [ ] Setup file tree UI for available flows : https://codesandbox.io/s/nlzui
- [x] Create a workflow type that will encapsulate a list of flows
- [x] Each flow will have parameters, inputs and outputs that will be used by the children flows
- [ ] Flow can handle multiple parents
- [ ] Run Simple bot base on a workflow
- [ ] Run backtest based on a workflow
- [ ] Add flows : ClosePosition, Scenario
## Backtests ## Backtests

View File

@@ -1,27 +0,0 @@
```mermaid
classDiagram
Workflow <|-- Flow
class Workflow{
String Name
Usage Usage : Trading|Task
Flow[] Flows
String Description
}
class Flow{
String Name
CategoryType Category
FlowType Type
FlowParameters Parameters
String Description
FlowType? AcceptedInput
OutputType[]? Outputs
Flow[]? ChildrenFlow
Flow? ParentFlow
Output? Output : Signal|Text|Candles
MapInput(AcceptedInput, ParentFlow.Output)
Run(ParentFlow.Output)
LoadChildren()
ExecuteChildren()
}
```

View File

@@ -1,4 +0,0 @@
{
"schemaVersion": 2,
"dockerfilePath": "./src/Managing.Pinky/Dockerfile-pinky"
}

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

@@ -3,11 +3,8 @@
# Navigate to the src directory # Navigate to the src directory
cd ../src cd ../src
# Build the managing.api image # Build the managing.api image (now includes all workers as background services)
docker build -t managing.api -f Managing.Api/Dockerfile . --no-cache docker build -t managing.api -f Managing.Api/Dockerfile . --no-cache
# Build the managing.api.workers image
docker build -t managing.api.workers -f Managing.Api.Workers/Dockerfile . --no-cache
# Start up the project using docker-compose # Start up the project using docker-compose
docker compose -f Managing.Docker/docker-compose.yml -f Managing.Docker/docker-compose.local.yml up -d docker compose -f Managing.Docker/docker-compose.yml -f Managing.Docker/docker-compose.local.yml up -d

View File

@@ -1,5 +1,4 @@
cd .. cd ..
cd .\src\ cd .\src\
docker build -t managing.api -f ./Managing.Api/Dockerfile . --no-cache docker build -t managing.api -f ./Managing.Api/Dockerfile . --no-cache
docker build -t managing.api.workers -f ./Managing.Api.Workers/Dockerfile . --no-cache
docker-compose -f ./Managing.Docker/docker-compose.yml -f ./Managing.Docker/docker-compose.local.yml up -d docker-compose -f ./Managing.Docker/docker-compose.yml -f ./Managing.Docker/docker-compose.local.yml up -d

View File

@@ -1,5 +1,4 @@
cd .. cd ..
cd .\src\ cd .\src\
docker build -t managing.api -f ./Managing.Api/Dockerfile . --no-cache docker build -t managing.api -f ./Managing.Api/Dockerfile . --no-cache
docker build -t managing.api.workers -f ./Managing.Api.Workers/Dockerfile . --no-cache
docker-compose -f ./Managing.Docker/docker-compose.yml -f ./Managing.Docker/docker-compose.sandbox.yml up -d docker-compose -f ./Managing.Docker/docker-compose.yml -f ./Managing.Docker/docker-compose.sandbox.yml up -d

View File

@@ -2,21 +2,16 @@ cd ..
cd .\src\ cd .\src\
ECHO "Stopping containers..." ECHO "Stopping containers..."
docker stop sandbox-managing.api-1 docker stop sandbox-managing.api-1
docker stop sandbox-managing.api.workers-1
ECHO "Contaiters stopped" ECHO "Contaiters stopped"
ECHO "Removing containers..." ECHO "Removing containers..."
docker rm sandbox-managing.api-1 docker rm sandbox-managing.api-1
docker rm sandbox-managing.api.workers-1
ECHO "Containers removed" ECHO "Containers removed"
ECHO "Removing images..." ECHO "Removing images..."
docker rmi managing.api docker rmi managing.api
docker rmi managing.api:latest docker rmi managing.api:latest
docker rmi managing.api.workers
docker rmi managing.api.workers:latest
ECHO "Images removed" ECHO "Images removed"
ECHO "Building images..." ECHO "Building images..."
docker build -t managing.api -f ./Managing.Api/Dockerfile . --no-cache docker build -t managing.api -f ./Managing.Api/Dockerfile . --no-cache
docker build -t managing.api.workers -f ./Managing.Api.Workers/Dockerfile . --no-cache
ECHO "Deploying..." ECHO "Deploying..."
docker-compose -f ./Managing.Docker/docker-compose.yml -f ./Managing.Docker/docker-compose.sandbox.yml up -d docker-compose -f ./Managing.Docker/docker-compose.yml -f ./Managing.Docker/docker-compose.sandbox.yml up -d
ECHO "Deployed" ECHO "Deployed"

View File

@@ -18,7 +18,7 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
LOGS_DIR="$SCRIPT_DIR/$LOGS_DIR_NAME" LOGS_DIR="$SCRIPT_DIR/$LOGS_DIR_NAME"
mkdir -p "$LOGS_DIR" || { echo "Failed to create logs directory: $LOGS_DIR"; exit 1; } mkdir -p "$LOGS_DIR" || { echo "Failed to create logs directory: $LOGS_DIR"; exit 1; }
LOG_FILE="./logs/migration_${ENVIRONMENT}_${TIMESTAMP}.log" LOG_FILE="$SCRIPT_DIR/logs/migration_${ENVIRONMENT}_${TIMESTAMP}.log"
# Colors for output # Colors for output
RED='\033[0;31m' RED='\033[0;31m'
@@ -155,6 +155,12 @@ extract_connection_details() {
log "📋 Extracted connection details: $DB_HOST:$DB_PORT/$DB_NAME (user: $DB_USER, password: $DB_PASSWORD)" log "📋 Extracted connection details: $DB_HOST:$DB_PORT/$DB_NAME (user: $DB_USER, password: $DB_PASSWORD)"
} }
# Helper function to get the first migration name
get_first_migration() {
local first_migration=$(cd "$DB_PROJECT_PATH" && dotnet ef migrations list --no-build --startup-project "$API_PROJECT_PATH" | head -1 | awk '{print $1}')
echo "$first_migration"
}
# Helper function to test PostgreSQL connectivity # Helper function to test PostgreSQL connectivity
test_postgres_connectivity() { test_postgres_connectivity() {
if ! command -v psql >/dev/null 2>&1; then if ! command -v psql >/dev/null 2>&1; then
@@ -243,13 +249,6 @@ else
error "❌ Failed to build Managing.Infrastructure.Database project" error "❌ Failed to build Managing.Infrastructure.Database project"
fi fi
log "🔧 Building Managing.Api project..."
if (cd "$API_PROJECT_PATH" && dotnet build); then
log "✅ Managing.Api project built successfully"
else
error "❌ Failed to build Managing.Api project"
fi
# Step 1: Check Database Connection and Create if Needed # Step 1: Check Database Connection and Create if Needed
log "🔧 Step 1: Checking database connection and creating database if needed..." log "🔧 Step 1: Checking database connection and creating database if needed..."
@@ -417,13 +416,23 @@ else
error " This is critical. Please review the previous error messages and your connection string for '$ENVIRONMENT'." error " This is critical. Please review the previous error messages and your connection string for '$ENVIRONMENT'."
fi fi
# Step 2: Create Backup # Step 2: Create database backup (only if database exists)
log "📦 Step 2: Creating database backup using pg_dump..." log "📦 Step 2: Checking if database backup is needed..."
# Define the actual backup file path (absolute) # Check if the target database exists
BACKUP_FILE="$BACKUP_DIR/managing_${ENVIRONMENT}_backup_${TIMESTAMP}.sql" DB_EXISTS=false
# Backup file display path (relative to script execution) if PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "postgres" -c "SELECT 1 FROM pg_database WHERE datname='$DB_NAME';" 2>/dev/null | grep -q "1 row"; then
BACKUP_FILE_DISPLAY="$BACKUP_DIR_NAME/$ENVIRONMENT/managing_${ENVIRONMENT}_backup_${TIMESTAMP}.sql" DB_EXISTS=true
log "✅ Target database '$DB_NAME' exists - proceeding with backup"
else
log " Target database '$DB_NAME' does not exist - skipping backup"
fi
if [ "$DB_EXISTS" = "true" ]; then
# Define the actual backup file path (absolute)
BACKUP_FILE="$BACKUP_DIR/managing_${ENVIRONMENT}_backup_${TIMESTAMP}.sql"
# Backup file display path (relative to script execution)
BACKUP_FILE_DISPLAY="$BACKUP_DIR_NAME/$ENVIRONMENT/managing_${ENVIRONMENT}_backup_${TIMESTAMP}.sql"
# Create backup with retry logic # Create backup with retry logic
BACKUP_SUCCESS=false BACKUP_SUCCESS=false
@@ -439,11 +448,84 @@ for attempt in 1 2 3; do
else else
# If pg_dump fails, fall back to EF Core migration script # If pg_dump fails, fall back to EF Core migration script
warn "⚠️ pg_dump failed, falling back to EF Core migration script..." warn "⚠️ pg_dump failed, falling back to EF Core migration script..."
# Get the first migration name to generate complete script
FIRST_MIGRATION=$(get_first_migration)
if [ -n "$FIRST_MIGRATION" ]; then
log "📋 Generating complete backup script from initial migration: $FIRST_MIGRATION"
if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --from "$FIRST_MIGRATION" --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE"); then
log "✅ Complete EF Core Migration SQL Script generated: $BACKUP_FILE_DISPLAY"
BACKUP_SUCCESS=true
break
else
# Try fallback without specifying from migration
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE") 2>&1 || true)
if [ $attempt -lt 3 ]; then
warn "⚠️ Backup attempt $attempt failed. Retrying in 5 seconds..."
warn " EF CLI Output: $ERROR_OUTPUT"
sleep 5
else
error "❌ Database backup failed after 3 attempts."
error " EF CLI Output: $ERROR_OUTPUT"
error " Migration aborted for safety reasons."
fi
fi
else
# Fallback: generate script without specifying from migration
if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE"); then
log "✅ EF Core Migration SQL Script generated: $BACKUP_FILE_DISPLAY"
BACKUP_SUCCESS=true
break
else
# Try fallback without specifying from migration
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE") 2>&1 || true)
if [ $attempt -lt 3 ]; then
warn "⚠️ Backup attempt $attempt failed. Retrying in 5 seconds..."
warn " EF CLI Output: $ERROR_OUTPUT"
sleep 5
else
error "❌ Database backup failed after 3 attempts."
error " EF CLI Output: $ERROR_OUTPUT"
error " Migration aborted for safety reasons."
fi
fi
fi
fi
else
# If pg_dump is not available, use EF Core migration script
warn "⚠️ pg_dump not available, using EF Core migration script for backup..."
# Get the first migration name to generate complete script
FIRST_MIGRATION=$(get_first_migration)
if [ -n "$FIRST_MIGRATION" ]; then
log "📋 Generating complete backup script from initial migration: $FIRST_MIGRATION"
if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --from "$FIRST_MIGRATION" --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE"); then
log "✅ Complete EF Core Migration SQL Script generated: $BACKUP_FILE_DISPLAY"
BACKUP_SUCCESS=true
break
else
# Try fallback without specifying from migration
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE") 2>&1 || true)
if [ $attempt -lt 3 ]; then
warn "⚠️ Backup attempt $attempt failed. Retrying in 5 seconds..."
warn " EF CLI Output: $ERROR_OUTPUT"
sleep 5
else
error "❌ Database backup failed after 3 attempts."
error " EF CLI Output: $ERROR_OUTPUT"
error " Migration aborted for safety reasons."
fi
fi
else
# Fallback: generate script without specifying from migration
if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE"); then if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE"); then
log "✅ EF Core Migration SQL Script generated: $BACKUP_FILE_DISPLAY" log "✅ EF Core Migration SQL Script generated: $BACKUP_FILE_DISPLAY"
BACKUP_SUCCESS=true BACKUP_SUCCESS=true
break break
else else
# Try fallback without specifying from migration
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE") 2>&1 || true) ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE") 2>&1 || true)
if [ $attempt -lt 3 ]; then if [ $attempt -lt 3 ]; then
warn "⚠️ Backup attempt $attempt failed. Retrying in 5 seconds..." warn "⚠️ Backup attempt $attempt failed. Retrying in 5 seconds..."
@@ -456,33 +538,69 @@ for attempt in 1 2 3; do
fi fi
fi fi
fi fi
else
# If pg_dump is not available, use EF Core migration script
warn "⚠️ pg_dump not available, using EF Core migration script for backup..."
if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE"); then
log "✅ EF Core Migration SQL Script generated: $BACKUP_FILE_DISPLAY"
BACKUP_SUCCESS=true
break
else
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE") 2>&1 || true)
if [ $attempt -lt 3 ]; then
warn "⚠️ Backup attempt $attempt failed. Retrying in 5 seconds..."
warn " EF CLI Output: $ERROR_OUTPUT"
sleep 5
else
error "❌ Database backup failed after 3 attempts."
error " EF CLI Output: $ERROR_OUTPUT"
error " Migration aborted for safety reasons."
fi
fi
fi fi
done done
# Check if backup was successful before proceeding # Check if backup was successful before proceeding
if [ "$BACKUP_SUCCESS" != "true" ]; then if [ "$BACKUP_SUCCESS" != "true" ]; then
error "❌ Database backup failed. Migration aborted for safety." error "❌ Database backup failed. Migration aborted for safety."
error " Cannot proceed with migration without a valid backup." error " Cannot proceed with migration without a valid backup."
error " Please resolve backup issues and try again." error " Please resolve backup issues and try again."
fi
fi
# Step 2.5: Check for pending model changes and create migrations if needed
log "🔍 Step 2.5: Checking for pending model changes..."
# Check if there are any pending model changes that need migrations
PENDING_CHANGES_OUTPUT=$( (cd "$DB_PROJECT_PATH" && dotnet ef migrations add --dry-run --startup-project "$API_PROJECT_PATH" --name "PendingChanges_${TIMESTAMP}") 2>&1 || true )
if echo "$PENDING_CHANGES_OUTPUT" | grep -q "No pending model changes"; then
log "✅ No pending model changes detected - existing migrations are up to date"
else
log "⚠️ Pending model changes detected that require new migrations"
echo ""
echo "=========================================="
echo "📋 PENDING MODEL CHANGES DETECTED"
echo "=========================================="
echo "The following changes require new migrations:"
echo "$PENDING_CHANGES_OUTPUT"
echo ""
echo "Would you like to create a new migration now?"
echo "=========================================="
echo ""
read -p "🔧 Create new migration? (y/n): " create_migration
if [[ "$create_migration" =~ ^[Yy]$ ]]; then
log "📝 Creating new migration..."
# Get migration name from user
read -p "📝 Enter migration name (or press Enter for auto-generated name): " migration_name
if [ -z "$migration_name" ]; then
migration_name="Migration_${TIMESTAMP}"
fi
# Create the migration
if (cd "$DB_PROJECT_PATH" && dotnet ef migrations add "$migration_name" --startup-project "$API_PROJECT_PATH"); then
log "✅ Migration '$migration_name' created successfully"
# Show the created migration file
LATEST_MIGRATION=$(find "$DB_PROJECT_PATH/Migrations" -name "*${migration_name}.cs" | head -1)
if [ -n "$LATEST_MIGRATION" ]; then
log "📄 Migration file created: $(basename "$LATEST_MIGRATION")"
log " Location: $LATEST_MIGRATION"
fi
else
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && dotnet ef migrations add "$migration_name" --startup-project "$API_PROJECT_PATH") 2>&1 || true )
error "❌ Failed to create migration '$migration_name'"
error " EF CLI Output: $ERROR_OUTPUT"
error " Please resolve the model issues and try again."
fi
else
log "⚠️ Skipping migration creation. Proceeding with existing migrations only."
log " Note: If there are pending changes, the migration may fail."
fi
fi fi
# Step 3: Run Migration (This effectively is a retry if previous "update" failed, or a final apply) # Step 3: Run Migration (This effectively is a retry if previous "update" failed, or a final apply)
@@ -507,8 +625,88 @@ fi
# Generate migration script first (Microsoft recommended approach) # Generate migration script first (Microsoft recommended approach)
MIGRATION_SCRIPT="$BACKUP_DIR/migration_${ENVIRONMENT}_${TIMESTAMP}.sql" MIGRATION_SCRIPT="$BACKUP_DIR/migration_${ENVIRONMENT}_${TIMESTAMP}.sql"
log "📝 Step 3b: Generating migration script for pending migrations..." log "📝 Step 3b: Generating migration script for pending migrations..."
if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT"); then
log "✅ Migration script generated: $(basename "$MIGRATION_SCRIPT")" # Check if database is empty (no tables) to determine the best approach
log "🔍 Checking if database has existing tables..."
DB_HAS_TABLES=false
if command -v psql >/dev/null 2>&1; then
TABLE_COUNT=$(PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public';" 2>/dev/null | tr -d ' ' || echo "0")
if [ "$TABLE_COUNT" -gt 0 ]; then
DB_HAS_TABLES=true
log "✅ Database has $TABLE_COUNT existing tables - using idempotent script generation"
else
log "⚠️ Database appears to be empty - using full migration script generation"
fi
else
log "⚠️ psql not available - assuming database has tables and using idempotent script generation"
DB_HAS_TABLES=true
fi
# Generate migration script based on database state
if [ "$DB_HAS_TABLES" = "true" ]; then
# For databases with existing tables, we need to generate a complete script
# that includes all migrations from the beginning
log "📝 Generating complete migration script from initial migration..."
# Get the first migration name to generate script from the beginning
FIRST_MIGRATION=$(get_first_migration)
if [ -n "$FIRST_MIGRATION" ]; then
log "📋 Generating complete script for all migrations (idempotent)..."
if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT"); then
log "✅ Complete migration script generated (all migrations, idempotent): $(basename "$MIGRATION_SCRIPT")"
else
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT") 2>&1 || true )
error "❌ Failed to generate complete migration script."
error " EF CLI Output: $ERROR_OUTPUT"
error " Check the .NET project logs for detailed errors."
error " Backup script available at: $BACKUP_FILE_DISPLAY"
fi
else
# Fallback: generate script without specifying from migration
log "📝 Fallback: Generating migration script without specifying from migration..."
if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT"); then
log "✅ Migration script generated (idempotent): $(basename "$MIGRATION_SCRIPT")"
else
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT") 2>&1 || true )
error "❌ Failed to generate idempotent migration script."
error " EF CLI Output: $ERROR_OUTPUT"
error " Check the .NET project logs for detailed errors."
error " Backup script available at: $BACKUP_FILE_DISPLAY"
fi
fi
else
# Use full script generation for empty databases (generate script from the very beginning)
log "📝 Generating full migration script for empty database..."
# Get the first migration name to generate script from the beginning
FIRST_MIGRATION=$(get_first_migration)
if [ -n "$FIRST_MIGRATION" ]; then
log "📋 Generating complete script for all migrations..."
if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT"); then
log "✅ Complete migration script generated (all migrations): $(basename "$MIGRATION_SCRIPT")"
else
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT") 2>&1 || true )
error "❌ Failed to generate complete migration script."
error " EF CLI Output: $ERROR_OUTPUT"
error " Check the .NET project logs for detailed errors."
error " Backup script available at: $BACKUP_FILE_DISPLAY"
fi
else
# Fallback: generate script without specifying from migration
log "📝 Fallback: Generating migration script without specifying from migration..."
if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT"); then
log "✅ Migration script generated (fallback): $(basename "$MIGRATION_SCRIPT")"
else
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT") 2>&1 || true )
error "❌ Failed to generate fallback migration script."
error " EF CLI Output: $ERROR_OUTPUT"
error " Check the .NET project logs for detailed errors."
error " Backup script available at: $BACKUP_FILE_DISPLAY"
fi
fi
fi
# Show the migration script path to the user for review # Show the migration script path to the user for review
echo "" echo ""
@@ -519,8 +717,26 @@ if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef mig
echo "Environment: $ENVIRONMENT" echo "Environment: $ENVIRONMENT"
echo "Database: $DB_HOST:$DB_PORT/$DB_NAME" echo "Database: $DB_HOST:$DB_PORT/$DB_NAME"
echo "" echo ""
# Show a preview of the migration script content
if [ -f "$MIGRATION_SCRIPT" ]; then
SCRIPT_SIZE=$(wc -l < "$MIGRATION_SCRIPT")
echo "📄 Migration script contains $SCRIPT_SIZE lines"
# Show first 20 lines as preview
echo ""
echo "📋 PREVIEW (first 20 lines):"
echo "----------------------------------------"
head -20 "$MIGRATION_SCRIPT" | sed 's/^/ /'
if [ "$SCRIPT_SIZE" -gt 20 ]; then
echo " ... (showing first 20 lines of $SCRIPT_SIZE total)"
fi
echo "----------------------------------------"
echo ""
fi
echo "⚠️ IMPORTANT: Please review the migration script before proceeding!" echo "⚠️ IMPORTANT: Please review the migration script before proceeding!"
echo " You can examine the script with: cat $MIGRATION_SCRIPT" echo " You can examine the full script with: cat $MIGRATION_SCRIPT"
echo " Or open it in your editor to review the changes." echo " Or open it in your editor to review the changes."
echo "" echo ""
echo "==========================================" echo "=========================================="
@@ -564,16 +780,15 @@ if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef mig
fi fi
fi fi
# Clean up migration script after successful application # Save a copy of the migration script for reference before cleaning up
rm -f "$MIGRATION_SCRIPT" MIGRATION_SCRIPT_COPY="$BACKUP_DIR/migration_${ENVIRONMENT}_${TIMESTAMP}_applied.sql"
if [ -f "$MIGRATION_SCRIPT" ]; then
cp "$MIGRATION_SCRIPT" "$MIGRATION_SCRIPT_COPY"
log "📝 Migration script saved for reference: $(basename "$MIGRATION_SCRIPT_COPY")"
fi
else # Clean up temporary migration script after successful application
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT") 2>&1 || true ) rm -f "$MIGRATION_SCRIPT"
error "❌ Failed to generate migration script."
error " EF CLI Output: $ERROR_OUTPUT"
error " Check the .NET project logs for detailed errors."
error " Backup script available at: $BACKUP_FILE_DISPLAY"
fi
# Step 4: Verify Migration # Step 4: Verify Migration
log "🔍 Step 4: Verifying migration status..." log "🔍 Step 4: Verifying migration status..."

View File

@@ -1 +0,0 @@

View File

@@ -15,7 +15,6 @@ COPY ["/src/Managing.Common/Managing.Common.csproj", "Managing.Common/"]
COPY ["/src/Managing.Core/Managing.Core.csproj", "Managing.Core/"] COPY ["/src/Managing.Core/Managing.Core.csproj", "Managing.Core/"]
COPY ["/src/Managing.Application.Abstractions/Managing.Application.Abstractions.csproj", "Managing.Application.Abstractions/"] COPY ["/src/Managing.Application.Abstractions/Managing.Application.Abstractions.csproj", "Managing.Application.Abstractions/"]
COPY ["/src/Managing.Domain/Managing.Domain.csproj", "Managing.Domain/"] COPY ["/src/Managing.Domain/Managing.Domain.csproj", "Managing.Domain/"]
COPY ["/src/Managing.Application.Workers/Managing.Application.Workers.csproj", "Managing.Application.Workers/"]
COPY ["/src/Managing.Infrastructure.Messengers/Managing.Infrastructure.Messengers.csproj", "Managing.Infrastructure.Messengers/"] COPY ["/src/Managing.Infrastructure.Messengers/Managing.Infrastructure.Messengers.csproj", "Managing.Infrastructure.Messengers/"]
COPY ["/src/Managing.Infrastructure.Exchanges/Managing.Infrastructure.Exchanges.csproj", "Managing.Infrastructure.Exchanges/"] COPY ["/src/Managing.Infrastructure.Exchanges/Managing.Infrastructure.Exchanges.csproj", "Managing.Infrastructure.Exchanges/"]
COPY ["/src/Managing.Infrastructure.Database/Managing.Infrastructure.Databases.csproj", "Managing.Infrastructure.Database/"] COPY ["/src/Managing.Infrastructure.Database/Managing.Infrastructure.Databases.csproj", "Managing.Infrastructure.Database/"]

View File

@@ -15,7 +15,6 @@ COPY ["/src/Managing.Common/Managing.Common.csproj", "Managing.Common/"]
COPY ["/src/Managing.Core/Managing.Core.csproj", "Managing.Core/"] COPY ["/src/Managing.Core/Managing.Core.csproj", "Managing.Core/"]
COPY ["/src/Managing.Application.Abstractions/Managing.Application.Abstractions.csproj", "Managing.Application.Abstractions/"] COPY ["/src/Managing.Application.Abstractions/Managing.Application.Abstractions.csproj", "Managing.Application.Abstractions/"]
COPY ["/src/Managing.Domain/Managing.Domain.csproj", "Managing.Domain/"] COPY ["/src/Managing.Domain/Managing.Domain.csproj", "Managing.Domain/"]
COPY ["/src/Managing.Application.Workers/Managing.Application.Workers.csproj", "Managing.Application.Workers/"]
COPY ["/src/Managing.Infrastructure.Messengers/Managing.Infrastructure.Messengers.csproj", "Managing.Infrastructure.Messengers/"] COPY ["/src/Managing.Infrastructure.Messengers/Managing.Infrastructure.Messengers.csproj", "Managing.Infrastructure.Messengers/"]
COPY ["/src/Managing.Infrastructure.Exchanges/Managing.Infrastructure.Exchanges.csproj", "Managing.Infrastructure.Exchanges/"] COPY ["/src/Managing.Infrastructure.Exchanges/Managing.Infrastructure.Exchanges.csproj", "Managing.Infrastructure.Exchanges/"]
COPY ["/src/Managing.Infrastructure.Database/Managing.Infrastructure.Databases.csproj", "Managing.Infrastructure.Database/"] COPY ["/src/Managing.Infrastructure.Database/Managing.Infrastructure.Databases.csproj", "Managing.Infrastructure.Database/"]

View File

@@ -1,32 +0,0 @@
using Managing.Application.Workers.Abstractions;
using Managing.Domain.Workers;
using Microsoft.AspNetCore.Mvc;
using static Managing.Common.Enums;
namespace Managing.Api.Workers.Controllers;
[ApiController]
[Route("[controller]")]
[Produces("application/json")]
public class WorkerController : ControllerBase
{
private readonly IWorkerService _workerService;
public WorkerController(IWorkerService workerService)
{
_workerService = workerService;
}
[HttpGet]
public async Task<ActionResult<List<Worker>>> GetWorkers()
{
var workers = await _workerService.GetWorkers();
return Ok(workers.ToList());
}
[HttpPatch]
public async Task<ActionResult> ToggleWorker(WorkerType workerType)
{
return Ok(await _workerService.ToggleWorker(workerType));
}
}

View File

@@ -1,34 +0,0 @@
# Use the official Microsoft ASP.NET Core runtime as the base image.
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
# Use the official Microsoft .NET SDK image to build the code.
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["Managing.Api.Workers/Managing.Api.Workers.csproj", "Managing.Api.Workers/"]
COPY ["Managing.Bootstrap/Managing.Bootstrap.csproj", "Managing.Bootstrap/"]
COPY ["Managing.Infrastructure.Storage/Managing.Infrastructure.Storage.csproj", "Managing.Infrastructure.Storage/"]
COPY ["Managing.Application/Managing.Application.csproj", "Managing.Application/"]
COPY ["Managing.Common/Managing.Common.csproj", "Managing.Common/"]
COPY ["Managing.Core/Managing.Core.csproj", "Managing.Core/"]
COPY ["Managing.Application.Abstractions/Managing.Application.Abstractions.csproj", "Managing.Application.Abstractions/"]
COPY ["Managing.Domain/Managing.Domain.csproj", "Managing.Domain/"]
COPY ["Managing.Application.Workers/Managing.Application.Workers.csproj", "Managing.Application.Workers/"]
COPY ["Managing.Infrastructure.Messengers/Managing.Infrastructure.Messengers.csproj", "Managing.Infrastructure.Messengers/"]
COPY ["Managing.Infrastructure.Exchanges/Managing.Infrastructure.Exchanges.csproj", "Managing.Infrastructure.Exchanges/"]
COPY ["Managing.Infrastructure.Database/Managing.Infrastructure.Databases.csproj", "Managing.Infrastructure.Database/"]
RUN dotnet restore "Managing.Api.Workers/Managing.Api.Workers.csproj"
COPY . .
WORKDIR "/src/Managing.Api.Workers"
RUN dotnet build "Managing.Api.Workers.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "Managing.Api.Workers.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
COPY managing_cert.pfx .
ENTRYPOINT ["dotnet", "Managing.Api.Workers.dll"]

View File

@@ -1,20 +0,0 @@
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Managing.Api.Workers.Filters
{
public class EnumSchemaFilter : ISchemaFilter
{
public void Apply(OpenApiSchema model, SchemaFilterContext context)
{
if (context.Type.IsEnum)
{
model.Enum.Clear();
Enum.GetNames(context.Type)
.ToList()
.ForEach(n => model.Enum.Add(new OpenApiString(n)));
}
}
}
}

View File

@@ -1,60 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Platforms>AnyCPU;x64</Platforms>
<UserSecretsId>3900ce93-de15-49e5-9a61-7dc2209939ca</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerfileContext>..\..</DockerfileContext>
<DockerComposeProjectPath>..\..\docker-compose.dcproj</DockerComposeProjectPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AspNetCore.HealthChecks.MongoDb" Version="8.1.0"/>
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="9.0.0"/>
<PackageReference Include="AspNetCore.HealthChecks.Uris" Version="9.0.0"/>
<PackageReference Include="Essential.LoggerProvider.Elasticsearch" Version="1.3.2"/>
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.20.1"/>
<PackageReference Include="NSwag.AspNetCore" Version="14.0.7"/>
<PackageReference Include="Sentry.AspNetCore" Version="5.5.1"/>
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1"/>
<PackageReference Include="Serilog.Enrichers.Environment" Version="2.3.0"/>
<PackageReference Include="Serilog.Exceptions" Version="8.4.0"/>
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1"/>
<PackageReference Include="Serilog.Sinks.Debug" Version="2.0.0"/>
<PackageReference Include="Serilog.Sinks.Elasticsearch" Version="10.0.0"/>
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="6.6.1"/>
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="6.6.1"/>
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.6.1"/>
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="6.6.1"/>
<PackageReference Include="xunit" Version="2.8.0"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Managing.Bootstrap\Managing.Bootstrap.csproj"/>
<ProjectReference Include="..\Managing.Aspire.ServiceDefaults\Managing.Aspire.ServiceDefaults.csproj"/>
<ProjectReference Include="..\Managing.Core\Managing.Core.csproj"/>
</ItemGroup>
<ItemGroup>
<Content Update="appsettings.Oda.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="appsettings.Sandbox.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="appsettings.SandboxLocal.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="appsettings.Production.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="appsettings.KaiServer.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View File

@@ -1,90 +0,0 @@
using Sentry;
using System.Text;
namespace Managing.Api.Workers.Middleware
{
public class SentryDiagnosticsMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<SentryDiagnosticsMiddleware> _logger;
public SentryDiagnosticsMiddleware(RequestDelegate next, ILogger<SentryDiagnosticsMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
// Only activate for the /api/sentry-diagnostics endpoint
if (context.Request.Path.StartsWithSegments("/api/sentry-diagnostics"))
{
await HandleDiagnosticsRequest(context);
return;
}
await _next(context);
}
private async Task HandleDiagnosticsRequest(HttpContext context)
{
var response = new StringBuilder();
response.AppendLine("Sentry Diagnostics Report");
response.AppendLine("========================");
response.AppendLine($"Timestamp: {DateTime.Now}");
response.AppendLine();
// Check if Sentry is initialized
response.AppendLine("## Sentry SDK Status");
response.AppendLine($"Sentry Enabled: {SentrySdk.IsEnabled}");
response.AppendLine($"Application Environment: {Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}");
response.AppendLine();
// Send a test event
response.AppendLine("## Test Event");
try
{
var id = SentrySdk.CaptureMessage($"Diagnostics test from {context.Request.Host} at {DateTime.Now}", SentryLevel.Info);
response.AppendLine($"Test Event ID: {id}");
response.AppendLine("Test event was sent to Sentry. Check your Sentry dashboard to confirm it was received.");
// Try to send an exception too
try
{
throw new Exception("Test exception from diagnostics middleware");
}
catch (Exception ex)
{
var exceptionId = SentrySdk.CaptureException(ex);
response.AppendLine($"Test Exception ID: {exceptionId}");
}
}
catch (Exception ex)
{
response.AppendLine($"Error sending test event: {ex.Message}");
response.AppendLine(ex.StackTrace);
}
response.AppendLine();
response.AppendLine("## Connectivity Check");
response.AppendLine("If events are not appearing in Sentry, check the following:");
response.AppendLine("1. Verify your DSN is correct in appsettings.json");
response.AppendLine("2. Ensure your network allows outbound HTTPS connections to sentry.apps.managing.live");
response.AppendLine("3. Check Sentry server logs for any ingestion issues");
response.AppendLine("4. Verify your Sentry project is correctly configured to receive events");
// Return the diagnostic information
context.Response.ContentType = "text/plain";
await context.Response.WriteAsync(response.ToString());
}
}
// Extension method used to add the middleware to the HTTP request pipeline.
public static class SentryDiagnosticsMiddlewareExtensions
{
public static IApplicationBuilder UseSentryDiagnostics(this IApplicationBuilder builder)
{
return builder.UseMiddleware<SentryDiagnosticsMiddleware>();
}
}
}

View File

@@ -1,217 +0,0 @@
using System.Text.Json.Serialization;
using HealthChecks.UI.Client;
using Managing.Api.Workers.Filters;
using Managing.Application.Hubs;
using Managing.Bootstrap;
using Managing.Common;
using Managing.Core.Middleawares;
using Managing.Infrastructure.Databases.InfluxDb.Models;
using Managing.Infrastructure.Databases.PostgreSql;
using Managing.Infrastructure.Evm.Models.Privy;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.OpenApi.Models;
using NSwag;
using NSwag.Generation.Processors.Security;
using Serilog;
using Serilog.Events;
using Serilog.Sinks.Elasticsearch;
using OpenApiSecurityRequirement = Microsoft.OpenApi.Models.OpenApiSecurityRequirement;
using OpenApiSecurityScheme = NSwag.OpenApiSecurityScheme;
// Builder
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.SetBasePath(AppContext.BaseDirectory);
builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json");
var influxUrl = builder.Configuration.GetSection(Constants.Databases.InfluxDb)["Url"];
var web3ProxyUrl = builder.Configuration.GetSection("Web3Proxy")["BaseUrl"];
var postgreSqlConnectionString = builder.Configuration.GetSection("PostgreSql")["ConnectionString"];
// Initialize Sentry
SentrySdk.Init(options =>
{
// A Sentry Data Source Name (DSN) is required.
options.Dsn = builder.Configuration["Sentry:Dsn"];
// When debug is enabled, the Sentry client will emit detailed debugging information to the console.
options.Debug = false;
// Adds request URL and headers, IP and name for users, etc.
options.SendDefaultPii = true;
// This option is recommended. It enables Sentry's "Release Health" feature.
options.AutoSessionTracking = true;
// Enabling this option is recommended for client applications only. It ensures all threads use the same global scope.
options.IsGlobalModeEnabled = false;
// Example sample rate for your transactions: captures 10% of transactions
options.TracesSampleRate = 0.1;
options.Environment = builder.Environment.EnvironmentName;
});
// Add service discovery for Aspire
builder.Services.AddServiceDiscovery();
// Configure health checks
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy(), ["live"])
.AddUrlGroup(new Uri($"{influxUrl}/health"), name: "influxdb", tags: ["database"])
.AddUrlGroup(new Uri($"{web3ProxyUrl}/health"), name: "web3proxy", tags: ["api"]);
builder.WebHost.UseUrls("http://localhost:5001");
builder.Host.UseSerilog((hostBuilder, loggerConfiguration) =>
{
var envName = builder.Environment.EnvironmentName.ToLower().Replace(".", "-");
var indexFormat = $"managing-worker-{envName}-" + "{0:yyyy.MM.dd}";
var yourTemplateName = "dotnetlogs";
var es = new ElasticsearchSinkOptions(new Uri(hostBuilder.Configuration["ElasticConfiguration:Uri"]))
{
IndexFormat = indexFormat.ToLower(),
AutoRegisterTemplate = true,
OverwriteTemplate = true,
TemplateName = yourTemplateName,
AutoRegisterTemplateVersion = AutoRegisterTemplateVersion.ESv7,
TypeName = null,
BatchAction = ElasticOpType.Create,
MinimumLogEventLevel = LogEventLevel.Information,
DetectElasticsearchVersion = true,
RegisterTemplateFailure = RegisterTemplateRecovery.IndexAnyway,
};
loggerConfiguration
.WriteTo.Console()
.WriteTo.Elasticsearch(es);
});
builder.Services.AddOptions();
builder.Services.Configure<InfluxDbSettings>(builder.Configuration.GetSection(Constants.Databases.InfluxDb));
builder.Services.Configure<PrivySettings>(builder.Configuration.GetSection(Constants.ThirdParty.Privy));
builder.Services.AddControllers().AddJsonOptions(options =>
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()));
builder.Services.AddCors(o => o.AddPolicy("CorsPolicy", builder =>
{
builder
.SetIsOriginAllowed((host) => true)
.AllowAnyOrigin()
.WithOrigins("http://localhost:3000/")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
}));
builder.Services.AddSignalR().AddJsonProtocol();
// Add PostgreSQL DbContext for worker services
builder.Services.AddDbContext<ManagingDbContext>(options =>
{
options.UseNpgsql(postgreSqlConnectionString, npgsqlOptions =>
{
npgsqlOptions.CommandTimeout(60);
npgsqlOptions.EnableRetryOnFailure(maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(10),
errorCodesToAdd: null);
});
if (builder.Environment.IsDevelopment())
{
options.EnableDetailedErrors();
options.EnableSensitiveDataLogging();
options.EnableThreadSafetyChecks();
}
options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
options.EnableServiceProviderCaching();
options.LogTo(msg => Console.WriteLine(msg), LogLevel.Warning);
}, ServiceLifetime.Scoped);
builder.Services.RegisterWorkersDependencies(builder.Configuration);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApiDocument(document =>
{
document.AddSecurity("JWT", Enumerable.Empty<string>(), new OpenApiSecurityScheme
{
Type = OpenApiSecuritySchemeType.ApiKey,
Name = "Authorization",
In = OpenApiSecurityApiKeyLocation.Header,
Description = "Type into the textbox: Bearer {your JWT token}."
});
document.OperationProcessors.Add(
new AspNetCoreOperationSecurityScopeProcessor("JWT"));
});
builder.Services.AddSwaggerGen(options =>
{
options.SchemaFilter<EnumSchemaFilter>();
options.AddSecurityDefinition("Bearer,", new Microsoft.OpenApi.Models.OpenApiSecurityScheme
{
Description = "Please insert your JWT Token into field : Bearer {your_token}",
Name = "Authorization",
Type = SecuritySchemeType.Http,
In = ParameterLocation.Header,
Scheme = "Bearer",
BearerFormat = "JWT"
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new Microsoft.OpenApi.Models.OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
new string[] { }
}
});
});
builder.WebHost.SetupDiscordBot();
// App
var app = builder.Build();
app.UseSerilogRequestLogging();
app.UseOpenApi();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Managing Workers v1");
c.RoutePrefix = string.Empty;
});
app.UseCors("CorsPolicy");
// Add Sentry diagnostics middleware (now using shared version from Core)
app.UseSentryDiagnostics();
// Using shared GlobalErrorHandlingMiddleware from Core project
app.UseMiddleware<GlobalErrorHandlingMiddleware>();
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHub<PositionHub>("/positionhub");
endpoints.MapHealthChecks("/health", new HealthCheckOptions
{
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
endpoints.MapHealthChecks("/alive", new HealthCheckOptions
{
Predicate = r => r.Tags.Contains("live"),
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
});
app.Run();

View File

@@ -1,19 +0,0 @@
{
"InfluxDb": {
"Url": "http://localhost:8086/",
"Token": ""
},
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Information",
"System": "Warning"
}
}
},
"ElasticConfiguration": {
"Uri": "http://localhost:9200"
},
"AllowedHosts": "*"
}

View File

@@ -1,24 +0,0 @@
{
"InfluxDb": {
"Url": "http://influxdb:8086/",
"Organization": "managing-org",
"Token": "Fw2FPL2OwTzDHzSbR2Sd5xs0EKQYy00Q-hYKYAhr9cC1_q5YySONpxuf_Ck0PTjyUiF13xXmi__bu_pXH-H9zA=="
},
"Privy": {
"AppId": "cm6f47n1l003jx7mjwaembhup",
"AppSecret": "63Chz2z5M8TgR5qc8dznSLRAGTHTyPU4cjdQobrBF1Cx5tszZpTuFgyrRd7hZ2k6HpwDz3GEwQZzsCqHb8Z311bF"
},
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Information",
"System": "Warning"
}
}
},
"ElasticConfiguration": {
"Uri": "http://elasticsearch:9200"
},
"AllowedHosts": "*"
}

View File

@@ -1,38 +0,0 @@
{
"InfluxDb": {
"Url": "http://localhost:8086/",
"Organization": "managing-org",
"Token": "Fw2FPL2OwTzDHzSbR2Sd5xs0EKQYy00Q-hYKYAhr9cC1_q5YySONpxuf_Ck0PTjyUiF13xXmi__bu_pXH-H9zA=="
},
"Privy": {
"AppId": "cm6f47n1l003jx7mjwaembhup",
"AppSecret": "63Chz2z5M8TgR5qc8dznSLRAGTHTyPU4cjdQobrBF1Cx5tszZpTuFgyrRd7hZ2k6HpwDz3GEwQZzsCqHb8Z311bF"
},
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Information",
"System": "Warning"
}
}
},
"ElasticConfiguration": {
"Uri": "http://localhost:9200"
},
"AllowedHosts": "*",
"WorkerPricesFifteenMinutes": false,
"WorkerPricesOneHour": false,
"WorkerPricesFourHours": false,
"WorkerPricesOneDay": false,
"WorkerPricesFiveMinutes": false,
"WorkerFee": false,
"WorkerPositionManager": false,
"WorkerPositionFetcher": false,
"WorkerSpotlight": false,
"WorkerTraderWatcher": false,
"WorkerLeaderboard": false,
"WorkerFundingRatesWatcher": false,
"WorkerGeneticAlgorithm": false,
"WorkerBundleBacktest": true
}

View File

@@ -1,30 +0,0 @@
{
"InfluxDb": {
"Url": "https://influx-db.apps.managing.live",
"Organization": "managing-org",
"Token": "_BtklT_aQ7GRqWG-HGILYEd8MJzxdbxxckPadzUsRofnwJBKQuXYLbCrVcLD7TrD4BlXgGAsyuqQItsOtanfBw=="
},
"Privy": {
"AppId": "cm6kkz5ke00n5ffmpwdbr05mp",
"AppSecret": "3STq1UyPJ5WHixArBcVBKecWtyR4QpgZ1uju4HHvvJH2RwtacJnvoyzuaiNC8Xibi4rQb3eeH2YtncKrMxCYiV3a"
},
"Web3Proxy": {
"BaseUrl": "http://srv-captain--managing-web3:4111"
},
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Information",
"System": "Warning"
}
}
},
"ElasticConfiguration": {
"Uri": "http://elasticsearch:9200"
},
"Sentry": {
"Dsn": "https://698e00d7cb404b049aff3881e5a47f6b@bugcenter.apps.managing.live/1"
},
"AllowedHosts": "*"
}

View File

@@ -1,24 +0,0 @@
{
"InfluxDb": {
"Url": "http://srv-captain--influx-db:8086/",
"Organization": "managing-org",
"Token": "eOuXcXhH7CS13Iw4CTiDDpRjIjQtEVPOloD82pLPOejI4n0BsEj1YzUw0g3Cs1mdDG5m-RaxCavCMsVTtS5wIQ=="
},
"Privy": {
"AppId": "cm6f47n1l003jx7mjwaembhup",
"AppSecret": "63Chz2z5M8TgR5qc8dznSLRAGTHTyPU4cjdQobrBF1Cx5tszZpTuFgyrRd7hZ2k6HpwDz3GEwQZzsCqHb8Z311bF"
},
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Information",
"System": "Warning"
}
}
},
"ElasticConfiguration": {
"Uri": "http://elasticsearch:9200"
},
"AllowedHosts": "*"
}

View File

@@ -1,24 +0,0 @@
{
"InfluxDb": {
"Url": "https://influx-db.apps.managing.live",
"Organization": "managing-org",
"Token": "eOuXcXhH7CS13Iw4CTiDDpRjIjQtEVPOloD82pLPOejI4n0BsEj1YzUw0g3Cs1mdDG5m-RaxCavCMsVTtS5wIQ=="
},
"Privy": {
"AppId": "cm6f47n1l003jx7mjwaembhup",
"AppSecret": "63Chz2z5M8TgR5qc8dznSLRAGTHTyPU4cjdQobrBF1Cx5tszZpTuFgyrRd7hZ2k6HpwDz3GEwQZzsCqHb8Z311bF"
},
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Information",
"System": "Warning"
}
}
},
"ElasticConfiguration": {
"Uri": "http://elasticsearch:9200"
},
"AllowedHosts": "*"
}

View File

@@ -1,53 +0,0 @@
{
"InfluxDb": {
"Url": "http://influxdb:8086/",
"Organization": "",
"Token": ""
},
"PostgreSql": {
"ConnectionString": "Host=localhost;Port=5432;Database=managing;Username=postgres;Password=postgres"
},
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Information",
"System": "Warning"
}
}
},
"N8n": {
"WebhookUrl": "https://n8n.kai.managing.live/webhook/fa9308b6-983b-42ec-b085-71599d655951"
},
"ElasticConfiguration": {
"Uri": "http://elasticsearch:9200"
},
"Web3Proxy": {
"BaseUrl": "http://localhost:4111"
},
"Sentry": {
"Dsn": "https://8fdb299b69df4f9d9b709c8d4a556608@bugcenter.apps.managing.live/2",
"MinimumEventLevel": "Error",
"SendDefaultPii": true,
"MaxBreadcrumbs": 50,
"SampleRate": 1.0,
"TracesSampleRate": 0.2,
"Debug": false
},
"Discord": {
"BotActivity": "with jobs",
"HandleUserAction": true,
"ApplicationId": "1132062339592622221",
"PublicKey": "e422f3326307788608eceba919497d3f2758cc64d20bb8a6504c695192404808",
"TokenId": "MTEzMjA2MjMzOTU5MjYyMjIyMQ.GySuNX.rU-9uIX6-yDthBjT_sbXioaJGyJva2ABNNEaj4",
"SignalChannelId": 966080506473099314,
"TradesChannelId": 998374177763491851,
"TroublesChannelId": 1015761955321040917,
"CopyTradingChannelId": 1132022887012909126,
"RequestsChannelId": 1018589494968078356,
"FundingRateChannelId": 1263566138709774336,
"LeaderboardChannelId": 1133169725237633095,
"ButtonExpirationMinutes": 10
},
"AllowedHosts": "*"
}

View File

@@ -0,0 +1,80 @@
# Admin Feature Documentation
## Overview
The admin feature allows specific users to manage all bots in the system, regardless of ownership. Admin users can start, stop, delete, and modify any bot without owning the associated account.
## How It Works
Admin privileges are granted through environment variables, making it secure and environment-specific. The system checks if a user is an admin by comparing their username against a comma-separated list of admin usernames configured in the environment.
## Configuration
### Environment Variable
Set the `AdminUsers` environment variable with a comma-separated list of usernames:
```bash
AdminUsers=admin1,superuser,john.doe
```
### CapRover Configuration
In your CapRover dashboard:
1. Go to your app's settings
2. Navigate to "Environment Variables"
3. Add a new environment variable:
- Key: `AdminUsers`
- Value: `admin1,superuser,john.doe` (replace with actual admin usernames)
### Local Development
For local development, you can set this in your `appsettings.Development.json`:
```json
{
"AdminUsers": "admin1,superuser,john.doe"
}
```
## Admin Capabilities
Admin users can perform all bot operations without ownership restrictions:
- **Start/Save Bot**: Create and start bots for any account
- **Stop Bot**: Stop any running bot
- **Delete Bot**: Delete any bot
- **Restart Bot**: Restart any bot
- **Open/Close Positions**: Manually open or close positions for any bot
- **Update Configuration**: Modify any bot's configuration
- **View Bot Configuration**: Access any bot's configuration details
## Security Notes
1. **Environment-Based**: Admin users are configured via environment variables, not through the API
2. **No Privilege Escalation**: Regular users cannot grant themselves admin access
3. **Audit Logging**: All admin actions are logged with the admin user's context
4. **Case-Insensitive**: Username matching is case-insensitive for convenience
## Implementation Details
The admin feature is implemented using:
- `IAdminConfigurationService`: Checks if a user is an admin
- Updated `UserOwnsBotAccount` method: Returns true for admin users
- Dependency injection: Service is registered as a singleton
- Configuration reading: Reads from `AdminUsers` environment variable
## Example Usage
1. **Set Admin Users**:
```bash
AdminUsers=alice,bob,charlie
```
2. **Admin Operations**:
- Alice, Bob, or Charlie can now manage any bot in the system
- They can use all existing bot endpoints without ownership restrictions
- All operations are logged with their username for audit purposes
## Troubleshooting
- **Admin not working**: Check if the username exactly matches the configuration (case-insensitive)
- **No admins configured**: Check the `AdminUsers` environment variable is set
- **Multiple environments**: Each environment (dev, staging, prod) should have its own admin configuration

View File

@@ -1,27 +0,0 @@
using Managing.Application.Abstractions.Services;
namespace Managing.Api.Authorization;
public class JwtMiddleware
{
private readonly RequestDelegate _next;
public JwtMiddleware(RequestDelegate next, IConfiguration config)
{
_next = next;
}
public async Task Invoke(HttpContext context, IUserService userService, IJwtUtils jwtUtils)
{
var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
var userId = jwtUtils.ValidateJwtToken(token);
if (userId != null)
{
// attach user to context on successful jwt validation
context.Items["User"] = await userService.GetUserByAddressAsync(userId);
}
await _next(context);
}
}

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();
@@ -342,7 +340,7 @@ public class BacktestController : BaseController
// Convert IndicatorRequest objects to Indicator domain objects // Convert IndicatorRequest objects to Indicator domain objects
foreach (var indicatorRequest in request.Config.Scenario.Indicators) foreach (var indicatorRequest in request.Config.Scenario.Indicators)
{ {
var indicator = new Indicator(indicatorRequest.Name, indicatorRequest.Type) var indicator = new IndicatorBase(indicatorRequest.Name, indicatorRequest.Type)
{ {
SignalType = indicatorRequest.SignalType, SignalType = indicatorRequest.SignalType,
MinimumHistory = indicatorRequest.MinimumHistory, MinimumHistory = indicatorRequest.MinimumHistory,
@@ -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,18 +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)
{ {
return new MoneyManagement return new MoneyManagement

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
using Managing.Api.Models.Requests; using Managing.Api.Extensions;
using Managing.Api.Models.Requests;
using Managing.Api.Models.Responses; using Managing.Api.Models.Responses;
using Managing.Application.Abstractions; using Managing.Application.Abstractions.Grains;
using Managing.Application.Abstractions.Services; using Managing.Application.Abstractions.Services;
using Managing.Application.Hubs; using Managing.Application.Hubs;
using Managing.Application.ManageBot.Commands; using Managing.Application.ManageBot.Commands;
@@ -8,7 +9,6 @@ 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.Statistics; using Managing.Domain.Statistics;
using Managing.Domain.Strategies; using Managing.Domain.Strategies;
using Managing.Domain.Strategies.Base; using Managing.Domain.Strategies.Base;
@@ -25,6 +25,7 @@ namespace Managing.Api.Controllers;
/// Controller for handling data-related operations such as retrieving tickers, spotlight data, and candles. /// Controller for handling data-related operations such as retrieving tickers, spotlight data, and candles.
/// Requires authorization for access. /// Requires authorization for access.
/// </summary> /// </summary>
[AllowAnonymous]
[ApiController] [ApiController]
[Route("[controller]")] [Route("[controller]")]
public class DataController : ControllerBase public class DataController : ControllerBase
@@ -33,9 +34,11 @@ public class DataController : ControllerBase
private readonly IAccountService _accountService; private readonly IAccountService _accountService;
private readonly ICacheService _cacheService; private readonly ICacheService _cacheService;
private readonly IStatisticService _statisticService; private readonly IStatisticService _statisticService;
private readonly IAgentService _agentService;
private readonly IHubContext<CandleHub> _hubContext; private readonly IHubContext<CandleHub> _hubContext;
private readonly IMediator _mediator; private readonly IMediator _mediator;
private readonly ITradingService _tradingService; private readonly ITradingService _tradingService;
private readonly IGrainFactory _grainFactory;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="DataController"/> class. /// Initializes a new instance of the <see cref="DataController"/> class.
@@ -47,22 +50,27 @@ public class DataController : ControllerBase
/// <param name="hubContext">SignalR hub context for real-time communication.</param> /// <param name="hubContext">SignalR hub context for real-time communication.</param>
/// <param name="mediator">Mediator for handling commands and queries.</param> /// <param name="mediator">Mediator for handling commands and queries.</param>
/// <param name="tradingService">Service for trading operations.</param> /// <param name="tradingService">Service for trading operations.</param>
/// <param name="grainFactory">Orleans grain factory for accessing grains.</param>
public DataController( public DataController(
IExchangeService exchangeService, IExchangeService exchangeService,
IAccountService accountService, IAccountService accountService,
ICacheService cacheService, ICacheService cacheService,
IStatisticService statisticService, IStatisticService statisticService,
IAgentService agentService,
IHubContext<CandleHub> hubContext, IHubContext<CandleHub> hubContext,
IMediator mediator, IMediator mediator,
ITradingService tradingService) ITradingService tradingService,
IGrainFactory grainFactory)
{ {
_exchangeService = exchangeService; _exchangeService = exchangeService;
_accountService = accountService; _accountService = accountService;
_cacheService = cacheService; _cacheService = cacheService;
_statisticService = statisticService; _statisticService = statisticService;
_agentService = agentService;
_hubContext = hubContext; _hubContext = hubContext;
_mediator = mediator; _mediator = mediator;
_tradingService = tradingService; _tradingService = tradingService;
_grainFactory = grainFactory;
} }
/// <summary> /// <summary>
@@ -244,7 +252,7 @@ public class DataController : ControllerBase
{ {
return Ok(new CandlesWithIndicatorsResponse return Ok(new CandlesWithIndicatorsResponse
{ {
Candles = new List<Candle>(), Candles = new HashSet<Candle>(),
IndicatorsValues = new Dictionary<IndicatorType, IndicatorsResultBase>() IndicatorsValues = new Dictionary<IndicatorType, IndicatorsResultBase>()
}); });
} }
@@ -290,8 +298,8 @@ public class DataController : ControllerBase
} }
// Get active bots // Get active bots
var activeBots = await _mediator.Send(new GetActiveBotsCommand()); var activeBots = await _mediator.Send(new GetBotsByStatusCommand(BotStatus.Running));
var currentCount = activeBots.Count; var currentCount = activeBots.Count();
// Get previous count from cache // Get previous count from cache
var previousCount = _cacheService.GetValue<int>(previousCountKey); var previousCount = _cacheService.GetValue<int>(previousCountKey);
@@ -332,22 +340,12 @@ public class DataController : ControllerBase
[HttpGet("GetTopStrategies")] [HttpGet("GetTopStrategies")]
public async Task<ActionResult<TopStrategiesViewModel>> GetTopStrategies() public async Task<ActionResult<TopStrategiesViewModel>> GetTopStrategies()
{ {
const string cacheKey = "TopStrategies";
// Check if the top strategies are already cached
var cachedStrategies = _cacheService.GetValue<TopStrategiesViewModel>(cacheKey);
if (cachedStrategies != null)
{
return Ok(cachedStrategies);
}
// Get active bots // Get active bots
var activeBots = await _mediator.Send(new GetActiveBotsCommand()); var activeBots = await _mediator.Send(new GetBotsByStatusCommand(BotStatus.Running));
// Calculate PnL for each bot once and store in a list of tuples // Calculate PnL for each bot once and store in a list of tuples
var botsWithPnL = activeBots var botsWithPnL = activeBots
.Select(bot => new { Bot = bot, PnL = bot.GetProfitAndLoss() }) .Select(bot => new { Bot = bot, PnL = bot.Pnl, agentName = bot.User.AgentName })
.OrderByDescending(item => item.PnL) .OrderByDescending(item => item.PnL)
.Take(3) .Take(3)
.ToList(); .ToList();
@@ -359,17 +357,92 @@ public class DataController : ControllerBase
.Select(item => new StrategyPerformance .Select(item => new StrategyPerformance
{ {
StrategyName = item.Bot.Name, StrategyName = item.Bot.Name,
PnL = item.PnL PnL = item.PnL,
AgentName = item.agentName,
}) })
.ToList() .ToList()
}; };
// Cache the result for 10 minutes
_cacheService.SaveValue(cacheKey, topStrategies, TimeSpan.FromMinutes(10));
return Ok(topStrategies); return Ok(topStrategies);
} }
/// <summary>
/// Retrieves the top 3 performing strategies based on ROI percentage.
/// </summary>
/// <returns>A <see cref="TopStrategiesByRoiViewModel"/> containing the top performing strategies by ROI.</returns>
[HttpGet("GetTopStrategiesByRoi")]
public async Task<ActionResult<TopStrategiesByRoiViewModel>> GetTopStrategiesByRoi()
{
// Get active bots
var activeBots = await _mediator.Send(new GetBotsByStatusCommand(BotStatus.Running));
// Filter bots with valid ROI data and order by ROI
var botsWithRoi = activeBots
.Select(bot => new { Bot = bot, Roi = bot.Roi, PnL = bot.Pnl, Volume = bot.Volume })
.OrderByDescending(item => item.Roi)
.Take(3)
.ToList();
// Map to view model
var topStrategiesByRoi = new TopStrategiesByRoiViewModel
{
TopStrategiesByRoi = botsWithRoi
.Select(item => new StrategyRoiPerformance
{
StrategyName = item.Bot.Name,
Roi = item.Roi,
PnL = item.PnL,
Volume = item.Volume
})
.ToList()
};
return Ok(topStrategiesByRoi);
}
/// <summary>
/// Retrieves the top 3 performing agents based on PnL.
/// </summary>
/// <returns>A <see cref="TopAgentsByPnLViewModel"/> containing the top performing agents by PnL.</returns>
[HttpGet("GetTopAgentsByPnL")]
public async Task<ActionResult<TopAgentsByPnLViewModel>> GetTopAgentsByPnL()
{
try
{
// Get all agent summaries
var allAgentSummaries = await _mediator.Send(new GetAllAgentSummariesCommand("Total"));
// Filter agents with valid PnL data and order by PnL
var agentsWithPnL = allAgentSummaries
.Where(agent => agent.TotalPnL != 0) // Only include agents with actual PnL
.OrderByDescending(agent => agent.TotalPnL)
.Take(3)
.ToList();
// Map to view model
var topAgentsByPnL = new TopAgentsByPnLViewModel
{
TopAgentsByPnL = agentsWithPnL
.Select(agent => new AgentPerformance
{
AgentName = agent.AgentName,
PnL = agent.TotalPnL,
TotalROI = agent.TotalROI,
TotalVolume = agent.TotalVolume,
ActiveStrategiesCount = agent.ActiveStrategiesCount,
TotalBalance = agent.TotalBalance
})
.ToList()
};
return Ok(topAgentsByPnL);
}
catch (Exception ex)
{
return StatusCode(500, $"Error retrieving top agents by PnL: {ex.Message}");
}
}
/// <summary> /// <summary>
/// Retrieves list of the active strategies for a user with detailed information /// Retrieves list of the active strategies for a user with detailed information
/// </summary> /// </summary>
@@ -378,24 +451,18 @@ public class DataController : ControllerBase
[HttpGet("GetUserStrategies")] [HttpGet("GetUserStrategies")]
public async Task<ActionResult<List<UserStrategyDetailsViewModel>>> GetUserStrategies(string agentName) public async Task<ActionResult<List<UserStrategyDetailsViewModel>>> GetUserStrategies(string agentName)
{ {
string cacheKey = $"UserStrategies_{agentName}";
// Check if the user strategy details are already cached
var cachedDetails = _cacheService.GetValue<List<UserStrategyDetailsViewModel>>(cacheKey);
if (cachedDetails != null && cachedDetails.Count > 0)
{
return Ok(cachedDetails);
}
// Get all strategies for the specified user // Get all strategies for the specified user
var userStrategies = await _mediator.Send(new GetUserStrategiesCommand(agentName)); var userStrategies = await _mediator.Send(new GetUserStrategiesCommand(agentName));
// Convert to detailed view model with additional information // Get all positions for all strategies in a single database call to avoid DbContext concurrency issues
var result = userStrategies.Select(strategy => MapStrategyToViewModel(strategy)).ToList(); var strategyIdentifiers = userStrategies.Select(s => s.Identifier).ToList();
var allPositions = await _tradingService.GetPositionsByInitiatorIdentifiersAsync(strategyIdentifiers);
var positionsByIdentifier = allPositions.GroupBy(p => p.InitiatorIdentifier)
.ToDictionary(g => g.Key, g => g.ToList());
// Cache the results for 5 minutes // Convert to detailed view model with additional information
_cacheService.SaveValue(cacheKey, result, TimeSpan.FromMinutes(5)); var result = userStrategies.Select(strategy => MapStrategyToViewModel(strategy, positionsByIdentifier))
.ToList();
return Ok(result); return Ok(result);
} }
@@ -409,16 +476,6 @@ public class DataController : ControllerBase
[HttpGet("GetUserStrategy")] [HttpGet("GetUserStrategy")]
public async Task<ActionResult<UserStrategyDetailsViewModel>> GetUserStrategy(string agentName, string strategyName) public async Task<ActionResult<UserStrategyDetailsViewModel>> GetUserStrategy(string agentName, string strategyName)
{ {
string cacheKey = $"UserStrategy_{agentName}_{strategyName}";
// Check if the user strategy details are already cached
var cachedDetails = _cacheService.GetValue<UserStrategyDetailsViewModel>(cacheKey);
if (cachedDetails != null)
{
return Ok(cachedDetails);
}
// Get the specific strategy for the user // Get the specific strategy for the user
var strategy = await _mediator.Send(new GetUserStrategyCommand(agentName, strategyName)); var strategy = await _mediator.Send(new GetUserStrategyCommand(agentName, strategyName));
@@ -428,181 +485,234 @@ public class DataController : ControllerBase
} }
// Map the strategy to a view model using the shared method // Map the strategy to a view model using the shared method
var result = MapStrategyToViewModel(strategy); var result = await MapStrategyToViewModelAsync(strategy);
// Cache the results for 5 minutes
_cacheService.SaveValue(cacheKey, result, TimeSpan.FromMinutes(5));
return Ok(result); return Ok(result);
} }
/// <summary>
/// Maps a trading bot to a strategy view model with detailed statistics using pre-fetched positions
/// </summary>
/// <param name="strategy">The trading bot to map</param>
/// <param name="positionsByIdentifier">Pre-fetched positions grouped by initiator identifier</param>
/// <returns>A view model with detailed strategy information</returns>
private UserStrategyDetailsViewModel MapStrategyToViewModel(Bot strategy,
Dictionary<Guid, List<Position>> positionsByIdentifier)
{
// Calculate ROI percentage based on PnL relative to account value
decimal pnl = strategy.Pnl;
// If we had initial investment amount, we could calculate ROI like:
decimal initialInvestment = 1000; // Example placeholder, ideally should come from the account
decimal roi = pnl != 0 ? (pnl / initialInvestment) * 100 : 0;
// Calculate volume statistics
decimal totalVolume = strategy.Volume;
decimal volumeLast24h = strategy.Volume;
// Calculate win/loss statistics
(int wins, int losses) = (strategy.TradeWins, strategy.TradeLosses);
int winRate = wins + losses > 0 ? (wins * 100) / (wins + losses) : 0;
// Calculate ROI for last 24h
decimal roiLast24h = strategy.Roi;
// Get positions for this strategy from pre-fetched data
var positions = positionsByIdentifier.TryGetValue(strategy.Identifier, out var strategyPositions)
? strategyPositions
: new List<Position>();
return new UserStrategyDetailsViewModel
{
Name = strategy.Name,
State = strategy.Status,
PnL = pnl,
ROIPercentage = roi,
ROILast24H = roiLast24h,
Runtime = strategy.StartupTime,
WinRate = winRate,
TotalVolumeTraded = totalVolume,
VolumeLast24H = volumeLast24h,
Wins = wins,
Losses = losses,
Positions = positions,
Identifier = strategy.Identifier,
WalletBalances = new Dictionary<DateTime, decimal>(),
};
}
/// <summary> /// <summary>
/// Maps a trading bot to a strategy view model with detailed statistics /// Maps a trading bot to a strategy view model with detailed statistics
/// </summary> /// </summary>
/// <param name="strategy">The trading bot to map</param> /// <param name="strategy">The trading bot to map</param>
/// <returns>A view model with detailed strategy information</returns> /// <returns>A view model with detailed strategy information</returns>
private UserStrategyDetailsViewModel MapStrategyToViewModel(ITradingBot strategy) private async Task<UserStrategyDetailsViewModel> MapStrategyToViewModelAsync(Bot strategy)
{ {
// Get the runtime directly from the bot
TimeSpan runtimeSpan = strategy.GetRuntime();
// Get the startup time from the bot's internal property
// If bot is not running, we use MinValue as a placeholder
DateTime startupTime = DateTime.MinValue;
if (strategy is Bot bot && bot.StartupTime != DateTime.MinValue)
{
startupTime = bot.StartupTime;
}
// Calculate ROI percentage based on PnL relative to account value // Calculate ROI percentage based on PnL relative to account value
decimal pnl = strategy.GetProfitAndLoss(); decimal pnl = strategy.Pnl;
// If we had initial investment amount, we could calculate ROI like: // If we had initial investment amount, we could calculate ROI like:
decimal initialInvestment = 1000; // Example placeholder, ideally should come from the account decimal initialInvestment = 1000; // Example placeholder, ideally should come from the account
decimal roi = pnl != 0 ? (pnl / initialInvestment) * 100 : 0; decimal roi = pnl != 0 ? (pnl / initialInvestment) * 100 : 0;
// Calculate volume statistics // Calculate volume statistics
decimal totalVolume = TradingBox.GetTotalVolumeTraded(strategy.Positions); decimal totalVolume = strategy.Volume;
decimal volumeLast24h = TradingBox.GetLast24HVolumeTraded(strategy.Positions); decimal volumeLast24h = strategy.Volume;
// Calculate win/loss statistics // Calculate win/loss statistics
(int wins, int losses) = TradingBox.GetWinLossCount(strategy.Positions); (int wins, int losses) = (strategy.TradeWins, strategy.TradeLosses);
int winRate = wins + losses > 0 ? (wins * 100) / (wins + losses) : 0;
// Calculate ROI for last 24h // Calculate ROI for last 24h
decimal roiLast24h = TradingBox.GetLast24HROI(strategy.Positions); decimal roiLast24h = strategy.Roi;
// Fetch positions associated with this bot
var positions = await _tradingService.GetPositionsByInitiatorIdentifierAsync(strategy.Identifier);
return new UserStrategyDetailsViewModel return new UserStrategyDetailsViewModel
{ {
Name = strategy.Name, Name = strategy.Name,
ScenarioName = strategy.Config.ScenarioName, State = strategy.Status,
State = strategy.GetStatus() == BotStatus.Up.ToString() ? "RUNNING" :
strategy.GetStatus() == BotStatus.Down.ToString() ? "STOPPED" : "UNUSED",
PnL = pnl, PnL = pnl,
ROIPercentage = roi, ROIPercentage = roi,
ROILast24H = roiLast24h, ROILast24H = roiLast24h,
Runtime = startupTime, Runtime = strategy.StartupTime,
WinRate = strategy.GetWinRate(), WinRate = winRate,
TotalVolumeTraded = totalVolume, TotalVolumeTraded = totalVolume,
VolumeLast24H = volumeLast24h, VolumeLast24H = volumeLast24h,
Wins = wins, Wins = wins,
Losses = losses, Losses = losses,
Positions = strategy.Positions.OrderByDescending(p => p.Date) Positions = positions.ToList(),
.ToList(), // Include sorted positions with most recent first
Identifier = strategy.Identifier, Identifier = strategy.Identifier,
WalletBalances = new Dictionary<DateTime, decimal>(),
}; };
} }
/// <summary> /// <summary>
/// Retrieves a summary of platform activity across all agents /// Retrieves a summary of platform activity across all agents (platform-level data only)
/// Uses Orleans grain for efficient caching and real-time updates
/// </summary> /// </summary>
/// <param name="timeFilter">Time filter to apply (24H, 3D, 1W, 1M, 1Y, Total)</param> /// <returns>A summary of platform activity without individual agent details</returns>
/// <returns>A summary of platform activity including per-agent statistics</returns>
[HttpGet("GetPlatformSummary")] [HttpGet("GetPlatformSummary")]
public async Task<ActionResult<PlatformSummaryViewModel>> GetPlatformSummary(string timeFilter = "Total") public async Task<ActionResult<PlatformSummaryViewModel>> GetPlatformSummary()
{ {
// Validate time filter try
var validTimeFilters = new[] { "24H", "3D", "1W", "1M", "1Y", "Total" };
if (!validTimeFilters.Contains(timeFilter))
{ {
timeFilter = "Total"; // Default to Total if invalid // Get the platform summary grain
var platformSummaryGrain = _grainFactory.GetGrain<IPlatformSummaryGrain>("platform-summary");
// Get the platform summary from the grain (handles caching and real-time updates)
var abstractionsSummary = await platformSummaryGrain.GetPlatformSummaryAsync();
// Convert to API ViewModel
var summary = abstractionsSummary.ToApiViewModel();
return Ok(summary);
}
catch (Exception ex)
{
// Log the error and return a fallback response
// In production, you might want to return cached data or partial data
return StatusCode(500, $"Error retrieving platform summary: {ex.Message}");
}
}
/// <summary>
/// Retrieves a paginated list of agent summaries for the agent index page
/// </summary>
/// <param name="page">Page number (defaults to 1)</param>
/// <param name="pageSize">Number of items per page (defaults to 10, max 100)</param>
/// <param name="sortBy">Field to sort by (TotalPnL, TotalROI, Wins, Losses, AgentName, CreatedAt, UpdatedAt)</param>
/// <param name="sortOrder">Sort order - "asc" or "desc" (defaults to "desc")</param>
/// <param name="agentNames">Optional comma-separated list of agent names to filter by</param>
/// <returns>A paginated list of agent summaries sorted by the specified field</returns>
[AllowAnonymous]
[HttpGet("GetAgentIndexPaginated")]
public async Task<ActionResult<PaginatedAgentIndexResponse>> GetAgentIndexPaginated(
int page = 1,
int pageSize = 10,
SortableFields sortBy = SortableFields.TotalPnL,
string sortOrder = "desc",
string? agentNames = null)
{
// Validate pagination parameters
if (page < 1)
{
return BadRequest("Page must be greater than 0");
} }
string cacheKey = $"PlatformSummary_{timeFilter}"; if (pageSize < 1 || pageSize > 100)
// Check if the platform summary is already cached
var cachedSummary = _cacheService.GetValue<PlatformSummaryViewModel>(cacheKey);
if (cachedSummary != null)
{ {
return Ok(cachedSummary); return BadRequest("Page size must be between 1 and 100");
} }
// Get all agents and their strategies // Validate sort order
var agentsWithStrategies = await _mediator.Send(new GetAllAgentsCommand(timeFilter)); if (sortOrder != "asc" && sortOrder != "desc")
// Create the platform summary
var summary = new PlatformSummaryViewModel
{ {
TotalAgents = agentsWithStrategies.Count, return BadRequest("Sort order must be 'asc' or 'desc'");
TotalActiveStrategies = agentsWithStrategies.Values.Sum(list => list.Count), }
TimeFilter = timeFilter
};
// Calculate total platform metrics // Parse agent names filter
decimal totalPlatformPnL = 0; IEnumerable<string>? agentNamesList = null;
decimal totalPlatformVolume = 0; if (!string.IsNullOrWhiteSpace(agentNames))
decimal totalPlatformVolumeLast24h = 0;
// Create summaries for each agent
foreach (var agent in agentsWithStrategies)
{ {
var user = agent.Key; agentNamesList = agentNames.Split(',', StringSplitOptions.RemoveEmptyEntries)
var strategies = agent.Value; .Select(name => name.Trim())
.Where(name => !string.IsNullOrWhiteSpace(name))
.ToList();
}
if (strategies.Count == 0) // Get paginated results from database
{ var command = new GetPaginatedAgentSummariesCommand(page, pageSize, sortBy, sortOrder, agentNamesList);
continue; // Skip agents with no strategies var result = await _mediator.Send(command);
} var agentSummaries = result.Results;
var totalCount = result.TotalCount;
// Combine all positions from all strategies
var allPositions = strategies.SelectMany<ITradingBot, Position>(s => s.Positions).ToList();
// Calculate agent metrics
decimal totalPnL = TradingBox.GetPnLInTimeRange(allPositions, timeFilter);
decimal pnlLast24h = TradingBox.GetPnLInTimeRange(allPositions, "24H");
decimal totalROI = TradingBox.GetROIInTimeRange(allPositions, timeFilter);
decimal roiLast24h = TradingBox.GetROIInTimeRange(allPositions, "24H");
(int wins, int losses) = TradingBox.GetWinLossCountInTimeRange(allPositions, timeFilter);
// Calculate trading volumes
decimal totalVolume = TradingBox.GetTotalVolumeTraded(allPositions);
decimal volumeLast24h = TradingBox.GetLast24HVolumeTraded(allPositions);
// Map to view models
var agentSummaryViewModels = new List<AgentSummaryViewModel>();
foreach (var agentSummary in agentSummaries)
{
// Calculate win rate // Calculate win rate
int averageWinRate = 0; int averageWinRate = 0;
if (wins + losses > 0) if (agentSummary.Wins + agentSummary.Losses > 0)
{ {
averageWinRate = (wins * 100) / (wins + losses); averageWinRate = (agentSummary.Wins * 100) / (agentSummary.Wins + agentSummary.Losses);
} }
// Add to agent summaries // Map to view model
var agentSummary = new AgentSummaryViewModel var agentSummaryViewModel = new AgentSummaryViewModel
{ {
AgentName = user.AgentName, AgentName = agentSummary.AgentName,
TotalPnL = totalPnL, TotalPnL = agentSummary.TotalPnL,
PnLLast24h = pnlLast24h, TotalROI = agentSummary.TotalROI,
TotalROI = totalROI, Wins = agentSummary.Wins,
ROILast24h = roiLast24h, Losses = agentSummary.Losses,
Wins = wins, ActiveStrategiesCount = agentSummary.ActiveStrategiesCount,
Losses = losses, TotalVolume = agentSummary.TotalVolume,
AverageWinRate = averageWinRate, TotalBalance = agentSummary.TotalBalance,
ActiveStrategiesCount = strategies.Count,
TotalVolume = totalVolume,
VolumeLast24h = volumeLast24h
}; };
summary.AgentSummaries.Add(agentSummary); agentSummaryViewModels.Add(agentSummaryViewModel);
// Add to platform totals
totalPlatformPnL += totalPnL;
totalPlatformVolume += totalVolume;
totalPlatformVolumeLast24h += volumeLast24h;
} }
// Set the platform totals var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
summary.TotalPlatformPnL = totalPlatformPnL;
summary.TotalPlatformVolume = totalPlatformVolume;
summary.TotalPlatformVolumeLast24h = totalPlatformVolumeLast24h;
// Sort agent summaries by total PnL (highest first) var response = new PaginatedAgentIndexResponse
summary.AgentSummaries = summary.AgentSummaries.OrderByDescending(a => a.TotalPnL).ToList(); {
AgentSummaries = agentSummaryViewModels,
TotalCount = totalCount,
CurrentPage = page,
PageSize = pageSize,
TotalPages = totalPages,
HasNextPage = page < totalPages,
HasPreviousPage = page > 1,
SortBy = sortBy,
SortOrder = sortOrder,
FilteredAgentNames = agentNames
};
// Cache the results for 5 minutes return Ok(response);
_cacheService.SaveValue(cacheKey, summary, TimeSpan.FromMinutes(5));
return Ok(summary);
} }
/// <summary> /// <summary>
@@ -618,7 +728,7 @@ public class DataController : ControllerBase
DateTime startDate, DateTime startDate,
DateTime? endDate = null) DateTime? endDate = null)
{ {
var balances = await _statisticService.GetAgentBalances(agentName, startDate, endDate); var balances = await _agentService.GetAgentBalances(agentName, startDate, endDate);
return Ok(balances); return Ok(balances);
} }
@@ -637,7 +747,7 @@ public class DataController : ControllerBase
int page = 1, int page = 1,
int pageSize = 10) int pageSize = 10)
{ {
var (agents, totalCount) = await _statisticService.GetBestAgents(startDate, endDate, page, pageSize); var (agents, totalCount) = await _agentService.GetBestAgents(startDate, endDate, page, pageSize);
var response = new BestAgentsResponse var response = new BestAgentsResponse
{ {
@@ -651,6 +761,32 @@ public class DataController : ControllerBase
return Ok(response); return Ok(response);
} }
/// <summary>
/// Retrieves an array of online agent names
/// </summary>
/// <returns>An array of online agent names</returns>
[HttpGet("GetOnlineAgent")]
public async Task<ActionResult<IEnumerable<string>>> GetOnlineAgent()
{
const string cacheKey = "OnlineAgentNames";
// Check if the online agent names are already cached
var cachedAgentNames = _cacheService.GetValue<List<string>>(cacheKey);
if (cachedAgentNames != null)
{
return Ok(cachedAgentNames);
}
// Get only online agent names
var onlineAgentNames = await _mediator.Send(new GetOnlineAgentNamesCommand());
// Cache the results for 2 minutes
_cacheService.SaveValue(cacheKey, onlineAgentNames, TimeSpan.FromMinutes(2));
return Ok(onlineAgentNames);
}
/// <summary> /// <summary>
/// Maps a ScenarioRequest to a domain Scenario object. /// Maps a ScenarioRequest to a domain Scenario object.
/// </summary> /// </summary>
@@ -662,7 +798,7 @@ public class DataController : ControllerBase
foreach (var indicatorRequest in scenarioRequest.Indicators) foreach (var indicatorRequest in scenarioRequest.Indicators)
{ {
var indicator = new Indicator(indicatorRequest.Name, indicatorRequest.Type) var indicator = new IndicatorBase(indicatorRequest.Name, indicatorRequest.Type)
{ {
SignalType = indicatorRequest.SignalType, SignalType = indicatorRequest.SignalType,
MinimumHistory = indicatorRequest.MinimumHistory, MinimumHistory = indicatorRequest.MinimumHistory,

View File

@@ -197,23 +197,23 @@ public class ScenarioController : BaseController
}; };
} }
private static IndicatorViewModel MapToIndicatorViewModel(Indicator indicator) private static IndicatorViewModel MapToIndicatorViewModel(IndicatorBase indicatorBase)
{ {
return new IndicatorViewModel return new IndicatorViewModel
{ {
Name = indicator.Name, Name = indicatorBase.Name,
Type = indicator.Type, Type = indicatorBase.Type,
SignalType = indicator.SignalType, SignalType = indicatorBase.SignalType,
MinimumHistory = indicator.MinimumHistory, MinimumHistory = indicatorBase.MinimumHistory,
Period = indicator.Period, Period = indicatorBase.Period,
FastPeriods = indicator.FastPeriods, FastPeriods = indicatorBase.FastPeriods,
SlowPeriods = indicator.SlowPeriods, SlowPeriods = indicatorBase.SlowPeriods,
SignalPeriods = indicator.SignalPeriods, SignalPeriods = indicatorBase.SignalPeriods,
Multiplier = indicator.Multiplier, Multiplier = indicatorBase.Multiplier,
SmoothPeriods = indicator.SmoothPeriods, SmoothPeriods = indicatorBase.SmoothPeriods,
StochPeriods = indicator.StochPeriods, StochPeriods = indicatorBase.StochPeriods,
CyclePeriods = indicator.CyclePeriods, CyclePeriods = indicatorBase.CyclePeriods,
UserName = indicator.User?.Name UserName = indicatorBase.User?.Name
}; };
} }
} }

View File

@@ -1,5 +1,6 @@
using Managing.Application.Abstractions; using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Services; using Managing.Application.Abstractions.Services;
using Managing.Application.Shared;
using Managing.Application.Trading.Commands; using Managing.Application.Trading.Commands;
using Managing.Domain.MoneyManagements; using Managing.Domain.MoneyManagements;
using Managing.Domain.Trades; using Managing.Domain.Trades;
@@ -26,6 +27,8 @@ public class TradingController : BaseController
private readonly IMoneyManagementService _moneyManagementService; private readonly IMoneyManagementService _moneyManagementService;
private readonly IMediator _mediator; private readonly IMediator _mediator;
private readonly ILogger<TradingController> _logger; private readonly ILogger<TradingController> _logger;
private readonly IAdminConfigurationService _adminService;
private readonly IAccountService _accountService;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="TradingController"/> class. /// Initializes a new instance of the <see cref="TradingController"/> class.
@@ -35,13 +38,16 @@ public class TradingController : BaseController
/// <param name="closeTradeCommandHandler">Command handler for closing trades.</param> /// <param name="closeTradeCommandHandler">Command handler for closing trades.</param>
/// <param name="tradingService">Service for trading operations.</param> /// <param name="tradingService">Service for trading operations.</param>
/// <param name="mediator">Mediator for handling commands and requests.</param> /// <param name="mediator">Mediator for handling commands and requests.</param>
/// <param name="adminService">Service for checking admin privileges.</param>
/// <param name="accountService">Service for account operations.</param>
public TradingController( public TradingController(
ILogger<TradingController> logger, ILogger<TradingController> logger,
ICommandHandler<OpenPositionRequest, Position> openTradeCommandHandler, ICommandHandler<OpenPositionRequest, Position> openTradeCommandHandler,
ICommandHandler<ClosePositionCommand, Position> closeTradeCommandHandler, ICommandHandler<ClosePositionCommand, Position> closeTradeCommandHandler,
ITradingService tradingService, ITradingService tradingService,
IMediator mediator, IMoneyManagementService moneyManagementService, IMediator mediator, IMoneyManagementService moneyManagementService,
IUserService userService) : base(userService) IUserService userService, IAdminConfigurationService adminService,
IAccountService accountService) : base(userService)
{ {
_logger = logger; _logger = logger;
_openTradeCommandHandler = openTradeCommandHandler; _openTradeCommandHandler = openTradeCommandHandler;
@@ -49,6 +55,8 @@ public class TradingController : BaseController
_tradingService = tradingService; _tradingService = tradingService;
_mediator = mediator; _mediator = mediator;
_moneyManagementService = moneyManagementService; _moneyManagementService = moneyManagementService;
_adminService = adminService;
_accountService = accountService;
} }
@@ -85,7 +93,7 @@ public class TradingController : BaseController
/// <param name="identifier">The unique identifier of the position to close.</param> /// <param name="identifier">The unique identifier of the position to close.</param>
/// <returns>The closed position.</returns> /// <returns>The closed position.</returns>
[HttpPost("ClosePosition")] [HttpPost("ClosePosition")]
public async Task<ActionResult<Position>> ClosePosition(string identifier) public async Task<ActionResult<Position>> ClosePosition(Guid identifier)
{ {
var position = await _tradingService.GetPositionByIdentifierAsync(identifier); var position = await _tradingService.GetPositionByIdentifierAsync(identifier);
var result = await _closeTradeCommandHandler.Handle(new ClosePositionCommand(position)); var result = await _closeTradeCommandHandler.Handle(new ClosePositionCommand(position));
@@ -149,6 +157,7 @@ public class TradingController : BaseController
/// <summary> /// <summary>
/// Initializes a Privy wallet address for the user. /// Initializes a Privy wallet address for the user.
/// Only admins can initialize any address, regular users can only initialize their own addresses.
/// </summary> /// </summary>
/// <param name="publicAddress">The public address of the Privy wallet to initialize.</param> /// <param name="publicAddress">The public address of the Privy wallet to initialize.</param>
/// <returns>The initialization response containing success status and transaction hashes.</returns> /// <returns>The initialization response containing success status and transaction hashes.</returns>
@@ -162,6 +171,18 @@ public class TradingController : BaseController
try try
{ {
var user = await GetUser();
if (user == null)
{
return Unauthorized("User not found");
}
// Check if user has permission to initialize this address
if (!await CanUserInitializeAddress(user.Name, publicAddress))
{
return Forbid("You don't have permission to initialize this wallet address. You can only initialize your own wallet addresses.");
}
var result = await _tradingService.InitPrivyWallet(publicAddress); var result = await _tradingService.InitPrivyWallet(publicAddress);
return Ok(result); return Ok(result);
} }
@@ -175,4 +196,42 @@ public class TradingController : BaseController
}); });
} }
} }
/// <summary>
/// Checks if the user can initialize the given public address.
/// Admins can initialize any address, regular users can only initialize their own addresses.
/// </summary>
/// <param name="userName">The username to check</param>
/// <param name="publicAddress">The public address to initialize</param>
/// <returns>True if the user can initialize the address, false otherwise</returns>
private async Task<bool> CanUserInitializeAddress(string userName, string publicAddress)
{
// Admin users can initialize any address
if (_adminService.IsUserAdmin(userName))
{
_logger.LogInformation("Admin user {UserName} initializing address {Address}", userName, publicAddress);
return true;
}
try
{
// Regular users can only initialize their own addresses
// Check if the address belongs to one of the user's accounts
var account = await _accountService.GetAccountByKey(publicAddress, true, false);
if (account?.User?.Name == userName)
{
_logger.LogInformation("User {UserName} initializing their own address {Address}", userName, publicAddress);
return true;
}
_logger.LogWarning("User {UserName} attempted to initialize address {Address} that doesn't belong to them", userName, publicAddress);
return false;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Unable to verify ownership of address {Address} for user {UserName}", publicAddress, userName);
return false;
}
}
} }

View File

@@ -26,7 +26,8 @@ public class UserController : BaseController
/// <param name="userService">Service for user-related operations.</param> /// <param name="userService">Service for user-related operations.</param>
/// <param name="jwtUtils">Utility for JWT token operations.</param> /// <param name="jwtUtils">Utility for JWT token operations.</param>
/// <param name="webhookService">Service for webhook operations.</param> /// <param name="webhookService">Service for webhook operations.</param>
public UserController(IConfiguration config, IUserService userService, IJwtUtils jwtUtils, IWebhookService webhookService) public UserController(IConfiguration config, IUserService userService, IJwtUtils jwtUtils,
IWebhookService webhookService)
: base(userService) : base(userService)
{ {
_config = config; _config = config;
@@ -40,7 +41,7 @@ public class UserController : BaseController
/// <param name="login">The login request containing user credentials.</param> /// <param name="login">The login request containing user credentials.</param>
/// <returns>A JWT token if authentication is successful; otherwise, an Unauthorized result.</returns> /// <returns>A JWT token if authentication is successful; otherwise, an Unauthorized result.</returns>
[AllowAnonymous] [AllowAnonymous]
[HttpPost] [HttpPost("create-token")]
public async Task<ActionResult<string>> CreateToken([FromBody] LoginRequest login) public async Task<ActionResult<string>> CreateToken([FromBody] LoginRequest login)
{ {
var user = await _userService.Authenticate(login.Name, login.Address, login.Message, login.Signature); var user = await _userService.Authenticate(login.Name, login.Address, login.Message, login.Signature);
@@ -58,10 +59,12 @@ public class UserController : BaseController
/// Gets the current user's information. /// Gets the current user's information.
/// </summary> /// </summary>
/// <returns>The current user's information.</returns> /// <returns>The current user's information.</returns>
[Authorize]
[HttpGet] [HttpGet]
public async Task<ActionResult<User>> GetCurrentUser() public async Task<ActionResult<User>> GetCurrentUser()
{ {
var user = await base.GetUser(); var user = await base.GetUser();
user = await _userService.GetUserByName(user.Name);
return Ok(user); return Ok(user);
} }
@@ -70,6 +73,7 @@ public class UserController : BaseController
/// </summary> /// </summary>
/// <param name="agentName">The new agent name to set.</param> /// <param name="agentName">The new agent name to set.</param>
/// <returns>The updated user with the new agent name.</returns> /// <returns>The updated user with the new agent name.</returns>
[Authorize]
[HttpPut("agent-name")] [HttpPut("agent-name")]
public async Task<ActionResult<User>> UpdateAgentName([FromBody] string agentName) public async Task<ActionResult<User>> UpdateAgentName([FromBody] string agentName)
{ {
@@ -83,6 +87,7 @@ public class UserController : BaseController
/// </summary> /// </summary>
/// <param name="avatarUrl">The new avatar URL to set.</param> /// <param name="avatarUrl">The new avatar URL to set.</param>
/// <returns>The updated user with the new avatar URL.</returns> /// <returns>The updated user with the new avatar URL.</returns>
[Authorize]
[HttpPut("avatar")] [HttpPut("avatar")]
public async Task<ActionResult<User>> UpdateAvatarUrl([FromBody] string avatarUrl) public async Task<ActionResult<User>> UpdateAvatarUrl([FromBody] string avatarUrl)
{ {
@@ -96,6 +101,7 @@ public class UserController : BaseController
/// </summary> /// </summary>
/// <param name="telegramChannel">The new Telegram channel to set.</param> /// <param name="telegramChannel">The new Telegram channel to set.</param>
/// <returns>The updated user with the new Telegram channel.</returns> /// <returns>The updated user with the new Telegram channel.</returns>
[Authorize]
[HttpPut("telegram-channel")] [HttpPut("telegram-channel")]
public async Task<ActionResult<User>> UpdateTelegramChannel([FromBody] string telegramChannel) public async Task<ActionResult<User>> UpdateTelegramChannel([FromBody] string telegramChannel)
{ {
@@ -108,20 +114,21 @@ public class UserController : BaseController
try try
{ {
var welcomeMessage = $"🎉 **Trading Bot - Welcome!**\n\n" + var welcomeMessage = $"🎉 **Trading Bot - Welcome!**\n\n" +
$"🎯 **Agent:** {user.Name}\n" + $"🎯 **Agent:** {user.Name}\n" +
$"📡 **Channel ID:** {telegramChannel}\n" + $"📡 **Channel ID:** {telegramChannel}\n" +
$"⏰ **Setup Time:** {DateTime.UtcNow:MMM dd, yyyy • HH:mm:ss} UTC\n\n" + $"⏰ **Setup Time:** {DateTime.UtcNow:MMM dd, yyyy • HH:mm:ss} UTC\n\n" +
$"🔔 **Notification Types:**\n" + $"🔔 **Notification Types:**\n" +
$"• 📈 Position Opens & Closes\n" + $"• 📈 Position Opens & Closes\n" +
$"• 🤖 Bot configuration changes\n\n" + $"• 🤖 Bot configuration changes\n\n" +
$"🚀 **Welcome aboard!** Your trading notifications are now live."; $"🚀 **Welcome aboard!** Your trading notifications are now live.";
await _webhookService.SendMessage(welcomeMessage, telegramChannel); await _webhookService.SendMessage(welcomeMessage, telegramChannel);
} }
catch (Exception ex) catch (Exception ex)
{ {
// Log the error but don't fail the update operation // Log the error but don't fail the update operation
Console.WriteLine($"Failed to send welcome message to telegram channel {telegramChannel}: {ex.Message}"); Console.WriteLine(
$"Failed to send welcome message to telegram channel {telegramChannel}: {ex.Message}");
} }
} }
@@ -132,6 +139,7 @@ public class UserController : BaseController
/// Tests the Telegram channel configuration by sending a test message. /// Tests the Telegram channel configuration by sending a test message.
/// </summary> /// </summary>
/// <returns>A message indicating the test result.</returns> /// <returns>A message indicating the test result.</returns>
[Authorize]
[HttpPost("telegram-channel/test")] [HttpPost("telegram-channel/test")]
public async Task<ActionResult<string>> TestTelegramChannel() public async Task<ActionResult<string>> TestTelegramChannel()
{ {
@@ -144,7 +152,7 @@ public class UserController : BaseController
try try
{ {
var testMessage = $"🚀 **Trading Bot - Channel Test**\n\n" + var testMessage = $"🚀 **Trading Bot - Channel Test**\n\n" +
$"🎯 **Agent:** {user.Name}\n" + $"🎯 **Agent:** {user.Name}\n" +
$"📡 **Channel ID:** {user.TelegramChannel}\n" + $"📡 **Channel ID:** {user.TelegramChannel}\n" +
$"⏰ **Test Time:** {DateTime.UtcNow:MMM dd, yyyy • HH:mm:ss} UTC\n\n" + $"⏰ **Test Time:** {DateTime.UtcNow:MMM dd, yyyy • HH:mm:ss} UTC\n\n" +
@@ -155,7 +163,8 @@ public class UserController : BaseController
await _webhookService.SendMessage(testMessage, user.TelegramChannel); await _webhookService.SendMessage(testMessage, user.TelegramChannel);
return Ok($"Test message sent successfully to Telegram channel {user.TelegramChannel}. Please check your Telegram to verify delivery."); return Ok(
$"Test message sent successfully to Telegram channel {user.TelegramChannel}. Please check your Telegram to verify delivery.");
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -163,4 +172,3 @@ public class UserController : BaseController
} }
} }
} }

View File

@@ -1,72 +0,0 @@
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Services;
using Managing.Domain.Workflows;
using Managing.Domain.Workflows.Synthetics;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Managing.Api.Controllers
{
/// <summary>
/// Controller for managing workflows, including creating, retrieving, and deleting workflows.
/// Requires authorization for access.
/// </summary>
[Authorize]
public class WorkflowController : BaseController
{
private readonly IWorkflowService _workflowService;
/// <summary>
/// Initializes a new instance of the <see cref="WorkflowController"/> class.
/// </summary>
/// <param name="WorkflowService">Service for managing workflows.</param>
/// <param name="userService">Service for user-related operations.</param>
public WorkflowController(IWorkflowService WorkflowService, IUserService userService) : base(userService)
{
_workflowService = WorkflowService;
}
/// <summary>
/// Creates a new workflow or updates an existing one based on the provided workflow request.
/// </summary>
/// <param name="workflowRequest">The workflow request containing the details of the workflow to be created or updated.</param>
/// <returns>The created or updated workflow.</returns>
[HttpPost]
public async Task<ActionResult<Workflow>> PostWorkflow([ModelBinder]SyntheticWorkflow workflowRequest)
{
return Ok(await _workflowService.InsertOrUpdateWorkflow(workflowRequest));
}
/// <summary>
/// Retrieves all workflows.
/// </summary>
/// <returns>A list of all workflows.</returns>
[HttpGet]
public ActionResult<IEnumerable<SyntheticWorkflow>> GetWorkflows()
{
return Ok(_workflowService.GetWorkflows());
}
/// <summary>
/// Retrieves all available flows.
/// </summary>
/// <returns>A list of all available flows.</returns>
[HttpGet]
[Route("flows")]
public async Task<ActionResult<IEnumerable<IFlow>>> GetAvailableFlows()
{
return Ok(await _workflowService.GetAvailableFlows());
}
/// <summary>
/// Deletes a workflow by name.
/// </summary>
/// <param name="name">The name of the workflow to delete.</param>
/// <returns>An ActionResult indicating the outcome of the operation.</returns>
[HttpDelete]
public ActionResult DeleteWorkflow(string name)
{
return Ok(_workflowService.DeleteWorkflow(name));
}
}
}

View File

@@ -15,7 +15,6 @@ COPY ["Managing.Common/Managing.Common.csproj", "Managing.Common/"]
COPY ["Managing.Core/Managing.Core.csproj", "Managing.Core/"] COPY ["Managing.Core/Managing.Core.csproj", "Managing.Core/"]
COPY ["Managing.Application.Abstractions/Managing.Application.Abstractions.csproj", "Managing.Application.Abstractions/"] COPY ["Managing.Application.Abstractions/Managing.Application.Abstractions.csproj", "Managing.Application.Abstractions/"]
COPY ["Managing.Domain/Managing.Domain.csproj", "Managing.Domain/"] COPY ["Managing.Domain/Managing.Domain.csproj", "Managing.Domain/"]
COPY ["Managing.Application.Workers/Managing.Application.Workers.csproj", "Managing.Application.Workers/"]
COPY ["Managing.Infrastructure.Messengers/Managing.Infrastructure.Messengers.csproj", "Managing.Infrastructure.Messengers/"] COPY ["Managing.Infrastructure.Messengers/Managing.Infrastructure.Messengers.csproj", "Managing.Infrastructure.Messengers/"]
COPY ["Managing.Infrastructure.Exchanges/Managing.Infrastructure.Exchanges.csproj", "Managing.Infrastructure.Exchanges/"] COPY ["Managing.Infrastructure.Exchanges/Managing.Infrastructure.Exchanges.csproj", "Managing.Infrastructure.Exchanges/"]
COPY ["Managing.Infrastructure.Database/Managing.Infrastructure.Databases.csproj", "Managing.Infrastructure.Database/"] COPY ["Managing.Infrastructure.Database/Managing.Infrastructure.Databases.csproj", "Managing.Infrastructure.Database/"]

View File

@@ -0,0 +1,42 @@
using Managing.Api.Models.Responses;
using Managing.Common;
using AbstractionsPlatformSummaryViewModel = Managing.Application.Abstractions.Models.PlatformSummaryViewModel;
namespace Managing.Api.Extensions;
/// <summary>
/// Extension methods for converting between Platform Summary ViewModels
/// </summary>
public static class PlatformSummaryExtensions
{
/// <summary>
/// Converts from the Abstractions PlatformSummaryViewModel to the API PlatformSummaryViewModel
/// </summary>
public static PlatformSummaryViewModel ToApiViewModel(this AbstractionsPlatformSummaryViewModel abstractionsModel)
{
return new PlatformSummaryViewModel
{
TotalAgents = abstractionsModel.TotalAgents,
TotalActiveStrategies = abstractionsModel.TotalActiveStrategies,
TotalPlatformPnL = abstractionsModel.TotalPlatformPnL,
TotalPlatformVolume = abstractionsModel.TotalPlatformVolume,
TotalPlatformVolumeLast24h = abstractionsModel.TotalPlatformVolumeLast24h,
TotalOpenInterest = abstractionsModel.TotalOpenInterest,
TotalPositionCount = abstractionsModel.TotalPositionCount,
AgentsChange24h = abstractionsModel.AgentsChange24h,
StrategiesChange24h = abstractionsModel.StrategiesChange24h,
PnLChange24h = abstractionsModel.PnLChange24h,
VolumeChange24h = abstractionsModel.VolumeChange24h,
OpenInterestChange24h = abstractionsModel.OpenInterestChange24h,
PositionCountChange24h = abstractionsModel.PositionCountChange24h,
VolumeByAsset = abstractionsModel.VolumeByAsset ?? new Dictionary<Enums.Ticker, decimal>(),
PositionCountByAsset = abstractionsModel.PositionCountByAsset ?? new Dictionary<Enums.Ticker, int>(),
PositionCountByDirection = abstractionsModel.PositionCountByDirection?.ToDictionary(
kvp => kvp.Key,
kvp => kvp.Value) ?? new Dictionary<Enums.TradeDirection, int>(),
LastUpdated = abstractionsModel.LastUpdated,
Last24HourSnapshot = abstractionsModel.Last24HourSnapshot,
VolumeHistory = abstractionsModel.VolumeHistory,
};
}
}

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"/>
@@ -51,5 +53,8 @@
<Content Update="appsettings.Production.json"> <Content Update="appsettings.Production.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="appsettings.KaiServer.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -0,0 +1,55 @@
using static Managing.Common.Enums;
namespace Managing.Api.Models.Requests;
/// <summary>
/// Request model for getting paginated bots with filtering and sorting
/// </summary>
public class GetBotsPaginatedRequest
{
/// <summary>
/// Page number (1-based). Default is 1.
/// </summary>
public int PageNumber { get; set; } = 1;
/// <summary>
/// Number of items per page. Default is 10, maximum is 100.
/// </summary>
public int PageSize { get; set; } = 10;
/// <summary>
/// Filter by bot status. If null, returns bots of all statuses.
/// </summary>
public BotStatus? Status { get; set; }
/// <summary>
/// Filter by user ID. If null, returns bots for all users.
/// </summary>
public int? UserId { get; set; }
/// <summary>
/// Filter by bot name (partial match, case-insensitive). If null, no name filtering is applied.
/// </summary>
public string? Name { get; set; }
/// <summary>
/// Filter by ticker (partial match, case-insensitive). If null, no ticker filtering is applied.
/// </summary>
public string? Ticker { get; set; }
/// <summary>
/// Filter by agent name (partial match, case-insensitive). If null, no agent name filtering is applied.
/// </summary>
public string? AgentName { get; set; }
/// <summary>
/// Sort field. Valid values: "Name", "Ticker", "Status", "CreateDate", "StartupTime", "Pnl", "WinRate", "AgentName".
/// Default is "CreateDate".
/// </summary>
public string SortBy { get; set; } = "CreateDate";
/// <summary>
/// Sort direction. Default is "Desc" (descending).
/// </summary>
public string SortDirection { get; set; } = "Desc";
}

View File

@@ -13,7 +13,7 @@ public class UpdateBotConfigRequest
/// The unique identifier of the bot to update /// The unique identifier of the bot to update
/// </summary> /// </summary>
[Required] [Required]
public string Identifier { get; set; } public Guid Identifier { get; set; }
/// <summary> /// <summary>
/// The new trading bot configuration request /// The new trading bot configuration request

View File

@@ -1,3 +1,6 @@
using Managing.Application.Abstractions.Models;
using Managing.Common;
namespace Managing.Api.Models.Responses namespace Managing.Api.Models.Responses
{ {
/// <summary> /// <summary>
@@ -15,21 +18,11 @@ namespace Managing.Api.Models.Responses
/// </summary> /// </summary>
public decimal TotalPnL { get; set; } public decimal TotalPnL { get; set; }
/// <summary>
/// Profit and loss in the last 24 hours in USD
/// </summary>
public decimal PnLLast24h { get; set; }
/// <summary> /// <summary>
/// Total return on investment as a percentage /// Total return on investment as a percentage
/// </summary> /// </summary>
public decimal TotalROI { get; set; } public decimal TotalROI { get; set; }
/// <summary>
/// Return on investment in the last 24 hours as a percentage
/// </summary>
public decimal ROILast24h { get; set; }
/// <summary> /// <summary>
/// Number of winning trades /// Number of winning trades
/// </summary> /// </summary>
@@ -40,10 +33,6 @@ namespace Managing.Api.Models.Responses
/// </summary> /// </summary>
public int Losses { get; set; } public int Losses { get; set; }
/// <summary>
/// Average win rate as a percentage
/// </summary>
public int AverageWinRate { get; set; }
/// <summary> /// <summary>
/// Number of active strategies for this agent /// Number of active strategies for this agent
@@ -56,43 +45,118 @@ namespace Managing.Api.Models.Responses
public decimal TotalVolume { get; set; } public decimal TotalVolume { get; set; }
/// <summary> /// <summary>
/// Volume traded in the last 24 hours in USD /// Total balance including USDC and open position values (without leverage, including PnL)
/// </summary> /// </summary>
public decimal VolumeLast24h { get; set; } public decimal TotalBalance { get; set; }
} }
/// <summary> /// <summary>
/// Platform-wide statistics including per-agent summaries /// Platform-wide statistics without individual agent details
/// </summary> /// </summary>
public class PlatformSummaryViewModel public class PlatformSummaryViewModel
{ {
/// <summary> /// <summary>
/// Total number of agents on the platform /// Total number of agents on the platform
/// </summary> /// </summary>
public int TotalAgents { get; set; } public required int TotalAgents { get; set; }
/// <summary> /// <summary>
/// Total number of active strategies across all agents /// Total number of active strategies across all agents
/// </summary> /// </summary>
public int TotalActiveStrategies { get; set; } public required int TotalActiveStrategies { get; set; }
/// <summary> /// <summary>
/// Total platform-wide profit and loss in USD /// Total platform-wide profit and loss in USD
/// </summary> /// </summary>
public decimal TotalPlatformPnL { get; set; } public required decimal TotalPlatformPnL { get; set; }
/// <summary> /// <summary>
/// Total volume traded across all agents in USD /// Total volume traded across all agents in USD
/// </summary> /// </summary>
public decimal TotalPlatformVolume { get; set; } public required decimal TotalPlatformVolume { get; set; }
/// <summary> /// <summary>
/// Total volume traded across all agents in the last 24 hours in USD /// Total volume traded across all agents in the last 24 hours in USD
/// </summary> /// </summary>
public decimal TotalPlatformVolumeLast24h { get; set; } public required decimal TotalPlatformVolumeLast24h { get; set; }
/// <summary> /// <summary>
/// Summaries for each agent /// Total open interest across all positions in USD
/// </summary>
public required decimal TotalOpenInterest { get; set; }
/// <summary>
/// Total number of open positions across all strategies
/// </summary>
public required int TotalPositionCount { get; set; }
// 24-hour changes
/// <summary>
/// Change in agent count over the last 24 hours
/// </summary>
public required int AgentsChange24h { get; set; }
/// <summary>
/// Change in strategy count over the last 24 hours
/// </summary>
public required int StrategiesChange24h { get; set; }
/// <summary>
/// Change in PnL over the last 24 hours
/// </summary>
public required decimal PnLChange24h { get; set; }
/// <summary>
/// Change in volume over the last 24 hours
/// </summary>
public required decimal VolumeChange24h { get; set; }
/// <summary>
/// Change in open interest over the last 24 hours
/// </summary>
public required decimal OpenInterestChange24h { get; set; }
/// <summary>
/// Change in position count over the last 24 hours
/// </summary>
public required int PositionCountChange24h { get; set; }
// Breakdowns
/// <summary>
/// Volume breakdown by asset/ticker
/// </summary>
public required Dictionary<Enums.Ticker, decimal> VolumeByAsset { get; set; }
/// <summary>
/// Position count breakdown by asset/ticker
/// </summary>
public required Dictionary<Enums.Ticker, int> PositionCountByAsset { get; set; }
/// <summary>
/// Position count breakdown by direction (Long/Short)
/// </summary>
public required Dictionary<Enums.TradeDirection, int> PositionCountByDirection { get; set; }
// Metadata
/// <summary>
/// When the data was last updated
/// </summary>
public required DateTime LastUpdated { get; set; }
/// <summary>
/// When the last 24-hour snapshot was taken
/// </summary>
public required DateTime Last24HourSnapshot { get; set; }
public List<VolumeHistoryPoint> VolumeHistory { get; internal set; }
}
/// <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

@@ -12,10 +12,11 @@ public class CandlesWithIndicatorsResponse
/// <summary> /// <summary>
/// The list of candles. /// The list of candles.
/// </summary> /// </summary>
public List<Candle> Candles { get; set; } = new List<Candle>(); public HashSet<Candle> Candles { get; set; } = new HashSet<Candle>();
/// <summary> /// <summary>
/// The calculated indicators values. /// The calculated indicators values.
/// </summary> /// </summary>
public Dictionary<IndicatorType, IndicatorsResultBase> IndicatorsValues { get; set; } = new Dictionary<IndicatorType, IndicatorsResultBase>(); public Dictionary<IndicatorType, IndicatorsResultBase> IndicatorsValues { get; set; } =
new Dictionary<IndicatorType, IndicatorsResultBase>();
} }

View File

@@ -0,0 +1,64 @@
using static Managing.Common.Enums;
namespace Managing.Api.Models.Responses;
/// <summary>
/// Response model for paginated agent index results
/// </summary>
public class PaginatedAgentIndexResponse
{
/// <summary>
/// The list of agent summaries for the current page
/// </summary>
public IEnumerable<AgentSummaryViewModel> AgentSummaries { get; set; } = new List<AgentSummaryViewModel>();
/// <summary>
/// Total number of agents across all pages
/// </summary>
public int TotalCount { get; set; }
/// <summary>
/// Current page number
/// </summary>
public int CurrentPage { get; set; }
/// <summary>
/// Number of items per page
/// </summary>
public int PageSize { get; set; }
/// <summary>
/// Total number of pages
/// </summary>
public int TotalPages { get; set; }
/// <summary>
/// Whether there are more pages available
/// </summary>
public bool HasNextPage { get; set; }
/// <summary>
/// Whether there are previous pages available
/// </summary>
public bool HasPreviousPage { get; set; }
/// <summary>
/// Time filter applied to the data
/// </summary>
public string TimeFilter { get; set; } = "Total";
/// <summary>
/// Field used for sorting
/// </summary>
public SortableFields SortBy { get; set; } = SortableFields.TotalPnL;
/// <summary>
/// Sort order (asc or desc)
/// </summary>
public string SortOrder { get; set; } = "desc";
/// <summary>
/// Comma-separated list of agent names used for filtering (if any)
/// </summary>
public string? FilteredAgentNames { get; set; }
}

View File

@@ -0,0 +1,43 @@
namespace Managing.Api.Models.Responses;
/// <summary>
/// Generic pagination response model
/// </summary>
/// <typeparam name="T">The type of items in the response</typeparam>
public class PaginatedResponse<T>
{
/// <summary>
/// The items for the current page
/// </summary>
public List<T> Items { get; set; } = new();
/// <summary>
/// Total number of items across all pages
/// </summary>
public int TotalCount { get; set; }
/// <summary>
/// Current page number (1-based)
/// </summary>
public int PageNumber { get; set; }
/// <summary>
/// Number of items per page
/// </summary>
public int PageSize { get; set; }
/// <summary>
/// Total number of pages
/// </summary>
public int TotalPages { get; set; }
/// <summary>
/// Whether there is a previous page
/// </summary>
public bool HasPreviousPage { get; set; }
/// <summary>
/// Whether there is a next page
/// </summary>
public bool HasNextPage { get; set; }
}

View File

@@ -14,6 +14,34 @@ namespace Managing.Api.Models.Responses
/// Profit and Loss value of the strategy /// Profit and Loss value of the strategy
/// </summary> /// </summary>
public decimal PnL { get; set; } public decimal PnL { get; set; }
public string AgentName { get; set; }
}
/// <summary>
/// Represents a high-performing strategy with its name and ROI value
/// </summary>
public class StrategyRoiPerformance
{
/// <summary>
/// Name of the strategy bot
/// </summary>
public string StrategyName { get; set; }
/// <summary>
/// Return on Investment percentage of the strategy
/// </summary>
public decimal Roi { get; set; }
/// <summary>
/// Profit and Loss value of the strategy
/// </summary>
public decimal PnL { get; set; }
/// <summary>
/// Volume traded by the strategy
/// </summary>
public decimal Volume { get; set; }
} }
/// <summary> /// <summary>
@@ -26,4 +54,62 @@ namespace Managing.Api.Models.Responses
/// </summary> /// </summary>
public List<StrategyPerformance> TopStrategies { get; set; } = new List<StrategyPerformance>(); public List<StrategyPerformance> TopStrategies { get; set; } = new List<StrategyPerformance>();
} }
/// <summary>
/// View model containing the top performing strategies by ROI
/// </summary>
public class TopStrategiesByRoiViewModel
{
/// <summary>
/// List of the top performing strategies by ROI
/// </summary>
public List<StrategyRoiPerformance> TopStrategiesByRoi { get; set; } = new List<StrategyRoiPerformance>();
}
/// <summary>
/// Represents a high-performing agent with its name and PnL value
/// </summary>
public class AgentPerformance
{
/// <summary>
/// Name of the agent
/// </summary>
public string AgentName { get; set; }
/// <summary>
/// Profit and Loss value of the agent
/// </summary>
public decimal PnL { get; set; }
/// <summary>
/// Total ROI percentage of the agent
/// </summary>
public decimal TotalROI { get; set; }
/// <summary>
/// Total volume traded by the agent
/// </summary>
public decimal TotalVolume { get; set; }
/// <summary>
/// Number of active strategies for this agent
/// </summary>
public int ActiveStrategiesCount { get; set; }
/// <summary>
/// Total balance including USDC and open position values
/// </summary>
public decimal TotalBalance { get; set; }
}
/// <summary>
/// View model containing the top performing agents by PnL
/// </summary>
public class TopAgentsByPnLViewModel
{
/// <summary>
/// List of the top performing agents by PnL
/// </summary>
public List<AgentPerformance> TopAgentsByPnL { get; set; } = new List<AgentPerformance>();
}
} }

View File

@@ -1,7 +1,8 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Managing.Domain.Bots;
using Managing.Domain.Candles; using Managing.Domain.Candles;
using Managing.Domain.Indicators;
using Managing.Domain.Trades; using Managing.Domain.Trades;
using static Managing.Common.Enums;
namespace Managing.Api.Models.Responses namespace Managing.Api.Models.Responses
{ {
@@ -14,16 +15,16 @@ namespace Managing.Api.Models.Responses
public string Status { get; internal set; } public string Status { get; internal set; }
/// <summary> /// <summary>
/// List of signals generated by the bot /// Dictionary of signals generated by the bot, keyed by signal identifier
/// </summary> /// </summary>
[Required] [Required]
public List<LightSignal> Signals { get; internal set; } public Dictionary<string, LightSignal> Signals { get; internal set; }
/// <summary> /// <summary>
/// List of positions opened by the bot /// Dictionary of positions opened by the bot, keyed by position identifier
/// </summary> /// </summary>
[Required] [Required]
public List<Position> Positions { get; internal set; } public Dictionary<Guid, Position> Positions { get; internal set; }
/// <summary> /// <summary>
/// Candles used by the bot for analysis /// Candles used by the bot for analysis
@@ -55,12 +56,6 @@ namespace Managing.Api.Models.Responses
[Required] [Required]
public string AgentName { get; set; } public string AgentName { get; set; }
/// <summary>
/// The full trading bot configuration
/// </summary>
[Required]
public TradingBotConfig Config { get; internal set; }
/// <summary> /// <summary>
/// The time when the bot was created /// The time when the bot was created
/// </summary> /// </summary>
@@ -72,5 +67,13 @@ namespace Managing.Api.Models.Responses
/// </summary> /// </summary>
[Required] [Required]
public DateTime StartupTime { get; internal set; } public DateTime StartupTime { get; internal set; }
[Required] public string Name { get; set; }
/// <summary>
/// The ticker/symbol being traded by this bot
/// </summary>
[Required]
public Ticker Ticker { get; set; }
} }
} }

View File

@@ -1,3 +1,4 @@
using Managing.Common;
using Managing.Domain.Trades; using Managing.Domain.Trades;
namespace Managing.Api.Models.Responses namespace Managing.Api.Models.Responses
@@ -15,7 +16,7 @@ namespace Managing.Api.Models.Responses
/// <summary> /// <summary>
/// Current state of the strategy (RUNNING, STOPPED, UNUSED) /// Current state of the strategy (RUNNING, STOPPED, UNUSED)
/// </summary> /// </summary>
public string State { get; set; } public Enums.BotStatus State { get; set; }
/// <summary> /// <summary>
/// Total profit or loss generated by the strategy in USD /// Total profit or loss generated by the strategy in USD
@@ -63,11 +64,12 @@ namespace Managing.Api.Models.Responses
public int Losses { get; set; } public int Losses { get; set; }
/// <summary> /// <summary>
/// List of all positions executed by this strategy /// Dictionary of all positions executed by this strategy, keyed by position identifier
/// </summary> /// </summary>
public List<Position> Positions { get; set; } = new List<Position>(); public List<Position> Positions { get; set; } = new List<Position>();
public string Identifier { get; set; } public Guid Identifier { get; set; }
public string ScenarioName { get; set; }
public Dictionary<DateTime, decimal> WalletBalances { get; set; } = new Dictionary<DateTime, decimal>();
} }
} }

View File

@@ -1,11 +1,12 @@
using System.Security.Claims;
using System.Text; using System.Text;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using HealthChecks.UI.Client; using HealthChecks.UI.Client;
using Managing.Api.Authorization; using Managing.Api.Authorization;
using Managing.Api.Filters; using Managing.Api.Filters;
using Managing.Api.HealthChecks; using Managing.Api.HealthChecks;
using Managing.Application.Abstractions.Services;
using Managing.Application.Hubs; using Managing.Application.Hubs;
using Managing.Application.Workers;
using Managing.Bootstrap; using Managing.Bootstrap;
using Managing.Common; using Managing.Common;
using Managing.Core.Middleawares; using Managing.Core.Middleawares;
@@ -156,21 +157,71 @@ builder.Services.Configure<PrivySettings>(builder.Configuration.GetSection(Const
builder.Services.AddControllers().AddJsonOptions(options => builder.Services.AddControllers().AddJsonOptions(options =>
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter())); options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()));
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(o => builder.Services
{ .AddAuthentication(options =>
o.SaveToken = true;
o.TokenValidationParameters = new TokenValidationParameters
{ {
ValidIssuer = builder.Configuration["Authentication:Schemes:Bearer:ValidIssuer"], options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
ValidAudience = builder.Configuration["Authentication:Schemes:Bearer:ValidAudiences"], options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
IssuerSigningKey = new SymmetricSecurityKey })
(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Secret"])), .AddJwtBearer(o =>
ValidateIssuer = false, {
ValidateAudience = false, o.SaveToken = true;
ValidateIssuerSigningKey = true o.TokenValidationParameters = new TokenValidationParameters
}; {
}); ValidIssuer = builder.Configuration["Authentication:Schemes:Bearer:ValidIssuer"],
builder.Services.AddAuthorization(); ValidAudience = builder.Configuration["Authentication:Schemes:Bearer:ValidAudiences"],
IssuerSigningKey = new SymmetricSecurityKey
(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Secret"])),
ValidateIssuer = false,
ValidateAudience = false,
ValidateIssuerSigningKey = true
};
o.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
// If you want to get the token from a custom header or query string
// var accessToken = context.Request.Query["access_token"];
// if (!string.IsNullOrEmpty(accessToken) &&
// context.HttpContext.Request.Path.StartsWithSegments("/hub"))
// {
// context.Token = accessToken;
// }
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
context.Response.Headers.Add("Token-Expired", "true");
}
return Task.CompletedTask;
},
// --- IMPORTANT: Attach User to Context Here ---
OnTokenValidated = async context =>
{
var userService = context.HttpContext.RequestServices.GetRequiredService<IUserService>();
// Assuming your JWT token contains a 'nameid' claim (or similar) for the user ID
var userId = context.Principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (!string.IsNullOrEmpty(userId))
{
// Fetch the full user object from your service
var user = await userService.GetUserByAddressAsync(userId);
if (user != null)
{
// Attach the user object to HttpContext.Items
context.HttpContext.Items["User"] = user;
}
}
await Task.CompletedTask;
}
// --- END IMPORTANT ---
};
});
builder.Services.AddCors(o => o.AddPolicy("CorsPolicy", builder => builder.Services.AddCors(o => o.AddPolicy("CorsPolicy", builder =>
{ {
builder builder
@@ -186,6 +237,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 is always configured, but grains can be controlled
builder.Host.ConfigureOrleans(builder.Configuration, builder.Environment.IsProduction());
builder.Services.AddEndpointsApiExplorer(); builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApiDocument(document => builder.Services.AddOpenApiDocument(document =>
{ {
@@ -229,12 +284,6 @@ builder.Services.AddSwaggerGen(options =>
}); });
builder.WebHost.SetupDiscordBot(); builder.WebHost.SetupDiscordBot();
if (builder.Configuration.GetValue<bool>("EnableBotManager", false))
{
builder.Services.AddHostedService<BotManagerWorker>();
}
// Workers are now registered in ApiBootstrap.cs
// App // App
var app = builder.Build(); var app = builder.Build();
@@ -254,14 +303,9 @@ app.UseSentryDiagnostics();
// Using shared GlobalErrorHandlingMiddleware from core project // Using shared GlobalErrorHandlingMiddleware from core project
app.UseMiddleware<GlobalErrorHandlingMiddleware>(); app.UseMiddleware<GlobalErrorHandlingMiddleware>();
app.UseMiddleware<JwtMiddleware>();
app.UseHttpsRedirection(); app.UseHttpsRedirection();
app.UseRouting(); app.UseRouting();
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
app.UseEndpoints(endpoints => app.UseEndpoints(endpoints =>
@@ -283,4 +327,16 @@ app.UseEndpoints(endpoints =>
}); });
}); });
app.Run(); // Conditionally run the application based on deployment mode
var deploymentMode = builder.Configuration.GetValue<bool>("DeploymentMode", false);
if (!deploymentMode)
{
Console.WriteLine("Application starting in normal mode...");
app.Run();
}
else
{
Console.WriteLine("Application configured for deployment mode - skipping app.Run()");
Console.WriteLine("All services have been configured and the application is ready for deployment.");
}

View File

@@ -0,0 +1,116 @@
# Orleans Configuration
This document explains how to configure Orleans usage in the Managing API.
## Overview
The Managing API now always runs Orleans for infrastructure, but supports configurable grain execution through the `RunOrleansGrains` configuration option. This allows you to control whether Orleans grains (bots, timers, reminders) are active during runtime.
## Configuration Options
### 1. Configuration File (appsettings.json)
Add the Orleans grain control parameter to your appsettings file:
```json
{
"RunOrleansGrains": true
}
```
- `RunOrleansGrains`: Boolean value that controls whether Orleans grains (bots, timers, reminders) are active
- `true` (default): Grains are active and can run timers/reminders
- `false`: Grains are inactive, no timers or reminders will execute
**Note**: Orleans infrastructure is always enabled and configured. This flag only controls grain execution.
### 2. Environment Variables
You can also control Orleans grain execution through environment variables:
```bash
# Enable Orleans grains (bots, timers, reminders)
export RUN_ORLEANS_GRAINS=true
# Disable Orleans grains (infrastructure still runs)
export RUN_ORLEANS_GRAINS=false
```
**Note**: Environment variables take precedence over configuration file settings.
## Configuration Files
The following configuration files have been updated with Orleans grain control settings:
- `appsettings.json` - Base configuration (RunOrleansGrains: true)
- `appsettings.Development.json` - Development configuration (RunOrleansGrains: false)
- `appsettings.Oda.json` - Oda environment (RunOrleansGrains: true)
- `appsettings.Oda-docker.json` - Oda Docker environment (RunOrleansGrains: true)
- `appsettings.Sandbox.json` - Sandbox environment (RunOrleansGrains: true)
- `appsettings.SandboxLocal.json` - Local Sandbox environment (RunOrleansGrains: true)
- `appsettings.Production.json` - Production environment (RunOrleansGrains: true)
## Use Cases
### Development Environment
- Set `RunOrleansGrains: false` to run Orleans infrastructure without active grains
- Useful for development and testing without bot execution overhead
### Testing Environment
- Set `RunOrleansGrains: false` to test Orleans infrastructure without running bots
- Useful for integration testing without triggering actual trading operations
### Production Environment
- Set `RunOrleansGrains: true` to enable full Orleans grain functionality
- Required for production trading bot operations
### Docker/Container Environment
- Use environment variables for easy configuration
- Example: `docker run -e RUN_ORLEANS_GRAINS=false ...`
## Implementation Details
The Orleans configuration is implemented in:
1. **ApiBootstrap.cs** - Orleans configuration logic
2. **Program.cs** - Application startup Orleans configuration
## Security Considerations
- Orleans configuration is not sensitive and can be included in configuration files
- Environment variables provide runtime flexibility without code changes
- Default behavior maintains backward compatibility (Orleans enabled by default)
## Troubleshooting
### Orleans Not Starting
1. Orleans infrastructure is always enabled
2. Ensure PostgreSQL connection string for Orleans is configured
3. Check application logs for connection errors
### Grains Not Running
1. Check `RunOrleansGrains` configuration value
2. Verify environment variable `RUN_ORLEANS_GRAINS` is not set to `false`
3. Ensure Orleans infrastructure is running properly
### Configuration Not Applied
1. Verify configuration file syntax
2. Check environment variable spelling (`RUN_ORLEANS_GRAINS`)
3. Restart the application after configuration changes
## Migration
Existing deployments will continue to work as Orleans grains are enabled by default. To control Orleans grain behavior:
### Disable Grains (Keep Orleans Infrastructure)
1. Add `"RunOrleansGrains": false` to your appsettings file, or
2. Set environment variable `RUN_ORLEANS_GRAINS=false`
### Development/Testing Setup
```json
{
"RunOrleansGrains": false
}
```
**Note**: Orleans infrastructure is always enabled and cannot be disabled.

View File

@@ -0,0 +1,119 @@
# Orleans Clustering Troubleshooting Guide
## Overview
This document provides troubleshooting steps for Orleans clustering issues, particularly the "Connection attempt to endpoint failed" errors that can occur in Docker deployments.
## Common Issues and Solutions
### 1. Connection Timeout Errors
**Error**: `Connection attempt to endpoint S10.0.0.9:11111:114298801 timed out after 00:00:05`
**Cause**: Orleans silos are trying to connect to each other but the network configuration is preventing proper communication.
**Solutions**:
#### A. Environment Variables
You can disable Orleans clustering completely by setting:
```bash
DISABLE_ORLEANS_CLUSTERING=true
```
This will fall back to localhost clustering mode for testing.
#### B. Docker Network Configuration
Ensure the Docker compose file includes proper network configuration:
```yaml
services:
managing.api:
ports:
- "11111:11111" # Orleans silo port
- "30000:30000" # Orleans gateway port
hostname: managing-api
```
#### C. Database Connection Issues
If the Orleans database is unavailable, the system will automatically fall back to:
- Localhost clustering
- Memory-based grain storage
### 2. Configuration Options
#### Production Settings
In `appsettings.Production.json`:
```json
{
"RunOrleansGrains": true,
"Orleans": {
"EnableClustering": true,
"ConnectionTimeout": 60,
"MaxJoinAttempts": 3
}
}
```
#### Environment Variables
- `RUN_ORLEANS_GRAINS`: Enable/disable Orleans grains (true/false)
- `DISABLE_ORLEANS_CLUSTERING`: Force localhost clustering (true/false)
- `ORLEANS_ADVERTISED_IP`: Set specific IP address for Orleans clustering (e.g., "192.168.1.100")
- `ASPNETCORE_ENVIRONMENT`: Set environment (Production/Development/etc.)
### 3. Network Configuration Improvements
The following improvements have been made to handle Docker networking issues:
1. **Endpoint Configuration**:
- `listenOnAnyHostAddress: true` allows binding to all network interfaces
- Increased timeout values for better reliability
2. **Fallback Mechanisms**:
- Automatic fallback to localhost clustering if database unavailable
- Memory storage fallback for grain persistence
3. **Improved Timeouts**:
- Response timeout: 60 seconds
- Probe timeout: 10 seconds
- Join attempt timeout: 120 seconds
### 4. Monitoring and Debugging
#### Orleans Dashboard
Available in development mode at: `http://localhost:9999`
- Username: admin
- Password: admin
#### Health Checks
Monitor application health at:
- `/health` - Full health check
- `/alive` - Basic liveness check
### 5. Emergency Procedures
If Orleans clustering is causing deployment issues:
1. **Immediate Fix**: Set environment variable `DISABLE_ORLEANS_CLUSTERING=true`
2. **Restart Services**: Restart the managing.api container
3. **Check Logs**: Monitor for connection timeout errors
4. **Database Check**: Verify PostgreSQL Orleans database connectivity
### 6. Database Requirements
Orleans requires these PostgreSQL databases:
- Main application database (from `PostgreSql:ConnectionString`)
- Orleans clustering database (from `PostgreSql:Orleans`)
If either is unavailable, the system will gracefully degrade functionality.
## Testing the Fix
1. Deploy with the updated configuration
2. Monitor logs for Orleans connection errors
3. Verify grain functionality through the dashboard (development) or API endpoints
4. Test failover scenarios by temporarily disabling database connectivity
## Related Files
- `src/Managing.Bootstrap/ApiBootstrap.cs` - Orleans configuration
- `src/Managing.Docker/docker-compose.yml` - Docker networking
- `src/Managing.Api/appsettings.*.json` - Environment-specific settings

View File

@@ -0,0 +1,125 @@
# TradingController Security Enhancement
## Overview
The `InitPrivyWallet` endpoint in `TradingController` has been enhanced with admin role security. This ensures that only authorized users can initialize wallet addresses.
## Security Rules
### For Regular Users
- Can **only** initialize wallet addresses that they own
- The system verifies ownership by checking if the provided public address exists in one of the user's accounts
- If a user tries to initialize an address they don't own, they receive a `403 Forbidden` response
### For Admin Users
- Can initialize **any** wallet address in the system
- Admin status is determined by the `AdminUsers` environment variable
- All admin actions are logged for audit purposes
## Implementation Details
### Authentication Flow
1. **User Authentication**: Endpoint requires valid JWT token
2. **Admin Check**: System checks if user is in the admin list via `IAdminConfigurationService`
3. **Ownership Verification**: For non-admin users, verifies address ownership via `IAccountService.GetAccountByKey()`
4. **Action Logging**: All operations are logged with user context
### Security Validation
```csharp
private async Task<bool> CanUserInitializeAddress(string userName, string publicAddress)
{
// Admin users can initialize any address
if (_adminService.IsUserAdmin(userName))
{
return true;
}
// Regular users can only initialize their own addresses
var account = await _accountService.GetAccountByKey(publicAddress, true, false);
return account?.User?.Name == userName;
}
```
## Error Responses
### 401 Unauthorized
- Missing or invalid JWT token
- User not found in system
### 403 Forbidden
- Non-admin user trying to initialize address they don't own
- Message: "You don't have permission to initialize this wallet address. You can only initialize your own wallet addresses."
### 400 Bad Request
- Empty or null public address provided
### 500 Internal Server Error
- System error during wallet initialization
- Database connectivity issues
- External service failures
## Admin Configuration
Set admin users via environment variable:
```bash
AdminUsers=admin1,admin2,superuser
```
## Audit Logging
All operations are logged with appropriate context:
**Admin Operations:**
```
Admin user {UserName} initializing address {Address}
```
**User Operations:**
```
User {UserName} initializing their own address {Address}
```
**Security Violations:**
```
User {UserName} attempted to initialize address {Address} that doesn't belong to them
```
## Usage Examples
### Regular User - Own Address
```bash
POST /Trading/InitPrivyWallet
Authorization: Bearer {user-jwt-token}
Content-Type: application/json
"0x1234567890123456789012345678901234567890"
```
**Result**: ✅ Success (if address belongs to user)
### Regular User - Other's Address
```bash
POST /Trading/InitPrivyWallet
Authorization: Bearer {user-jwt-token}
Content-Type: application/json
"0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
```
**Result**: ❌ 403 Forbidden
### Admin User - Any Address
```bash
POST /Trading/InitPrivyWallet
Authorization: Bearer {admin-jwt-token}
Content-Type: application/json
"0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
```
**Result**: ✅ Success (admin can initialize any address)
## Security Benefits
1. **Prevents Unauthorized Access**: Users cannot initialize wallets they don't own
2. **Admin Oversight**: Admins can manage any wallet for system administration
3. **Audit Trail**: All actions are logged for compliance and security monitoring
4. **Clear Authorization**: Explicit permission checks with meaningful error messages
5. **Secure Configuration**: Admin privileges configured via environment variables, not API calls

View File

@@ -0,0 +1,10 @@
{
"RunOrleansGrains": false,
"DeploymentMode": false,
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -30,5 +30,6 @@
"RequestsChannelId": 1018589494968078356, "RequestsChannelId": 1018589494968078356,
"ButtonExpirationMinutes": 10 "ButtonExpirationMinutes": 10
}, },
"RunOrleansGrains": true,
"AllowedHosts": "*" "AllowedHosts": "*"
} }

View File

@@ -30,5 +30,19 @@
"RequestsChannelId": 1018589494968078356, "RequestsChannelId": 1018589494968078356,
"ButtonExpirationMinutes": 2 "ButtonExpirationMinutes": 2
}, },
"RunOrleansGrains": true,
"WorkerPricesFifteenMinutes": false,
"WorkerPricesOneHour": false,
"WorkerPricesFourHours": false,
"WorkerPricesOneDay": false,
"WorkerPricesFiveMinutes": false,
"WorkerSpotlight": false,
"WorkerTraderWatcher": false,
"WorkerLeaderboard": false,
"WorkerFundingRatesWatcher": false,
"WorkerGeneticAlgorithm": false,
"WorkerBundleBacktest": true,
"WorkerBalancesTracking": false,
"WorkerNotifyBundleBacktest": false,
"AllowedHosts": "*" "AllowedHosts": "*"
} }

View File

@@ -5,7 +5,8 @@
"Token": "Fw2FPL2OwTzDHzSbR2Sd5xs0EKQYy00Q-hYKYAhr9cC1_q5YySONpxuf_Ck0PTjyUiF13xXmi__bu_pXH-H9zA==" "Token": "Fw2FPL2OwTzDHzSbR2Sd5xs0EKQYy00Q-hYKYAhr9cC1_q5YySONpxuf_Ck0PTjyUiF13xXmi__bu_pXH-H9zA=="
}, },
"PostgreSql": { "PostgreSql": {
"ConnectionString": "Host=localhost;Port=5432;Database=managing;Username=postgres;Password=postgres" "ConnectionString": "Host=localhost;Port=5432;Database=managing;Username=postgres;Password=postgres",
"Orleans": "Host=localhost;Port=5432;Database=orleans;Username=postgres;Password=postgres"
}, },
"Privy": { "Privy": {
"AppId": "cm6f47n1l003jx7mjwaembhup", "AppId": "cm6f47n1l003jx7mjwaembhup",
@@ -23,9 +24,6 @@
"ElasticConfiguration": { "ElasticConfiguration": {
"Uri": "http://elasticsearch:9200" "Uri": "http://elasticsearch:9200"
}, },
"Sentry": {
"Dsn": "https://698e00d7cb404b049aff3881e5a47f6b@bugcenter.apps.managing.live/1"
},
"Discord": { "Discord": {
"ApplicationId": "", "ApplicationId": "",
"PublicKey": "", "PublicKey": "",
@@ -36,8 +34,25 @@
"RequestsChannelId": 1018589494968078356, "RequestsChannelId": 1018589494968078356,
"ButtonExpirationMinutes": 2 "ButtonExpirationMinutes": 2
}, },
"RunOrleansGrains": true,
"AllowedHosts": "*", "AllowedHosts": "*",
"KAIGEN_SECRET_KEY": "KaigenXCowchain",
"KAIGEN_CREDITS_ENABLED": false,
"WorkerBotManager": true, "WorkerBotManager": true,
"WorkerBalancesTracking": false, "WorkerBalancesTracking": false,
"WorkerNotifyBundleBacktest": true "WorkerNotifyBundleBacktest": false,
"WorkerPricesFifteenMinutes": true,
"WorkerPricesOneHour": false,
"WorkerPricesFourHours": false,
"WorkerPricesOneDay": false,
"WorkerPricesFiveMinutes": false,
"WorkerFee": false,
"WorkerPositionManager": false,
"WorkerPositionFetcher": false,
"WorkerSpotlight": false,
"WorkerTraderWatcher": false,
"WorkerLeaderboard": false,
"WorkerFundingRatesWatcher": false,
"WorkerGeneticAlgorithm": false,
"WorkerBundleBacktest": false
} }

View File

@@ -1,6 +1,7 @@
{ {
"PostgreSql": { "PostgreSql": {
"ConnectionString": "Host=apps.prod.live;Port=5432;Database=managing;Username=postgres;Password=postgres" "ConnectionString": "Host=managing-postgre.apps.managing.live;Port=5432;Database=managing;Username=postgres;Password=29032b13a5bc4d37",
"Orleans": "Host=managing-postgre.apps.managing.live;Port=5432;Database=orleans;Username=postgres;Password=29032b13a5bc4d37"
}, },
"InfluxDb": { "InfluxDb": {
"Url": "https://influx-db.apps.managing.live", "Url": "https://influx-db.apps.managing.live",
@@ -26,8 +27,12 @@
"ElasticConfiguration": { "ElasticConfiguration": {
"Uri": "http://elasticsearch:9200" "Uri": "http://elasticsearch:9200"
}, },
"Sentry": { "RunOrleansGrains": true,
"Dsn": "https://698e00d7cb404b049aff3881e5a47f6b@bugcenter.apps.managing.live/1" "DeploymentMode": false,
"Orleans": {
"EnableClustering": true,
"ConnectionTimeout": 60,
"MaxJoinAttempts": 3
}, },
"AllowedHosts": "*", "AllowedHosts": "*",
"WorkerBotManager": true, "WorkerBotManager": true,

View File

@@ -1,6 +1,7 @@
{ {
"PostgreSql": { "PostgreSql": {
"ConnectionString": "Host=managing-postgre.apps.managing.live;Port=5432;Database=managing;Username=postgres;Password=29032b13a5bc4d37" "ConnectionString": "Host=managing-postgre.apps.managing.live;Port=5432;Database=managing;Username=postgres;Password=29032b13a5bc4d37",
"Orleans": "Host=managing-postgre.apps.managing.live;Port=5432;Database=orleans;Username=postgres;Password=29032b13a5bc4d37"
}, },
"InfluxDb": { "InfluxDb": {
"Url": "http://srv-captain--influx-db:8086/", "Url": "http://srv-captain--influx-db:8086/",
@@ -12,7 +13,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"
}, },
@@ -31,8 +32,6 @@
"ElasticConfiguration": { "ElasticConfiguration": {
"Uri": "http://elasticsearch:9200" "Uri": "http://elasticsearch:9200"
}, },
"Sentry": { "RunOrleansGrains": true,
"Dsn": "https://698e00d7cb404b049aff3881e5a47f6b@bugcenter.apps.managing.live/1"
},
"AllowedHosts": "*" "AllowedHosts": "*"
} }

View File

@@ -1,4 +1,8 @@
{ {
"PostgreSql": {
"ConnectionString": "Host=managing-postgre.apps.managing.live;Port=5432;Database=managing;Username=postgres;Password=29032b13a5bc4d37",
"Orleans": "Host=managing-postgre.apps.managing.live;Port=5432;Database=orleans;Username=postgres;Password=29032b13a5bc4d37"
},
"InfluxDb": { "InfluxDb": {
"Url": "https://influx-db.apps.managing.live", "Url": "https://influx-db.apps.managing.live",
"Organization": "managing-org", "Organization": "managing-org",
@@ -8,9 +12,6 @@
"AppId": "cm6f47n1l003jx7mjwaembhup", "AppId": "cm6f47n1l003jx7mjwaembhup",
"AppSecret": "63Chz2z5M8TgR5qc8dznSLRAGTHTyPU4cjdQobrBF1Cx5tszZpTuFgyrRd7hZ2k6HpwDz3GEwQZzsCqHb8Z311bF" "AppSecret": "63Chz2z5M8TgR5qc8dznSLRAGTHTyPU4cjdQobrBF1Cx5tszZpTuFgyrRd7hZ2k6HpwDz3GEwQZzsCqHb8Z311bF"
}, },
"PostgreSql": {
"ConnectionString": "Host=managing-postgre.apps.managing.live;Port=5432;Database=managing;Username=postgres;Password=29032b13a5bc4d37"
},
"Serilog": { "Serilog": {
"MinimumLevel": { "MinimumLevel": {
"Default": "Information", "Default": "Information",
@@ -23,6 +24,7 @@
"ElasticConfiguration": { "ElasticConfiguration": {
"Uri": "http://elasticsearch:9200" "Uri": "http://elasticsearch:9200"
}, },
"RunOrleansGrains": true,
"AllowedHosts": "*", "AllowedHosts": "*",
"WorkerBotManager": false, "WorkerBotManager": false,
"WorkerBalancesTracking": false "WorkerBalancesTracking": false

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"
}, },
@@ -32,7 +32,7 @@
"WebhookUrl": "https://n8n.kai.managing.live/webhook/fa9308b6-983b-42ec-b085-71599d655951" "WebhookUrl": "https://n8n.kai.managing.live/webhook/fa9308b6-983b-42ec-b085-71599d655951"
}, },
"Sentry": { "Sentry": {
"Dsn": "https://698e00d7cb404b049aff3881e5a47f6b@bugcenter.apps.managing.live/1", "Dsn": "https://fe12add48c56419bbdfa86227c188e7a@glitch.kai.managing.live/1",
"MinimumEventLevel": "Error", "MinimumEventLevel": "Error",
"SendDefaultPii": true, "SendDefaultPii": true,
"MaxBreadcrumbs": 50, "MaxBreadcrumbs": 50,
@@ -66,5 +66,21 @@
"FundingRateChannelId": 1263566138709774336, "FundingRateChannelId": 1263566138709774336,
"ButtonExpirationMinutes": 10 "ButtonExpirationMinutes": 10
}, },
"RunOrleansGrains": true,
"DeploymentMode": false,
"WorkerPricesFifteenMinutes": false,
"WorkerPricesOneHour": false,
"WorkerPricesFourHours": false,
"WorkerPricesOneDay": false,
"WorkerPricesFiveMinutes": false,
"WorkerSpotlight": false,
"WorkerTraderWatcher": false,
"WorkerLeaderboard": false,
"WorkerFundingRatesWatcher": false,
"WorkerGeneticAlgorithm": false,
"WorkerBundleBacktest": false,
"WorkerBalancesTracking": false,
"WorkerNotifyBundleBacktest": false,
"AdminUsers": "",
"AllowedHosts": "*" "AllowedHosts": "*"
} }

View File

@@ -0,0 +1,55 @@
using Orleans;
using static Managing.Common.Enums;
namespace Managing.Application.Abstractions.Grains;
/// <summary>
/// A small serializable class to store bot metadata.
/// This is a very lean object, perfect for fast storage and retrieval.
/// </summary>
[GenerateSerializer]
public class BotRegistryEntry
{
/// <summary>
/// The unique identifier of the bot
/// </summary>
[Id(0)]
public Guid Identifier { get; set; }
/// <summary>
/// The unique identifier of the user who owns the bot
/// </summary>
[Id(1)]
public int UserId { get; set; }
/// <summary>
/// The current operational status of the bot
/// </summary>
[Id(2)]
public BotStatus Status { get; set; }
/// <summary>
/// When the bot was registered in the registry
/// </summary>
[Id(3)]
public DateTime RegisteredAt { get; set; } = DateTime.UtcNow;
/// <summary>
/// When the bot status was last updated
/// </summary>
[Id(4)]
public DateTime LastStatusUpdate { get; set; } = DateTime.UtcNow;
public BotRegistryEntry()
{
}
public BotRegistryEntry(Guid identifier, int userId, BotStatus status = BotStatus.Saved)
{
Identifier = identifier;
UserId = userId;
Status = status;
RegisteredAt = DateTime.UtcNow;
LastStatusUpdate = DateTime.UtcNow;
}
}

View File

@@ -0,0 +1,36 @@
using Orleans;
namespace Managing.Application.Abstractions.Grains;
/// <summary>
/// Orleans grain state for BotRegistry.
/// This class represents the persistent state of the bot registry grain.
/// All properties must be serializable for Orleans state management.
/// </summary>
[GenerateSerializer]
public class BotRegistryState
{
/// <summary>
/// Dictionary containing all registered bots. The key is the identifier.
/// </summary>
[Id(0)]
public Dictionary<Guid, BotRegistryEntry> Bots { get; set; } = new();
/// <summary>
/// When the registry was last updated
/// </summary>
[Id(1)]
public DateTime LastUpdated { get; set; } = DateTime.UtcNow;
/// <summary>
/// Total number of bots currently registered
/// </summary>
[Id(2)]
public int TotalBotsCount { get; set; }
/// <summary>
/// Number of active bots (status = Up)
/// </summary>
[Id(3)]
public int ActiveBotsCount { get; set; }
}

View File

@@ -0,0 +1,27 @@
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, HashSet<Candle> candles, User user = null, bool save = false, bool withCandles = false, string requestId = null, object metadata = null);
}

View File

@@ -0,0 +1,51 @@
using Orleans;
using static Managing.Common.Enums;
namespace Managing.Application.Abstractions.Grains;
/// <summary>
/// Orleans grain interface for LiveBotRegistry operations.
/// This interface defines the distributed, async operations available for the bot registry.
/// The registry acts as a central, durable directory for all LiveTradingBot grains.
/// </summary>
public interface ILiveBotRegistryGrain : IGrainWithIntegerKey
{
/// <summary>
/// Registers a new bot with its user ID. This should be called by the LiveTradingBotGrain when it is first initialized.
/// The initial status will be BotStatus.Up.
/// </summary>
/// <param name="identifier">The unique identifier of the bot</param>
/// <param name="userId">The unique identifier of the user who owns the bot</param>
/// <returns>A task that represents the asynchronous operation</returns>
Task RegisterBot(Guid identifier, int userId);
/// <summary>
/// Removes a bot from the registry. This should be a full removal, perhaps called when a user permanently deletes a bot.
/// </summary>
/// <param name="identifier">The unique identifier of the bot to unregister</param>
/// <returns>A task that represents the asynchronous operation</returns>
Task UnregisterBot(Guid identifier);
/// <summary>
/// Returns a list of all bots in the registry. This is for a management dashboard to see all bots in the system.
/// </summary>
/// <returns>A list of all BotRegistryEntry objects in the registry</returns>
Task<List<BotRegistryEntry>> GetAllBots();
/// <summary>
/// Returns a list of all bots associated with a specific user. This is the primary method for a user's watchlist.
/// </summary>
/// <param name="userId">The unique identifier of the user</param>
/// <returns>A list of BotRegistryEntry objects for the specified user</returns>
Task<List<BotRegistryEntry>> GetBotsForUser(int userId);
/// <summary>
/// A dedicated method for updating only the bot's Status field (Up/Down).
/// This will be called by LiveTradingBot's StartAsync and StopAsync methods.
/// </summary>
/// <param name="identifier">The unique identifier of the bot</param>
/// <param name="status">The new status to set for the bot</param>
/// <returns>A task that represents the asynchronous operation</returns>
Task UpdateBotStatus(Guid identifier, BotStatus status);
Task<BotStatus> GetBotStatus(Guid identifier);
}

View File

@@ -0,0 +1,42 @@
using Managing.Domain.Accounts;
using Managing.Domain.Bots;
using Managing.Domain.Trades;
using Managing.Domain.Users;
using Orleans;
using static Managing.Common.Enums;
namespace Managing.Application.Abstractions.Grains;
/// <summary>
/// Orleans grain interface for TradingBot operations.
/// This interface defines the distributed, async operations available for trading bots.
/// </summary>
public interface ILiveTradingBotGrain : IGrainWithGuidKey
{
/// <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>
/// Gets comprehensive bot data including positions, signals, and performance metrics
/// </summary>
Task<LiveTradingBotModel> GetBotDataAsync();
Task CreateAsync(TradingBotConfig config, User user);
Task StartAsync();
Task StopAsync();
Task<bool> UpdateConfiguration(TradingBotConfig newConfig);
Task<Account> GetAccount();
Task<TradingBotConfig> GetConfiguration();
Task<Position> ClosePositionAsync(Guid positionId);
Task RestartAsync();
/// <summary>
/// Deletes the bot and cleans up all associated resources
/// </summary>
Task DeleteAsync();
}

View File

@@ -0,0 +1,139 @@
using Managing.Application.Abstractions.Models;
using Orleans;
using static Managing.Common.Enums;
namespace Managing.Application.Abstractions.Grains;
/// <summary>
/// Grain interface for managing platform-wide summary metrics
/// </summary>
public interface IPlatformSummaryGrain : IGrainWithStringKey
{
/// <summary>
/// Gets the current platform summary data
/// </summary>
Task<PlatformSummaryViewModel> GetPlatformSummaryAsync();
/// <summary>
/// Forces a refresh of all platform data
/// </summary>
Task RefreshDataAsync();
/// <summary>
/// Gets the total volume traded across all strategies
/// </summary>
Task<decimal> GetTotalVolumeAsync();
/// <summary>
/// Gets the total PnL across all strategies
/// </summary>
Task<decimal> GetTotalPnLAsync();
/// <summary>
/// Gets the total open interest across all positions
/// </summary>
Task<decimal> GetTotalOpenInterest();
/// <summary>
/// Gets the total number of open positions
/// </summary>
Task<int> GetTotalPositionCountAsync();
/// <summary>
/// Gets the daily volume history for the last 30 days for chart visualization
/// </summary>
Task<List<VolumeHistoryPoint>> GetVolumeHistoryAsync();
// Event handlers for immediate updates
/// <summary>
/// Updates the active strategy count
/// </summary>
Task UpdateActiveStrategyCountAsync(int newActiveCount);
Task OnPositionOpenedAsync(PositionOpenedEvent evt);
Task OnPositionClosedAsync(PositionClosedEvent evt);
Task OnTradeExecutedAsync(TradeExecutedEvent evt);
}
/// <summary>
/// Base class for platform metrics events
/// </summary>
[GenerateSerializer]
public abstract class PlatformMetricsEvent
{
[Id(0)]
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
}
/// <summary>
/// Event fired when a new position is opened
/// </summary>
[GenerateSerializer]
public class PositionOpenedEvent : PlatformMetricsEvent
{
[Id(1)]
public Guid PositionId { get; set; }
[Id(2)]
public Ticker Ticker { get; set; }
[Id(3)]
public decimal Volume { get; set; }
[Id(4)]
public TradeDirection Direction { get; set; }
}
/// <summary>
/// Event fired when a position is closed
/// </summary>
[GenerateSerializer]
public class PositionClosedEvent : PlatformMetricsEvent
{
[Id(1)]
public Guid PositionId { get; set; }
[Id(2)]
public Ticker Ticker { get; set; }
[Id(3)]
public decimal RealizedPnL { get; set; }
[Id(4)]
public decimal Volume { get; set; }
[Id(5)]
public decimal InitialVolume { get; set; }
}
/// <summary>
/// Event fired when a trade is executed
/// </summary>
[GenerateSerializer]
public class TradeExecutedEvent : PlatformMetricsEvent
{
[Id(1)]
public Guid TradeId { get; set; }
[Id(2)]
public Guid PositionId { get; set; }
[Id(3)]
public Guid StrategyId { get; set; }
[Id(4)]
public Ticker Ticker { get; set; }
[Id(5)]
public decimal Volume { get; set; }
[Id(6)]
public decimal PnL { get; set; }
[Id(7)]
public decimal Fee { get; set; }
[Id(8)]
public TradeDirection Direction { get; set; }
}

View File

@@ -0,0 +1,134 @@
using Orleans;
using static Managing.Common.Enums;
namespace Managing.Application.Abstractions.Grains;
/// <summary>
/// State model for Platform Summary Grain
/// </summary>
[GenerateSerializer]
public class PlatformSummaryGrainState
{
[Id(0)]
public DateTime LastUpdated { get; set; }
[Id(1)]
public DateTime LastSnapshot { get; set; }
[Id(2)]
public bool HasPendingChanges { get; set; }
// Current metrics
[Id(3)]
public int TotalAgents { get; set; }
[Id(4)]
public int TotalActiveStrategies { get; set; }
[Id(5)]
public decimal TotalPlatformPnL { get; set; }
[Id(6)]
public decimal TotalPlatformVolume { get; set; }
[Id(7)]
public decimal TotalOpenInterest { get; set; }
[Id(8)]
public int TotalPositionCount { get; set; }
// 24-hour ago values (for comparison)
[Id(9)]
public int TotalAgents24hAgo { get; set; }
[Id(10)]
public int TotalActiveStrategies24hAgo { get; set; }
[Id(11)]
public decimal TotalPlatformPnL24hAgo { get; set; }
[Id(12)]
public decimal TotalPlatformVolume24hAgo { get; set; }
[Id(13)]
public decimal TotalOpenInterest24hAgo { get; set; }
[Id(14)]
public int TotalPositionCount24hAgo { get; set; }
// Historical snapshots
[Id(15)]
public List<HourlySnapshot> HourlySnapshots { get; set; } = new();
[Id(16)]
public List<DailySnapshot> DailySnapshots { get; set; } = new();
// Volume breakdown by asset
[Id(17)]
public Dictionary<Ticker, decimal> VolumeByAsset { get; set; } = new();
// Position count breakdown
[Id(18)]
public Dictionary<Ticker, int> PositionCountByAsset { get; set; } = new();
[Id(19)]
public Dictionary<TradeDirection, int> PositionCountByDirection { get; set; } = new();
}
/// <summary>
/// Hourly snapshot of platform metrics
/// </summary>
[GenerateSerializer]
public class HourlySnapshot
{
[Id(0)]
public DateTime Timestamp { get; set; }
[Id(1)]
public int TotalAgents { get; set; }
[Id(2)]
public int TotalStrategies { get; set; }
[Id(3)]
public decimal TotalVolume { get; set; }
[Id(4)]
public decimal TotalPnL { get; set; }
[Id(5)]
public decimal TotalOpenInterest { get; set; }
[Id(6)]
public int TotalPositionCount { get; set; }
}
/// <summary>
/// Daily snapshot of platform metrics
/// </summary>
[GenerateSerializer]
public class DailySnapshot
{
[Id(0)]
public DateTime Date { get; set; }
[Id(1)]
public int TotalAgents { get; set; }
[Id(2)]
public int TotalStrategies { get; set; }
[Id(3)]
public decimal TotalVolume { get; set; }
[Id(4)]
public decimal TotalPnL { get; set; }
[Id(5)]
public decimal TotalOpenInterest { get; set; }
[Id(6)]
public int TotalPositionCount { get; set; }
}

View File

@@ -1,14 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Managing.Common\Managing.Common.csproj" /> <ProjectReference Include="..\Managing.Common\Managing.Common.csproj"/>
<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,147 @@
using Orleans;
using static Managing.Common.Enums;
namespace Managing.Application.Abstractions.Models;
/// <summary>
/// Platform-wide statistics without individual agent details
/// </summary>
[GenerateSerializer]
public class PlatformSummaryViewModel
{
/// <summary>
/// Total number of agents on the platform
/// </summary>
[Id(0)]
public required int TotalAgents { get; set; }
/// <summary>
/// Total number of active strategies across all agents
/// </summary>
[Id(1)]
public required int TotalActiveStrategies { get; set; }
/// <summary>
/// Total platform-wide profit and loss in USD
/// </summary>
[Id(2)]
public required decimal TotalPlatformPnL { get; set; }
/// <summary>
/// Total volume traded across all agents in USD
/// </summary>
[Id(3)]
public required decimal TotalPlatformVolume { get; set; }
/// <summary>
/// Total volume traded across all agents in the last 24 hours in USD
/// </summary>
[Id(4)]
public required decimal TotalPlatformVolumeLast24h { get; set; }
/// <summary>
/// Total open interest across all positions in USD
/// </summary>
[Id(5)]
public required decimal TotalOpenInterest { get; set; }
/// <summary>
/// Total number of open positions across all strategies
/// </summary>
[Id(6)]
public required int TotalPositionCount { get; set; }
// 24-hour changes
/// <summary>
/// Change in agent count over the last 24 hours
/// </summary>
[Id(7)]
public required int AgentsChange24h { get; set; }
/// <summary>
/// Change in strategy count over the last 24 hours
/// </summary>
[Id(8)]
public required int StrategiesChange24h { get; set; }
/// <summary>
/// Change in PnL over the last 24 hours
/// </summary>
[Id(9)]
public required decimal PnLChange24h { get; set; }
/// <summary>
/// Change in volume over the last 24 hours
/// </summary>
[Id(10)]
public required decimal VolumeChange24h { get; set; }
/// <summary>
/// Change in open interest over the last 24 hours
/// </summary>
[Id(11)]
public required decimal OpenInterestChange24h { get; set; }
/// <summary>
/// Change in position count over the last 24 hours
/// </summary>
[Id(12)]
public required int PositionCountChange24h { get; set; }
// Breakdowns
/// <summary>
/// Volume breakdown by asset/ticker
/// </summary>
[Id(13)]
public required Dictionary<Ticker, decimal> VolumeByAsset { get; set; }
/// <summary>
/// Position count breakdown by asset/ticker
/// </summary>
[Id(14)]
public required Dictionary<Ticker, int> PositionCountByAsset { get; set; }
/// <summary>
/// Position count breakdown by direction (Long/Short)
/// </summary>
[Id(15)]
public required Dictionary<TradeDirection, int> PositionCountByDirection { get; set; }
// Metadata
/// <summary>
/// When the data was last updated
/// </summary>
[Id(16)]
public required DateTime LastUpdated { get; set; }
/// <summary>
/// When the last 24-hour snapshot was taken
/// </summary>
[Id(17)]
public required DateTime Last24HourSnapshot { get; set; }
/// <summary>
/// Daily volume history for the last 30 days for chart visualization
/// </summary>
[Id(18)]
public required List<VolumeHistoryPoint> VolumeHistory { get; set; }
}
/// <summary>
/// Represents a volume data point for historical charting
/// </summary>
[GenerateSerializer]
public class VolumeHistoryPoint
{
/// <summary>
/// Date of the volume measurement
/// </summary>
[Id(0)]
public required DateTime Date { get; set; }
/// <summary>
/// Total volume for that date in USD
/// </summary>
[Id(1)]
public required decimal Volume { get; set; }
}

View File

@@ -0,0 +1,31 @@
using Managing.Domain.Statistics;
using static Managing.Common.Enums;
namespace Managing.Application.Abstractions.Repositories;
public interface IAgentSummaryRepository
{
Task<AgentSummary?> GetByUserIdAsync(int userId);
Task<AgentSummary?> GetByAgentNameAsync(string agentName);
Task<IEnumerable<AgentSummary>> GetAllAsync();
Task InsertAsync(AgentSummary agentSummary);
Task UpdateAsync(AgentSummary agentSummary);
Task SaveOrUpdateAsync(AgentSummary agentSummary);
/// <summary>
/// Gets paginated agent summaries with sorting and filtering
/// </summary>
/// <param name="page">Page number (1-based)</param>
/// <param name="pageSize">Number of items per page</param>
/// <param name="sortBy">Field to sort by</param>
/// <param name="sortOrder">Sort order (asc or desc)</param>
/// <param name="agentNames">Optional list of agent names to filter by</param>
/// <returns>Tuple containing the paginated results and total count</returns>
Task<(IEnumerable<AgentSummary> Results, int TotalCount)> GetPaginatedAsync(
int page,
int pageSize,
SortableFields sortBy,
string sortOrder,
IEnumerable<string>? agentNames = null);
Task<IEnumerable<AgentSummary>> GetAllAgentWithRunningBots();
}

View File

@@ -1,12 +1,40 @@
using Managing.Domain.Bots; using Managing.Domain.Bots;
using static Managing.Common.Enums;
namespace Managing.Application.Abstractions.Repositories; namespace Managing.Application.Abstractions.Repositories;
public interface IBotRepository public interface IBotRepository
{ {
Task InsertBotAsync(BotBackup bot); Task InsertBotAsync(Bot bot);
Task<IEnumerable<BotBackup>> GetBotsAsync(); Task<IEnumerable<Bot>> GetBotsAsync();
Task UpdateBackupBot(BotBackup bot); Task UpdateBot(Bot bot);
Task DeleteBotBackup(string botName); Task DeleteBot(Guid identifier);
Task<BotBackup?> GetBotByIdentifierAsync(string identifier); Task<Bot> GetBotByIdentifierAsync(Guid identifier);
Task<IEnumerable<Bot>> GetBotsByIdsAsync(IEnumerable<Guid> identifiers);
Task<IEnumerable<Bot>> GetBotsByUserIdAsync(int id);
Task<IEnumerable<Bot>> GetBotsByStatusAsync(BotStatus status);
Task<Bot> GetBotByNameAsync(string name);
/// <summary>
/// Gets paginated bots with filtering and sorting
/// </summary>
/// <param name="pageNumber">Page number (1-based)</param>
/// <param name="pageSize">Number of items per page</param>
/// <param name="status">Filter by status (optional)</param>
/// <param name="userId">Filter by user ID (optional)</param>
/// <param name="name">Filter by name (partial match, case-insensitive)</param>
/// <param name="ticker">Filter by ticker (partial match, case-insensitive)</param>
/// <param name="agentName">Filter by agent name (partial match, case-insensitive)</param>
/// <param name="sortBy">Sort field</param>
/// <param name="sortDirection">Sort direction ("Asc" or "Desc")</param>
/// <returns>Tuple containing the bots for the current page and total count</returns>
Task<(IEnumerable<Bot> Bots, int TotalCount)> GetBotsPaginatedAsync(
int pageNumber,
int pageSize,
BotStatus? status = null,
string? name = null,
string? ticker = null,
string? agentName = null,
string sortBy = "CreateDate",
string sortDirection = "Desc");
} }

View File

@@ -5,22 +5,24 @@ namespace Managing.Application.Abstractions.Repositories;
public interface ICandleRepository public interface ICandleRepository
{ {
Task<IList<Candle>> GetCandles( Task<HashSet<Candle>> GetCandles(
Enums.TradingExchanges exchange,
Enums.Ticker ticker,
Enums.Timeframe timeframe,
DateTime start);
Task<IList<Candle>> GetCandles(
Enums.TradingExchanges exchange, Enums.TradingExchanges exchange,
Enums.Ticker ticker, Enums.Ticker ticker,
Enums.Timeframe timeframe, Enums.Timeframe timeframe,
DateTime start, DateTime start,
DateTime end); int? limit = null);
Task<HashSet<Candle>> GetCandles(
Enums.TradingExchanges exchange,
Enums.Ticker ticker,
Enums.Timeframe timeframe,
DateTime start,
DateTime end,
int? limit = null);
Task<IList<Enums.Ticker>> GetTickersAsync( Task<IList<Enums.Ticker>> GetTickersAsync(
Enums.TradingExchanges exchange, Enums.TradingExchanges exchange,
Enums.Timeframe timeframe, Enums.Timeframe timeframe,
DateTime start); DateTime start);
void InsertCandle(Candle candle); Task InsertCandle(Candle candle);
} }

View File

@@ -13,18 +13,24 @@ public interface ITradingRepository
Task<Signal> GetSignalByIdentifierAsync(string identifier, User user = null); Task<Signal> GetSignalByIdentifierAsync(string identifier, User user = null);
Task InsertPositionAsync(Position position); Task InsertPositionAsync(Position position);
Task UpdatePositionAsync(Position position); Task UpdatePositionAsync(Position position);
Task<Indicator> GetStrategyByNameAsync(string strategy); Task<IndicatorBase> GetStrategyByNameAsync(string strategy);
Task InsertScenarioAsync(Scenario scenario); Task InsertScenarioAsync(Scenario scenario);
Task InsertStrategyAsync(Indicator indicator); Task InsertIndicatorAsync(IndicatorBase indicator);
Task<IEnumerable<Scenario>> GetScenariosAsync(); Task<IEnumerable<Scenario>> GetScenariosAsync();
Task<IEnumerable<Indicator>> GetStrategiesAsync(); Task<IEnumerable<Scenario>> GetScenariosByUserAsync(User user);
Task<IEnumerable<Indicator>> GetIndicatorsAsync(); Task<IEnumerable<IndicatorBase>> GetStrategiesAsync();
Task<IEnumerable<IndicatorBase>> GetIndicatorsAsync();
Task DeleteScenarioAsync(string name); Task DeleteScenarioAsync(string name);
Task DeleteIndicatorAsync(string name); Task DeleteIndicatorAsync(string name);
Task<Position> GetPositionByIdentifierAsync(string identifier); Task<Position> GetPositionByIdentifierAsync(Guid identifier);
Task<IEnumerable<Position>> GetPositionsAsync(PositionInitiator positionInitiator); Task<IEnumerable<Position>> GetPositionsAsync(PositionInitiator positionInitiator);
Task<IEnumerable<Position>> GetPositionsByStatusAsync(PositionStatus positionStatus); Task<IEnumerable<Position>> GetPositionsByStatusAsync(PositionStatus positionStatus);
Task<IEnumerable<Position>> GetPositionsByInitiatorIdentifierAsync(Guid initiatorIdentifier);
Task<IEnumerable<Position>> GetPositionsByInitiatorIdentifiersAsync(IEnumerable<Guid> initiatorIdentifiers);
Task<IEnumerable<Position>> GetAllPositionsAsync();
Task UpdateScenarioAsync(Scenario scenario); Task UpdateScenarioAsync(Scenario scenario);
Task UpdateStrategyAsync(Indicator indicator); Task UpdateStrategyAsync(IndicatorBase indicatorBase);
Task<IndicatorBase> GetStrategyByNameUserAsync(string name, User user);
Task<Scenario> GetScenarioByNameUserAsync(string scenarioName, User user);
} }

View File

@@ -6,6 +6,6 @@ public interface IUserRepository
{ {
Task<User> GetUserByAgentNameAsync(string agentName); Task<User> GetUserByAgentNameAsync(string agentName);
Task<User> GetUserByNameAsync(string name); Task<User> GetUserByNameAsync(string name);
Task InsertUserAsync(User user); Task<IEnumerable<User>> GetAllUsersAsync();
Task UpdateUser(User user); Task SaveOrUpdateUserAsync(User user);
} }

View File

@@ -1,12 +0,0 @@
using Managing.Domain.Workflows.Synthetics;
namespace Managing.Application.Abstractions.Repositories;
public interface IWorkflowRepository
{
bool DeleteWorkflow(string name);
Task<SyntheticWorkflow> GetWorkflow(string name);
IEnumerable<SyntheticWorkflow> GetWorkflows();
Task InsertWorkflow(SyntheticWorkflow workflow);
Task UpdateWorkflow(SyntheticWorkflow workflow);
}

View File

@@ -9,7 +9,7 @@ public interface IAccountService
Task<Account> CreateAccount(User user, Account account); Task<Account> CreateAccount(User user, Account account);
bool DeleteAccount(User user, string name); bool DeleteAccount(User user, string name);
IEnumerable<Account> GetAccountsByUser(User user, bool hideSecrets = true); IEnumerable<Account> GetAccountsByUser(User user, bool hideSecrets = true);
Task<IEnumerable<Account>> GetAccountsByUserAsync(User user, bool hideSecrets = true); Task<IEnumerable<Account>> GetAccountsByUserAsync(User user, bool hideSecrets = true, bool getBalance = false);
Task<IEnumerable<Account>> GetAccounts(bool hideSecrets, bool getBalance); Task<IEnumerable<Account>> GetAccounts(bool hideSecrets, bool getBalance);
Task<IEnumerable<Account>> GetAccountsAsync(bool hideSecrets, bool getBalance); Task<IEnumerable<Account>> GetAccountsAsync(bool hideSecrets, bool getBalance);
Task<Account> GetAccount(string name, bool hideSecrets, bool getBalance); Task<Account> GetAccount(string name, bool hideSecrets, bool getBalance);

View File

@@ -0,0 +1,17 @@
using Managing.Domain.Statistics;
namespace Managing.Application.Abstractions.Services;
public interface IAgentService
{
Task<AgentBalanceHistory> GetAgentBalances(string agentName, DateTime start, DateTime? end = null);
Task<(IList<AgentBalanceHistory> Agents, int TotalCount)> GetBestAgents(DateTime start, DateTime? end = null,
int page = 1,
int pageSize = 10);
Task SaveOrUpdateAgentSummary(AgentSummary agentSummary);
Task<IEnumerable<AgentSummary>> GetAllAgentSummaries();
Task<IEnumerable<string>> GetAllOnlineAgents();
}

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,10 +42,10 @@ 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, HashSet<Candle> candles,
User user = null, User user = null,
bool withCandles = false, bool withCandles = false,
string requestId = null, string requestId = null,

View File

@@ -45,16 +45,18 @@ public interface IExchangeService
Task<List<Trade>> GetTrades(Account account, Ticker ticker); Task<List<Trade>> GetTrades(Account account, Ticker ticker);
Task<bool> CancelOrder(Account account, Ticker ticker); Task<bool> CancelOrder(Account account, Ticker ticker);
decimal GetFee(Account account, bool isForPaperTrading = false); decimal GetFee(Account account, bool isForPaperTrading = false);
Candle GetCandle(Account account, Ticker ticker, DateTime date); Task<Candle> GetCandle(Account account, Ticker ticker, DateTime date);
Task<decimal> GetQuantityInPosition(Account account, Ticker ticker); Task<decimal> GetQuantityInPosition(Account account, Ticker ticker);
Task<List<Candle>> GetCandlesInflux(TradingExchanges exchange, Ticker ticker, DateTime startDate, Task<HashSet<Candle>> GetCandlesInflux(TradingExchanges exchange, Ticker ticker, DateTime startDate,
Timeframe timeframe); Timeframe timeframe, int? limit = null);
Task<List<Candle>> GetCandlesInflux(TradingExchanges exchange, Ticker ticker, DateTime startDate, Task<HashSet<Candle>> GetCandlesInflux(TradingExchanges exchange, Ticker ticker, DateTime startDate,
Timeframe timeframe, DateTime endDate); Timeframe timeframe, DateTime endDate, int? limit = null);
Task<decimal> GetBestPrice(Account account, Ticker ticker, decimal lastPrice, decimal quantity,
TradeDirection direction);
Task<decimal> GetBestPrice(Account account, Ticker ticker, decimal lastPrice, decimal quantity, TradeDirection direction);
Orderbook GetOrderbook(Account account, Ticker ticker); Orderbook GetOrderbook(Account account, Ticker ticker);
Trade BuildEmptyTrade(Ticker ticker, decimal price, decimal quantity, TradeDirection direction, decimal? leverage, Trade BuildEmptyTrade(Ticker ticker, decimal price, decimal quantity, TradeDirection direction, decimal? leverage,

View File

@@ -1,7 +1,7 @@
using Managing.Domain.MoneyManagements; using Managing.Domain.MoneyManagements;
using Managing.Domain.Users; using Managing.Domain.Users;
namespace Managing.Application.Abstractions namespace Managing.Application.Abstractions.Services
{ {
public interface IMoneyManagementService public interface IMoneyManagementService
{ {

View File

@@ -1,6 +1,6 @@
using static Managing.Common.Enums; using static Managing.Common.Enums;
namespace Managing.Application.Workers.Abstractions; namespace Managing.Application.Abstractions.Services;
public interface IPricesService public interface IPricesService
{ {

View File

@@ -6,12 +6,6 @@ namespace Managing.Application.Abstractions.Services;
public interface IStatisticService public interface IStatisticService
{ {
Task<AgentBalanceHistory> GetAgentBalances(string agentName, DateTime start, DateTime? end = null);
Task<(IList<AgentBalanceHistory> Agents, int TotalCount)> GetBestAgents(DateTime start, DateTime? end = null,
int page = 1,
int pageSize = 10);
List<Trader> GetBadTraders(); List<Trader> GetBadTraders();
Task<List<Trader>> GetBadTradersAsync(); Task<List<Trader>> GetBadTradersAsync();
List<Trader> GetBestTraders(); List<Trader> GetBestTraders();

View File

@@ -1,4 +1,5 @@
using Managing.Domain.Bots; using Managing.Domain.Bots;
using Managing.Domain.Indicators;
using Managing.Domain.Synth.Models; using Managing.Domain.Synth.Models;
using static Managing.Common.Enums; using static Managing.Common.Enums;
@@ -94,7 +95,7 @@ public interface ISynthPredictionService
/// <param name="botConfig">Bot configuration with Synth settings</param> /// <param name="botConfig">Bot configuration with Synth settings</param>
/// <returns>Risk assessment result</returns> /// <returns>Risk assessment result</returns>
Task<SynthRiskResult> MonitorPositionRiskAsync(Ticker ticker, TradeDirection direction, decimal currentPrice, Task<SynthRiskResult> MonitorPositionRiskAsync(Ticker ticker, TradeDirection direction, decimal currentPrice,
decimal liquidationPrice, string positionIdentifier, TradingBotConfig botConfig); decimal liquidationPrice, Guid positionIdentifier, TradingBotConfig botConfig);
/// <summary> /// <summary>
/// Estimates liquidation price based on money management settings /// Estimates liquidation price based on money management settings
@@ -103,5 +104,6 @@ public interface ISynthPredictionService
/// <param name="direction">Position direction</param> /// <param name="direction">Position direction</param>
/// <param name="moneyManagement">Money management settings</param> /// <param name="moneyManagement">Money management settings</param>
/// <returns>Estimated liquidation price</returns> /// <returns>Estimated liquidation price</returns>
decimal EstimateLiquidationPrice(decimal currentPrice, TradeDirection direction, LightMoneyManagement moneyManagement); decimal EstimateLiquidationPrice(decimal currentPrice, TradeDirection direction,
LightMoneyManagement moneyManagement);
} }

View File

@@ -1,12 +1,14 @@
using Managing.Domain.Accounts; using Managing.Domain.Accounts;
using Managing.Domain.Bots; using Managing.Domain.Bots;
using Managing.Domain.Candles; using Managing.Domain.Candles;
using Managing.Domain.Indicators;
using Managing.Domain.Scenarios; using Managing.Domain.Scenarios;
using Managing.Domain.Statistics; using Managing.Domain.Statistics;
using Managing.Domain.Strategies; using Managing.Domain.Strategies;
using Managing.Domain.Strategies.Base; using Managing.Domain.Strategies.Base;
using Managing.Domain.Synth.Models; using Managing.Domain.Synth.Models;
using Managing.Domain.Trades; using Managing.Domain.Trades;
using Managing.Domain.Users;
using Managing.Infrastructure.Evm.Models.Privy; using Managing.Infrastructure.Evm.Models.Privy;
using static Managing.Common.Enums; using static Managing.Common.Enums;
@@ -17,21 +19,25 @@ public interface ITradingService
Task<Scenario> GetScenarioByNameAsync(string scenario); Task<Scenario> GetScenarioByNameAsync(string scenario);
Task InsertPositionAsync(Position position); Task InsertPositionAsync(Position position);
Task UpdatePositionAsync(Position position); Task UpdatePositionAsync(Position position);
Task<Indicator> GetStrategyByNameAsync(string strategy); Task<IndicatorBase> GetIndicatorByNameAsync(string strategy);
Task InsertScenarioAsync(Scenario scenario); Task InsertScenarioAsync(Scenario scenario);
Task InsertStrategyAsync(Indicator indicator); Task InsertIndicatorAsync(IndicatorBase indicatorBase);
Task<IEnumerable<Scenario>> GetScenariosAsync(); Task<IEnumerable<Scenario>> GetScenariosAsync();
Task<IEnumerable<Indicator>> GetStrategiesAsync(); Task<IEnumerable<Scenario>> GetScenariosByUserAsync(User user);
Task<IEnumerable<IndicatorBase>> GetIndicatorsAsync();
Task DeleteScenarioAsync(string name); Task DeleteScenarioAsync(string name);
Task DeleteStrategyAsync(string name); Task DeleteIndicatorAsync(string name);
Task<Position> GetPositionByIdentifierAsync(string identifier); Task<Position> GetPositionByIdentifierAsync(Guid identifier);
Task<Position> ManagePosition(Account account, Position position); Task<Position> ManagePosition(Account account, Position position);
Task WatchTrader(); Task WatchTrader();
Task<IEnumerable<Trader>> GetTradersWatch(); Task<IEnumerable<Trader>> GetTradersWatch();
Task UpdateScenarioAsync(Scenario scenario); Task UpdateScenarioAsync(Scenario scenario);
Task UpdateStrategyAsync(Indicator indicator); Task UpdateIndicatorAsync(IndicatorBase indicatorBase);
Task<IEnumerable<Position>> GetBrokerPositions(Account account); Task<IEnumerable<Position>> GetBrokerPositions(Account account);
Task<IEnumerable<Position>> GetAllDatabasePositionsAsync();
Task<IEnumerable<Position>> GetPositionsByInitiatorIdentifierAsync(Guid initiatorIdentifier);
Task<IEnumerable<Position>> GetPositionsByInitiatorIdentifiersAsync(IEnumerable<Guid> initiatorIdentifiers);
Task<PrivyInitAddressResponse> InitPrivyWallet(string publicAddress); Task<PrivyInitAddressResponse> InitPrivyWallet(string publicAddress);
// Synth API integration methods // Synth API integration methods
@@ -43,7 +49,7 @@ public interface ITradingService
TradingBotConfig botConfig, bool isBacktest); TradingBotConfig botConfig, bool isBacktest);
Task<SynthRiskResult> MonitorSynthPositionRiskAsync(Ticker ticker, TradeDirection direction, decimal currentPrice, Task<SynthRiskResult> MonitorSynthPositionRiskAsync(Ticker ticker, TradeDirection direction, decimal currentPrice,
decimal liquidationPrice, string positionIdentifier, TradingBotConfig botConfig); decimal liquidationPrice, Guid positionIdentifier, TradingBotConfig botConfig);
/// <summary> /// <summary>
/// Calculates indicators values for a given scenario and candles. /// Calculates indicators values for a given scenario and candles.
@@ -53,5 +59,8 @@ public interface ITradingService
/// <returns>A dictionary of indicator types to their calculated values.</returns> /// <returns>A dictionary of indicator types to their calculated values.</returns>
Dictionary<IndicatorType, IndicatorsResultBase> CalculateIndicatorsValuesAsync( Dictionary<IndicatorType, IndicatorsResultBase> CalculateIndicatorsValuesAsync(
Scenario scenario, Scenario scenario,
List<Candle> candles); HashSet<Candle> candles);
Task<IndicatorBase?> GetIndicatorByNameUserAsync(string name, User user);
Task<Scenario?> GetScenarioByNameUserAsync(string scenarioName, User user);
} }

View File

@@ -5,9 +5,12 @@ namespace Managing.Application.Abstractions.Services;
public interface IUserService public interface IUserService
{ {
Task<User> Authenticate(string name, string address, string message, string signature); Task<User> Authenticate(string name, string address, string message, string signature);
Task<User> GetUserByAddressAsync(string address); Task<User> GetUserByAddressAsync(string address, bool useCache = true);
Task<User> UpdateAgentName(User user, string agentName); Task<User> UpdateAgentName(User user, string agentName);
Task<User> UpdateAvatarUrl(User user, string avatarUrl); Task<User> UpdateAvatarUrl(User user, string avatarUrl);
Task<User> UpdateTelegramChannel(User user, string telegramChannel); Task<User> UpdateTelegramChannel(User user, string telegramChannel);
Task<User> GetUser(string name); Task<User> GetUserByName(string name);
Task<User> GetUserByAgentName(string agentName);
Task<User> GetUserByIdAsync(int userId);
Task<IEnumerable<User>> GetAllUsersAsync();
} }

View File

@@ -15,5 +15,7 @@ namespace Managing.Application.Abstractions.Services
string orderType = "market", double? triggerRatio = null, double allowedSlippage = 0.5); string orderType = "market", double? triggerRatio = null, double allowedSlippage = 0.5);
Task<SwapInfos> SendTokenAsync(string senderAddress, string recipientAddress, Ticker ticker, decimal amount, int? chainId = null); Task<SwapInfos> SendTokenAsync(string senderAddress, string recipientAddress, Ticker ticker, decimal amount, int? chainId = null);
Task<List<Balance>> GetWalletBalanceAsync(string address, Ticker[] assets, string[] chains);
} }
} }

View File

@@ -1,7 +1,7 @@
using Managing.Domain.Workers; using Managing.Domain.Workers;
using static Managing.Common.Enums; using static Managing.Common.Enums;
namespace Managing.Application.Workers.Abstractions; namespace Managing.Application.Abstractions.Services;
public interface IWorkerService public interface IWorkerService
{ {

View File

@@ -3,16 +3,15 @@ using System.Diagnostics;
using Managing.Application.Abstractions; 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.Backtests;
using Managing.Application.Bots.Base;
using Managing.Application.Hubs; using Managing.Application.Hubs;
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;
using Managing.Domain.Scenarios; using Managing.Domain.Scenarios;
using Managing.Domain.Strategies;
using Managing.Domain.Strategies.Signals;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
using Moq; using Moq;
using Newtonsoft.Json; using Newtonsoft.Json;
@@ -23,7 +22,6 @@ namespace Managing.Application.Tests
{ {
public class BotsTests : BaseTests public class BotsTests : BaseTests
{ {
private readonly IBotFactory _botFactory;
private readonly IBacktester _backtester; private readonly IBacktester _backtester;
private readonly string _reportPath; private readonly string _reportPath;
private string _analysePath; private string _analysePath;
@@ -38,20 +36,12 @@ namespace Managing.Application.Tests
var scenarioService = new Mock<IScenarioService>().Object; var scenarioService = new Mock<IScenarioService>().Object;
var messengerService = new Mock<IMessengerService>().Object; var messengerService = new Mock<IMessengerService>().Object;
var kaigenService = new Mock<IKaigenService>().Object; var kaigenService = new Mock<IKaigenService>().Object;
var backupBotService = new Mock<IBackupBotService>().Object;
var hubContext = new Mock<IHubContext<BacktestHub>>().Object; var hubContext = new Mock<IHubContext<BacktestHub>>().Object;
var tradingBotLogger = TradingBaseTests.CreateTradingBotLogger(); var tradingBotLogger = TradingBaseTests.CreateTradingBotLogger();
var backtestLogger = TradingBaseTests.CreateBacktesterLogger(); var backtestLogger = TradingBaseTests.CreateBacktesterLogger();
var botService = new Mock<IBotService>().Object; var botService = new Mock<IBotService>().Object;
_botFactory = new BotFactory( _backtester = new Backtester(_exchangeService, backtestRepository, backtestLogger,
_exchangeService, scenarioService, _accountService.Object, messengerService, kaigenService, hubContext, null);
tradingBotLogger,
discordService,
_accountService.Object,
_tradingService.Object,
botService, backupBotService);
_backtester = new Backtester(_exchangeService, _botFactory, backtestRepository, backtestLogger,
scenarioService, _accountService.Object, messengerService, kaigenService, hubContext);
_elapsedTimes = new List<double>(); _elapsedTimes = new List<double>();
// Initialize cross-platform file paths // Initialize cross-platform file paths
@@ -69,7 +59,6 @@ namespace Managing.Application.Tests
// Arrange // Arrange
var scenario = new Scenario("FlippingScenario"); var scenario = new Scenario("FlippingScenario");
var strategy = ScenarioHelpers.BuildIndicator(IndicatorType.RsiDivergence, "RsiDiv", period: 14); var strategy = ScenarioHelpers.BuildIndicator(IndicatorType.RsiDivergence, "RsiDiv", period: 14);
scenario.AddIndicator(strategy);
var localCandles = var localCandles =
FileHelpers.ReadJson<List<Candle>>($"{ticker.ToString()}-{timeframe.ToString()}-candles.json"); FileHelpers.ReadJson<List<Candle>>($"{ticker.ToString()}-{timeframe.ToString()}-candles.json");
@@ -78,7 +67,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,
@@ -94,10 +83,11 @@ namespace Managing.Application.Tests
// Act // Act
var backtestResult = var backtestResult =
await _backtester.RunTradingBotBacktest(config, localCandles.TakeLast(500).ToList(), null, false); await _backtester.RunTradingBotBacktest(config, DateTime.UtcNow.AddDays(-6),
DateTime.UtcNow, null, false, false);
var json = JsonConvert.SerializeObject(backtestResult, Formatting.None); var json = JsonConvert.SerializeObject(backtestResult, Formatting.None);
File.WriteAllText($"{ticker.ToString()}-{timeframe.ToString()}-{Guid.NewGuid()}.json", json); File.WriteAllText($"{ticker}-{timeframe}-{Guid.NewGuid()}.json", json);
// WriteCsvReport(backtestResult.GetStringReport()); // WriteCsvReport(backtestResult.GetStringReport());
// Assert // Assert
@@ -120,15 +110,13 @@ namespace Managing.Application.Tests
{ {
// Arrange // Arrange
var scenario = new Scenario("ScalpingScenario"); var scenario = new Scenario("ScalpingScenario");
var strategy = ScenarioHelpers.BuildIndicator(IndicatorType.RsiDivergence, "RsiDiv", period: 5);
scenario.AddIndicator(strategy);
var config = new TradingBotConfig var config = new TradingBotConfig
{ {
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,
@@ -159,10 +147,13 @@ namespace Managing.Application.Tests
int days) int days)
{ {
// Arrange // Arrange
var scenario = new Scenario("ScalpingScenario"); var scenario = new Scenario("ScalpingScenario")
var strategy = ScenarioHelpers.BuildIndicator(IndicatorType.MacdCross, "RsiDiv", fastPeriods: 12, {
slowPeriods: 26, signalPeriods: 9); Indicators = new List<IndicatorBase>
scenario.AddIndicator(strategy); {
new MacdCrossIndicatorBase("MacdCross", 12, 26, 9)
}
};
var moneyManagement = new MoneyManagement() var moneyManagement = new MoneyManagement()
{ {
@@ -177,7 +168,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 +185,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,11 +225,13 @@ 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); scenario.Indicators = new List<IndicatorBase>
scenario.AddIndicator(strategy); {
new RsiDivergenceIndicatorBase("RsiDiv", (int)i)
};
// -0.5 to -5 // -0.5 to -5
for (decimal s = stopLossRange[0]; s < stopLossRange[1]; s += stopLossRange[2]) for (decimal s = stopLossRange[0]; s < stopLossRange[1]; s += stopLossRange[2])
@@ -263,51 +256,53 @@ namespace Managing.Application.Tests
{ {
BotType.SimpleBot => throw new NotImplementedException(), BotType.SimpleBot => throw new NotImplementedException(),
BotType.ScalpingBot => _backtester.RunTradingBotBacktest(new TradingBotConfig BotType.ScalpingBot => _backtester.RunTradingBotBacktest(new TradingBotConfig
{ {
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,
IsForBacktest = true, IsForBacktest = true,
CooldownPeriod = 1, CooldownPeriod = 1,
MaxLossStreak = 0, MaxLossStreak = 0,
FlipPosition = false, FlipPosition = false,
Name = "Test", Name = "Test",
FlipOnlyWhenInProfit = true, FlipOnlyWhenInProfit = true,
MaxPositionTimeHours = null, MaxPositionTimeHours = null,
CloseEarlyWhenProfitable = false CloseEarlyWhenProfitable = false
}, candles, null, false).Result, }, DateTime.UtcNow.AddDays(-6),
DateTime.UtcNow, null, false, false).Result,
BotType.FlippingBot => _backtester.RunTradingBotBacktest(new TradingBotConfig BotType.FlippingBot => _backtester.RunTradingBotBacktest(new TradingBotConfig
{ {
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,
IsForBacktest = true, IsForBacktest = true,
CooldownPeriod = 1, CooldownPeriod = 1,
MaxLossStreak = 0, MaxLossStreak = 0,
FlipPosition = true, FlipPosition = true,
Name = "Test", Name = "Test",
FlipOnlyWhenInProfit = true, FlipOnlyWhenInProfit = true,
MaxPositionTimeHours = null, MaxPositionTimeHours = null,
CloseEarlyWhenProfitable = false CloseEarlyWhenProfitable = false
}, candles, null, false).Result, }, DateTime.UtcNow.AddDays(-6),
DateTime.UtcNow, null, false, false).Result,
_ => throw new NotImplementedException(), _ => throw new NotImplementedException(),
}; };
timer.Stop(); timer.Stop();
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);
@@ -377,9 +372,10 @@ namespace Managing.Application.Tests
return; return;
var scenario = new Scenario("ScalpingScenario"); var scenario = new Scenario("ScalpingScenario");
var strategy = ScenarioHelpers.BuildIndicator(indicatorType, "RsiDiv", fastPeriods: 12, scenario.Indicators = new List<IndicatorBase>
slowPeriods: 26, signalPeriods: 9); {
scenario.AddIndicator(strategy); new MacdCrossIndicatorBase("MacdCross", 12, 26, 9)
};
// -0.5 to -5 // -0.5 to -5
for (decimal s = stopLossRange[0]; s < stopLossRange[1]; s += stopLossRange[2]) for (decimal s = stopLossRange[0]; s < stopLossRange[1]; s += stopLossRange[2])
@@ -403,47 +399,48 @@ namespace Managing.Application.Tests
{ {
BotType.SimpleBot => throw new NotImplementedException(), BotType.SimpleBot => throw new NotImplementedException(),
BotType.ScalpingBot => _backtester.RunTradingBotBacktest(new TradingBotConfig BotType.ScalpingBot => _backtester.RunTradingBotBacktest(new TradingBotConfig
{ {
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,
IsForBacktest = true, IsForBacktest = true,
CooldownPeriod = 1, CooldownPeriod = 1,
MaxLossStreak = 0, MaxLossStreak = 0,
FlipPosition = false, FlipPosition = false,
Name = "Test", Name = "Test",
FlipOnlyWhenInProfit = true, FlipOnlyWhenInProfit = true,
MaxPositionTimeHours = null, MaxPositionTimeHours = null,
CloseEarlyWhenProfitable = false CloseEarlyWhenProfitable = false
}, candles, null).Result, }, DateTime.UtcNow.AddDays(-6),
DateTime.UtcNow, null, false, false).Result,
BotType.FlippingBot => _backtester.RunTradingBotBacktest(new TradingBotConfig BotType.FlippingBot => _backtester.RunTradingBotBacktest(new TradingBotConfig
{ {
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,
IsForBacktest = true, IsForBacktest = true,
CooldownPeriod = 1, CooldownPeriod = 1,
MaxLossStreak = 0, MaxLossStreak = 0,
FlipPosition = true, FlipPosition = true,
Name = "Test", Name = "Test",
FlipOnlyWhenInProfit = true, FlipOnlyWhenInProfit = true,
MaxPositionTimeHours = null, MaxPositionTimeHours = null,
CloseEarlyWhenProfitable = false CloseEarlyWhenProfitable = false
}, candles, null).Result, }, DateTime.UtcNow.AddDays(-6),
DateTime.UtcNow, null, false, false).Result,
_ => throw new NotImplementedException(), _ => throw new NotImplementedException(),
}; };
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 +658,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,
@@ -675,28 +672,11 @@ namespace Managing.Application.Tests
CloseEarlyWhenProfitable = false CloseEarlyWhenProfitable = false
}; };
var backtestResult = _backtester.RunTradingBotBacktest(config, candles, null).Result; var backtestResult = _backtester.RunTradingBotBacktest(config, DateTime.UtcNow.AddDays(-6),
DateTime.UtcNow, null, false, false).Result;
timer.Stop(); 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);
@@ -1063,8 +1042,13 @@ namespace Managing.Application.Tests
{ {
foreach (var parameterSet in strategyConfig.ParameterSets) foreach (var parameterSet in strategyConfig.ParameterSets)
{ {
var scenario = BuildScenario($"{strategyConfig.Name}_{parameterSet.Name}", var scenario = new Scenario($"{strategyConfig.Name}_{parameterSet.Name}")
new[] { (strategyConfig, parameterSet) }); {
Indicators = new List<IndicatorBase>
{
new RsiDivergenceIndicatorBase("RsiDiv", (int)parameterSet.Period)
}
};
scenarios.Add(scenario); scenarios.Add(scenario);
} }
} }
@@ -1089,7 +1073,13 @@ namespace Managing.Application.Tests
{ {
var scenarioName = string.Join("_", var scenarioName = string.Join("_",
paramCombo.Select(p => $"{p.strategyConfig.Name}_{p.parameterSet.Name}")); paramCombo.Select(p => $"{p.strategyConfig.Name}_{p.parameterSet.Name}"));
var scenario = BuildScenario(scenarioName, paramCombo); var scenario = new Scenario(scenarioName)
{
Indicators = new List<IndicatorBase>
{
new RsiDivergenceIndicatorBase("RsiDiv", (int)paramCombo.First().parameterSet.Period)
}
};
scenario.LoopbackPeriod = 15; scenario.LoopbackPeriod = 15;
scenarios.Add(scenario); scenarios.Add(scenario);
} }
@@ -1098,31 +1088,6 @@ namespace Managing.Application.Tests
return scenarios; return scenarios;
} }
private Scenario BuildScenario(string scenarioName,
IEnumerable<(StrategyConfiguration strategyConfig, ParameterSet parameterSet)> strategyParams)
{
var scenario = new Scenario(scenarioName);
foreach (var (strategyConfig, parameterSet) in strategyParams)
{
var strategy = ScenarioHelpers.BuildIndicator(
strategyConfig.Type,
$"{strategyConfig.Name}_{parameterSet.Name}",
period: parameterSet.Period,
fastPeriods: parameterSet.FastPeriods,
slowPeriods: parameterSet.SlowPeriods,
signalPeriods: parameterSet.SignalPeriods,
multiplier: parameterSet.Multiplier,
stochPeriods: parameterSet.StochPeriods,
smoothPeriods: parameterSet.SmoothPeriods,
cyclePeriods: parameterSet.CyclePeriods);
scenario.AddIndicator(strategy);
}
return scenario;
}
private IEnumerable<IEnumerable<T>> GetCombinations<T>(IEnumerable<T> elements, int k) private IEnumerable<IEnumerable<T>> GetCombinations<T>(IEnumerable<T> elements, int k)
{ {
return k == 0 return k == 0

View File

@@ -1,5 +1,7 @@
using Managing.Application.Abstractions.Services; using Managing.Application.Abstractions.Services;
using Managing.Domain.Accounts; using Managing.Domain.Accounts;
using Managing.Domain.Candles;
using Managing.Domain.Indicators;
using Managing.Domain.Strategies.Signals; using Managing.Domain.Strategies.Signals;
using Managing.Domain.Strategies.Trends; using Managing.Domain.Strategies.Trends;
using Xunit; using Xunit;
@@ -7,31 +9,30 @@ using static Managing.Common.Enums;
namespace Managing.Application.Tests namespace Managing.Application.Tests
{ {
public class IndicatorTests public class IndicatorBaseTests
{ {
private readonly IExchangeService _exchangeService; private readonly IExchangeService _exchangeService;
public IndicatorTests() public IndicatorBaseTests()
{ {
_exchangeService = TradingBaseTests.GetExchangeService(); _exchangeService = TradingBaseTests.GetExchangeService();
} }
[Theory] [Theory]
[InlineData(TradingExchanges.Binance, Ticker.ADA, Timeframe.OneDay)] [InlineData(TradingExchanges.Binance, Ticker.ADA, Timeframe.OneDay)]
public void Should_Return_Signal_On_Rsi_BullishDivergence2(TradingExchanges exchange, Ticker ticker, public async Task Should_Return_Signal_On_Rsi_BullishDivergence2(TradingExchanges exchange, Ticker ticker,
Timeframe timeframe) Timeframe timeframe)
{ {
var account = GetAccount(exchange); var account = GetAccount(exchange);
// Arrange // Arrange
var rsiStrategy = new RsiDivergenceIndicator("unittest", 5); var rsiStrategy = new RsiDivergenceIndicatorBase("unittest", 5);
var candles = _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(-50), timeframe).Result; var candles = await _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(-50), timeframe);
var resultSignal = new List<LightSignal>(); var resultSignal = new List<LightSignal>();
// Act // Act
foreach (var candle in candles) foreach (var candle in candles)
{ {
rsiStrategy.Candles.Enqueue(candle); var signals = rsiStrategy.Run(new HashSet<Candle> { candle });
var signals = rsiStrategy.Run();
} }
if (rsiStrategy.Signals != null && rsiStrategy.Signals.Count > 0) if (rsiStrategy.Signals != null && rsiStrategy.Signals.Count > 0)
@@ -52,20 +53,19 @@ namespace Managing.Application.Tests
[Theory] [Theory]
[InlineData(TradingExchanges.Binance, Ticker.ADA, Timeframe.OneDay)] [InlineData(TradingExchanges.Binance, Ticker.ADA, Timeframe.OneDay)]
public void Shoud_Return_Signal_On_Rsi_BearishDivergence(TradingExchanges exchange, Ticker ticker, public async Task Shoud_Return_Signal_On_Rsi_BearishDivergence(TradingExchanges exchange, Ticker ticker,
Timeframe timeframe) Timeframe timeframe)
{ {
// Arrange // Arrange
var account = GetAccount(exchange); var account = GetAccount(exchange);
var rsiStrategy = new RsiDivergenceIndicator("unittest", 5); var rsiStrategy = new RsiDivergenceIndicatorBase("unittest", 5);
var candles = _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(-50), timeframe).Result; var candles = await _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(-50), timeframe);
var resultSignal = new List<LightSignal>(); var resultSignal = new List<LightSignal>();
// Act // Act
foreach (var candle in candles) foreach (var candle in candles)
{ {
rsiStrategy.Candles.Enqueue(candle); var signals = rsiStrategy.Run(new HashSet<Candle> { candle });
var signals = rsiStrategy.Run();
} }
if (rsiStrategy.Signals != null && rsiStrategy.Signals.Count > 0) if (rsiStrategy.Signals != null && rsiStrategy.Signals.Count > 0)
@@ -84,15 +84,14 @@ namespace Managing.Application.Tests
{ {
// Arrange // Arrange
var account = GetAccount(exchange); var account = GetAccount(exchange);
var rsiStrategy = new MacdCrossIndicator("unittest", 12, 26, 9); var rsiStrategy = new MacdCrossIndicatorBase("unittest", 12, 26, 9);
var candles = await _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(days), timeframe); var candles = await _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(days), timeframe);
var resultSignal = new List<LightSignal>(); var resultSignal = new List<LightSignal>();
// Act // Act
foreach (var candle in candles) foreach (var candle in candles)
{ {
rsiStrategy.Candles.Enqueue(candle); var signals = rsiStrategy.Run(new HashSet<Candle> { candle });
var signals = rsiStrategy.Run();
} }
if (rsiStrategy.Signals != null && rsiStrategy.Signals.Count > 0) if (rsiStrategy.Signals != null && rsiStrategy.Signals.Count > 0)
@@ -106,20 +105,20 @@ namespace Managing.Application.Tests
[Theory] [Theory]
[InlineData(TradingExchanges.Ftx, Ticker.ADA, Timeframe.OneDay, -500)] [InlineData(TradingExchanges.Ftx, Ticker.ADA, Timeframe.OneDay, -500)]
public void Shoud_Return_Signal_On_SuperTrend(TradingExchanges exchange, Ticker ticker, Timeframe timeframe, public async Task Shoud_Return_Signal_On_SuperTrend(TradingExchanges exchange, Ticker ticker,
Timeframe timeframe,
int days) int days)
{ {
// Arrange // Arrange
var account = GetAccount(exchange); var account = GetAccount(exchange);
var superTrendStrategy = new SuperTrendIndicator("unittest", 10, 3); var superTrendStrategy = new SuperTrendIndicatorBase("unittest", 10, 3);
var candles = _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(days), timeframe).Result; var candles = await _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(days), timeframe);
var resultSignal = new List<LightSignal>(); var resultSignal = new List<LightSignal>();
// Act // Act
foreach (var candle in candles) foreach (var candle in candles)
{ {
superTrendStrategy.Candles.Enqueue(candle); var signals = superTrendStrategy.Run(new HashSet<Candle> { candle });
var signals = superTrendStrategy.Run();
} }
if (superTrendStrategy.Signals != null && superTrendStrategy.Signals.Count > 0) if (superTrendStrategy.Signals != null && superTrendStrategy.Signals.Count > 0)
@@ -133,21 +132,20 @@ namespace Managing.Application.Tests
[Theory] [Theory]
[InlineData(TradingExchanges.Ftx, Ticker.ADA, Timeframe.OneDay, -500)] [InlineData(TradingExchanges.Ftx, Ticker.ADA, Timeframe.OneDay, -500)]
public void Shoud_Return_Signal_On_ChandelierExist(TradingExchanges exchange, Ticker ticker, public async Task Shoud_Return_Signal_On_ChandelierExist(TradingExchanges exchange, Ticker ticker,
Timeframe timeframe, int days) Timeframe timeframe, int days)
{ {
// Arrange // Arrange
var account = GetAccount(exchange); var account = GetAccount(exchange);
var chandelierExitStrategy = new ChandelierExitIndicator("unittest", 22, 3); var chandelierExitStrategy = new ChandelierExitIndicatorBase("unittest", 22, 3);
var candles = _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(days), timeframe, false) var candles =
.Result; await _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(days), timeframe, false);
var resultSignal = new List<LightSignal>(); var resultSignal = new List<LightSignal>();
// Act // Act
foreach (var candle in candles) foreach (var candle in candles)
{ {
chandelierExitStrategy.Candles.Enqueue(candle); var signals = chandelierExitStrategy.Run(new HashSet<Candle> { candle });
var signals = chandelierExitStrategy.Run();
} }
if (chandelierExitStrategy.Signals is { Count: > 0 }) if (chandelierExitStrategy.Signals is { Count: > 0 })
@@ -161,20 +159,19 @@ namespace Managing.Application.Tests
[Theory] [Theory]
[InlineData(TradingExchanges.Ftx, Ticker.ADA, Timeframe.OneDay, -500)] [InlineData(TradingExchanges.Ftx, Ticker.ADA, Timeframe.OneDay, -500)]
public void Shoud_Return_Signal_On_EmaTrend(TradingExchanges exchange, Ticker ticker, Timeframe timeframe, public async Task Shoud_Return_Signal_On_EmaTrend(TradingExchanges exchange, Ticker ticker, Timeframe timeframe,
int days) int days)
{ {
// Arrange // Arrange
var account = GetAccount(exchange); var account = GetAccount(exchange);
var emaTrendSrategy = new EmaTrendIndicator("unittest", 200); var emaTrendSrategy = new EmaTrendIndicatorBase("unittest", 200);
var candles = _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(days), timeframe).Result; var candles = await _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(days), timeframe);
var resultSignal = new List<LightSignal>(); var resultSignal = new List<LightSignal>();
// Act // Act
foreach (var candle in candles) foreach (var candle in candles)
{ {
emaTrendSrategy.Candles.Enqueue(candle); var signals = emaTrendSrategy.Run(new HashSet<Candle> { candle });
var signals = emaTrendSrategy.Run();
} }
if (emaTrendSrategy.Signals != null && emaTrendSrategy.Signals.Count > 0) if (emaTrendSrategy.Signals != null && emaTrendSrategy.Signals.Count > 0)
@@ -189,13 +186,13 @@ namespace Managing.Application.Tests
[Theory] [Theory]
[InlineData(TradingExchanges.Evm, Ticker.BTC, Timeframe.FifteenMinutes, -50)] [InlineData(TradingExchanges.Evm, Ticker.BTC, Timeframe.FifteenMinutes, -50)]
public void Shoud_Return_Signal_On_StochRsi(TradingExchanges exchange, Ticker ticker, Timeframe timeframe, public async Task Shoud_Return_Signal_On_StochRsi(TradingExchanges exchange, Ticker ticker, Timeframe timeframe,
int days) int days)
{ {
// Arrange // Arrange
var account = GetAccount(exchange); var account = GetAccount(exchange);
var stochRsiStrategy = new StochRsiTrendIndicator("unittest", 14, 14, 3, 1); var stochRsiStrategy = new StochRsiTrendIndicatorBase("unittest", 14, 14, 3, 1);
var candles = _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(days), timeframe).Result; var candles = await _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(days), timeframe);
var resultSignal = new List<LightSignal>(); var resultSignal = new List<LightSignal>();
// var json = JsonConvert.SerializeObject(candles); // var json = JsonConvert.SerializeObject(candles);
@@ -205,8 +202,7 @@ namespace Managing.Application.Tests
// Act // Act
foreach (var candle in candles) foreach (var candle in candles)
{ {
stochRsiStrategy.Candles.Enqueue(candle); var signals = stochRsiStrategy.Run(new HashSet<Candle> { candle });
var signals = stochRsiStrategy.Run();
} }
if (stochRsiStrategy.Signals != null && stochRsiStrategy.Signals.Count > 0) if (stochRsiStrategy.Signals != null && stochRsiStrategy.Signals.Count > 0)

View File

@@ -8,15 +8,15 @@
</PropertyGroup> </PropertyGroup>
<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" />
<PackageReference Include="MSTest.TestAdapter" Version="3.3.1"/> <PackageReference Include="MSTest.TestAdapter" Version="3.3.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.3.1"/> <PackageReference Include="MSTest.TestFramework" Version="3.3.1" />
<PackageReference Include="xunit" Version="2.8.0"/> <PackageReference Include="xunit" Version="2.8.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.0"> <PackageReference Include="xunit.runner.visualstudio" Version="2.8.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -24,17 +24,17 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Managing.Api\Managing.Api.csproj"/> <ProjectReference Include="..\Managing.Api\Managing.Api.csproj" />
<ProjectReference Include="..\Managing.Application\Managing.Application.csproj"/> <ProjectReference Include="..\Managing.Application\Managing.Application.csproj" />
<ProjectReference Include="..\Managing.Common\Managing.Common.csproj"/> <ProjectReference Include="..\Managing.Common\Managing.Common.csproj" />
<ProjectReference Include="..\Managing.Core\Managing.Core.csproj"/> <ProjectReference Include="..\Managing.Core\Managing.Core.csproj" />
<ProjectReference Include="..\Managing.Domain\Managing.Domain.csproj"/> <ProjectReference Include="..\Managing.Domain\Managing.Domain.csproj" />
<ProjectReference Include="..\Managing.Infrastructure.Exchanges\Managing.Infrastructure.Exchanges.csproj"/> <ProjectReference Include="..\Managing.Infrastructure.Exchanges\Managing.Infrastructure.Exchanges.csproj" />
<ProjectReference Include="..\Managing.Infrastructure.Tests\Managing.Infrastructure.Tests.csproj"/> <ProjectReference Include="..\Managing.Infrastructure.Tests\Managing.Infrastructure.Tests.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Data\"/> <Folder Include="Data\" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -1,7 +1,8 @@
using Managing.Application.Trading; using Managing.Application.Trading.Commands;
using Managing.Application.Trading.Commands; using Managing.Application.Trading.Handlers;
using Managing.Domain.Trades; using Managing.Domain.Trades;
using Managing.Domain.Users; using Managing.Domain.Users;
using Microsoft.Extensions.DependencyInjection;
using Moq; using Moq;
using Xunit; using Xunit;
using static Managing.Common.Enums; using static Managing.Common.Enums;
@@ -45,19 +46,22 @@ public class PositionTests : BaseTests
// _ = new GetAccountPositioqwnInfoListOutputDTO().DecodeOutput(hexPositions).d // _ = new GetAccountPositioqwnInfoListOutputDTO().DecodeOutput(hexPositions).d
// //
var openTrade = await _exchangeService.GetTrade(_account, "", Ticker.GMX); var openTrade = await _exchangeService.GetTrade(_account, "", Ticker.GMX);
var position = new Position("", "", TradeDirection.Long, Ticker.GMX, MoneyManagement, PositionInitiator.User, var position = new Position(Guid.NewGuid(), "", TradeDirection.Long, Ticker.GMX, MoneyManagement,
PositionInitiator.User,
DateTime.UtcNow, new User()) DateTime.UtcNow, new User())
{ {
Open = openTrade Open = openTrade
}; };
var command = new ClosePositionCommand(position); var command = new ClosePositionCommand(position);
_ = _tradingService.Setup(m => m.GetPositionByIdentifierAsync(It.IsAny<string>())).ReturnsAsync(position); _ = _tradingService.Setup(m => m.GetPositionByIdentifierAsync(It.IsAny<Guid>())).ReturnsAsync(position);
_ = _tradingService.Setup(m => m.GetPositionByIdentifierAsync(It.IsAny<string>())).ReturnsAsync(position); _ = _tradingService.Setup(m => m.GetPositionByIdentifierAsync(It.IsAny<Guid>())).ReturnsAsync(position);
var mockScope = new Mock<IServiceScopeFactory>();
var handler = new ClosePositionCommandHandler( var handler = new ClosePositionCommandHandler(
_exchangeService, _exchangeService,
_accountService.Object, _accountService.Object,
_tradingService.Object); _tradingService.Object,
mockScope.Object);
var closedPosition = await handler.Handle(command); var closedPosition = await handler.Handle(command);
Assert.NotNull(closedPosition); Assert.NotNull(closedPosition);

View File

@@ -214,7 +214,7 @@ namespace Managing.Application.Tests
private static Position GetFakeShortPosition() private static Position GetFakeShortPosition()
{ {
return new Position("", "FakeAccount", TradeDirection.Short, Ticker.BTC, null, return new Position(Guid.NewGuid(), "FakeAccount", TradeDirection.Short, Ticker.BTC, null,
PositionInitiator.PaperTrading, DateTime.UtcNow, new User()) PositionInitiator.PaperTrading, DateTime.UtcNow, new User())
{ {
Open = new Trade(DateTime.Now, TradeDirection.Short, TradeStatus.Filled, Open = new Trade(DateTime.Now, TradeDirection.Short, TradeStatus.Filled,
@@ -230,7 +230,7 @@ namespace Managing.Application.Tests
private static Position GetSolanaLongPosition() private static Position GetSolanaLongPosition()
{ {
return new Position("", "FakeAccount", TradeDirection.Long, Ticker.BTC, null, return new Position(Guid.NewGuid(), "FakeAccount", TradeDirection.Long, Ticker.BTC, null,
PositionInitiator.PaperTrading, DateTime.UtcNow, new User()) PositionInitiator.PaperTrading, DateTime.UtcNow, new User())
{ {
Open = new Trade(DateTime.Now, TradeDirection.Long, TradeStatus.Filled, Open = new Trade(DateTime.Now, TradeDirection.Long, TradeStatus.Filled,
@@ -250,7 +250,7 @@ namespace Managing.Application.Tests
private static Position GetFakeLongPosition() private static Position GetFakeLongPosition()
{ {
return new Position("", "FakeAccount", TradeDirection.Long, Ticker.BTC, null, return new Position(Guid.NewGuid(), "FakeAccount", TradeDirection.Long, Ticker.BTC, null,
PositionInitiator.PaperTrading, DateTime.UtcNow, new User()) PositionInitiator.PaperTrading, DateTime.UtcNow, new User())
{ {
Open = new Trade(DateTime.Now, TradeDirection.Short, TradeStatus.Filled, Open = new Trade(DateTime.Now, TradeDirection.Short, TradeStatus.Filled,

View File

@@ -1,23 +1,21 @@
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.Backtests;
using Managing.Application.Bots; using Managing.Application.Bots;
using Managing.Infrastructure.Databases; using Managing.Infrastructure.Databases;
using Managing.Infrastructure.Databases.InfluxDb; 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>();
} }

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