Compare commits
71 Commits
20b0881084
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 955c358138 | |||
| 750f6cebbb | |||
| 14f5cb0971 | |||
| 7271889bdf | |||
| 3dbd2e91ea | |||
| 4ff2ccdae3 | |||
| 7923b38a26 | |||
| 2861a7f469 | |||
| 6df6061d66 | |||
| eeb2923646 | |||
| d2975be0f5 | |||
| 9841219e8b | |||
| ae4d5b8abe | |||
| 6ade009901 | |||
| 137444a045 | |||
| e73af1dd3a | |||
| 994fd5d9a6 | |||
| 8315c36f30 | |||
| ece75b1973 | |||
| 513f880243 | |||
| b4f6dc871b | |||
| 4292e9e02f | |||
| 289fd25dc3 | |||
| b178f15beb | |||
| cd93dede4e | |||
| f58d1cea3b | |||
| 0966eace58 | |||
| 54bf914e95 | |||
| 8eefab4597 | |||
| b4a4656b3b | |||
| 7528405845 | |||
| 0a4a4e1398 | |||
| e6c3ec139a | |||
| 2622da05e6 | |||
| 580ce4d9c9 | |||
| 8d37b04d3f | |||
| 4a45d6c970 | |||
| 345d76e06f | |||
| cfb04e9dc9 | |||
| 0a2b7aa335 | |||
| 6a2e4e81b1 | |||
| e4049045c3 | |||
| aacb92018f | |||
| 9d0c7cf834 | |||
| 46a6cdcd87 | |||
| b1c1c8725d | |||
| a0bd2e2100 | |||
| 93502ca7cc | |||
| 93a6f9fd9e | |||
| b70018ba15 | |||
| 5dcb5c318e | |||
| ea85d8d519 | |||
| 36529ae403 | |||
| 2dcbcc3ef2 | |||
| 0c8c3de807 | |||
| 3d3f71ac7a | |||
| 7d92031059 | |||
| 843239d187 | |||
| 6c63b80f4a | |||
| eaf18189e4 | |||
| 4d63b9e970 | |||
| 2f1abb3f05 | |||
| 05d44d0c25 | |||
| 434f61f2de | |||
|
|
082ae8714b | ||
| cd378587aa | |||
| 5fabfbfadd | |||
| 857ca348ba | |||
| 6cd28a4edb | |||
| c454e87d7a | |||
| 4b0da0e864 |
@@ -94,4 +94,5 @@ Key Principles
|
||||
- After finishing the editing, build the project
|
||||
- you have to pass from controller -> application -> repository, do not inject repository inside controllers
|
||||
- dont use command line to edit file, use agent mode capabilities to do it
|
||||
- when dividing, make sure variable is not zero
|
||||
|
||||
|
||||
4
.github/workflows/dotnet.yml
vendored
4
.github/workflows/dotnet.yml
vendored
@@ -24,7 +24,3 @@ jobs:
|
||||
run: dotnet restore ./src/Managing.Api/Managing.Api.csproj
|
||||
- name: Build API
|
||||
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
|
||||
|
||||
114
WORKER_CONSOLIDATION_SUMMARY.md
Normal file
114
WORKER_CONSOLIDATION_SUMMARY.md
Normal 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.
|
||||
@@ -107,22 +107,6 @@
|
||||
- [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
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
```
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"dockerfilePath": "./src/Managing.Pinky/Dockerfile-pinky"
|
||||
}
|
||||
@@ -3,11 +3,8 @@
|
||||
# Navigate to the src directory
|
||||
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
|
||||
|
||||
# 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
|
||||
docker compose -f Managing.Docker/docker-compose.yml -f Managing.Docker/docker-compose.local.yml up -d
|
||||
@@ -1,5 +1,4 @@
|
||||
cd ..
|
||||
cd .\src\
|
||||
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
|
||||
@@ -1,5 +1,4 @@
|
||||
cd ..
|
||||
cd .\src\
|
||||
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
|
||||
@@ -2,21 +2,16 @@ cd ..
|
||||
cd .\src\
|
||||
ECHO "Stopping containers..."
|
||||
docker stop sandbox-managing.api-1
|
||||
docker stop sandbox-managing.api.workers-1
|
||||
ECHO "Contaiters stopped"
|
||||
ECHO "Removing containers..."
|
||||
docker rm sandbox-managing.api-1
|
||||
docker rm sandbox-managing.api.workers-1
|
||||
ECHO "Containers removed"
|
||||
ECHO "Removing images..."
|
||||
docker rmi managing.api
|
||||
docker rmi managing.api:latest
|
||||
docker rmi managing.api.workers
|
||||
docker rmi managing.api.workers:latest
|
||||
ECHO "Images removed"
|
||||
ECHO "Building images..."
|
||||
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..."
|
||||
docker-compose -f ./Managing.Docker/docker-compose.yml -f ./Managing.Docker/docker-compose.sandbox.yml up -d
|
||||
ECHO "Deployed"
|
||||
@@ -18,7 +18,7 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||
LOGS_DIR="$SCRIPT_DIR/$LOGS_DIR_NAME"
|
||||
mkdir -p "$LOGS_DIR" || { echo "Failed to create logs directory: $LOGS_DIR"; exit 1; }
|
||||
|
||||
LOG_FILE="./logs/migration_${ENVIRONMENT}_${TIMESTAMP}.log"
|
||||
LOG_FILE="$SCRIPT_DIR/logs/migration_${ENVIRONMENT}_${TIMESTAMP}.log"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
@@ -155,6 +155,12 @@ extract_connection_details() {
|
||||
log "📋 Extracted connection details: $DB_HOST:$DB_PORT/$DB_NAME (user: $DB_USER, password: $DB_PASSWORD)"
|
||||
}
|
||||
|
||||
# Helper function to get the first migration name
|
||||
get_first_migration() {
|
||||
local first_migration=$(cd "$DB_PROJECT_PATH" && dotnet ef migrations list --no-build --startup-project "$API_PROJECT_PATH" | head -1 | awk '{print $1}')
|
||||
echo "$first_migration"
|
||||
}
|
||||
|
||||
# Helper function to test PostgreSQL connectivity
|
||||
test_postgres_connectivity() {
|
||||
if ! command -v psql >/dev/null 2>&1; then
|
||||
@@ -243,13 +249,6 @@ else
|
||||
error "❌ Failed to build Managing.Infrastructure.Database project"
|
||||
fi
|
||||
|
||||
log "🔧 Building Managing.Api project..."
|
||||
if (cd "$API_PROJECT_PATH" && dotnet build); then
|
||||
log "✅ Managing.Api project built successfully"
|
||||
else
|
||||
error "❌ Failed to build Managing.Api project"
|
||||
fi
|
||||
|
||||
# Step 1: Check Database Connection and Create if Needed
|
||||
log "🔧 Step 1: Checking database connection and creating database if needed..."
|
||||
|
||||
@@ -417,13 +416,23 @@ else
|
||||
error " This is critical. Please review the previous error messages and your connection string for '$ENVIRONMENT'."
|
||||
fi
|
||||
|
||||
# Step 2: Create Backup
|
||||
log "📦 Step 2: Creating database backup using pg_dump..."
|
||||
# Step 2: Create database backup (only if database exists)
|
||||
log "📦 Step 2: Checking if database backup is needed..."
|
||||
|
||||
# Define the actual backup file path (absolute)
|
||||
BACKUP_FILE="$BACKUP_DIR/managing_${ENVIRONMENT}_backup_${TIMESTAMP}.sql"
|
||||
# Backup file display path (relative to script execution)
|
||||
BACKUP_FILE_DISPLAY="$BACKUP_DIR_NAME/$ENVIRONMENT/managing_${ENVIRONMENT}_backup_${TIMESTAMP}.sql"
|
||||
# Check if the target database exists
|
||||
DB_EXISTS=false
|
||||
if PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "postgres" -c "SELECT 1 FROM pg_database WHERE datname='$DB_NAME';" 2>/dev/null | grep -q "1 row"; then
|
||||
DB_EXISTS=true
|
||||
log "✅ Target database '$DB_NAME' exists - proceeding with backup"
|
||||
else
|
||||
log "ℹ️ Target database '$DB_NAME' does not exist - skipping backup"
|
||||
fi
|
||||
|
||||
if [ "$DB_EXISTS" = "true" ]; then
|
||||
# Define the actual backup file path (absolute)
|
||||
BACKUP_FILE="$BACKUP_DIR/managing_${ENVIRONMENT}_backup_${TIMESTAMP}.sql"
|
||||
# Backup file display path (relative to script execution)
|
||||
BACKUP_FILE_DISPLAY="$BACKUP_DIR_NAME/$ENVIRONMENT/managing_${ENVIRONMENT}_backup_${TIMESTAMP}.sql"
|
||||
|
||||
# Create backup with retry logic
|
||||
BACKUP_SUCCESS=false
|
||||
@@ -439,11 +448,84 @@ for attempt in 1 2 3; do
|
||||
else
|
||||
# If pg_dump fails, fall back to EF Core migration script
|
||||
warn "⚠️ pg_dump failed, falling back to EF Core migration script..."
|
||||
|
||||
# Get the first migration name to generate complete script
|
||||
FIRST_MIGRATION=$(get_first_migration)
|
||||
|
||||
if [ -n "$FIRST_MIGRATION" ]; then
|
||||
log "📋 Generating complete backup script from initial migration: $FIRST_MIGRATION"
|
||||
if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --from "$FIRST_MIGRATION" --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE"); then
|
||||
log "✅ Complete EF Core Migration SQL Script generated: $BACKUP_FILE_DISPLAY"
|
||||
BACKUP_SUCCESS=true
|
||||
break
|
||||
else
|
||||
# Try fallback without specifying from migration
|
||||
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE") 2>&1 || true)
|
||||
if [ $attempt -lt 3 ]; then
|
||||
warn "⚠️ Backup attempt $attempt failed. Retrying in 5 seconds..."
|
||||
warn " EF CLI Output: $ERROR_OUTPUT"
|
||||
sleep 5
|
||||
else
|
||||
error "❌ Database backup failed after 3 attempts."
|
||||
error " EF CLI Output: $ERROR_OUTPUT"
|
||||
error " Migration aborted for safety reasons."
|
||||
fi
|
||||
fi
|
||||
else
|
||||
# Fallback: generate script without specifying from migration
|
||||
if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE"); then
|
||||
log "✅ EF Core Migration SQL Script generated: $BACKUP_FILE_DISPLAY"
|
||||
BACKUP_SUCCESS=true
|
||||
break
|
||||
else
|
||||
# Try fallback without specifying from migration
|
||||
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE") 2>&1 || true)
|
||||
if [ $attempt -lt 3 ]; then
|
||||
warn "⚠️ Backup attempt $attempt failed. Retrying in 5 seconds..."
|
||||
warn " EF CLI Output: $ERROR_OUTPUT"
|
||||
sleep 5
|
||||
else
|
||||
error "❌ Database backup failed after 3 attempts."
|
||||
error " EF CLI Output: $ERROR_OUTPUT"
|
||||
error " Migration aborted for safety reasons."
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
else
|
||||
# If pg_dump is not available, use EF Core migration script
|
||||
warn "⚠️ pg_dump not available, using EF Core migration script for backup..."
|
||||
|
||||
# Get the first migration name to generate complete script
|
||||
FIRST_MIGRATION=$(get_first_migration)
|
||||
|
||||
if [ -n "$FIRST_MIGRATION" ]; then
|
||||
log "📋 Generating complete backup script from initial migration: $FIRST_MIGRATION"
|
||||
if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --from "$FIRST_MIGRATION" --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE"); then
|
||||
log "✅ Complete EF Core Migration SQL Script generated: $BACKUP_FILE_DISPLAY"
|
||||
BACKUP_SUCCESS=true
|
||||
break
|
||||
else
|
||||
# Try fallback without specifying from migration
|
||||
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE") 2>&1 || true)
|
||||
if [ $attempt -lt 3 ]; then
|
||||
warn "⚠️ Backup attempt $attempt failed. Retrying in 5 seconds..."
|
||||
warn " EF CLI Output: $ERROR_OUTPUT"
|
||||
sleep 5
|
||||
else
|
||||
error "❌ Database backup failed after 3 attempts."
|
||||
error " EF CLI Output: $ERROR_OUTPUT"
|
||||
error " Migration aborted for safety reasons."
|
||||
fi
|
||||
fi
|
||||
else
|
||||
# Fallback: generate script without specifying from migration
|
||||
if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE"); then
|
||||
log "✅ EF Core Migration SQL Script generated: $BACKUP_FILE_DISPLAY"
|
||||
BACKUP_SUCCESS=true
|
||||
break
|
||||
else
|
||||
# Try fallback without specifying from migration
|
||||
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE") 2>&1 || true)
|
||||
if [ $attempt -lt 3 ]; then
|
||||
warn "⚠️ Backup attempt $attempt failed. Retrying in 5 seconds..."
|
||||
@@ -456,33 +538,69 @@ for attempt in 1 2 3; do
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
else
|
||||
# If pg_dump is not available, use EF Core migration script
|
||||
warn "⚠️ pg_dump not available, using EF Core migration script for backup..."
|
||||
if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE"); then
|
||||
log "✅ EF Core Migration SQL Script generated: $BACKUP_FILE_DISPLAY"
|
||||
BACKUP_SUCCESS=true
|
||||
break
|
||||
else
|
||||
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE") 2>&1 || true)
|
||||
if [ $attempt -lt 3 ]; then
|
||||
warn "⚠️ Backup attempt $attempt failed. Retrying in 5 seconds..."
|
||||
warn " EF CLI Output: $ERROR_OUTPUT"
|
||||
sleep 5
|
||||
else
|
||||
error "❌ Database backup failed after 3 attempts."
|
||||
error " EF CLI Output: $ERROR_OUTPUT"
|
||||
error " Migration aborted for safety reasons."
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Check if backup was successful before proceeding
|
||||
if [ "$BACKUP_SUCCESS" != "true" ]; then
|
||||
error "❌ Database backup failed. Migration aborted for safety."
|
||||
error " Cannot proceed with migration without a valid backup."
|
||||
error " Please resolve backup issues and try again."
|
||||
# Check if backup was successful before proceeding
|
||||
if [ "$BACKUP_SUCCESS" != "true" ]; then
|
||||
error "❌ Database backup failed. Migration aborted for safety."
|
||||
error " Cannot proceed with migration without a valid backup."
|
||||
error " Please resolve backup issues and try again."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Step 2.5: Check for pending model changes and create migrations if needed
|
||||
log "🔍 Step 2.5: Checking for pending model changes..."
|
||||
|
||||
# Check if there are any pending model changes that need migrations
|
||||
PENDING_CHANGES_OUTPUT=$( (cd "$DB_PROJECT_PATH" && dotnet ef migrations add --dry-run --startup-project "$API_PROJECT_PATH" --name "PendingChanges_${TIMESTAMP}") 2>&1 || true )
|
||||
|
||||
if echo "$PENDING_CHANGES_OUTPUT" | grep -q "No pending model changes"; then
|
||||
log "✅ No pending model changes detected - existing migrations are up to date"
|
||||
else
|
||||
log "⚠️ Pending model changes detected that require new migrations"
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "📋 PENDING MODEL CHANGES DETECTED"
|
||||
echo "=========================================="
|
||||
echo "The following changes require new migrations:"
|
||||
echo "$PENDING_CHANGES_OUTPUT"
|
||||
echo ""
|
||||
echo "Would you like to create a new migration now?"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
read -p "🔧 Create new migration? (y/n): " create_migration
|
||||
|
||||
if [[ "$create_migration" =~ ^[Yy]$ ]]; then
|
||||
log "📝 Creating new migration..."
|
||||
|
||||
# Get migration name from user
|
||||
read -p "📝 Enter migration name (or press Enter for auto-generated name): " migration_name
|
||||
if [ -z "$migration_name" ]; then
|
||||
migration_name="Migration_${TIMESTAMP}"
|
||||
fi
|
||||
|
||||
# Create the migration
|
||||
if (cd "$DB_PROJECT_PATH" && dotnet ef migrations add "$migration_name" --startup-project "$API_PROJECT_PATH"); then
|
||||
log "✅ Migration '$migration_name' created successfully"
|
||||
|
||||
# Show the created migration file
|
||||
LATEST_MIGRATION=$(find "$DB_PROJECT_PATH/Migrations" -name "*${migration_name}.cs" | head -1)
|
||||
if [ -n "$LATEST_MIGRATION" ]; then
|
||||
log "📄 Migration file created: $(basename "$LATEST_MIGRATION")"
|
||||
log " Location: $LATEST_MIGRATION"
|
||||
fi
|
||||
else
|
||||
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && dotnet ef migrations add "$migration_name" --startup-project "$API_PROJECT_PATH") 2>&1 || true )
|
||||
error "❌ Failed to create migration '$migration_name'"
|
||||
error " EF CLI Output: $ERROR_OUTPUT"
|
||||
error " Please resolve the model issues and try again."
|
||||
fi
|
||||
else
|
||||
log "⚠️ Skipping migration creation. Proceeding with existing migrations only."
|
||||
log " Note: If there are pending changes, the migration may fail."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Step 3: Run Migration (This effectively is a retry if previous "update" failed, or a final apply)
|
||||
@@ -507,8 +625,88 @@ fi
|
||||
# Generate migration script first (Microsoft recommended approach)
|
||||
MIGRATION_SCRIPT="$BACKUP_DIR/migration_${ENVIRONMENT}_${TIMESTAMP}.sql"
|
||||
log "📝 Step 3b: Generating migration script for pending migrations..."
|
||||
if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT"); then
|
||||
log "✅ Migration script generated: $(basename "$MIGRATION_SCRIPT")"
|
||||
|
||||
# Check if database is empty (no tables) to determine the best approach
|
||||
log "🔍 Checking if database has existing tables..."
|
||||
DB_HAS_TABLES=false
|
||||
if command -v psql >/dev/null 2>&1; then
|
||||
TABLE_COUNT=$(PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public';" 2>/dev/null | tr -d ' ' || echo "0")
|
||||
if [ "$TABLE_COUNT" -gt 0 ]; then
|
||||
DB_HAS_TABLES=true
|
||||
log "✅ Database has $TABLE_COUNT existing tables - using idempotent script generation"
|
||||
else
|
||||
log "⚠️ Database appears to be empty - using full migration script generation"
|
||||
fi
|
||||
else
|
||||
log "⚠️ psql not available - assuming database has tables and using idempotent script generation"
|
||||
DB_HAS_TABLES=true
|
||||
fi
|
||||
|
||||
# Generate migration script based on database state
|
||||
if [ "$DB_HAS_TABLES" = "true" ]; then
|
||||
# For databases with existing tables, we need to generate a complete script
|
||||
# that includes all migrations from the beginning
|
||||
log "📝 Generating complete migration script from initial migration..."
|
||||
|
||||
# Get the first migration name to generate script from the beginning
|
||||
FIRST_MIGRATION=$(get_first_migration)
|
||||
|
||||
if [ -n "$FIRST_MIGRATION" ]; then
|
||||
log "📋 Generating complete script for all migrations (idempotent)..."
|
||||
if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT"); then
|
||||
log "✅ Complete migration script generated (all migrations, idempotent): $(basename "$MIGRATION_SCRIPT")"
|
||||
else
|
||||
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT") 2>&1 || true )
|
||||
error "❌ Failed to generate complete migration script."
|
||||
error " EF CLI Output: $ERROR_OUTPUT"
|
||||
error " Check the .NET project logs for detailed errors."
|
||||
error " Backup script available at: $BACKUP_FILE_DISPLAY"
|
||||
fi
|
||||
else
|
||||
# Fallback: generate script without specifying from migration
|
||||
log "📝 Fallback: Generating migration script without specifying from migration..."
|
||||
if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT"); then
|
||||
log "✅ Migration script generated (idempotent): $(basename "$MIGRATION_SCRIPT")"
|
||||
else
|
||||
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT") 2>&1 || true )
|
||||
error "❌ Failed to generate idempotent migration script."
|
||||
error " EF CLI Output: $ERROR_OUTPUT"
|
||||
error " Check the .NET project logs for detailed errors."
|
||||
error " Backup script available at: $BACKUP_FILE_DISPLAY"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
# Use full script generation for empty databases (generate script from the very beginning)
|
||||
log "📝 Generating full migration script for empty database..."
|
||||
|
||||
# Get the first migration name to generate script from the beginning
|
||||
FIRST_MIGRATION=$(get_first_migration)
|
||||
|
||||
if [ -n "$FIRST_MIGRATION" ]; then
|
||||
log "📋 Generating complete script for all migrations..."
|
||||
if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT"); then
|
||||
log "✅ Complete migration script generated (all migrations): $(basename "$MIGRATION_SCRIPT")"
|
||||
else
|
||||
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT") 2>&1 || true )
|
||||
error "❌ Failed to generate complete migration script."
|
||||
error " EF CLI Output: $ERROR_OUTPUT"
|
||||
error " Check the .NET project logs for detailed errors."
|
||||
error " Backup script available at: $BACKUP_FILE_DISPLAY"
|
||||
fi
|
||||
else
|
||||
# Fallback: generate script without specifying from migration
|
||||
log "📝 Fallback: Generating migration script without specifying from migration..."
|
||||
if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT"); then
|
||||
log "✅ Migration script generated (fallback): $(basename "$MIGRATION_SCRIPT")"
|
||||
else
|
||||
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT") 2>&1 || true )
|
||||
error "❌ Failed to generate fallback migration script."
|
||||
error " EF CLI Output: $ERROR_OUTPUT"
|
||||
error " Check the .NET project logs for detailed errors."
|
||||
error " Backup script available at: $BACKUP_FILE_DISPLAY"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Show the migration script path to the user for review
|
||||
echo ""
|
||||
@@ -519,8 +717,26 @@ if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef mig
|
||||
echo "Environment: $ENVIRONMENT"
|
||||
echo "Database: $DB_HOST:$DB_PORT/$DB_NAME"
|
||||
echo ""
|
||||
|
||||
# Show a preview of the migration script content
|
||||
if [ -f "$MIGRATION_SCRIPT" ]; then
|
||||
SCRIPT_SIZE=$(wc -l < "$MIGRATION_SCRIPT")
|
||||
echo "📄 Migration script contains $SCRIPT_SIZE lines"
|
||||
|
||||
# Show first 20 lines as preview
|
||||
echo ""
|
||||
echo "📋 PREVIEW (first 20 lines):"
|
||||
echo "----------------------------------------"
|
||||
head -20 "$MIGRATION_SCRIPT" | sed 's/^/ /'
|
||||
if [ "$SCRIPT_SIZE" -gt 20 ]; then
|
||||
echo " ... (showing first 20 lines of $SCRIPT_SIZE total)"
|
||||
fi
|
||||
echo "----------------------------------------"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo "⚠️ IMPORTANT: Please review the migration script before proceeding!"
|
||||
echo " You can examine the script with: cat $MIGRATION_SCRIPT"
|
||||
echo " You can examine the full script with: cat $MIGRATION_SCRIPT"
|
||||
echo " Or open it in your editor to review the changes."
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
@@ -564,16 +780,15 @@ if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef mig
|
||||
fi
|
||||
fi
|
||||
|
||||
# Clean up migration script after successful application
|
||||
rm -f "$MIGRATION_SCRIPT"
|
||||
# Save a copy of the migration script for reference before cleaning up
|
||||
MIGRATION_SCRIPT_COPY="$BACKUP_DIR/migration_${ENVIRONMENT}_${TIMESTAMP}_applied.sql"
|
||||
if [ -f "$MIGRATION_SCRIPT" ]; then
|
||||
cp "$MIGRATION_SCRIPT" "$MIGRATION_SCRIPT_COPY"
|
||||
log "📝 Migration script saved for reference: $(basename "$MIGRATION_SCRIPT_COPY")"
|
||||
fi
|
||||
|
||||
else
|
||||
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT") 2>&1 || true )
|
||||
error "❌ Failed to generate migration script."
|
||||
error " EF CLI Output: $ERROR_OUTPUT"
|
||||
error " Check the .NET project logs for detailed errors."
|
||||
error " Backup script available at: $BACKUP_FILE_DISPLAY"
|
||||
fi
|
||||
# Clean up temporary migration script after successful application
|
||||
rm -f "$MIGRATION_SCRIPT"
|
||||
|
||||
# Step 4: Verify Migration
|
||||
log "🔍 Step 4: Verifying migration status..."
|
||||
|
||||
@@ -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.Application.Abstractions/Managing.Application.Abstractions.csproj", "Managing.Application.Abstractions/"]
|
||||
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.Exchanges/Managing.Infrastructure.Exchanges.csproj", "Managing.Infrastructure.Exchanges/"]
|
||||
COPY ["/src/Managing.Infrastructure.Database/Managing.Infrastructure.Databases.csproj", "Managing.Infrastructure.Database/"]
|
||||
|
||||
@@ -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.Application.Abstractions/Managing.Application.Abstractions.csproj", "Managing.Application.Abstractions/"]
|
||||
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.Exchanges/Managing.Infrastructure.Exchanges.csproj", "Managing.Infrastructure.Exchanges/"]
|
||||
COPY ["/src/Managing.Infrastructure.Database/Managing.Infrastructure.Databases.csproj", "Managing.Infrastructure.Database/"]
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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": "*"
|
||||
}
|
||||
@@ -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": "*"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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": "*"
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"PostgreSql": {
|
||||
"ConnectionString": "Host=managing-postgre.apps.managing.live;Port=5432;Database=managing;Username=postgres;Password=29032b13a5bc4d37"
|
||||
},
|
||||
"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": "*"
|
||||
}
|
||||
@@ -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": "*"
|
||||
}
|
||||
@@ -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": "*"
|
||||
}
|
||||
80
src/Managing.Api/ADMIN_FEATURE.md
Normal file
80
src/Managing.Api/ADMIN_FEATURE.md
Normal 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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -340,7 +340,7 @@ public class BacktestController : BaseController
|
||||
// Convert IndicatorRequest objects to Indicator domain objects
|
||||
foreach (var indicatorRequest in request.Config.Scenario.Indicators)
|
||||
{
|
||||
var indicator = new Indicator(indicatorRequest.Name, indicatorRequest.Type)
|
||||
var indicator = new IndicatorBase(indicatorRequest.Name, indicatorRequest.Type)
|
||||
{
|
||||
SignalType = indicatorRequest.SignalType,
|
||||
MinimumHistory = indicatorRequest.MinimumHistory,
|
||||
@@ -706,7 +706,6 @@ public class BacktestController : BaseController
|
||||
}
|
||||
|
||||
|
||||
|
||||
public MoneyManagement Map(MoneyManagementRequest moneyManagementRequest)
|
||||
{
|
||||
return new MoneyManagement
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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.Application.Abstractions;
|
||||
using Managing.Application.Abstractions.Grains;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.Hubs;
|
||||
using Managing.Application.ManageBot.Commands;
|
||||
@@ -8,7 +9,6 @@ using Managing.Domain.Backtests;
|
||||
using Managing.Domain.Bots;
|
||||
using Managing.Domain.Candles;
|
||||
using Managing.Domain.Scenarios;
|
||||
using Managing.Domain.Shared.Helpers;
|
||||
using Managing.Domain.Statistics;
|
||||
using Managing.Domain.Strategies;
|
||||
using Managing.Domain.Strategies.Base;
|
||||
@@ -25,6 +25,7 @@ namespace Managing.Api.Controllers;
|
||||
/// Controller for handling data-related operations such as retrieving tickers, spotlight data, and candles.
|
||||
/// Requires authorization for access.
|
||||
/// </summary>
|
||||
[AllowAnonymous]
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
public class DataController : ControllerBase
|
||||
@@ -33,9 +34,11 @@ public class DataController : ControllerBase
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly ICacheService _cacheService;
|
||||
private readonly IStatisticService _statisticService;
|
||||
private readonly IAgentService _agentService;
|
||||
private readonly IHubContext<CandleHub> _hubContext;
|
||||
private readonly IMediator _mediator;
|
||||
private readonly ITradingService _tradingService;
|
||||
private readonly IGrainFactory _grainFactory;
|
||||
|
||||
/// <summary>
|
||||
/// 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="mediator">Mediator for handling commands and queries.</param>
|
||||
/// <param name="tradingService">Service for trading operations.</param>
|
||||
/// <param name="grainFactory">Orleans grain factory for accessing grains.</param>
|
||||
public DataController(
|
||||
IExchangeService exchangeService,
|
||||
IAccountService accountService,
|
||||
ICacheService cacheService,
|
||||
IStatisticService statisticService,
|
||||
IAgentService agentService,
|
||||
IHubContext<CandleHub> hubContext,
|
||||
IMediator mediator,
|
||||
ITradingService tradingService)
|
||||
ITradingService tradingService,
|
||||
IGrainFactory grainFactory)
|
||||
{
|
||||
_exchangeService = exchangeService;
|
||||
_accountService = accountService;
|
||||
_cacheService = cacheService;
|
||||
_statisticService = statisticService;
|
||||
_agentService = agentService;
|
||||
_hubContext = hubContext;
|
||||
_mediator = mediator;
|
||||
_tradingService = tradingService;
|
||||
_grainFactory = grainFactory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -244,7 +252,7 @@ public class DataController : ControllerBase
|
||||
{
|
||||
return Ok(new CandlesWithIndicatorsResponse
|
||||
{
|
||||
Candles = new List<Candle>(),
|
||||
Candles = new HashSet<Candle>(),
|
||||
IndicatorsValues = new Dictionary<IndicatorType, IndicatorsResultBase>()
|
||||
});
|
||||
}
|
||||
@@ -290,8 +298,8 @@ public class DataController : ControllerBase
|
||||
}
|
||||
|
||||
// Get active bots
|
||||
var activeBots = await _mediator.Send(new GetActiveBotsCommand());
|
||||
var currentCount = activeBots.Count;
|
||||
var activeBots = await _mediator.Send(new GetBotsByStatusCommand(BotStatus.Running));
|
||||
var currentCount = activeBots.Count();
|
||||
|
||||
// Get previous count from cache
|
||||
var previousCount = _cacheService.GetValue<int>(previousCountKey);
|
||||
@@ -332,22 +340,12 @@ public class DataController : ControllerBase
|
||||
[HttpGet("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
|
||||
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
|
||||
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)
|
||||
.Take(3)
|
||||
.ToList();
|
||||
@@ -359,17 +357,92 @@ public class DataController : ControllerBase
|
||||
.Select(item => new StrategyPerformance
|
||||
{
|
||||
StrategyName = item.Bot.Name,
|
||||
PnL = item.PnL
|
||||
PnL = item.PnL,
|
||||
AgentName = item.agentName,
|
||||
})
|
||||
.ToList()
|
||||
};
|
||||
|
||||
// Cache the result for 10 minutes
|
||||
_cacheService.SaveValue(cacheKey, topStrategies, TimeSpan.FromMinutes(10));
|
||||
|
||||
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>
|
||||
/// Retrieves list of the active strategies for a user with detailed information
|
||||
/// </summary>
|
||||
@@ -378,24 +451,18 @@ public class DataController : ControllerBase
|
||||
[HttpGet("GetUserStrategies")]
|
||||
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
|
||||
var userStrategies = await _mediator.Send(new GetUserStrategiesCommand(agentName));
|
||||
|
||||
// Convert to detailed view model with additional information
|
||||
var result = userStrategies.Select(strategy => MapStrategyToViewModel(strategy)).ToList();
|
||||
// Get all positions for all strategies in a single database call to avoid DbContext concurrency issues
|
||||
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
|
||||
_cacheService.SaveValue(cacheKey, result, TimeSpan.FromMinutes(5));
|
||||
// Convert to detailed view model with additional information
|
||||
var result = userStrategies.Select(strategy => MapStrategyToViewModel(strategy, positionsByIdentifier))
|
||||
.ToList();
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
@@ -409,16 +476,6 @@ public class DataController : ControllerBase
|
||||
[HttpGet("GetUserStrategy")]
|
||||
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
|
||||
var strategy = await _mediator.Send(new GetUserStrategyCommand(agentName, strategyName));
|
||||
|
||||
@@ -428,240 +485,234 @@ public class DataController : ControllerBase
|
||||
}
|
||||
|
||||
// Map the strategy to a view model using the shared method
|
||||
var result = MapStrategyToViewModel(strategy);
|
||||
|
||||
// Cache the results for 5 minutes
|
||||
_cacheService.SaveValue(cacheKey, result, TimeSpan.FromMinutes(5));
|
||||
var result = await MapStrategyToViewModelAsync(strategy);
|
||||
|
||||
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>
|
||||
/// Maps a trading bot to a strategy view model with detailed statistics
|
||||
/// </summary>
|
||||
/// <param name="strategy">The trading bot to map</param>
|
||||
/// <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
|
||||
decimal pnl = strategy.GetProfitAndLoss();
|
||||
decimal pnl = strategy.Pnl;
|
||||
|
||||
// If we had initial investment amount, we could calculate ROI like:
|
||||
decimal initialInvestment = 1000; // Example placeholder, ideally should come from the account
|
||||
decimal roi = pnl != 0 ? (pnl / initialInvestment) * 100 : 0;
|
||||
|
||||
// Calculate volume statistics
|
||||
decimal totalVolume = TradingBox.GetTotalVolumeTraded(strategy.Positions);
|
||||
decimal volumeLast24h = TradingBox.GetLast24HVolumeTraded(strategy.Positions);
|
||||
decimal totalVolume = strategy.Volume;
|
||||
decimal volumeLast24h = strategy.Volume;
|
||||
|
||||
// Calculate win/loss statistics
|
||||
(int wins, int losses) = TradingBox.GetWinLossCount(strategy.Positions);
|
||||
(int wins, int losses) = (strategy.TradeWins, strategy.TradeLosses);
|
||||
|
||||
int winRate = wins + losses > 0 ? (wins * 100) / (wins + losses) : 0;
|
||||
// Calculate ROI for last 24h
|
||||
decimal roiLast24h = TradingBox.GetLast24HROI(strategy.Positions);
|
||||
decimal roiLast24h = strategy.Roi;
|
||||
|
||||
// Fetch positions associated with this bot
|
||||
var positions = await _tradingService.GetPositionsByInitiatorIdentifierAsync(strategy.Identifier);
|
||||
|
||||
return new UserStrategyDetailsViewModel
|
||||
{
|
||||
Name = strategy.Name,
|
||||
ScenarioName = strategy.Config.ScenarioName,
|
||||
State = strategy.GetStatus() == BotStatus.Up.ToString() ? "RUNNING" :
|
||||
strategy.GetStatus() == BotStatus.Down.ToString() ? "STOPPED" : "UNUSED",
|
||||
State = strategy.Status,
|
||||
PnL = pnl,
|
||||
ROIPercentage = roi,
|
||||
ROILast24H = roiLast24h,
|
||||
Runtime = startupTime,
|
||||
WinRate = strategy.GetWinRate(),
|
||||
Runtime = strategy.StartupTime,
|
||||
WinRate = winRate,
|
||||
TotalVolumeTraded = totalVolume,
|
||||
VolumeLast24H = volumeLast24h,
|
||||
Wins = wins,
|
||||
Losses = losses,
|
||||
Positions = strategy.Positions.OrderByDescending(p => p.Date)
|
||||
.ToList(), // Include sorted positions with most recent first
|
||||
Positions = positions.ToList(),
|
||||
Identifier = strategy.Identifier,
|
||||
WalletBalances = new Dictionary<DateTime, decimal>(),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a summary of platform activity across all agents (platform-level data only)
|
||||
/// Uses Orleans grain for efficient caching and real-time updates
|
||||
/// </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>
|
||||
[HttpGet("GetPlatformSummary")]
|
||||
public async Task<ActionResult<PlatformSummaryViewModel>> GetPlatformSummary(string timeFilter = "Total")
|
||||
public async Task<ActionResult<PlatformSummaryViewModel>> GetPlatformSummary()
|
||||
{
|
||||
// Validate time filter
|
||||
var validTimeFilters = new[] { "24H", "3D", "1W", "1M", "1Y", "Total" };
|
||||
if (!validTimeFilters.Contains(timeFilter))
|
||||
try
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
string cacheKey = $"PlatformSummary_{timeFilter}";
|
||||
|
||||
// Check if the platform summary is already cached
|
||||
var cachedSummary = _cacheService.GetValue<PlatformSummaryViewModel>(cacheKey);
|
||||
|
||||
if (cachedSummary != null)
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Ok(cachedSummary);
|
||||
// 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}");
|
||||
}
|
||||
|
||||
// Get all agents and their strategies
|
||||
var agentsWithStrategies = await _mediator.Send(new GetAllAgentsCommand(timeFilter));
|
||||
|
||||
// Create the platform summary
|
||||
var summary = new PlatformSummaryViewModel
|
||||
{
|
||||
TotalAgents = agentsWithStrategies.Count,
|
||||
TotalActiveStrategies = agentsWithStrategies.Values.Sum(list => list.Count),
|
||||
TimeFilter = timeFilter
|
||||
};
|
||||
|
||||
// Calculate total platform metrics
|
||||
decimal totalPlatformPnL = 0;
|
||||
decimal totalPlatformVolume = 0;
|
||||
decimal totalPlatformVolumeLast24h = 0;
|
||||
|
||||
// Calculate totals from all agents
|
||||
foreach (var agent in agentsWithStrategies)
|
||||
{
|
||||
var strategies = agent.Value;
|
||||
|
||||
if (strategies.Count == 0)
|
||||
{
|
||||
continue; // Skip agents with no strategies
|
||||
}
|
||||
|
||||
// Combine all positions from all strategies
|
||||
var allPositions = strategies.SelectMany<ITradingBot, Position>(s => s.Positions).ToList();
|
||||
|
||||
// Calculate agent metrics for platform totals
|
||||
decimal totalPnL = TradingBox.GetPnLInTimeRange(allPositions, timeFilter);
|
||||
decimal totalVolume = TradingBox.GetTotalVolumeTraded(allPositions);
|
||||
decimal volumeLast24h = TradingBox.GetLast24HVolumeTraded(allPositions);
|
||||
|
||||
// Add to platform totals
|
||||
totalPlatformPnL += totalPnL;
|
||||
totalPlatformVolume += totalVolume;
|
||||
totalPlatformVolumeLast24h += volumeLast24h;
|
||||
}
|
||||
|
||||
// Set the platform totals
|
||||
summary.TotalPlatformPnL = totalPlatformPnL;
|
||||
summary.TotalPlatformVolume = totalPlatformVolume;
|
||||
summary.TotalPlatformVolumeLast24h = totalPlatformVolumeLast24h;
|
||||
|
||||
// Cache the results for 5 minutes
|
||||
_cacheService.SaveValue(cacheKey, summary, TimeSpan.FromMinutes(5));
|
||||
|
||||
return Ok(summary);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a list of agent summaries for the agent index page
|
||||
/// Retrieves a paginated list of agent summaries for the agent index page
|
||||
/// </summary>
|
||||
/// <param name="timeFilter">Time filter to apply (24H, 3D, 1W, 1M, 1Y, Total)</param>
|
||||
/// <returns>A list of agent summaries sorted by performance</returns>
|
||||
[HttpGet("GetAgentIndex")]
|
||||
public async Task<ActionResult<AgentIndexViewModel>> GetAgentIndex(string timeFilter = "Total")
|
||||
/// <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 time filter
|
||||
var validTimeFilters = new[] { "24H", "3D", "1W", "1M", "1Y", "Total" };
|
||||
if (!validTimeFilters.Contains(timeFilter))
|
||||
// Validate pagination parameters
|
||||
if (page < 1)
|
||||
{
|
||||
timeFilter = "Total"; // Default to Total if invalid
|
||||
return BadRequest("Page must be greater than 0");
|
||||
}
|
||||
|
||||
string cacheKey = $"AgentIndex_{timeFilter}";
|
||||
|
||||
// Check if the agent index is already cached
|
||||
var cachedIndex = _cacheService.GetValue<AgentIndexViewModel>(cacheKey);
|
||||
|
||||
if (cachedIndex != null)
|
||||
if (pageSize < 1 || pageSize > 100)
|
||||
{
|
||||
return Ok(cachedIndex);
|
||||
return BadRequest("Page size must be between 1 and 100");
|
||||
}
|
||||
|
||||
// Get all agents and their strategies
|
||||
var agentsWithStrategies = await _mediator.Send(new GetAllAgentsCommand(timeFilter));
|
||||
|
||||
// Create the agent index response
|
||||
var agentIndex = new AgentIndexViewModel
|
||||
// Validate sort order
|
||||
if (sortOrder != "asc" && sortOrder != "desc")
|
||||
{
|
||||
TimeFilter = timeFilter
|
||||
};
|
||||
return BadRequest("Sort order must be 'asc' or 'desc'");
|
||||
}
|
||||
|
||||
// Create summaries for each agent
|
||||
foreach (var agent in agentsWithStrategies)
|
||||
// Parse agent names filter
|
||||
IEnumerable<string>? agentNamesList = null;
|
||||
if (!string.IsNullOrWhiteSpace(agentNames))
|
||||
{
|
||||
var user = agent.Key;
|
||||
var strategies = agent.Value;
|
||||
agentNamesList = agentNames.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(name => name.Trim())
|
||||
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
if (strategies.Count == 0)
|
||||
{
|
||||
continue; // Skip agents with no strategies
|
||||
}
|
||||
|
||||
// Combine all positions from all strategies
|
||||
var allPositions = strategies.SelectMany<ITradingBot, Position>(s => s.Positions).ToList();
|
||||
|
||||
// Calculate agent metrics
|
||||
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);
|
||||
// Get paginated results from database
|
||||
var command = new GetPaginatedAgentSummariesCommand(page, pageSize, sortBy, sortOrder, agentNamesList);
|
||||
var result = await _mediator.Send(command);
|
||||
var agentSummaries = result.Results;
|
||||
var totalCount = result.TotalCount;
|
||||
|
||||
// Map to view models
|
||||
var agentSummaryViewModels = new List<AgentSummaryViewModel>();
|
||||
foreach (var agentSummary in agentSummaries)
|
||||
{
|
||||
// Calculate win rate
|
||||
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
|
||||
var agentSummary = new AgentSummaryViewModel
|
||||
// Map to view model
|
||||
var agentSummaryViewModel = new AgentSummaryViewModel
|
||||
{
|
||||
AgentName = user.AgentName,
|
||||
TotalPnL = totalPnL,
|
||||
PnLLast24h = pnlLast24h,
|
||||
TotalROI = totalROI,
|
||||
ROILast24h = roiLast24h,
|
||||
Wins = wins,
|
||||
Losses = losses,
|
||||
AverageWinRate = averageWinRate,
|
||||
ActiveStrategiesCount = strategies.Count,
|
||||
TotalVolume = totalVolume,
|
||||
VolumeLast24h = volumeLast24h
|
||||
AgentName = agentSummary.AgentName,
|
||||
TotalPnL = agentSummary.TotalPnL,
|
||||
TotalROI = agentSummary.TotalROI,
|
||||
Wins = agentSummary.Wins,
|
||||
Losses = agentSummary.Losses,
|
||||
ActiveStrategiesCount = agentSummary.ActiveStrategiesCount,
|
||||
TotalVolume = agentSummary.TotalVolume,
|
||||
TotalBalance = agentSummary.TotalBalance,
|
||||
};
|
||||
|
||||
agentIndex.AgentSummaries.Add(agentSummary);
|
||||
agentSummaryViewModels.Add(agentSummaryViewModel);
|
||||
}
|
||||
|
||||
// Sort agent summaries by total PnL (highest first)
|
||||
agentIndex.AgentSummaries = agentIndex.AgentSummaries.OrderByDescending(a => a.TotalPnL).ToList();
|
||||
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
|
||||
|
||||
// Cache the results for 5 minutes
|
||||
_cacheService.SaveValue(cacheKey, agentIndex, TimeSpan.FromMinutes(5));
|
||||
var response = new PaginatedAgentIndexResponse
|
||||
{
|
||||
AgentSummaries = agentSummaryViewModels,
|
||||
TotalCount = totalCount,
|
||||
CurrentPage = page,
|
||||
PageSize = pageSize,
|
||||
TotalPages = totalPages,
|
||||
HasNextPage = page < totalPages,
|
||||
HasPreviousPage = page > 1,
|
||||
SortBy = sortBy,
|
||||
SortOrder = sortOrder,
|
||||
FilteredAgentNames = agentNames
|
||||
};
|
||||
|
||||
return Ok(agentIndex);
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -677,7 +728,7 @@ public class DataController : ControllerBase
|
||||
DateTime startDate,
|
||||
DateTime? endDate = null)
|
||||
{
|
||||
var balances = await _statisticService.GetAgentBalances(agentName, startDate, endDate);
|
||||
var balances = await _agentService.GetAgentBalances(agentName, startDate, endDate);
|
||||
return Ok(balances);
|
||||
}
|
||||
|
||||
@@ -696,7 +747,7 @@ public class DataController : ControllerBase
|
||||
int page = 1,
|
||||
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
|
||||
{
|
||||
@@ -710,6 +761,32 @@ public class DataController : ControllerBase
|
||||
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>
|
||||
/// Maps a ScenarioRequest to a domain Scenario object.
|
||||
/// </summary>
|
||||
@@ -721,7 +798,7 @@ public class DataController : ControllerBase
|
||||
|
||||
foreach (var indicatorRequest in scenarioRequest.Indicators)
|
||||
{
|
||||
var indicator = new Indicator(indicatorRequest.Name, indicatorRequest.Type)
|
||||
var indicator = new IndicatorBase(indicatorRequest.Name, indicatorRequest.Type)
|
||||
{
|
||||
SignalType = indicatorRequest.SignalType,
|
||||
MinimumHistory = indicatorRequest.MinimumHistory,
|
||||
|
||||
@@ -197,23 +197,23 @@ public class ScenarioController : BaseController
|
||||
};
|
||||
}
|
||||
|
||||
private static IndicatorViewModel MapToIndicatorViewModel(Indicator indicator)
|
||||
private static IndicatorViewModel MapToIndicatorViewModel(IndicatorBase indicatorBase)
|
||||
{
|
||||
return new IndicatorViewModel
|
||||
{
|
||||
Name = indicator.Name,
|
||||
Type = indicator.Type,
|
||||
SignalType = indicator.SignalType,
|
||||
MinimumHistory = indicator.MinimumHistory,
|
||||
Period = indicator.Period,
|
||||
FastPeriods = indicator.FastPeriods,
|
||||
SlowPeriods = indicator.SlowPeriods,
|
||||
SignalPeriods = indicator.SignalPeriods,
|
||||
Multiplier = indicator.Multiplier,
|
||||
SmoothPeriods = indicator.SmoothPeriods,
|
||||
StochPeriods = indicator.StochPeriods,
|
||||
CyclePeriods = indicator.CyclePeriods,
|
||||
UserName = indicator.User?.Name
|
||||
Name = indicatorBase.Name,
|
||||
Type = indicatorBase.Type,
|
||||
SignalType = indicatorBase.SignalType,
|
||||
MinimumHistory = indicatorBase.MinimumHistory,
|
||||
Period = indicatorBase.Period,
|
||||
FastPeriods = indicatorBase.FastPeriods,
|
||||
SlowPeriods = indicatorBase.SlowPeriods,
|
||||
SignalPeriods = indicatorBase.SignalPeriods,
|
||||
Multiplier = indicatorBase.Multiplier,
|
||||
SmoothPeriods = indicatorBase.SmoothPeriods,
|
||||
StochPeriods = indicatorBase.StochPeriods,
|
||||
CyclePeriods = indicatorBase.CyclePeriods,
|
||||
UserName = indicatorBase.User?.Name
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using Managing.Application.Abstractions;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.Shared;
|
||||
using Managing.Application.Trading.Commands;
|
||||
using Managing.Domain.MoneyManagements;
|
||||
using Managing.Domain.Trades;
|
||||
@@ -26,6 +27,8 @@ public class TradingController : BaseController
|
||||
private readonly IMoneyManagementService _moneyManagementService;
|
||||
private readonly IMediator _mediator;
|
||||
private readonly ILogger<TradingController> _logger;
|
||||
private readonly IAdminConfigurationService _adminService;
|
||||
private readonly IAccountService _accountService;
|
||||
|
||||
/// <summary>
|
||||
/// 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="tradingService">Service for trading operations.</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(
|
||||
ILogger<TradingController> logger,
|
||||
ICommandHandler<OpenPositionRequest, Position> openTradeCommandHandler,
|
||||
ICommandHandler<ClosePositionCommand, Position> closeTradeCommandHandler,
|
||||
ITradingService tradingService,
|
||||
IMediator mediator, IMoneyManagementService moneyManagementService,
|
||||
IUserService userService) : base(userService)
|
||||
IUserService userService, IAdminConfigurationService adminService,
|
||||
IAccountService accountService) : base(userService)
|
||||
{
|
||||
_logger = logger;
|
||||
_openTradeCommandHandler = openTradeCommandHandler;
|
||||
@@ -49,6 +55,8 @@ public class TradingController : BaseController
|
||||
_tradingService = tradingService;
|
||||
_mediator = mediator;
|
||||
_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>
|
||||
/// <returns>The closed position.</returns>
|
||||
[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 result = await _closeTradeCommandHandler.Handle(new ClosePositionCommand(position));
|
||||
@@ -149,6 +157,7 @@ public class TradingController : BaseController
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a Privy wallet address for the user.
|
||||
/// Only admins can initialize any address, regular users can only initialize their own addresses.
|
||||
/// </summary>
|
||||
/// <param name="publicAddress">The public address of the Privy wallet to initialize.</param>
|
||||
/// <returns>The initialization response containing success status and transaction hashes.</returns>
|
||||
@@ -162,6 +171,18 @@ public class TradingController : BaseController
|
||||
|
||||
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);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,8 @@ public class UserController : BaseController
|
||||
/// <param name="userService">Service for user-related operations.</param>
|
||||
/// <param name="jwtUtils">Utility for JWT token 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)
|
||||
{
|
||||
_config = config;
|
||||
@@ -40,7 +41,7 @@ public class UserController : BaseController
|
||||
/// <param name="login">The login request containing user credentials.</param>
|
||||
/// <returns>A JWT token if authentication is successful; otherwise, an Unauthorized result.</returns>
|
||||
[AllowAnonymous]
|
||||
[HttpPost]
|
||||
[HttpPost("create-token")]
|
||||
public async Task<ActionResult<string>> CreateToken([FromBody] LoginRequest login)
|
||||
{
|
||||
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.
|
||||
/// </summary>
|
||||
/// <returns>The current user's information.</returns>
|
||||
[Authorize]
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<User>> GetCurrentUser()
|
||||
{
|
||||
var user = await base.GetUser();
|
||||
user = await _userService.GetUserByName(user.Name);
|
||||
return Ok(user);
|
||||
}
|
||||
|
||||
@@ -70,6 +73,7 @@ public class UserController : BaseController
|
||||
/// </summary>
|
||||
/// <param name="agentName">The new agent name to set.</param>
|
||||
/// <returns>The updated user with the new agent name.</returns>
|
||||
[Authorize]
|
||||
[HttpPut("agent-name")]
|
||||
public async Task<ActionResult<User>> UpdateAgentName([FromBody] string agentName)
|
||||
{
|
||||
@@ -83,6 +87,7 @@ public class UserController : BaseController
|
||||
/// </summary>
|
||||
/// <param name="avatarUrl">The new avatar URL to set.</param>
|
||||
/// <returns>The updated user with the new avatar URL.</returns>
|
||||
[Authorize]
|
||||
[HttpPut("avatar")]
|
||||
public async Task<ActionResult<User>> UpdateAvatarUrl([FromBody] string avatarUrl)
|
||||
{
|
||||
@@ -96,6 +101,7 @@ public class UserController : BaseController
|
||||
/// </summary>
|
||||
/// <param name="telegramChannel">The new Telegram channel to set.</param>
|
||||
/// <returns>The updated user with the new Telegram channel.</returns>
|
||||
[Authorize]
|
||||
[HttpPut("telegram-channel")]
|
||||
public async Task<ActionResult<User>> UpdateTelegramChannel([FromBody] string telegramChannel)
|
||||
{
|
||||
@@ -108,20 +114,21 @@ public class UserController : BaseController
|
||||
try
|
||||
{
|
||||
var welcomeMessage = $"🎉 **Trading Bot - Welcome!**\n\n" +
|
||||
$"🎯 **Agent:** {user.Name}\n" +
|
||||
$"📡 **Channel ID:** {telegramChannel}\n" +
|
||||
$"⏰ **Setup Time:** {DateTime.UtcNow:MMM dd, yyyy • HH:mm:ss} UTC\n\n" +
|
||||
$"🔔 **Notification Types:**\n" +
|
||||
$"• 📈 Position Opens & Closes\n" +
|
||||
$"• 🤖 Bot configuration changes\n\n" +
|
||||
$"🚀 **Welcome aboard!** Your trading notifications are now live.";
|
||||
$"🎯 **Agent:** {user.Name}\n" +
|
||||
$"📡 **Channel ID:** {telegramChannel}\n" +
|
||||
$"⏰ **Setup Time:** {DateTime.UtcNow:MMM dd, yyyy • HH:mm:ss} UTC\n\n" +
|
||||
$"🔔 **Notification Types:**\n" +
|
||||
$"• 📈 Position Opens & Closes\n" +
|
||||
$"• 🤖 Bot configuration changes\n\n" +
|
||||
$"🚀 **Welcome aboard!** Your trading notifications are now live.";
|
||||
|
||||
await _webhookService.SendMessage(welcomeMessage, telegramChannel);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log the error but don't fail the update operation
|
||||
Console.WriteLine($"Failed to send welcome message to telegram channel {telegramChannel}: {ex.Message}");
|
||||
Console.WriteLine(
|
||||
$"Failed to send welcome message to telegram channel {telegramChannel}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,6 +139,7 @@ public class UserController : BaseController
|
||||
/// Tests the Telegram channel configuration by sending a test message.
|
||||
/// </summary>
|
||||
/// <returns>A message indicating the test result.</returns>
|
||||
[Authorize]
|
||||
[HttpPost("telegram-channel/test")]
|
||||
public async Task<ActionResult<string>> TestTelegramChannel()
|
||||
{
|
||||
@@ -144,7 +152,7 @@ public class UserController : BaseController
|
||||
|
||||
try
|
||||
{
|
||||
var testMessage = $"🚀 **Trading Bot - Channel Test**\n\n" +
|
||||
var testMessage = $"🚀 **Trading Bot - Channel Test**\n\n" +
|
||||
$"🎯 **Agent:** {user.Name}\n" +
|
||||
$"📡 **Channel ID:** {user.TelegramChannel}\n" +
|
||||
$"⏰ **Test Time:** {DateTime.UtcNow:MMM dd, yyyy • HH:mm:ss} UTC\n\n" +
|
||||
@@ -155,7 +163,8 @@ public class UserController : BaseController
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -163,4 +172,3 @@ public class UserController : BaseController
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,6 @@ 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/"]
|
||||
|
||||
42
src/Managing.Api/Extensions/PlatformSummaryExtensions.cs
Normal file
42
src/Managing.Api/Extensions/PlatformSummaryExtensions.cs
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -53,5 +53,8 @@
|
||||
<Content Update="appsettings.Production.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Update="appsettings.KaiServer.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
55
src/Managing.Api/Models/Requests/GetBotsPaginatedRequest.cs
Normal file
55
src/Managing.Api/Models/Requests/GetBotsPaginatedRequest.cs
Normal 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";
|
||||
}
|
||||
@@ -13,7 +13,7 @@ public class UpdateBotConfigRequest
|
||||
/// The unique identifier of the bot to update
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string Identifier { get; set; }
|
||||
public Guid Identifier { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The new trading bot configuration request
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
using Managing.Application.Abstractions.Models;
|
||||
using Managing.Common;
|
||||
|
||||
namespace Managing.Api.Models.Responses
|
||||
{
|
||||
/// <summary>
|
||||
@@ -15,21 +18,11 @@ namespace Managing.Api.Models.Responses
|
||||
/// </summary>
|
||||
public decimal TotalPnL { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Profit and loss in the last 24 hours in USD
|
||||
/// </summary>
|
||||
public decimal PnLLast24h { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Total return on investment as a percentage
|
||||
/// </summary>
|
||||
public decimal TotalROI { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Return on investment in the last 24 hours as a percentage
|
||||
/// </summary>
|
||||
public decimal ROILast24h { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of winning trades
|
||||
/// </summary>
|
||||
@@ -40,10 +33,6 @@ namespace Managing.Api.Models.Responses
|
||||
/// </summary>
|
||||
public int Losses { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Average win rate as a percentage
|
||||
/// </summary>
|
||||
public int AverageWinRate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of active strategies for this agent
|
||||
@@ -56,9 +45,9 @@ namespace Managing.Api.Models.Responses
|
||||
public decimal TotalVolume { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Volume traded in the last 24 hours in USD
|
||||
/// Total balance including USDC and open position values (without leverage, including PnL)
|
||||
/// </summary>
|
||||
public decimal VolumeLast24h { get; set; }
|
||||
public decimal TotalBalance { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -69,32 +58,96 @@ namespace Managing.Api.Models.Responses
|
||||
/// <summary>
|
||||
/// Total number of agents on the platform
|
||||
/// </summary>
|
||||
public int TotalAgents { get; set; }
|
||||
public required int TotalAgents { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Total number of active strategies across all agents
|
||||
/// </summary>
|
||||
public int TotalActiveStrategies { get; set; }
|
||||
public required int TotalActiveStrategies { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Total platform-wide profit and loss in USD
|
||||
/// </summary>
|
||||
public decimal TotalPlatformPnL { get; set; }
|
||||
public required decimal TotalPlatformPnL { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Total volume traded across all agents in USD
|
||||
/// </summary>
|
||||
public decimal TotalPlatformVolume { get; set; }
|
||||
public required decimal TotalPlatformVolume { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Total volume traded across all agents in the last 24 hours in USD
|
||||
/// </summary>
|
||||
public decimal TotalPlatformVolumeLast24h { get; set; }
|
||||
public required decimal TotalPlatformVolumeLast24h { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Time filter applied to the data
|
||||
/// Total open interest across all positions in USD
|
||||
/// </summary>
|
||||
public string TimeFilter { get; set; } = "Total";
|
||||
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>
|
||||
|
||||
@@ -12,10 +12,11 @@ public class CandlesWithIndicatorsResponse
|
||||
/// <summary>
|
||||
/// The list of candles.
|
||||
/// </summary>
|
||||
public List<Candle> Candles { get; set; } = new List<Candle>();
|
||||
public HashSet<Candle> Candles { get; set; } = new HashSet<Candle>();
|
||||
|
||||
/// <summary>
|
||||
/// The calculated indicators values.
|
||||
/// </summary>
|
||||
public Dictionary<IndicatorType, IndicatorsResultBase> IndicatorsValues { get; set; } = new Dictionary<IndicatorType, IndicatorsResultBase>();
|
||||
public Dictionary<IndicatorType, IndicatorsResultBase> IndicatorsValues { get; set; } =
|
||||
new Dictionary<IndicatorType, IndicatorsResultBase>();
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
43
src/Managing.Api/Models/Responses/PaginatedResponse.cs
Normal file
43
src/Managing.Api/Models/Responses/PaginatedResponse.cs
Normal 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; }
|
||||
}
|
||||
@@ -14,6 +14,34 @@ namespace Managing.Api.Models.Responses
|
||||
/// Profit and Loss value of the strategy
|
||||
/// </summary>
|
||||
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>
|
||||
@@ -26,4 +54,62 @@ namespace Managing.Api.Models.Responses
|
||||
/// </summary>
|
||||
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>();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Managing.Domain.Bots;
|
||||
using Managing.Domain.Candles;
|
||||
using Managing.Domain.Indicators;
|
||||
using Managing.Domain.Trades;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Api.Models.Responses
|
||||
{
|
||||
@@ -14,16 +15,16 @@ namespace Managing.Api.Models.Responses
|
||||
public string Status { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// List of signals generated by the bot
|
||||
/// Dictionary of signals generated by the bot, keyed by signal identifier
|
||||
/// </summary>
|
||||
[Required]
|
||||
public List<LightSignal> Signals { get; internal set; }
|
||||
public Dictionary<string, LightSignal> Signals { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// List of positions opened by the bot
|
||||
/// Dictionary of positions opened by the bot, keyed by position identifier
|
||||
/// </summary>
|
||||
[Required]
|
||||
public List<Position> Positions { get; internal set; }
|
||||
public Dictionary<Guid, Position> Positions { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Candles used by the bot for analysis
|
||||
@@ -55,12 +56,6 @@ namespace Managing.Api.Models.Responses
|
||||
[Required]
|
||||
public string AgentName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The full trading bot configuration
|
||||
/// </summary>
|
||||
[Required]
|
||||
public TradingBotConfig Config { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// The time when the bot was created
|
||||
/// </summary>
|
||||
@@ -72,5 +67,13 @@ namespace Managing.Api.Models.Responses
|
||||
/// </summary>
|
||||
[Required]
|
||||
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; }
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using Managing.Common;
|
||||
using Managing.Domain.Trades;
|
||||
|
||||
namespace Managing.Api.Models.Responses
|
||||
@@ -15,7 +16,7 @@ namespace Managing.Api.Models.Responses
|
||||
/// <summary>
|
||||
/// Current state of the strategy (RUNNING, STOPPED, UNUSED)
|
||||
/// </summary>
|
||||
public string State { get; set; }
|
||||
public Enums.BotStatus State { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Total profit or loss generated by the strategy in USD
|
||||
@@ -63,11 +64,12 @@ namespace Managing.Api.Models.Responses
|
||||
public int Losses { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// List of all positions executed by this strategy
|
||||
/// Dictionary of all positions executed by this strategy, keyed by position identifier
|
||||
/// </summary>
|
||||
public List<Position> Positions { get; set; } = new List<Position>();
|
||||
|
||||
public string Identifier { get; set; }
|
||||
public string ScenarioName { get; set; }
|
||||
public Guid Identifier { get; set; }
|
||||
|
||||
public Dictionary<DateTime, decimal> WalletBalances { get; set; } = new Dictionary<DateTime, decimal>();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using HealthChecks.UI.Client;
|
||||
using Managing.Api.Authorization;
|
||||
using Managing.Api.Filters;
|
||||
using Managing.Api.HealthChecks;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.Hubs;
|
||||
using Managing.Application.Workers;
|
||||
using Managing.Bootstrap;
|
||||
using Managing.Common;
|
||||
using Managing.Core.Middleawares;
|
||||
@@ -156,21 +157,71 @@ builder.Services.Configure<PrivySettings>(builder.Configuration.GetSection(Const
|
||||
builder.Services.AddControllers().AddJsonOptions(options =>
|
||||
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()));
|
||||
|
||||
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(o =>
|
||||
{
|
||||
o.SaveToken = true;
|
||||
o.TokenValidationParameters = new TokenValidationParameters
|
||||
builder.Services
|
||||
.AddAuthentication(options =>
|
||||
{
|
||||
ValidIssuer = builder.Configuration["Authentication:Schemes:Bearer:ValidIssuer"],
|
||||
ValidAudience = builder.Configuration["Authentication:Schemes:Bearer:ValidAudiences"],
|
||||
IssuerSigningKey = new SymmetricSecurityKey
|
||||
(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Secret"])),
|
||||
ValidateIssuer = false,
|
||||
ValidateAudience = false,
|
||||
ValidateIssuerSigningKey = true
|
||||
};
|
||||
});
|
||||
builder.Services.AddAuthorization();
|
||||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
})
|
||||
.AddJwtBearer(o =>
|
||||
{
|
||||
o.SaveToken = true;
|
||||
o.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidIssuer = builder.Configuration["Authentication:Schemes:Bearer:ValidIssuer"],
|
||||
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
|
||||
@@ -187,7 +238,7 @@ builder.Services.AddScoped<IJwtUtils, JwtUtils>();
|
||||
|
||||
builder.Services.RegisterApiDependencies(builder.Configuration);
|
||||
|
||||
// Orleans Configuration
|
||||
// Orleans is always configured, but grains can be controlled
|
||||
builder.Host.ConfigureOrleans(builder.Configuration, builder.Environment.IsProduction());
|
||||
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
@@ -233,12 +284,6 @@ builder.Services.AddSwaggerGen(options =>
|
||||
});
|
||||
|
||||
builder.WebHost.SetupDiscordBot();
|
||||
if (builder.Configuration.GetValue<bool>("EnableBotManager", false))
|
||||
{
|
||||
builder.Services.AddHostedService<BotManagerWorker>();
|
||||
}
|
||||
|
||||
// Workers are now registered in ApiBootstrap.cs
|
||||
|
||||
// App
|
||||
var app = builder.Build();
|
||||
@@ -258,14 +303,9 @@ app.UseSentryDiagnostics();
|
||||
// Using shared GlobalErrorHandlingMiddleware from core project
|
||||
app.UseMiddleware<GlobalErrorHandlingMiddleware>();
|
||||
|
||||
app.UseMiddleware<JwtMiddleware>();
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
app.UseAuthentication();
|
||||
|
||||
app.UseAuthorization();
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
@@ -287,6 +327,16 @@ app.UseEndpoints(endpoints =>
|
||||
});
|
||||
});
|
||||
|
||||
// Conditionally run the application based on deployment mode
|
||||
var deploymentMode = builder.Configuration.GetValue<bool>("DeploymentMode", false);
|
||||
|
||||
|
||||
app.Run();
|
||||
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.");
|
||||
}
|
||||
116
src/Managing.Api/README-ORLEANS-CONFIGURATION.md
Normal file
116
src/Managing.Api/README-ORLEANS-CONFIGURATION.md
Normal 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.
|
||||
119
src/Managing.Api/README-ORLEANS-TROUBLESHOOTING.md
Normal file
119
src/Managing.Api/README-ORLEANS-TROUBLESHOOTING.md
Normal 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
|
||||
125
src/Managing.Api/TRADING_CONTROLLER_SECURITY.md
Normal file
125
src/Managing.Api/TRADING_CONTROLLER_SECURITY.md
Normal 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
|
||||
10
src/Managing.Api/appsettings.Development.json
Normal file
10
src/Managing.Api/appsettings.Development.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"RunOrleansGrains": false,
|
||||
"DeploymentMode": false,
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,5 +30,6 @@
|
||||
"RequestsChannelId": 1018589494968078356,
|
||||
"ButtonExpirationMinutes": 10
|
||||
},
|
||||
"RunOrleansGrains": true,
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
@@ -30,5 +30,19 @@
|
||||
"RequestsChannelId": 1018589494968078356,
|
||||
"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": "*"
|
||||
}
|
||||
@@ -5,7 +5,8 @@
|
||||
"Token": "Fw2FPL2OwTzDHzSbR2Sd5xs0EKQYy00Q-hYKYAhr9cC1_q5YySONpxuf_Ck0PTjyUiF13xXmi__bu_pXH-H9zA=="
|
||||
},
|
||||
"PostgreSql": {
|
||||
"ConnectionString": "Host=localhost;Port=5432;Database=managing;Username=postgres;Password=postgres"
|
||||
"ConnectionString": "Host=localhost;Port=5432;Database=managing;Username=postgres;Password=postgres",
|
||||
"Orleans": "Host=localhost;Port=5432;Database=orleans;Username=postgres;Password=postgres"
|
||||
},
|
||||
"Privy": {
|
||||
"AppId": "cm6f47n1l003jx7mjwaembhup",
|
||||
@@ -23,9 +24,6 @@
|
||||
"ElasticConfiguration": {
|
||||
"Uri": "http://elasticsearch:9200"
|
||||
},
|
||||
"Sentry": {
|
||||
"Dsn": "https://698e00d7cb404b049aff3881e5a47f6b@bugcenter.apps.managing.live/1"
|
||||
},
|
||||
"Discord": {
|
||||
"ApplicationId": "",
|
||||
"PublicKey": "",
|
||||
@@ -36,10 +34,25 @@
|
||||
"RequestsChannelId": 1018589494968078356,
|
||||
"ButtonExpirationMinutes": 2
|
||||
},
|
||||
"RunOrleansGrains": true,
|
||||
"AllowedHosts": "*",
|
||||
"KAIGEN_SECRET_KEY": "KaigenXCowchain",
|
||||
"KAIGEN_CREDITS_ENABLED": false,
|
||||
"WorkerBotManager": true,
|
||||
"WorkerBalancesTracking": false,
|
||||
"WorkerNotifyBundleBacktest": false,
|
||||
"KAIGEN_SECRET_KEY": "KaigenXCowchain",
|
||||
"KAIGEN_CREDITS_ENABLED": 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
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"PostgreSql": {
|
||||
"ConnectionString": "Host=apps.prod.live;Port=5432;Database=managing;Username=postgres;Password=postgres"
|
||||
"ConnectionString": "Host=managing-postgre.apps.managing.live;Port=5432;Database=managing;Username=postgres;Password=29032b13a5bc4d37",
|
||||
"Orleans": "Host=managing-postgre.apps.managing.live;Port=5432;Database=orleans;Username=postgres;Password=29032b13a5bc4d37"
|
||||
},
|
||||
"InfluxDb": {
|
||||
"Url": "https://influx-db.apps.managing.live",
|
||||
@@ -26,8 +27,12 @@
|
||||
"ElasticConfiguration": {
|
||||
"Uri": "http://elasticsearch:9200"
|
||||
},
|
||||
"Sentry": {
|
||||
"Dsn": "https://698e00d7cb404b049aff3881e5a47f6b@bugcenter.apps.managing.live/1"
|
||||
"RunOrleansGrains": true,
|
||||
"DeploymentMode": false,
|
||||
"Orleans": {
|
||||
"EnableClustering": true,
|
||||
"ConnectionTimeout": 60,
|
||||
"MaxJoinAttempts": 3
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"WorkerBotManager": true,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"PostgreSql": {
|
||||
"ConnectionString": "Host=managing-postgre.apps.managing.live;Port=5432;Database=managing;Username=postgres;Password=29032b13a5bc4d37"
|
||||
"ConnectionString": "Host=managing-postgre.apps.managing.live;Port=5432;Database=managing;Username=postgres;Password=29032b13a5bc4d37",
|
||||
"Orleans": "Host=managing-postgre.apps.managing.live;Port=5432;Database=orleans;Username=postgres;Password=29032b13a5bc4d37"
|
||||
},
|
||||
"InfluxDb": {
|
||||
"Url": "http://srv-captain--influx-db:8086/",
|
||||
@@ -31,8 +32,6 @@
|
||||
"ElasticConfiguration": {
|
||||
"Uri": "http://elasticsearch:9200"
|
||||
},
|
||||
"Sentry": {
|
||||
"Dsn": "https://698e00d7cb404b049aff3881e5a47f6b@bugcenter.apps.managing.live/1"
|
||||
},
|
||||
"RunOrleansGrains": true,
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"PostgreSql": {
|
||||
"ConnectionString": "Host=managing-postgre.apps.managing.live;Port=5432;Database=managing;Username=postgres;Password=29032b13a5bc4d37",
|
||||
"Orleans": "Host=managing-postgre.apps.managing.live;Port=5432;Database=orleans;Username=postgres;Password=29032b13a5bc4d37"
|
||||
},
|
||||
"InfluxDb": {
|
||||
"Url": "https://influx-db.apps.managing.live",
|
||||
"Organization": "managing-org",
|
||||
@@ -8,9 +12,6 @@
|
||||
"AppId": "cm6f47n1l003jx7mjwaembhup",
|
||||
"AppSecret": "63Chz2z5M8TgR5qc8dznSLRAGTHTyPU4cjdQobrBF1Cx5tszZpTuFgyrRd7hZ2k6HpwDz3GEwQZzsCqHb8Z311bF"
|
||||
},
|
||||
"PostgreSql": {
|
||||
"ConnectionString": "Host=managing-postgre.apps.managing.live;Port=5432;Database=managing;Username=postgres;Password=29032b13a5bc4d37"
|
||||
},
|
||||
"Serilog": {
|
||||
"MinimumLevel": {
|
||||
"Default": "Information",
|
||||
@@ -23,6 +24,7 @@
|
||||
"ElasticConfiguration": {
|
||||
"Uri": "http://elasticsearch:9200"
|
||||
},
|
||||
"RunOrleansGrains": true,
|
||||
"AllowedHosts": "*",
|
||||
"WorkerBotManager": false,
|
||||
"WorkerBalancesTracking": false
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"WebhookUrl": "https://n8n.kai.managing.live/webhook/fa9308b6-983b-42ec-b085-71599d655951"
|
||||
},
|
||||
"Sentry": {
|
||||
"Dsn": "https://698e00d7cb404b049aff3881e5a47f6b@bugcenter.apps.managing.live/1",
|
||||
"Dsn": "https://fe12add48c56419bbdfa86227c188e7a@glitch.kai.managing.live/1",
|
||||
"MinimumEventLevel": "Error",
|
||||
"SendDefaultPii": true,
|
||||
"MaxBreadcrumbs": 50,
|
||||
@@ -66,5 +66,21 @@
|
||||
"FundingRateChannelId": 1263566138709774336,
|
||||
"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": "*"
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -23,23 +23,5 @@ public interface IBacktestTradingBotGrain : IGrainWithGuidKey
|
||||
/// <param name="requestId">The request ID to associate with this backtest</param>
|
||||
/// <param name="metadata">Additional metadata to associate with this backtest</param>
|
||||
/// <returns>The complete backtest result</returns>
|
||||
Task<LightBacktest> RunBacktestAsync(TradingBotConfig config, List<Candle> candles, User user = null, bool save = false, bool withCandles = false, string requestId = null, object metadata = null);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current backtest progress
|
||||
/// </summary>
|
||||
/// <returns>Backtest progress information</returns>
|
||||
Task<BacktestProgress> GetBacktestProgressAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the progress of a backtest
|
||||
/// </summary>
|
||||
public class BacktestProgress
|
||||
{
|
||||
public bool IsInitialized { get; set; }
|
||||
public int TotalCandles { get; set; }
|
||||
public int ProcessedCandles { get; set; }
|
||||
public double ProgressPercentage { get; set; }
|
||||
public bool IsComplete { get; set; }
|
||||
Task<LightBacktest> RunBacktestAsync(TradingBotConfig config, HashSet<Candle> candles, User user = null, bool save = false, bool withCandles = false, string requestId = null, object metadata = null);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
using Managing.Application.Abstractions.Models;
|
||||
using Managing.Domain.Bots;
|
||||
using Managing.Domain.Trades;
|
||||
using Orleans;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.Abstractions.Grains;
|
||||
|
||||
/// <summary>
|
||||
/// Orleans grain interface for TradingBot operations.
|
||||
/// This interface defines the distributed, async operations available for trading bots.
|
||||
/// </summary>
|
||||
public interface ITradingBotGrain : IGrainWithGuidKey
|
||||
{
|
||||
/// <summary>
|
||||
/// Starts the trading bot asynchronously
|
||||
/// </summary>
|
||||
Task StartAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Stops the trading bot asynchronously
|
||||
/// </summary>
|
||||
Task StopAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current status of the trading bot
|
||||
/// </summary>
|
||||
Task<BotStatus> GetStatusAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current configuration of the trading bot
|
||||
/// </summary>
|
||||
Task<TradingBotConfig> GetConfigurationAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Updates the trading bot configuration
|
||||
/// </summary>
|
||||
/// <param name="newConfig">The new configuration to apply</param>
|
||||
/// <returns>True if the configuration was successfully updated</returns>
|
||||
Task<bool> UpdateConfigurationAsync(TradingBotConfig newConfig);
|
||||
|
||||
/// <summary>
|
||||
/// Manually opens a position in the specified direction
|
||||
/// </summary>
|
||||
/// <param name="direction">The direction of the trade (Long/Short)</param>
|
||||
/// <returns>The created Position object</returns>
|
||||
Task<Position> OpenPositionManuallyAsync(TradeDirection direction);
|
||||
|
||||
/// <summary>
|
||||
/// Toggles the bot between watch-only and trading mode
|
||||
/// </summary>
|
||||
Task ToggleIsForWatchOnlyAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Gets comprehensive bot data including positions, signals, and performance metrics
|
||||
/// </summary>
|
||||
Task<TradingBotResponse> GetBotDataAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Loads a bot backup into the grain state
|
||||
/// </summary>
|
||||
/// <param name="backup">The bot backup to load</param>
|
||||
Task LoadBackupAsync(BotBackup backup);
|
||||
|
||||
/// <summary>
|
||||
/// Forces a backup save of the current bot state
|
||||
/// </summary>
|
||||
Task SaveBackupAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current profit and loss for the bot
|
||||
/// </summary>
|
||||
Task<decimal> GetProfitAndLossAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current win rate percentage for the bot
|
||||
/// </summary>
|
||||
Task<int> GetWinRateAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the bot's execution count (number of Run cycles completed)
|
||||
/// </summary>
|
||||
Task<long> GetExecutionCountAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the bot's startup time
|
||||
/// </summary>
|
||||
Task<DateTime> GetStartupTimeAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the bot's creation date
|
||||
/// </summary>
|
||||
Task<DateTime> GetCreateDateAsync();
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Managing.Common\Managing.Common.csproj" />
|
||||
<ProjectReference Include="..\Managing.Domain\Managing.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Managing.Common\Managing.Common.csproj"/>
|
||||
<ProjectReference Include="..\Managing.Domain\Managing.Domain.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Orleans.Core.Abstractions" Version="9.2.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Orleans.Core.Abstractions" Version="9.2.1"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -1,12 +1,40 @@
|
||||
using Managing.Domain.Bots;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.Abstractions.Repositories;
|
||||
|
||||
public interface IBotRepository
|
||||
{
|
||||
Task InsertBotAsync(BotBackup bot);
|
||||
Task<IEnumerable<BotBackup>> GetBotsAsync();
|
||||
Task UpdateBackupBot(BotBackup bot);
|
||||
Task DeleteBotBackup(string botName);
|
||||
Task<BotBackup?> GetBotByIdentifierAsync(string identifier);
|
||||
Task InsertBotAsync(Bot bot);
|
||||
Task<IEnumerable<Bot>> GetBotsAsync();
|
||||
Task UpdateBot(Bot bot);
|
||||
Task DeleteBot(Guid 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");
|
||||
}
|
||||
@@ -5,22 +5,24 @@ namespace Managing.Application.Abstractions.Repositories;
|
||||
|
||||
public interface ICandleRepository
|
||||
{
|
||||
Task<IList<Candle>> GetCandles(
|
||||
Enums.TradingExchanges exchange,
|
||||
Enums.Ticker ticker,
|
||||
Enums.Timeframe timeframe,
|
||||
DateTime start);
|
||||
|
||||
Task<IList<Candle>> GetCandles(
|
||||
Task<HashSet<Candle>> GetCandles(
|
||||
Enums.TradingExchanges exchange,
|
||||
Enums.Ticker ticker,
|
||||
Enums.Timeframe timeframe,
|
||||
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(
|
||||
Enums.TradingExchanges exchange,
|
||||
Enums.Timeframe timeframe,
|
||||
DateTime start);
|
||||
void InsertCandle(Candle candle);
|
||||
Task InsertCandle(Candle candle);
|
||||
}
|
||||
|
||||
@@ -13,18 +13,24 @@ public interface ITradingRepository
|
||||
Task<Signal> GetSignalByIdentifierAsync(string identifier, User user = null);
|
||||
Task InsertPositionAsync(Position position);
|
||||
Task UpdatePositionAsync(Position position);
|
||||
Task<Indicator> GetStrategyByNameAsync(string strategy);
|
||||
Task<IndicatorBase> GetStrategyByNameAsync(string strategy);
|
||||
Task InsertScenarioAsync(Scenario scenario);
|
||||
Task InsertStrategyAsync(Indicator indicator);
|
||||
Task InsertIndicatorAsync(IndicatorBase indicator);
|
||||
Task<IEnumerable<Scenario>> GetScenariosAsync();
|
||||
Task<IEnumerable<Indicator>> GetStrategiesAsync();
|
||||
Task<IEnumerable<Indicator>> GetIndicatorsAsync();
|
||||
Task<IEnumerable<Scenario>> GetScenariosByUserAsync(User user);
|
||||
Task<IEnumerable<IndicatorBase>> GetStrategiesAsync();
|
||||
Task<IEnumerable<IndicatorBase>> GetIndicatorsAsync();
|
||||
Task DeleteScenarioAsync(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>> 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 UpdateStrategyAsync(Indicator indicator);
|
||||
Task UpdateStrategyAsync(IndicatorBase indicatorBase);
|
||||
Task<IndicatorBase> GetStrategyByNameUserAsync(string name, User user);
|
||||
Task<Scenario> GetScenarioByNameUserAsync(string scenarioName, User user);
|
||||
}
|
||||
@@ -6,6 +6,6 @@ public interface IUserRepository
|
||||
{
|
||||
Task<User> GetUserByAgentNameAsync(string agentName);
|
||||
Task<User> GetUserByNameAsync(string name);
|
||||
Task InsertUserAsync(User user);
|
||||
Task UpdateUser(User user);
|
||||
Task<IEnumerable<User>> GetAllUsersAsync();
|
||||
Task SaveOrUpdateUserAsync(User user);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -9,7 +9,7 @@ public interface IAccountService
|
||||
Task<Account> CreateAccount(User user, Account account);
|
||||
bool DeleteAccount(User user, string name);
|
||||
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>> GetAccountsAsync(bool hideSecrets, bool getBalance);
|
||||
Task<Account> GetAccount(string name, bool hideSecrets, bool getBalance);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -45,7 +45,7 @@ namespace Managing.Application.Abstractions.Services
|
||||
/// <returns>The lightweight backtest results</returns>
|
||||
Task<LightBacktest> RunTradingBotBacktest(
|
||||
TradingBotConfig config,
|
||||
List<Candle> candles,
|
||||
HashSet<Candle> candles,
|
||||
User user = null,
|
||||
bool withCandles = false,
|
||||
string requestId = null,
|
||||
|
||||
@@ -45,16 +45,18 @@ public interface IExchangeService
|
||||
Task<List<Trade>> GetTrades(Account account, Ticker ticker);
|
||||
Task<bool> CancelOrder(Account account, Ticker ticker);
|
||||
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<List<Candle>> GetCandlesInflux(TradingExchanges exchange, Ticker ticker, DateTime startDate,
|
||||
Timeframe timeframe);
|
||||
Task<HashSet<Candle>> GetCandlesInflux(TradingExchanges exchange, Ticker ticker, DateTime startDate,
|
||||
Timeframe timeframe, int? limit = null);
|
||||
|
||||
Task<List<Candle>> GetCandlesInflux(TradingExchanges exchange, Ticker ticker, DateTime startDate,
|
||||
Timeframe timeframe, DateTime endDate);
|
||||
Task<HashSet<Candle>> GetCandlesInflux(TradingExchanges exchange, Ticker ticker, DateTime startDate,
|
||||
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);
|
||||
|
||||
Trade BuildEmptyTrade(Ticker ticker, decimal price, decimal quantity, TradeDirection direction, decimal? leverage,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Managing.Domain.MoneyManagements;
|
||||
using Managing.Domain.Users;
|
||||
|
||||
namespace Managing.Application.Abstractions
|
||||
namespace Managing.Application.Abstractions.Services
|
||||
{
|
||||
public interface IMoneyManagementService
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.Workers.Abstractions;
|
||||
namespace Managing.Application.Abstractions.Services;
|
||||
|
||||
public interface IPricesService
|
||||
{
|
||||
@@ -6,12 +6,6 @@ namespace Managing.Application.Abstractions.Services;
|
||||
|
||||
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();
|
||||
Task<List<Trader>> GetBadTradersAsync();
|
||||
List<Trader> GetBestTraders();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Managing.Domain.Bots;
|
||||
using Managing.Domain.Indicators;
|
||||
using Managing.Domain.Synth.Models;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
@@ -94,7 +95,7 @@ public interface ISynthPredictionService
|
||||
/// <param name="botConfig">Bot configuration with Synth settings</param>
|
||||
/// <returns>Risk assessment result</returns>
|
||||
Task<SynthRiskResult> MonitorPositionRiskAsync(Ticker ticker, TradeDirection direction, decimal currentPrice,
|
||||
decimal liquidationPrice, string positionIdentifier, TradingBotConfig botConfig);
|
||||
decimal liquidationPrice, Guid positionIdentifier, TradingBotConfig botConfig);
|
||||
|
||||
/// <summary>
|
||||
/// Estimates liquidation price based on money management settings
|
||||
@@ -103,5 +104,6 @@ public interface ISynthPredictionService
|
||||
/// <param name="direction">Position direction</param>
|
||||
/// <param name="moneyManagement">Money management settings</param>
|
||||
/// <returns>Estimated liquidation price</returns>
|
||||
decimal EstimateLiquidationPrice(decimal currentPrice, TradeDirection direction, LightMoneyManagement moneyManagement);
|
||||
decimal EstimateLiquidationPrice(decimal currentPrice, TradeDirection direction,
|
||||
LightMoneyManagement moneyManagement);
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
using Managing.Domain.Accounts;
|
||||
using Managing.Domain.Bots;
|
||||
using Managing.Domain.Candles;
|
||||
using Managing.Domain.Indicators;
|
||||
using Managing.Domain.Scenarios;
|
||||
using Managing.Domain.Statistics;
|
||||
using Managing.Domain.Strategies;
|
||||
using Managing.Domain.Strategies.Base;
|
||||
using Managing.Domain.Synth.Models;
|
||||
using Managing.Domain.Trades;
|
||||
using Managing.Domain.Users;
|
||||
using Managing.Infrastructure.Evm.Models.Privy;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
@@ -17,21 +19,25 @@ public interface ITradingService
|
||||
Task<Scenario> GetScenarioByNameAsync(string scenario);
|
||||
Task InsertPositionAsync(Position position);
|
||||
Task UpdatePositionAsync(Position position);
|
||||
Task<Indicator> GetStrategyByNameAsync(string strategy);
|
||||
Task<IndicatorBase> GetIndicatorByNameAsync(string strategy);
|
||||
Task InsertScenarioAsync(Scenario scenario);
|
||||
Task InsertStrategyAsync(Indicator indicator);
|
||||
Task InsertIndicatorAsync(IndicatorBase indicatorBase);
|
||||
Task<IEnumerable<Scenario>> GetScenariosAsync();
|
||||
Task<IEnumerable<Indicator>> GetStrategiesAsync();
|
||||
Task<IEnumerable<Scenario>> GetScenariosByUserAsync(User user);
|
||||
Task<IEnumerable<IndicatorBase>> GetIndicatorsAsync();
|
||||
Task DeleteScenarioAsync(string name);
|
||||
Task DeleteStrategyAsync(string name);
|
||||
Task<Position> GetPositionByIdentifierAsync(string identifier);
|
||||
Task DeleteIndicatorAsync(string name);
|
||||
Task<Position> GetPositionByIdentifierAsync(Guid identifier);
|
||||
Task<Position> ManagePosition(Account account, Position position);
|
||||
|
||||
Task WatchTrader();
|
||||
Task<IEnumerable<Trader>> GetTradersWatch();
|
||||
Task UpdateScenarioAsync(Scenario scenario);
|
||||
Task UpdateStrategyAsync(Indicator indicator);
|
||||
Task UpdateIndicatorAsync(IndicatorBase indicatorBase);
|
||||
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);
|
||||
|
||||
// Synth API integration methods
|
||||
@@ -43,7 +49,7 @@ public interface ITradingService
|
||||
TradingBotConfig botConfig, bool isBacktest);
|
||||
|
||||
Task<SynthRiskResult> MonitorSynthPositionRiskAsync(Ticker ticker, TradeDirection direction, decimal currentPrice,
|
||||
decimal liquidationPrice, string positionIdentifier, TradingBotConfig botConfig);
|
||||
decimal liquidationPrice, Guid positionIdentifier, TradingBotConfig botConfig);
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
Dictionary<IndicatorType, IndicatorsResultBase> CalculateIndicatorsValuesAsync(
|
||||
Scenario scenario,
|
||||
List<Candle> candles);
|
||||
HashSet<Candle> candles);
|
||||
|
||||
Task<IndicatorBase?> GetIndicatorByNameUserAsync(string name, User user);
|
||||
Task<Scenario?> GetScenarioByNameUserAsync(string scenarioName, User user);
|
||||
}
|
||||
@@ -5,9 +5,12 @@ namespace Managing.Application.Abstractions.Services;
|
||||
public interface IUserService
|
||||
{
|
||||
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> UpdateAvatarUrl(User user, string avatarUrl);
|
||||
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();
|
||||
}
|
||||
@@ -15,5 +15,7 @@ namespace Managing.Application.Abstractions.Services
|
||||
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<List<Balance>> GetWalletBalanceAsync(string address, Ticker[] assets, string[] chains);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using Managing.Domain.Workers;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.Workers.Abstractions;
|
||||
namespace Managing.Application.Abstractions.Services;
|
||||
|
||||
public interface IWorkerService
|
||||
{
|
||||
@@ -3,15 +3,15 @@ using System.Diagnostics;
|
||||
using Managing.Application.Abstractions;
|
||||
using Managing.Application.Abstractions.Repositories;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.Backtesting;
|
||||
using Managing.Application.Bots.Base;
|
||||
using Managing.Application.Backtests;
|
||||
using Managing.Application.Hubs;
|
||||
using Managing.Application.ManageBot;
|
||||
using Managing.Core;
|
||||
using Managing.Domain.Bots;
|
||||
using Managing.Domain.Candles;
|
||||
using Managing.Domain.MoneyManagements;
|
||||
using Managing.Domain.Scenarios;
|
||||
using Managing.Domain.Strategies;
|
||||
using Managing.Domain.Strategies.Signals;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Moq;
|
||||
using Newtonsoft.Json;
|
||||
@@ -22,7 +22,6 @@ namespace Managing.Application.Tests
|
||||
{
|
||||
public class BotsTests : BaseTests
|
||||
{
|
||||
private readonly IBotFactory _botFactory;
|
||||
private readonly IBacktester _backtester;
|
||||
private readonly string _reportPath;
|
||||
private string _analysePath;
|
||||
@@ -37,19 +36,11 @@ namespace Managing.Application.Tests
|
||||
var scenarioService = new Mock<IScenarioService>().Object;
|
||||
var messengerService = new Mock<IMessengerService>().Object;
|
||||
var kaigenService = new Mock<IKaigenService>().Object;
|
||||
var backupBotService = new Mock<IBackupBotService>().Object;
|
||||
var hubContext = new Mock<IHubContext<BacktestHub>>().Object;
|
||||
var tradingBotLogger = TradingBaseTests.CreateTradingBotLogger();
|
||||
var backtestLogger = TradingBaseTests.CreateBacktesterLogger();
|
||||
var botService = new Mock<IBotService>().Object;
|
||||
_botFactory = new BotFactory(
|
||||
_exchangeService,
|
||||
tradingBotLogger,
|
||||
discordService,
|
||||
_accountService.Object,
|
||||
_tradingService.Object,
|
||||
botService, backupBotService);
|
||||
_backtester = new Backtester(_exchangeService, _botFactory, backtestRepository, backtestLogger,
|
||||
_backtester = new Backtester(_exchangeService, backtestRepository, backtestLogger,
|
||||
scenarioService, _accountService.Object, messengerService, kaigenService, hubContext, null);
|
||||
_elapsedTimes = new List<double>();
|
||||
|
||||
@@ -68,7 +59,6 @@ namespace Managing.Application.Tests
|
||||
// Arrange
|
||||
var scenario = new Scenario("FlippingScenario");
|
||||
var strategy = ScenarioHelpers.BuildIndicator(IndicatorType.RsiDivergence, "RsiDiv", period: 14);
|
||||
scenario.AddIndicator(strategy);
|
||||
var localCandles =
|
||||
FileHelpers.ReadJson<List<Candle>>($"{ticker.ToString()}-{timeframe.ToString()}-candles.json");
|
||||
|
||||
@@ -93,10 +83,11 @@ namespace Managing.Application.Tests
|
||||
|
||||
// Act
|
||||
var backtestResult =
|
||||
await _backtester.RunTradingBotBacktest(config, localCandles.TakeLast(500).ToList(), null, false);
|
||||
await _backtester.RunTradingBotBacktest(config, DateTime.UtcNow.AddDays(-6),
|
||||
DateTime.UtcNow, null, false, false);
|
||||
|
||||
var json = JsonConvert.SerializeObject(backtestResult, Formatting.None);
|
||||
File.WriteAllText($"{ticker.ToString()}-{timeframe.ToString()}-{Guid.NewGuid()}.json", json);
|
||||
File.WriteAllText($"{ticker}-{timeframe}-{Guid.NewGuid()}.json", json);
|
||||
// WriteCsvReport(backtestResult.GetStringReport());
|
||||
|
||||
// Assert
|
||||
@@ -119,8 +110,6 @@ namespace Managing.Application.Tests
|
||||
{
|
||||
// Arrange
|
||||
var scenario = new Scenario("ScalpingScenario");
|
||||
var strategy = ScenarioHelpers.BuildIndicator(IndicatorType.RsiDivergence, "RsiDiv", period: 5);
|
||||
scenario.AddIndicator(strategy);
|
||||
|
||||
var config = new TradingBotConfig
|
||||
{
|
||||
@@ -158,10 +147,13 @@ namespace Managing.Application.Tests
|
||||
int days)
|
||||
{
|
||||
// Arrange
|
||||
var scenario = new Scenario("ScalpingScenario");
|
||||
var strategy = ScenarioHelpers.BuildIndicator(IndicatorType.MacdCross, "RsiDiv", fastPeriods: 12,
|
||||
slowPeriods: 26, signalPeriods: 9);
|
||||
scenario.AddIndicator(strategy);
|
||||
var scenario = new Scenario("ScalpingScenario")
|
||||
{
|
||||
Indicators = new List<IndicatorBase>
|
||||
{
|
||||
new MacdCrossIndicatorBase("MacdCross", 12, 26, 9)
|
||||
}
|
||||
};
|
||||
|
||||
var moneyManagement = new MoneyManagement()
|
||||
{
|
||||
@@ -236,8 +228,10 @@ namespace Managing.Application.Tests
|
||||
Parallel.For((long)periodRange[0], periodRange[1], options, i =>
|
||||
{
|
||||
var scenario = new Scenario("ScalpingScenario");
|
||||
var strategy = ScenarioHelpers.BuildIndicator(indicatorType, "RsiDiv", period: (int)i);
|
||||
scenario.AddIndicator(strategy);
|
||||
scenario.Indicators = new List<IndicatorBase>
|
||||
{
|
||||
new RsiDivergenceIndicatorBase("RsiDiv", (int)i)
|
||||
};
|
||||
|
||||
// -0.5 to -5
|
||||
for (decimal s = stopLossRange[0]; s < stopLossRange[1]; s += stopLossRange[2])
|
||||
@@ -262,41 +256,43 @@ namespace Managing.Application.Tests
|
||||
{
|
||||
BotType.SimpleBot => throw new NotImplementedException(),
|
||||
BotType.ScalpingBot => _backtester.RunTradingBotBacktest(new TradingBotConfig
|
||||
{
|
||||
AccountName = _account.Name,
|
||||
MoneyManagement = moneyManagement,
|
||||
Ticker = ticker,
|
||||
Scenario = LightScenario.FromScenario(scenario),
|
||||
Timeframe = timeframe,
|
||||
IsForWatchingOnly = false,
|
||||
BotTradingBalance = 1000,
|
||||
IsForBacktest = true,
|
||||
CooldownPeriod = 1,
|
||||
MaxLossStreak = 0,
|
||||
FlipPosition = false,
|
||||
Name = "Test",
|
||||
FlipOnlyWhenInProfit = true,
|
||||
MaxPositionTimeHours = null,
|
||||
CloseEarlyWhenProfitable = false
|
||||
}, candles, null, false).Result,
|
||||
{
|
||||
AccountName = _account.Name,
|
||||
MoneyManagement = moneyManagement,
|
||||
Ticker = ticker,
|
||||
Scenario = LightScenario.FromScenario(scenario),
|
||||
Timeframe = timeframe,
|
||||
IsForWatchingOnly = false,
|
||||
BotTradingBalance = 1000,
|
||||
IsForBacktest = true,
|
||||
CooldownPeriod = 1,
|
||||
MaxLossStreak = 0,
|
||||
FlipPosition = false,
|
||||
Name = "Test",
|
||||
FlipOnlyWhenInProfit = true,
|
||||
MaxPositionTimeHours = null,
|
||||
CloseEarlyWhenProfitable = false
|
||||
}, DateTime.UtcNow.AddDays(-6),
|
||||
DateTime.UtcNow, null, false, false).Result,
|
||||
BotType.FlippingBot => _backtester.RunTradingBotBacktest(new TradingBotConfig
|
||||
{
|
||||
AccountName = _account.Name,
|
||||
MoneyManagement = moneyManagement,
|
||||
Ticker = ticker,
|
||||
Scenario = LightScenario.FromScenario(scenario),
|
||||
Timeframe = timeframe,
|
||||
IsForWatchingOnly = false,
|
||||
BotTradingBalance = 1000,
|
||||
IsForBacktest = true,
|
||||
CooldownPeriod = 1,
|
||||
MaxLossStreak = 0,
|
||||
FlipPosition = true,
|
||||
Name = "Test",
|
||||
FlipOnlyWhenInProfit = true,
|
||||
MaxPositionTimeHours = null,
|
||||
CloseEarlyWhenProfitable = false
|
||||
}, candles, null, false).Result,
|
||||
{
|
||||
AccountName = _account.Name,
|
||||
MoneyManagement = moneyManagement,
|
||||
Ticker = ticker,
|
||||
Scenario = LightScenario.FromScenario(scenario),
|
||||
Timeframe = timeframe,
|
||||
IsForWatchingOnly = false,
|
||||
BotTradingBalance = 1000,
|
||||
IsForBacktest = true,
|
||||
CooldownPeriod = 1,
|
||||
MaxLossStreak = 0,
|
||||
FlipPosition = true,
|
||||
Name = "Test",
|
||||
FlipOnlyWhenInProfit = true,
|
||||
MaxPositionTimeHours = null,
|
||||
CloseEarlyWhenProfitable = false
|
||||
}, DateTime.UtcNow.AddDays(-6),
|
||||
DateTime.UtcNow, null, false, false).Result,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
timer.Stop();
|
||||
@@ -376,9 +372,10 @@ namespace Managing.Application.Tests
|
||||
return;
|
||||
|
||||
var scenario = new Scenario("ScalpingScenario");
|
||||
var strategy = ScenarioHelpers.BuildIndicator(indicatorType, "RsiDiv", fastPeriods: 12,
|
||||
slowPeriods: 26, signalPeriods: 9);
|
||||
scenario.AddIndicator(strategy);
|
||||
scenario.Indicators = new List<IndicatorBase>
|
||||
{
|
||||
new MacdCrossIndicatorBase("MacdCross", 12, 26, 9)
|
||||
};
|
||||
|
||||
// -0.5 to -5
|
||||
for (decimal s = stopLossRange[0]; s < stopLossRange[1]; s += stopLossRange[2])
|
||||
@@ -402,41 +399,43 @@ namespace Managing.Application.Tests
|
||||
{
|
||||
BotType.SimpleBot => throw new NotImplementedException(),
|
||||
BotType.ScalpingBot => _backtester.RunTradingBotBacktest(new TradingBotConfig
|
||||
{
|
||||
AccountName = _account.Name,
|
||||
MoneyManagement = moneyManagement,
|
||||
Ticker = ticker,
|
||||
Scenario = LightScenario.FromScenario(scenario),
|
||||
Timeframe = timeframe,
|
||||
IsForWatchingOnly = false,
|
||||
BotTradingBalance = 1000,
|
||||
IsForBacktest = true,
|
||||
CooldownPeriod = 1,
|
||||
MaxLossStreak = 0,
|
||||
FlipPosition = false,
|
||||
Name = "Test",
|
||||
FlipOnlyWhenInProfit = true,
|
||||
MaxPositionTimeHours = null,
|
||||
CloseEarlyWhenProfitable = false
|
||||
}, candles, null).Result,
|
||||
{
|
||||
AccountName = _account.Name,
|
||||
MoneyManagement = moneyManagement,
|
||||
Ticker = ticker,
|
||||
Scenario = LightScenario.FromScenario(scenario),
|
||||
Timeframe = timeframe,
|
||||
IsForWatchingOnly = false,
|
||||
BotTradingBalance = 1000,
|
||||
IsForBacktest = true,
|
||||
CooldownPeriod = 1,
|
||||
MaxLossStreak = 0,
|
||||
FlipPosition = false,
|
||||
Name = "Test",
|
||||
FlipOnlyWhenInProfit = true,
|
||||
MaxPositionTimeHours = null,
|
||||
CloseEarlyWhenProfitable = false
|
||||
}, DateTime.UtcNow.AddDays(-6),
|
||||
DateTime.UtcNow, null, false, false).Result,
|
||||
BotType.FlippingBot => _backtester.RunTradingBotBacktest(new TradingBotConfig
|
||||
{
|
||||
AccountName = _account.Name,
|
||||
MoneyManagement = moneyManagement,
|
||||
Ticker = ticker,
|
||||
Scenario = LightScenario.FromScenario(scenario),
|
||||
Timeframe = timeframe,
|
||||
IsForWatchingOnly = false,
|
||||
BotTradingBalance = 1000,
|
||||
IsForBacktest = true,
|
||||
CooldownPeriod = 1,
|
||||
MaxLossStreak = 0,
|
||||
FlipPosition = true,
|
||||
Name = "Test",
|
||||
FlipOnlyWhenInProfit = true,
|
||||
MaxPositionTimeHours = null,
|
||||
CloseEarlyWhenProfitable = false
|
||||
}, candles, null).Result,
|
||||
{
|
||||
AccountName = _account.Name,
|
||||
MoneyManagement = moneyManagement,
|
||||
Ticker = ticker,
|
||||
Scenario = LightScenario.FromScenario(scenario),
|
||||
Timeframe = timeframe,
|
||||
IsForWatchingOnly = false,
|
||||
BotTradingBalance = 1000,
|
||||
IsForBacktest = true,
|
||||
CooldownPeriod = 1,
|
||||
MaxLossStreak = 0,
|
||||
FlipPosition = true,
|
||||
Name = "Test",
|
||||
FlipOnlyWhenInProfit = true,
|
||||
MaxPositionTimeHours = null,
|
||||
CloseEarlyWhenProfitable = false
|
||||
}, DateTime.UtcNow.AddDays(-6),
|
||||
DateTime.UtcNow, null, false, false).Result,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
|
||||
@@ -673,7 +672,8 @@ namespace Managing.Application.Tests
|
||||
CloseEarlyWhenProfitable = false
|
||||
};
|
||||
|
||||
var backtestResult = _backtester.RunTradingBotBacktest(config, candles, null).Result;
|
||||
var backtestResult = _backtester.RunTradingBotBacktest(config, DateTime.UtcNow.AddDays(-6),
|
||||
DateTime.UtcNow, null, false, false).Result;
|
||||
|
||||
timer.Stop();
|
||||
|
||||
@@ -1042,8 +1042,13 @@ namespace Managing.Application.Tests
|
||||
{
|
||||
foreach (var parameterSet in strategyConfig.ParameterSets)
|
||||
{
|
||||
var scenario = BuildScenario($"{strategyConfig.Name}_{parameterSet.Name}",
|
||||
new[] { (strategyConfig, parameterSet) });
|
||||
var scenario = new Scenario($"{strategyConfig.Name}_{parameterSet.Name}")
|
||||
{
|
||||
Indicators = new List<IndicatorBase>
|
||||
{
|
||||
new RsiDivergenceIndicatorBase("RsiDiv", (int)parameterSet.Period)
|
||||
}
|
||||
};
|
||||
scenarios.Add(scenario);
|
||||
}
|
||||
}
|
||||
@@ -1068,7 +1073,13 @@ namespace Managing.Application.Tests
|
||||
{
|
||||
var scenarioName = string.Join("_",
|
||||
paramCombo.Select(p => $"{p.strategyConfig.Name}_{p.parameterSet.Name}"));
|
||||
var scenario = BuildScenario(scenarioName, paramCombo);
|
||||
var scenario = new Scenario(scenarioName)
|
||||
{
|
||||
Indicators = new List<IndicatorBase>
|
||||
{
|
||||
new RsiDivergenceIndicatorBase("RsiDiv", (int)paramCombo.First().parameterSet.Period)
|
||||
}
|
||||
};
|
||||
scenario.LoopbackPeriod = 15;
|
||||
scenarios.Add(scenario);
|
||||
}
|
||||
@@ -1077,31 +1088,6 @@ namespace Managing.Application.Tests
|
||||
return scenarios;
|
||||
}
|
||||
|
||||
private Scenario BuildScenario(string scenarioName,
|
||||
IEnumerable<(StrategyConfiguration strategyConfig, ParameterSet parameterSet)> strategyParams)
|
||||
{
|
||||
var scenario = new Scenario(scenarioName);
|
||||
|
||||
foreach (var (strategyConfig, parameterSet) in strategyParams)
|
||||
{
|
||||
var strategy = ScenarioHelpers.BuildIndicator(
|
||||
strategyConfig.Type,
|
||||
$"{strategyConfig.Name}_{parameterSet.Name}",
|
||||
period: parameterSet.Period,
|
||||
fastPeriods: parameterSet.FastPeriods,
|
||||
slowPeriods: parameterSet.SlowPeriods,
|
||||
signalPeriods: parameterSet.SignalPeriods,
|
||||
multiplier: parameterSet.Multiplier,
|
||||
stochPeriods: parameterSet.StochPeriods,
|
||||
smoothPeriods: parameterSet.SmoothPeriods,
|
||||
cyclePeriods: parameterSet.CyclePeriods);
|
||||
|
||||
scenario.AddIndicator(strategy);
|
||||
}
|
||||
|
||||
return scenario;
|
||||
}
|
||||
|
||||
private IEnumerable<IEnumerable<T>> GetCombinations<T>(IEnumerable<T> elements, int k)
|
||||
{
|
||||
return k == 0
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Domain.Accounts;
|
||||
using Managing.Domain.Candles;
|
||||
using Managing.Domain.Indicators;
|
||||
using Managing.Domain.Strategies.Signals;
|
||||
using Managing.Domain.Strategies.Trends;
|
||||
using Xunit;
|
||||
@@ -7,31 +9,30 @@ using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.Tests
|
||||
{
|
||||
public class IndicatorTests
|
||||
public class IndicatorBaseTests
|
||||
{
|
||||
private readonly IExchangeService _exchangeService;
|
||||
|
||||
public IndicatorTests()
|
||||
public IndicatorBaseTests()
|
||||
{
|
||||
_exchangeService = TradingBaseTests.GetExchangeService();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(TradingExchanges.Binance, Ticker.ADA, Timeframe.OneDay)]
|
||||
public void Should_Return_Signal_On_Rsi_BullishDivergence2(TradingExchanges exchange, Ticker ticker,
|
||||
public async Task Should_Return_Signal_On_Rsi_BullishDivergence2(TradingExchanges exchange, Ticker ticker,
|
||||
Timeframe timeframe)
|
||||
{
|
||||
var account = GetAccount(exchange);
|
||||
// Arrange
|
||||
var rsiStrategy = new RsiDivergenceIndicator("unittest", 5);
|
||||
var candles = _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(-50), timeframe).Result;
|
||||
var rsiStrategy = new RsiDivergenceIndicatorBase("unittest", 5);
|
||||
var candles = await _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(-50), timeframe);
|
||||
var resultSignal = new List<LightSignal>();
|
||||
|
||||
// Act
|
||||
foreach (var candle in candles)
|
||||
{
|
||||
rsiStrategy.Candles.Enqueue(candle);
|
||||
var signals = rsiStrategy.Run();
|
||||
var signals = rsiStrategy.Run(new HashSet<Candle> { candle });
|
||||
}
|
||||
|
||||
if (rsiStrategy.Signals != null && rsiStrategy.Signals.Count > 0)
|
||||
@@ -52,20 +53,19 @@ namespace Managing.Application.Tests
|
||||
|
||||
[Theory]
|
||||
[InlineData(TradingExchanges.Binance, Ticker.ADA, Timeframe.OneDay)]
|
||||
public void Shoud_Return_Signal_On_Rsi_BearishDivergence(TradingExchanges exchange, Ticker ticker,
|
||||
public async Task Shoud_Return_Signal_On_Rsi_BearishDivergence(TradingExchanges exchange, Ticker ticker,
|
||||
Timeframe timeframe)
|
||||
{
|
||||
// Arrange
|
||||
var account = GetAccount(exchange);
|
||||
var rsiStrategy = new RsiDivergenceIndicator("unittest", 5);
|
||||
var candles = _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(-50), timeframe).Result;
|
||||
var rsiStrategy = new RsiDivergenceIndicatorBase("unittest", 5);
|
||||
var candles = await _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(-50), timeframe);
|
||||
var resultSignal = new List<LightSignal>();
|
||||
|
||||
// Act
|
||||
foreach (var candle in candles)
|
||||
{
|
||||
rsiStrategy.Candles.Enqueue(candle);
|
||||
var signals = rsiStrategy.Run();
|
||||
var signals = rsiStrategy.Run(new HashSet<Candle> { candle });
|
||||
}
|
||||
|
||||
if (rsiStrategy.Signals != null && rsiStrategy.Signals.Count > 0)
|
||||
@@ -84,15 +84,14 @@ namespace Managing.Application.Tests
|
||||
{
|
||||
// Arrange
|
||||
var account = GetAccount(exchange);
|
||||
var rsiStrategy = new MacdCrossIndicator("unittest", 12, 26, 9);
|
||||
var rsiStrategy = new MacdCrossIndicatorBase("unittest", 12, 26, 9);
|
||||
var candles = await _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(days), timeframe);
|
||||
var resultSignal = new List<LightSignal>();
|
||||
|
||||
// Act
|
||||
foreach (var candle in candles)
|
||||
{
|
||||
rsiStrategy.Candles.Enqueue(candle);
|
||||
var signals = rsiStrategy.Run();
|
||||
var signals = rsiStrategy.Run(new HashSet<Candle> { candle });
|
||||
}
|
||||
|
||||
if (rsiStrategy.Signals != null && rsiStrategy.Signals.Count > 0)
|
||||
@@ -106,20 +105,20 @@ namespace Managing.Application.Tests
|
||||
|
||||
[Theory]
|
||||
[InlineData(TradingExchanges.Ftx, Ticker.ADA, Timeframe.OneDay, -500)]
|
||||
public void Shoud_Return_Signal_On_SuperTrend(TradingExchanges exchange, Ticker ticker, Timeframe timeframe,
|
||||
public async Task Shoud_Return_Signal_On_SuperTrend(TradingExchanges exchange, Ticker ticker,
|
||||
Timeframe timeframe,
|
||||
int days)
|
||||
{
|
||||
// Arrange
|
||||
var account = GetAccount(exchange);
|
||||
var superTrendStrategy = new SuperTrendIndicator("unittest", 10, 3);
|
||||
var candles = _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(days), timeframe).Result;
|
||||
var superTrendStrategy = new SuperTrendIndicatorBase("unittest", 10, 3);
|
||||
var candles = await _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(days), timeframe);
|
||||
var resultSignal = new List<LightSignal>();
|
||||
|
||||
// Act
|
||||
foreach (var candle in candles)
|
||||
{
|
||||
superTrendStrategy.Candles.Enqueue(candle);
|
||||
var signals = superTrendStrategy.Run();
|
||||
var signals = superTrendStrategy.Run(new HashSet<Candle> { candle });
|
||||
}
|
||||
|
||||
if (superTrendStrategy.Signals != null && superTrendStrategy.Signals.Count > 0)
|
||||
@@ -133,21 +132,20 @@ namespace Managing.Application.Tests
|
||||
|
||||
[Theory]
|
||||
[InlineData(TradingExchanges.Ftx, Ticker.ADA, Timeframe.OneDay, -500)]
|
||||
public void Shoud_Return_Signal_On_ChandelierExist(TradingExchanges exchange, Ticker ticker,
|
||||
public async Task Shoud_Return_Signal_On_ChandelierExist(TradingExchanges exchange, Ticker ticker,
|
||||
Timeframe timeframe, int days)
|
||||
{
|
||||
// Arrange
|
||||
var account = GetAccount(exchange);
|
||||
var chandelierExitStrategy = new ChandelierExitIndicator("unittest", 22, 3);
|
||||
var candles = _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(days), timeframe, false)
|
||||
.Result;
|
||||
var chandelierExitStrategy = new ChandelierExitIndicatorBase("unittest", 22, 3);
|
||||
var candles =
|
||||
await _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(days), timeframe, false);
|
||||
var resultSignal = new List<LightSignal>();
|
||||
|
||||
// Act
|
||||
foreach (var candle in candles)
|
||||
{
|
||||
chandelierExitStrategy.Candles.Enqueue(candle);
|
||||
var signals = chandelierExitStrategy.Run();
|
||||
var signals = chandelierExitStrategy.Run(new HashSet<Candle> { candle });
|
||||
}
|
||||
|
||||
if (chandelierExitStrategy.Signals is { Count: > 0 })
|
||||
@@ -161,20 +159,19 @@ namespace Managing.Application.Tests
|
||||
|
||||
[Theory]
|
||||
[InlineData(TradingExchanges.Ftx, Ticker.ADA, Timeframe.OneDay, -500)]
|
||||
public void Shoud_Return_Signal_On_EmaTrend(TradingExchanges exchange, Ticker ticker, Timeframe timeframe,
|
||||
public async Task Shoud_Return_Signal_On_EmaTrend(TradingExchanges exchange, Ticker ticker, Timeframe timeframe,
|
||||
int days)
|
||||
{
|
||||
// Arrange
|
||||
var account = GetAccount(exchange);
|
||||
var emaTrendSrategy = new EmaTrendIndicator("unittest", 200);
|
||||
var candles = _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(days), timeframe).Result;
|
||||
var emaTrendSrategy = new EmaTrendIndicatorBase("unittest", 200);
|
||||
var candles = await _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(days), timeframe);
|
||||
var resultSignal = new List<LightSignal>();
|
||||
|
||||
// Act
|
||||
foreach (var candle in candles)
|
||||
{
|
||||
emaTrendSrategy.Candles.Enqueue(candle);
|
||||
var signals = emaTrendSrategy.Run();
|
||||
var signals = emaTrendSrategy.Run(new HashSet<Candle> { candle });
|
||||
}
|
||||
|
||||
if (emaTrendSrategy.Signals != null && emaTrendSrategy.Signals.Count > 0)
|
||||
@@ -189,13 +186,13 @@ namespace Managing.Application.Tests
|
||||
|
||||
[Theory]
|
||||
[InlineData(TradingExchanges.Evm, Ticker.BTC, Timeframe.FifteenMinutes, -50)]
|
||||
public void Shoud_Return_Signal_On_StochRsi(TradingExchanges exchange, Ticker ticker, Timeframe timeframe,
|
||||
public async Task Shoud_Return_Signal_On_StochRsi(TradingExchanges exchange, Ticker ticker, Timeframe timeframe,
|
||||
int days)
|
||||
{
|
||||
// Arrange
|
||||
var account = GetAccount(exchange);
|
||||
var stochRsiStrategy = new StochRsiTrendIndicator("unittest", 14, 14, 3, 1);
|
||||
var candles = _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(days), timeframe).Result;
|
||||
var stochRsiStrategy = new StochRsiTrendIndicatorBase("unittest", 14, 14, 3, 1);
|
||||
var candles = await _exchangeService.GetCandles(account, ticker, DateTime.Now.AddDays(days), timeframe);
|
||||
var resultSignal = new List<LightSignal>();
|
||||
|
||||
// var json = JsonConvert.SerializeObject(candles);
|
||||
@@ -205,8 +202,7 @@ namespace Managing.Application.Tests
|
||||
// Act
|
||||
foreach (var candle in candles)
|
||||
{
|
||||
stochRsiStrategy.Candles.Enqueue(candle);
|
||||
var signals = stochRsiStrategy.Run();
|
||||
var signals = stochRsiStrategy.Run(new HashSet<Candle> { candle });
|
||||
}
|
||||
|
||||
if (stochRsiStrategy.Signals != null && stochRsiStrategy.Signals.Count > 0)
|
||||
@@ -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.Users;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
using static Managing.Common.Enums;
|
||||
@@ -45,19 +46,22 @@ public class PositionTests : BaseTests
|
||||
// _ = new GetAccountPositioqwnInfoListOutputDTO().DecodeOutput(hexPositions).d
|
||||
//
|
||||
var openTrade = await _exchangeService.GetTrade(_account, "", Ticker.GMX);
|
||||
var position = new Position("", "", TradeDirection.Long, Ticker.GMX, MoneyManagement, PositionInitiator.User,
|
||||
var position = new Position(Guid.NewGuid(), "", TradeDirection.Long, Ticker.GMX, MoneyManagement,
|
||||
PositionInitiator.User,
|
||||
DateTime.UtcNow, new User())
|
||||
{
|
||||
Open = openTrade
|
||||
};
|
||||
var command = new ClosePositionCommand(position);
|
||||
_ = _tradingService.Setup(m => m.GetPositionByIdentifierAsync(It.IsAny<string>())).ReturnsAsync(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<Guid>())).ReturnsAsync(position);
|
||||
|
||||
var mockScope = new Mock<IServiceScopeFactory>();
|
||||
var handler = new ClosePositionCommandHandler(
|
||||
_exchangeService,
|
||||
_accountService.Object,
|
||||
_tradingService.Object);
|
||||
_tradingService.Object,
|
||||
mockScope.Object);
|
||||
|
||||
var closedPosition = await handler.Handle(command);
|
||||
Assert.NotNull(closedPosition);
|
||||
|
||||
@@ -214,7 +214,7 @@ namespace Managing.Application.Tests
|
||||
|
||||
private static Position GetFakeShortPosition()
|
||||
{
|
||||
return new Position("", "FakeAccount", TradeDirection.Short, Ticker.BTC, null,
|
||||
return new Position(Guid.NewGuid(), "FakeAccount", TradeDirection.Short, Ticker.BTC, null,
|
||||
PositionInitiator.PaperTrading, DateTime.UtcNow, new User())
|
||||
{
|
||||
Open = new Trade(DateTime.Now, TradeDirection.Short, TradeStatus.Filled,
|
||||
@@ -230,7 +230,7 @@ namespace Managing.Application.Tests
|
||||
|
||||
private static Position GetSolanaLongPosition()
|
||||
{
|
||||
return new Position("", "FakeAccount", TradeDirection.Long, Ticker.BTC, null,
|
||||
return new Position(Guid.NewGuid(), "FakeAccount", TradeDirection.Long, Ticker.BTC, null,
|
||||
PositionInitiator.PaperTrading, DateTime.UtcNow, new User())
|
||||
{
|
||||
Open = new Trade(DateTime.Now, TradeDirection.Long, TradeStatus.Filled,
|
||||
@@ -250,7 +250,7 @@ namespace Managing.Application.Tests
|
||||
|
||||
private static Position GetFakeLongPosition()
|
||||
{
|
||||
return new Position("", "FakeAccount", TradeDirection.Long, Ticker.BTC, null,
|
||||
return new Position(Guid.NewGuid(), "FakeAccount", TradeDirection.Long, Ticker.BTC, null,
|
||||
PositionInitiator.PaperTrading, DateTime.UtcNow, new User())
|
||||
{
|
||||
Open = new Trade(DateTime.Now, TradeDirection.Short, TradeStatus.Filled,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Managing.Application.Abstractions.Repositories;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.Backtesting;
|
||||
using Managing.Application.Backtests;
|
||||
using Managing.Application.Bots;
|
||||
using Managing.Infrastructure.Databases;
|
||||
using Managing.Infrastructure.Databases.InfluxDb;
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
using Managing.Domain.Workflows;
|
||||
using Xunit;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.Tests;
|
||||
|
||||
public class WorkflowTests : BaseTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Should_Create_Workflow_with_Feed_Ticker_Flow()
|
||||
{
|
||||
// Arrange
|
||||
var workflow = new Workflow
|
||||
{
|
||||
Name = "Bot trading",
|
||||
Usage = WorkflowUsage.Trading,
|
||||
Description = "Basic trading Workflow",
|
||||
Flows = new List<IFlow>()
|
||||
};
|
||||
|
||||
// var rsiDivFlow = new RsiDiv()
|
||||
// {
|
||||
// Parameters = "{\"Period\": 14,\"Timeframe\":1}",
|
||||
// Children = new List<IFlow>(),
|
||||
// };
|
||||
|
||||
// var tickerFlow = new FeedTicker(_exchangeService)
|
||||
// {
|
||||
// Parameters = "{\"Exchange\": 3,\"Ticker\":9,\"Timeframe\":1}",
|
||||
// Children = new List<IFlow>()
|
||||
// {
|
||||
// rsiDivFlow
|
||||
// }
|
||||
// };
|
||||
|
||||
// workflow.Flows.Add(tickerFlow);
|
||||
|
||||
// Act
|
||||
await workflow.Execute();
|
||||
|
||||
// Assert
|
||||
foreach (var f in workflow.Flows)
|
||||
{
|
||||
Assert.False(string.IsNullOrEmpty(f.Output));
|
||||
}
|
||||
|
||||
Assert.NotNull(workflow);
|
||||
Assert.NotNull(workflow.Flows);
|
||||
Assert.Single(workflow.Flows);
|
||||
Assert.Equal("Feed Ticker", workflow.Name);
|
||||
Assert.Equal(WorkflowUsage.Trading, workflow.Usage);
|
||||
Assert.Equal("Basic trading Workflow", workflow.Description);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.3" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Managing.Application.Abstractions\Managing.Application.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\Managing.Domain\Managing.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
</Project>
|
||||
27
src/Managing.Application/Abstractions/Grains/IAgentGrain.cs
Normal file
27
src/Managing.Application/Abstractions/Grains/IAgentGrain.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
namespace Managing.Application.Abstractions.Grains
|
||||
{
|
||||
public interface IAgentGrain : IGrainWithIntegerKey
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes the agent grain with user-specific data.
|
||||
/// </summary>
|
||||
/// <param name="userId">The ID of the user (used as grain key).</param>
|
||||
/// <param name="agentName">The display name of the agent.</param>
|
||||
Task InitializeAsync(int userId, string agentName);
|
||||
|
||||
/// <summary>
|
||||
/// Generates a summary of the agent's stats for the AgentRegistryGrain.
|
||||
/// </summary>
|
||||
Task UpdateSummary();
|
||||
|
||||
/// <summary>
|
||||
/// Registers a new bot with this agent.
|
||||
/// </summary>
|
||||
Task RegisterBotAsync(Guid botId);
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters a bot from this agent.
|
||||
/// </summary>
|
||||
Task UnregisterBotAsync(Guid botId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Managing.Domain.Bots;
|
||||
using Managing.Domain.Candles;
|
||||
using Managing.Domain.Indicators;
|
||||
|
||||
namespace Managing.Application.Abstractions.Grains;
|
||||
|
||||
/// <summary>
|
||||
/// Orleans grain interface for scenario execution and signal generation.
|
||||
/// This stateless grain handles candle management and signal generation for live trading.
|
||||
/// </summary>
|
||||
public interface IScenarioRunnerGrain : IGrainWithGuidKey
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates signals based on the current candles and scenario
|
||||
/// </summary>
|
||||
/// <param name="config">The trading bot configuration</param>
|
||||
/// <param name="previousSignals">Previous signals to consider</param>
|
||||
/// <param name="startDate">Start date</param>
|
||||
/// <returns>The generated signal or null if no signal</returns>
|
||||
Task<LightSignal> GetSignals(TradingBotConfig config, Dictionary<string, LightSignal> previousSignals, DateTime startDate,
|
||||
Candle candle);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
using Managing.Domain.Bots;
|
||||
|
||||
namespace Managing.Application.Abstractions
|
||||
{
|
||||
public interface IBotFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a trading bot using the unified TradingBot class
|
||||
/// </summary>
|
||||
/// <param name="config">The trading bot configuration</param>
|
||||
/// <returns>ITradingBot instance</returns>
|
||||
Task<ITradingBot> CreateTradingBot(TradingBotConfig config);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a trading bot for backtesting using the unified TradingBot class
|
||||
/// </summary>
|
||||
/// <param name="config">The trading bot configuration</param>
|
||||
/// <returns>ITradingBot instance configured for backtesting</returns>
|
||||
Task<ITradingBot> CreateBacktestTradingBot(TradingBotConfig config);
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,47 @@
|
||||
using Managing.Domain.Bots;
|
||||
using Managing.Domain.Users;
|
||||
using Managing.Domain.Workflows;
|
||||
using Managing.Domain.Trades;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.Abstractions;
|
||||
|
||||
public interface IBotService
|
||||
{
|
||||
Task SaveOrUpdateBotBackup(User user, string identifier, BotStatus status, TradingBotBackup data);
|
||||
void AddSimpleBotToCache(IBot bot);
|
||||
void AddTradingBotToCache(ITradingBot bot);
|
||||
List<ITradingBot> GetActiveBots();
|
||||
Task<IEnumerable<BotBackup>> GetSavedBotsAsync();
|
||||
Task StartBotFromBackup(BotBackup backupBot);
|
||||
Task<BotBackup> GetBotBackup(string identifier);
|
||||
Task<IEnumerable<Bot>> GetBotsAsync();
|
||||
Task<IEnumerable<Bot>> GetBotsByStatusAsync(BotStatus status);
|
||||
Task<BotStatus> StopBot(Guid identifier);
|
||||
Task<BotStatus> RestartBot(Guid identifier);
|
||||
Task<bool> DeleteBot(Guid identifier);
|
||||
Task<bool> UpdateBotConfiguration(Guid identifier, TradingBotConfig newConfig);
|
||||
Task<IEnumerable<string>> GetActiveBotsNamesAsync();
|
||||
Task<IEnumerable<Bot>> GetBotsByUser(int id);
|
||||
Task<IEnumerable<Bot>> GetBotsByIdsAsync(IEnumerable<Guid> botIds);
|
||||
Task<Bot> GetBotByName(string name);
|
||||
Task<Bot> GetBotByIdentifier(Guid identifier);
|
||||
Task<Position> OpenPositionManuallyAsync(Guid identifier, TradeDirection direction);
|
||||
Task<Position> ClosePositionAsync(Guid identifier, Guid positionId);
|
||||
Task<TradingBotConfig> GetBotConfig(Guid identifier);
|
||||
Task<bool> UpdateBotStatisticsAsync(Guid identifier);
|
||||
Task<bool> SaveBotStatisticsAsync(Bot bot);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a trading bot using the unified TradingBot class
|
||||
/// Gets paginated bots with filtering and sorting
|
||||
/// </summary>
|
||||
/// <param name="config">The trading bot configuration</param>
|
||||
/// <returns>ITradingBot instance</returns>
|
||||
Task<ITradingBot> CreateTradingBot(TradingBotConfig config);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a trading bot for backtesting using the unified TradingBot class
|
||||
/// </summary>
|
||||
/// <param name="config">The trading bot configuration</param>
|
||||
/// <returns>ITradingBot instance configured for backtesting</returns>
|
||||
Task<ITradingBot> CreateBacktestTradingBot(TradingBotConfig config);
|
||||
|
||||
IBot CreateSimpleBot(string botName, Workflow workflow);
|
||||
Task<string> StopBot(string botName);
|
||||
Task<bool> DeleteBot(string botName);
|
||||
Task<string> RestartBot(string botName);
|
||||
Task ToggleIsForWatchingOnly(string botName);
|
||||
Task<bool> UpdateBotConfiguration(string identifier, TradingBotConfig newConfig);
|
||||
/// <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="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");
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user