Compare commits

..

71 Commits

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

* Fix a bit more of the trading bot

* Advance on the tradingbot grain

* Fix build

* Fix db script

* Fix user login

* Fix a bit backtest

* Fix cooldown and backtest

* start fixing bot start

* Fix startup

* Setup local db

* Fix build and update candles and scenario

* Add bot registry

* Add reminder

* Updateing the grains

* fix bootstraping

* Save stats on tick

* Save bot data every tick

* Fix serialization

* fix save bot stats

* Fix get candles

* use dict instead of list for position

* Switch hashset to dict

* Fix a bit

* Fix bot launch and bot view

* add migrations

* Remove the tolist

* Add agent grain

* Save agent summary

* clean

* Add save bot

* Update get bots

* Add get bots

* Fix stop/restart

* fix Update config

* Update scanner table on new backtest saved

* Fix backtestRowDetails.tsx

* Fix agentIndex

* Update agentIndex

* Fix more things

* Update user cache

* Fix

* Fix account load/start/restart/run
2025-08-05 04:07:06 +07:00
cd378587aa Add wallet balances to the userStrategy 2025-07-31 21:34:05 +07:00
5fabfbfadd fix backtest credit 2025-07-31 20:58:37 +07:00
857ca348ba Add agentNames to the endpoint index 2025-07-31 16:51:26 +07:00
6cd28a4edb Return only online agent name 2025-07-31 15:55:03 +07:00
c454e87d7a Add new endpoint for the agent status 2025-07-30 22:36:49 +07:00
4b0da0e864 Add agent index with pagination 2025-07-30 22:27:01 +07:00
360 changed files with 16519 additions and 16360 deletions

View File

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

View File

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

View File

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

View File

@@ -107,22 +107,6 @@
- [x] Add button to display money management use by the bot
- [ ] 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,9 +416,19 @@ 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..."
# 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)
@@ -439,11 +448,18 @@ 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..."
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"
# 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..."
@@ -455,15 +471,42 @@ for attempt in 1 2 3; do
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..."
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"
# 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..."
@@ -475,6 +518,26 @@ for attempt in 1 2 3; do
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
done
@@ -484,6 +547,61 @@ if [ "$BACKUP_SUCCESS" != "true" ]; then
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)
log "🔄 Step 3: Running database migration (final application of pending migrations)..."
@@ -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..."
# 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 "Migration script generated: $(basename "$MIGRATION_SCRIPT")"
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,17 +780,16 @@ if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef mig
fi
fi
# Clean up migration script after successful application
rm -f "$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 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"
# 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
# Clean up temporary migration script after successful application
rm -f "$MIGRATION_SCRIPT"
# Step 4: Verify Migration
log "🔍 Step 4: Verifying migration status..."

View File

@@ -15,7 +15,6 @@ COPY ["/src/Managing.Common/Managing.Common.csproj", "Managing.Common/"]
COPY ["/src/Managing.Core/Managing.Core.csproj", "Managing.Core/"]
COPY ["/src/Managing.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/"]

View File

@@ -15,7 +15,6 @@ COPY ["/src/Managing.Common/Managing.Common.csproj", "Managing.Common/"]
COPY ["/src/Managing.Core/Managing.Core.csproj", "Managing.Core/"]
COPY ["/src/Managing.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/"]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,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": "*"
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
using Managing.Api.Models.Requests;
using Managing.Api.Extensions;
using Managing.Api.Models.Requests;
using Managing.Api.Models.Responses;
using Managing.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");
string cacheKey = $"PlatformSummary_{timeFilter}";
// Get the platform summary from the grain (handles caching and real-time updates)
var abstractionsSummary = await platformSummaryGrain.GetPlatformSummaryAsync();
// Check if the platform summary is already cached
var cachedSummary = _cacheService.GetValue<PlatformSummaryViewModel>(cacheKey);
if (cachedSummary != null)
{
return Ok(cachedSummary);
}
// 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));
// Convert to API ViewModel
var summary = abstractionsSummary.ToApiViewModel();
return Ok(summary);
}
catch (Exception ex)
{
// Log the error and return a fallback response
// In production, you might want to return cached data or partial data
return StatusCode(500, $"Error retrieving platform summary: {ex.Message}");
}
}
/// <summary>
/// Retrieves a 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
};
// Create summaries for each agent
foreach (var agent in agentsWithStrategies)
{
var user = agent.Key;
var strategies = agent.Value;
if (strategies.Count == 0)
{
continue; // Skip agents with no strategies
return BadRequest("Sort order must be 'asc' or 'desc'");
}
// Combine all positions from all strategies
var allPositions = strategies.SelectMany<ITradingBot, Position>(s => s.Positions).ToList();
// Parse agent names filter
IEnumerable<string>? agentNamesList = null;
if (!string.IsNullOrWhiteSpace(agentNames))
{
agentNamesList = agentNames.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(name => name.Trim())
.Where(name => !string.IsNullOrWhiteSpace(name))
.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,

View File

@@ -197,23 +197,23 @@ public class ScenarioController : BaseController
};
}
private static IndicatorViewModel MapToIndicatorViewModel(Indicator indicator)
private static IndicatorViewModel MapToIndicatorViewModel(IndicatorBase indicatorBase)
{
return new IndicatorViewModel
{
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
};
}
}

View File

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

View File

@@ -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)
{
@@ -121,7 +127,8 @@ public class UserController : BaseController
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()
{
@@ -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
}
}
}

View File

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

View File

@@ -15,7 +15,6 @@ COPY ["Managing.Common/Managing.Common.csproj", "Managing.Common/"]
COPY ["Managing.Core/Managing.Core.csproj", "Managing.Core/"]
COPY ["Managing.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/"]

View File

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

View File

@@ -53,5 +53,8 @@
<Content Update="appsettings.Production.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="appsettings.KaiServer.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View File

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

View File

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

View File

@@ -1,3 +1,6 @@
using Managing.Application.Abstractions.Models;
using Managing.Common;
namespace Managing.Api.Models.Responses
{
/// <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>

View File

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

View File

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

View File

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

View File

@@ -14,6 +14,34 @@ namespace Managing.Api.Models.Responses
/// Profit and Loss value of the strategy
/// </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>();
}
}

View File

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

View File

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

View File

@@ -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,7 +157,13 @@ 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 =>
builder.Services
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(o =>
{
o.SaveToken = true;
o.TokenValidationParameters = new TokenValidationParameters
@@ -169,8 +176,52 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJw
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.AddAuthorization();
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);
if (!deploymentMode)
{
Console.WriteLine("Application starting in normal mode...");
app.Run();
}
else
{
Console.WriteLine("Application configured for deployment mode - skipping app.Run()");
Console.WriteLine("All services have been configured and the application is ready for deployment.");
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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": "*"
}

View File

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

View File

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

View File

@@ -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": "*"
}

View File

@@ -1,4 +1,8 @@
{
"PostgreSql": {
"ConnectionString": "Host=managing-postgre.apps.managing.live;Port=5432;Database=managing;Username=postgres;Password=29032b13a5bc4d37",
"Orleans": "Host=managing-postgre.apps.managing.live;Port=5432;Database=orleans;Username=postgres;Password=29032b13a5bc4d37"
},
"InfluxDb": {
"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

View File

@@ -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": "*"
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,42 @@
using Managing.Domain.Accounts;
using Managing.Domain.Bots;
using Managing.Domain.Trades;
using Managing.Domain.Users;
using Orleans;
using static Managing.Common.Enums;
namespace Managing.Application.Abstractions.Grains;
/// <summary>
/// Orleans grain interface for TradingBot operations.
/// This interface defines the distributed, async operations available for trading bots.
/// </summary>
public interface ILiveTradingBotGrain : IGrainWithGuidKey
{
/// <summary>
/// Manually opens a position in the specified direction
/// </summary>
/// <param name="direction">The direction of the trade (Long/Short)</param>
/// <returns>The created Position object</returns>
Task<Position> OpenPositionManuallyAsync(TradeDirection direction);
/// <summary>
/// Gets comprehensive bot data including positions, signals, and performance metrics
/// </summary>
Task<LiveTradingBotModel> GetBotDataAsync();
Task CreateAsync(TradingBotConfig config, User user);
Task StartAsync();
Task StopAsync();
Task<bool> UpdateConfiguration(TradingBotConfig newConfig);
Task<Account> GetAccount();
Task<TradingBotConfig> GetConfiguration();
Task<Position> ClosePositionAsync(Guid positionId);
Task RestartAsync();
/// <summary>
/// Deletes the bot and cleans up all associated resources
/// </summary>
Task DeleteAsync();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,40 @@
using Managing.Domain.Bots;
using 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");
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,7 @@ public interface IAccountService
Task<Account> CreateAccount(User user, Account account);
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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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])
@@ -278,7 +272,8 @@ namespace Managing.Application.Tests
FlipOnlyWhenInProfit = true,
MaxPositionTimeHours = null,
CloseEarlyWhenProfitable = false
}, candles, null, false).Result,
}, DateTime.UtcNow.AddDays(-6),
DateTime.UtcNow, null, false, false).Result,
BotType.FlippingBot => _backtester.RunTradingBotBacktest(new TradingBotConfig
{
AccountName = _account.Name,
@@ -296,7 +291,8 @@ namespace Managing.Application.Tests
FlipOnlyWhenInProfit = true,
MaxPositionTimeHours = null,
CloseEarlyWhenProfitable = false
}, candles, null, false).Result,
}, 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])
@@ -418,7 +415,8 @@ namespace Managing.Application.Tests
FlipOnlyWhenInProfit = true,
MaxPositionTimeHours = null,
CloseEarlyWhenProfitable = false
}, candles, null).Result,
}, DateTime.UtcNow.AddDays(-6),
DateTime.UtcNow, null, false, false).Result,
BotType.FlippingBot => _backtester.RunTradingBotBacktest(new TradingBotConfig
{
AccountName = _account.Name,
@@ -436,7 +434,8 @@ namespace Managing.Application.Tests
FlipOnlyWhenInProfit = true,
MaxPositionTimeHours = null,
CloseEarlyWhenProfitable = false
}, candles, null).Result,
}, 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

View File

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

View File

@@ -1,7 +1,8 @@
using Managing.Application.Trading;
using Managing.Application.Trading.Commands;
using Managing.Application.Trading.Commands;
using Managing.Application.Trading.Handlers;
using Managing.Domain.Trades;
using Managing.Domain.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);

View File

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

View File

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

View File

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

View File

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

View 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);
}
}

View File

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

View File

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

View File

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

View File

@@ -1,9 +0,0 @@
using Managing.Domain.Workflows;
using Managing.Domain.Workflows.Synthetics;
namespace Managing.Application.Abstractions;
public interface IFlowFactory
{
IFlow BuildFlow(SyntheticFlow request);
}

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