Compare commits

..

60 Commits

Author SHA1 Message Date
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
332 changed files with 15792 additions and 15427 deletions

View File

@@ -94,4 +94,5 @@ Key Principles
- After finishing the editing, build the project - After finishing the editing, build the project
- you have to pass from controller -> application -> repository, do not inject repository inside controllers - you have to pass from controller -> application -> repository, do not inject repository inside controllers
- dont use command line to edit file, use agent mode capabilities to do it - dont use command line to edit file, use agent mode capabilities to do it
- when dividing, make sure variable is not zero

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
using Managing.Application.Workers.Abstractions; using Managing.Application.Abstractions.Services;
using Managing.Domain.Workers; using Managing.Domain.Workers;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using static Managing.Common.Enums; using static Managing.Common.Enums;

View File

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

View File

@@ -1,4 +1,8 @@
{ {
"PostgreSql": {
"ConnectionString": "Host=localhost;Port=5432;Database=managing;Username=postgres;Password=postgres",
"Orleans": "Host=localhost;Port=5432;Database=orleans;Username=postgres;Password=postgres"
},
"InfluxDb": { "InfluxDb": {
"Url": "http://localhost:8086/", "Url": "http://localhost:8086/",
"Organization": "managing-org", "Organization": "managing-org",

View File

@@ -1,4 +1,8 @@
{ {
"PostgreSql": {
"ConnectionString": "Host=managing-postgre.apps.managing.live;Port=5432;Database=managing;Username=postgres;Password=29032b13a5bc4d37",
"Orleans": "Host=managing-postgre.apps.managing.live;Port=5432;Database=orleans;Username=postgres;Password=29032b13a5bc4d37"
},
"InfluxDb": { "InfluxDb": {
"Url": "https://influx-db.apps.managing.live", "Url": "https://influx-db.apps.managing.live",
"Organization": "managing-org", "Organization": "managing-org",

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -85,7 +85,7 @@ public class TradingController : BaseController
/// <param name="identifier">The unique identifier of the position to close.</param> /// <param name="identifier">The unique identifier of the position to close.</param>
/// <returns>The closed position.</returns> /// <returns>The closed position.</returns>
[HttpPost("ClosePosition")] [HttpPost("ClosePosition")]
public async Task<ActionResult<Position>> ClosePosition(string identifier) public async Task<ActionResult<Position>> ClosePosition(Guid identifier)
{ {
var position = await _tradingService.GetPositionByIdentifierAsync(identifier); var position = await _tradingService.GetPositionByIdentifierAsync(identifier);
var result = await _closeTradeCommandHandler.Handle(new ClosePositionCommand(position)); var result = await _closeTradeCommandHandler.Handle(new ClosePositionCommand(position));

View File

@@ -26,7 +26,8 @@ public class UserController : BaseController
/// <param name="userService">Service for user-related operations.</param> /// <param name="userService">Service for user-related operations.</param>
/// <param name="jwtUtils">Utility for JWT token operations.</param> /// <param name="jwtUtils">Utility for JWT token operations.</param>
/// <param name="webhookService">Service for webhook operations.</param> /// <param name="webhookService">Service for webhook operations.</param>
public UserController(IConfiguration config, IUserService userService, IJwtUtils jwtUtils, IWebhookService webhookService) public UserController(IConfiguration config, IUserService userService, IJwtUtils jwtUtils,
IWebhookService webhookService)
: base(userService) : base(userService)
{ {
_config = config; _config = config;
@@ -40,7 +41,7 @@ public class UserController : BaseController
/// <param name="login">The login request containing user credentials.</param> /// <param name="login">The login request containing user credentials.</param>
/// <returns>A JWT token if authentication is successful; otherwise, an Unauthorized result.</returns> /// <returns>A JWT token if authentication is successful; otherwise, an Unauthorized result.</returns>
[AllowAnonymous] [AllowAnonymous]
[HttpPost] [HttpPost("create-token")]
public async Task<ActionResult<string>> CreateToken([FromBody] LoginRequest login) public async Task<ActionResult<string>> CreateToken([FromBody] LoginRequest login)
{ {
var user = await _userService.Authenticate(login.Name, login.Address, login.Message, login.Signature); var user = await _userService.Authenticate(login.Name, login.Address, login.Message, login.Signature);
@@ -58,10 +59,12 @@ public class UserController : BaseController
/// Gets the current user's information. /// Gets the current user's information.
/// </summary> /// </summary>
/// <returns>The current user's information.</returns> /// <returns>The current user's information.</returns>
[Authorize]
[HttpGet] [HttpGet]
public async Task<ActionResult<User>> GetCurrentUser() public async Task<ActionResult<User>> GetCurrentUser()
{ {
var user = await base.GetUser(); var user = await base.GetUser();
user = await _userService.GetUserByName(user.Name);
return Ok(user); return Ok(user);
} }
@@ -70,6 +73,7 @@ public class UserController : BaseController
/// </summary> /// </summary>
/// <param name="agentName">The new agent name to set.</param> /// <param name="agentName">The new agent name to set.</param>
/// <returns>The updated user with the new agent name.</returns> /// <returns>The updated user with the new agent name.</returns>
[Authorize]
[HttpPut("agent-name")] [HttpPut("agent-name")]
public async Task<ActionResult<User>> UpdateAgentName([FromBody] string agentName) public async Task<ActionResult<User>> UpdateAgentName([FromBody] string agentName)
{ {
@@ -83,6 +87,7 @@ public class UserController : BaseController
/// </summary> /// </summary>
/// <param name="avatarUrl">The new avatar URL to set.</param> /// <param name="avatarUrl">The new avatar URL to set.</param>
/// <returns>The updated user with the new avatar URL.</returns> /// <returns>The updated user with the new avatar URL.</returns>
[Authorize]
[HttpPut("avatar")] [HttpPut("avatar")]
public async Task<ActionResult<User>> UpdateAvatarUrl([FromBody] string avatarUrl) public async Task<ActionResult<User>> UpdateAvatarUrl([FromBody] string avatarUrl)
{ {
@@ -96,6 +101,7 @@ public class UserController : BaseController
/// </summary> /// </summary>
/// <param name="telegramChannel">The new Telegram channel to set.</param> /// <param name="telegramChannel">The new Telegram channel to set.</param>
/// <returns>The updated user with the new Telegram channel.</returns> /// <returns>The updated user with the new Telegram channel.</returns>
[Authorize]
[HttpPut("telegram-channel")] [HttpPut("telegram-channel")]
public async Task<ActionResult<User>> UpdateTelegramChannel([FromBody] string telegramChannel) public async Task<ActionResult<User>> UpdateTelegramChannel([FromBody] string telegramChannel)
{ {
@@ -121,7 +127,8 @@ public class UserController : BaseController
catch (Exception ex) catch (Exception ex)
{ {
// Log the error but don't fail the update operation // Log the error but don't fail the update operation
Console.WriteLine($"Failed to send welcome message to telegram channel {telegramChannel}: {ex.Message}"); Console.WriteLine(
$"Failed to send welcome message to telegram channel {telegramChannel}: {ex.Message}");
} }
} }
@@ -132,6 +139,7 @@ public class UserController : BaseController
/// Tests the Telegram channel configuration by sending a test message. /// Tests the Telegram channel configuration by sending a test message.
/// </summary> /// </summary>
/// <returns>A message indicating the test result.</returns> /// <returns>A message indicating the test result.</returns>
[Authorize]
[HttpPost("telegram-channel/test")] [HttpPost("telegram-channel/test")]
public async Task<ActionResult<string>> TestTelegramChannel() public async Task<ActionResult<string>> TestTelegramChannel()
{ {
@@ -155,7 +163,8 @@ public class UserController : BaseController
await _webhookService.SendMessage(testMessage, user.TelegramChannel); await _webhookService.SendMessage(testMessage, user.TelegramChannel);
return Ok($"Test message sent successfully to Telegram channel {user.TelegramChannel}. Please check your Telegram to verify delivery."); return Ok(
$"Test message sent successfully to Telegram channel {user.TelegramChannel}. Please check your Telegram to verify delivery.");
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -163,4 +172,3 @@ public class UserController : BaseController
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,6 @@
using Managing.Application.Abstractions.Models;
using Managing.Common;
namespace Managing.Api.Models.Responses namespace Managing.Api.Models.Responses
{ {
/// <summary> /// <summary>
@@ -15,21 +18,11 @@ namespace Managing.Api.Models.Responses
/// </summary> /// </summary>
public decimal TotalPnL { get; set; } public decimal TotalPnL { get; set; }
/// <summary>
/// Profit and loss in the last 24 hours in USD
/// </summary>
public decimal PnLLast24h { get; set; }
/// <summary> /// <summary>
/// Total return on investment as a percentage /// Total return on investment as a percentage
/// </summary> /// </summary>
public decimal TotalROI { get; set; } public decimal TotalROI { get; set; }
/// <summary>
/// Return on investment in the last 24 hours as a percentage
/// </summary>
public decimal ROILast24h { get; set; }
/// <summary> /// <summary>
/// Number of winning trades /// Number of winning trades
/// </summary> /// </summary>
@@ -40,10 +33,6 @@ namespace Managing.Api.Models.Responses
/// </summary> /// </summary>
public int Losses { get; set; } public int Losses { get; set; }
/// <summary>
/// Average win rate as a percentage
/// </summary>
public int AverageWinRate { get; set; }
/// <summary> /// <summary>
/// Number of active strategies for this agent /// Number of active strategies for this agent
@@ -56,9 +45,9 @@ namespace Managing.Api.Models.Responses
public decimal TotalVolume { get; set; } public decimal TotalVolume { get; set; }
/// <summary> /// <summary>
/// Volume traded in the last 24 hours in USD /// Total balance including USDC and open position values (without leverage, including PnL)
/// </summary> /// </summary>
public decimal VolumeLast24h { get; set; } public decimal TotalBalance { get; set; }
} }
/// <summary> /// <summary>
@@ -69,32 +58,96 @@ namespace Managing.Api.Models.Responses
/// <summary> /// <summary>
/// Total number of agents on the platform /// Total number of agents on the platform
/// </summary> /// </summary>
public int TotalAgents { get; set; } public required int TotalAgents { get; set; }
/// <summary> /// <summary>
/// Total number of active strategies across all agents /// Total number of active strategies across all agents
/// </summary> /// </summary>
public int TotalActiveStrategies { get; set; } public required int TotalActiveStrategies { get; set; }
/// <summary> /// <summary>
/// Total platform-wide profit and loss in USD /// Total platform-wide profit and loss in USD
/// </summary> /// </summary>
public decimal TotalPlatformPnL { get; set; } public required decimal TotalPlatformPnL { get; set; }
/// <summary> /// <summary>
/// Total volume traded across all agents in USD /// Total volume traded across all agents in USD
/// </summary> /// </summary>
public decimal TotalPlatformVolume { get; set; } public required decimal TotalPlatformVolume { get; set; }
/// <summary> /// <summary>
/// Total volume traded across all agents in the last 24 hours in USD /// Total volume traded across all agents in the last 24 hours in USD
/// </summary> /// </summary>
public decimal TotalPlatformVolumeLast24h { get; set; } public required decimal TotalPlatformVolumeLast24h { get; set; }
/// <summary> /// <summary>
/// Time filter applied to the data /// Total open interest across all positions in USD
/// </summary> /// </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> /// <summary>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,12 @@
using System.Security.Claims;
using System.Text; using System.Text;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using HealthChecks.UI.Client; using HealthChecks.UI.Client;
using Managing.Api.Authorization; using Managing.Api.Authorization;
using Managing.Api.Filters; using Managing.Api.Filters;
using Managing.Api.HealthChecks; using Managing.Api.HealthChecks;
using Managing.Application.Abstractions.Services;
using Managing.Application.Hubs; using Managing.Application.Hubs;
using Managing.Application.Workers;
using Managing.Bootstrap; using Managing.Bootstrap;
using Managing.Common; using Managing.Common;
using Managing.Core.Middleawares; using Managing.Core.Middleawares;
@@ -156,8 +157,14 @@ builder.Services.Configure<PrivySettings>(builder.Configuration.GetSection(Const
builder.Services.AddControllers().AddJsonOptions(options => builder.Services.AddControllers().AddJsonOptions(options =>
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter())); options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()));
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(o => builder.Services
{ .AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(o =>
{
o.SaveToken = true; o.SaveToken = true;
o.TokenValidationParameters = new TokenValidationParameters o.TokenValidationParameters = new TokenValidationParameters
{ {
@@ -169,8 +176,52 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJw
ValidateAudience = false, ValidateAudience = false,
ValidateIssuerSigningKey = true ValidateIssuerSigningKey = true
}; };
}); o.Events = new JwtBearerEvents
builder.Services.AddAuthorization(); {
OnMessageReceived = context =>
{
// If you want to get the token from a custom header or query string
// var accessToken = context.Request.Query["access_token"];
// if (!string.IsNullOrEmpty(accessToken) &&
// context.HttpContext.Request.Path.StartsWithSegments("/hub"))
// {
// context.Token = accessToken;
// }
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
context.Response.Headers.Add("Token-Expired", "true");
}
return Task.CompletedTask;
},
// --- IMPORTANT: Attach User to Context Here ---
OnTokenValidated = async context =>
{
var userService = context.HttpContext.RequestServices.GetRequiredService<IUserService>();
// Assuming your JWT token contains a 'nameid' claim (or similar) for the user ID
var userId = context.Principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (!string.IsNullOrEmpty(userId))
{
// Fetch the full user object from your service
var user = await userService.GetUserByAddressAsync(userId);
if (user != null)
{
// Attach the user object to HttpContext.Items
context.HttpContext.Items["User"] = user;
}
}
await Task.CompletedTask;
}
// --- END IMPORTANT ---
};
});
builder.Services.AddCors(o => o.AddPolicy("CorsPolicy", builder => builder.Services.AddCors(o => o.AddPolicy("CorsPolicy", builder =>
{ {
builder builder
@@ -187,7 +238,7 @@ builder.Services.AddScoped<IJwtUtils, JwtUtils>();
builder.Services.RegisterApiDependencies(builder.Configuration); 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.Host.ConfigureOrleans(builder.Configuration, builder.Environment.IsProduction());
builder.Services.AddEndpointsApiExplorer(); builder.Services.AddEndpointsApiExplorer();
@@ -233,12 +284,6 @@ builder.Services.AddSwaggerGen(options =>
}); });
builder.WebHost.SetupDiscordBot(); builder.WebHost.SetupDiscordBot();
if (builder.Configuration.GetValue<bool>("EnableBotManager", false))
{
builder.Services.AddHostedService<BotManagerWorker>();
}
// Workers are now registered in ApiBootstrap.cs
// App // App
var app = builder.Build(); var app = builder.Build();
@@ -258,14 +303,9 @@ app.UseSentryDiagnostics();
// Using shared GlobalErrorHandlingMiddleware from core project // Using shared GlobalErrorHandlingMiddleware from core project
app.UseMiddleware<GlobalErrorHandlingMiddleware>(); app.UseMiddleware<GlobalErrorHandlingMiddleware>();
app.UseMiddleware<JwtMiddleware>();
app.UseHttpsRedirection(); app.UseHttpsRedirection();
app.UseRouting(); app.UseRouting();
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
app.UseEndpoints(endpoints => app.UseEndpoints(endpoints =>
@@ -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)
app.Run(); {
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,10 @@
{
"RunOrleansGrains": false,
"DeploymentMode": false,
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

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

View File

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

View File

@@ -5,7 +5,8 @@
"Token": "Fw2FPL2OwTzDHzSbR2Sd5xs0EKQYy00Q-hYKYAhr9cC1_q5YySONpxuf_Ck0PTjyUiF13xXmi__bu_pXH-H9zA==" "Token": "Fw2FPL2OwTzDHzSbR2Sd5xs0EKQYy00Q-hYKYAhr9cC1_q5YySONpxuf_Ck0PTjyUiF13xXmi__bu_pXH-H9zA=="
}, },
"PostgreSql": { "PostgreSql": {
"ConnectionString": "Host=localhost;Port=5432;Database=managing;Username=postgres;Password=postgres" "ConnectionString": "Host=localhost;Port=5432;Database=managing;Username=postgres;Password=postgres",
"Orleans": "Host=localhost;Port=5432;Database=orleans;Username=postgres;Password=postgres"
}, },
"Privy": { "Privy": {
"AppId": "cm6f47n1l003jx7mjwaembhup", "AppId": "cm6f47n1l003jx7mjwaembhup",
@@ -23,9 +24,6 @@
"ElasticConfiguration": { "ElasticConfiguration": {
"Uri": "http://elasticsearch:9200" "Uri": "http://elasticsearch:9200"
}, },
"Sentry": {
"Dsn": "https://698e00d7cb404b049aff3881e5a47f6b@bugcenter.apps.managing.live/1"
},
"Discord": { "Discord": {
"ApplicationId": "", "ApplicationId": "",
"PublicKey": "", "PublicKey": "",
@@ -36,6 +34,7 @@
"RequestsChannelId": 1018589494968078356, "RequestsChannelId": 1018589494968078356,
"ButtonExpirationMinutes": 2 "ButtonExpirationMinutes": 2
}, },
"RunOrleansGrains": true,
"AllowedHosts": "*", "AllowedHosts": "*",
"WorkerBotManager": true, "WorkerBotManager": true,
"WorkerBalancesTracking": false, "WorkerBalancesTracking": false,

View File

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

View File

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

View File

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

View File

@@ -32,7 +32,7 @@
"WebhookUrl": "https://n8n.kai.managing.live/webhook/fa9308b6-983b-42ec-b085-71599d655951" "WebhookUrl": "https://n8n.kai.managing.live/webhook/fa9308b6-983b-42ec-b085-71599d655951"
}, },
"Sentry": { "Sentry": {
"Dsn": "https://698e00d7cb404b049aff3881e5a47f6b@bugcenter.apps.managing.live/1", "Dsn": "https://fe12add48c56419bbdfa86227c188e7a@glitch.kai.managing.live/1",
"MinimumEventLevel": "Error", "MinimumEventLevel": "Error",
"SendDefaultPii": true, "SendDefaultPii": true,
"MaxBreadcrumbs": 50, "MaxBreadcrumbs": 50,
@@ -66,5 +66,7 @@
"FundingRateChannelId": 1263566138709774336, "FundingRateChannelId": 1263566138709774336,
"ButtonExpirationMinutes": 10 "ButtonExpirationMinutes": 10
}, },
"RunOrleansGrains": true,
"DeploymentMode": false,
"AllowedHosts": "*" "AllowedHosts": "*"
} }

View File

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

View File

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

View File

@@ -23,23 +23,5 @@ public interface IBacktestTradingBotGrain : IGrainWithGuidKey
/// <param name="requestId">The request ID to associate with this backtest</param> /// <param name="requestId">The request ID to associate with this backtest</param>
/// <param name="metadata">Additional metadata to associate with this backtest</param> /// <param name="metadata">Additional metadata to associate with this backtest</param>
/// <returns>The complete backtest result</returns> /// <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); Task<LightBacktest> RunBacktestAsync(TradingBotConfig config, HashSet<Candle> candles, User user = null, bool save = false, bool withCandles = false, string requestId = null, object metadata = null);
/// <summary>
/// Gets the current backtest progress
/// </summary>
/// <returns>Backtest progress information</returns>
Task<BacktestProgress> GetBacktestProgressAsync();
}
/// <summary>
/// Represents the progress of a backtest
/// </summary>
public class BacktestProgress
{
public bool IsInitialized { get; set; }
public int TotalCandles { get; set; }
public int ProcessedCandles { get; set; }
public double ProgressPercentage { get; set; }
public bool IsComplete { get; set; }
} }

View File

@@ -0,0 +1,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

@@ -7,12 +7,12 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Managing.Common\Managing.Common.csproj" /> <ProjectReference Include="..\Managing.Common\Managing.Common.csproj"/>
<ProjectReference Include="..\Managing.Domain\Managing.Domain.csproj" /> <ProjectReference Include="..\Managing.Domain\Managing.Domain.csproj"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Orleans.Core.Abstractions" Version="9.2.1" /> <PackageReference Include="Microsoft.Orleans.Core.Abstractions" Version="9.2.1"/>
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -45,7 +45,7 @@ namespace Managing.Application.Abstractions.Services
/// <returns>The lightweight backtest results</returns> /// <returns>The lightweight backtest results</returns>
Task<LightBacktest> RunTradingBotBacktest( Task<LightBacktest> RunTradingBotBacktest(
TradingBotConfig config, TradingBotConfig config,
List<Candle> candles, HashSet<Candle> candles,
User user = null, User user = null,
bool withCandles = false, bool withCandles = false,
string requestId = null, string requestId = null,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,15 +3,15 @@ using System.Diagnostics;
using Managing.Application.Abstractions; using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services; using Managing.Application.Abstractions.Services;
using Managing.Application.Backtesting; using Managing.Application.Backtests;
using Managing.Application.Bots.Base;
using Managing.Application.Hubs; using Managing.Application.Hubs;
using Managing.Application.ManageBot;
using Managing.Core; using Managing.Core;
using Managing.Domain.Bots; using Managing.Domain.Bots;
using Managing.Domain.Candles; using Managing.Domain.Candles;
using Managing.Domain.MoneyManagements; using Managing.Domain.MoneyManagements;
using Managing.Domain.Scenarios; using Managing.Domain.Scenarios;
using Managing.Domain.Strategies;
using Managing.Domain.Strategies.Signals;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
using Moq; using Moq;
using Newtonsoft.Json; using Newtonsoft.Json;
@@ -22,7 +22,6 @@ namespace Managing.Application.Tests
{ {
public class BotsTests : BaseTests public class BotsTests : BaseTests
{ {
private readonly IBotFactory _botFactory;
private readonly IBacktester _backtester; private readonly IBacktester _backtester;
private readonly string _reportPath; private readonly string _reportPath;
private string _analysePath; private string _analysePath;
@@ -37,19 +36,11 @@ namespace Managing.Application.Tests
var scenarioService = new Mock<IScenarioService>().Object; var scenarioService = new Mock<IScenarioService>().Object;
var messengerService = new Mock<IMessengerService>().Object; var messengerService = new Mock<IMessengerService>().Object;
var kaigenService = new Mock<IKaigenService>().Object; var kaigenService = new Mock<IKaigenService>().Object;
var backupBotService = new Mock<IBackupBotService>().Object;
var hubContext = new Mock<IHubContext<BacktestHub>>().Object; var hubContext = new Mock<IHubContext<BacktestHub>>().Object;
var tradingBotLogger = TradingBaseTests.CreateTradingBotLogger(); var tradingBotLogger = TradingBaseTests.CreateTradingBotLogger();
var backtestLogger = TradingBaseTests.CreateBacktesterLogger(); var backtestLogger = TradingBaseTests.CreateBacktesterLogger();
var botService = new Mock<IBotService>().Object; var botService = new Mock<IBotService>().Object;
_botFactory = new BotFactory( _backtester = new Backtester(_exchangeService, backtestRepository, backtestLogger,
_exchangeService,
tradingBotLogger,
discordService,
_accountService.Object,
_tradingService.Object,
botService, backupBotService);
_backtester = new Backtester(_exchangeService, _botFactory, backtestRepository, backtestLogger,
scenarioService, _accountService.Object, messengerService, kaigenService, hubContext, null); scenarioService, _accountService.Object, messengerService, kaigenService, hubContext, null);
_elapsedTimes = new List<double>(); _elapsedTimes = new List<double>();
@@ -68,7 +59,6 @@ namespace Managing.Application.Tests
// Arrange // Arrange
var scenario = new Scenario("FlippingScenario"); var scenario = new Scenario("FlippingScenario");
var strategy = ScenarioHelpers.BuildIndicator(IndicatorType.RsiDivergence, "RsiDiv", period: 14); var strategy = ScenarioHelpers.BuildIndicator(IndicatorType.RsiDivergence, "RsiDiv", period: 14);
scenario.AddIndicator(strategy);
var localCandles = var localCandles =
FileHelpers.ReadJson<List<Candle>>($"{ticker.ToString()}-{timeframe.ToString()}-candles.json"); FileHelpers.ReadJson<List<Candle>>($"{ticker.ToString()}-{timeframe.ToString()}-candles.json");
@@ -93,10 +83,11 @@ namespace Managing.Application.Tests
// Act // Act
var backtestResult = var backtestResult =
await _backtester.RunTradingBotBacktest(config, localCandles.TakeLast(500).ToList(), null, false); await _backtester.RunTradingBotBacktest(config, DateTime.UtcNow.AddDays(-6),
DateTime.UtcNow, null, false, false);
var json = JsonConvert.SerializeObject(backtestResult, Formatting.None); var json = JsonConvert.SerializeObject(backtestResult, Formatting.None);
File.WriteAllText($"{ticker.ToString()}-{timeframe.ToString()}-{Guid.NewGuid()}.json", json); File.WriteAllText($"{ticker}-{timeframe}-{Guid.NewGuid()}.json", json);
// WriteCsvReport(backtestResult.GetStringReport()); // WriteCsvReport(backtestResult.GetStringReport());
// Assert // Assert
@@ -119,8 +110,6 @@ namespace Managing.Application.Tests
{ {
// Arrange // Arrange
var scenario = new Scenario("ScalpingScenario"); var scenario = new Scenario("ScalpingScenario");
var strategy = ScenarioHelpers.BuildIndicator(IndicatorType.RsiDivergence, "RsiDiv", period: 5);
scenario.AddIndicator(strategy);
var config = new TradingBotConfig var config = new TradingBotConfig
{ {
@@ -158,10 +147,13 @@ namespace Managing.Application.Tests
int days) int days)
{ {
// Arrange // Arrange
var scenario = new Scenario("ScalpingScenario"); var scenario = new Scenario("ScalpingScenario")
var strategy = ScenarioHelpers.BuildIndicator(IndicatorType.MacdCross, "RsiDiv", fastPeriods: 12, {
slowPeriods: 26, signalPeriods: 9); Indicators = new List<IndicatorBase>
scenario.AddIndicator(strategy); {
new MacdCrossIndicatorBase("MacdCross", 12, 26, 9)
}
};
var moneyManagement = new MoneyManagement() var moneyManagement = new MoneyManagement()
{ {
@@ -236,8 +228,10 @@ namespace Managing.Application.Tests
Parallel.For((long)periodRange[0], periodRange[1], options, i => Parallel.For((long)periodRange[0], periodRange[1], options, i =>
{ {
var scenario = new Scenario("ScalpingScenario"); var scenario = new Scenario("ScalpingScenario");
var strategy = ScenarioHelpers.BuildIndicator(indicatorType, "RsiDiv", period: (int)i); scenario.Indicators = new List<IndicatorBase>
scenario.AddIndicator(strategy); {
new RsiDivergenceIndicatorBase("RsiDiv", (int)i)
};
// -0.5 to -5 // -0.5 to -5
for (decimal s = stopLossRange[0]; s < stopLossRange[1]; s += stopLossRange[2]) for (decimal s = stopLossRange[0]; s < stopLossRange[1]; s += stopLossRange[2])
@@ -278,7 +272,8 @@ namespace Managing.Application.Tests
FlipOnlyWhenInProfit = true, FlipOnlyWhenInProfit = true,
MaxPositionTimeHours = null, MaxPositionTimeHours = null,
CloseEarlyWhenProfitable = false CloseEarlyWhenProfitable = false
}, candles, null, false).Result, }, DateTime.UtcNow.AddDays(-6),
DateTime.UtcNow, null, false, false).Result,
BotType.FlippingBot => _backtester.RunTradingBotBacktest(new TradingBotConfig BotType.FlippingBot => _backtester.RunTradingBotBacktest(new TradingBotConfig
{ {
AccountName = _account.Name, AccountName = _account.Name,
@@ -296,7 +291,8 @@ namespace Managing.Application.Tests
FlipOnlyWhenInProfit = true, FlipOnlyWhenInProfit = true,
MaxPositionTimeHours = null, MaxPositionTimeHours = null,
CloseEarlyWhenProfitable = false CloseEarlyWhenProfitable = false
}, candles, null, false).Result, }, DateTime.UtcNow.AddDays(-6),
DateTime.UtcNow, null, false, false).Result,
_ => throw new NotImplementedException(), _ => throw new NotImplementedException(),
}; };
timer.Stop(); timer.Stop();
@@ -376,9 +372,10 @@ namespace Managing.Application.Tests
return; return;
var scenario = new Scenario("ScalpingScenario"); var scenario = new Scenario("ScalpingScenario");
var strategy = ScenarioHelpers.BuildIndicator(indicatorType, "RsiDiv", fastPeriods: 12, scenario.Indicators = new List<IndicatorBase>
slowPeriods: 26, signalPeriods: 9); {
scenario.AddIndicator(strategy); new MacdCrossIndicatorBase("MacdCross", 12, 26, 9)
};
// -0.5 to -5 // -0.5 to -5
for (decimal s = stopLossRange[0]; s < stopLossRange[1]; s += stopLossRange[2]) for (decimal s = stopLossRange[0]; s < stopLossRange[1]; s += stopLossRange[2])
@@ -418,7 +415,8 @@ namespace Managing.Application.Tests
FlipOnlyWhenInProfit = true, FlipOnlyWhenInProfit = true,
MaxPositionTimeHours = null, MaxPositionTimeHours = null,
CloseEarlyWhenProfitable = false CloseEarlyWhenProfitable = false
}, candles, null).Result, }, DateTime.UtcNow.AddDays(-6),
DateTime.UtcNow, null, false, false).Result,
BotType.FlippingBot => _backtester.RunTradingBotBacktest(new TradingBotConfig BotType.FlippingBot => _backtester.RunTradingBotBacktest(new TradingBotConfig
{ {
AccountName = _account.Name, AccountName = _account.Name,
@@ -436,7 +434,8 @@ namespace Managing.Application.Tests
FlipOnlyWhenInProfit = true, FlipOnlyWhenInProfit = true,
MaxPositionTimeHours = null, MaxPositionTimeHours = null,
CloseEarlyWhenProfitable = false CloseEarlyWhenProfitable = false
}, candles, null).Result, }, DateTime.UtcNow.AddDays(-6),
DateTime.UtcNow, null, false, false).Result,
_ => throw new NotImplementedException(), _ => throw new NotImplementedException(),
}; };
@@ -673,7 +672,8 @@ namespace Managing.Application.Tests
CloseEarlyWhenProfitable = false CloseEarlyWhenProfitable = false
}; };
var backtestResult = _backtester.RunTradingBotBacktest(config, candles, null).Result; var backtestResult = _backtester.RunTradingBotBacktest(config, DateTime.UtcNow.AddDays(-6),
DateTime.UtcNow, null, false, false).Result;
timer.Stop(); timer.Stop();
@@ -1042,8 +1042,13 @@ namespace Managing.Application.Tests
{ {
foreach (var parameterSet in strategyConfig.ParameterSets) foreach (var parameterSet in strategyConfig.ParameterSets)
{ {
var scenario = BuildScenario($"{strategyConfig.Name}_{parameterSet.Name}", var scenario = new Scenario($"{strategyConfig.Name}_{parameterSet.Name}")
new[] { (strategyConfig, parameterSet) }); {
Indicators = new List<IndicatorBase>
{
new RsiDivergenceIndicatorBase("RsiDiv", (int)parameterSet.Period)
}
};
scenarios.Add(scenario); scenarios.Add(scenario);
} }
} }
@@ -1068,7 +1073,13 @@ namespace Managing.Application.Tests
{ {
var scenarioName = string.Join("_", var scenarioName = string.Join("_",
paramCombo.Select(p => $"{p.strategyConfig.Name}_{p.parameterSet.Name}")); paramCombo.Select(p => $"{p.strategyConfig.Name}_{p.parameterSet.Name}"));
var scenario = BuildScenario(scenarioName, paramCombo); var scenario = new Scenario(scenarioName)
{
Indicators = new List<IndicatorBase>
{
new RsiDivergenceIndicatorBase("RsiDiv", (int)paramCombo.First().parameterSet.Period)
}
};
scenario.LoopbackPeriod = 15; scenario.LoopbackPeriod = 15;
scenarios.Add(scenario); scenarios.Add(scenario);
} }
@@ -1077,31 +1088,6 @@ namespace Managing.Application.Tests
return scenarios; return scenarios;
} }
private Scenario BuildScenario(string scenarioName,
IEnumerable<(StrategyConfiguration strategyConfig, ParameterSet parameterSet)> strategyParams)
{
var scenario = new Scenario(scenarioName);
foreach (var (strategyConfig, parameterSet) in strategyParams)
{
var strategy = ScenarioHelpers.BuildIndicator(
strategyConfig.Type,
$"{strategyConfig.Name}_{parameterSet.Name}",
period: parameterSet.Period,
fastPeriods: parameterSet.FastPeriods,
slowPeriods: parameterSet.SlowPeriods,
signalPeriods: parameterSet.SignalPeriods,
multiplier: parameterSet.Multiplier,
stochPeriods: parameterSet.StochPeriods,
smoothPeriods: parameterSet.SmoothPeriods,
cyclePeriods: parameterSet.CyclePeriods);
scenario.AddIndicator(strategy);
}
return scenario;
}
private IEnumerable<IEnumerable<T>> GetCombinations<T>(IEnumerable<T> elements, int k) private IEnumerable<IEnumerable<T>> GetCombinations<T>(IEnumerable<T> elements, int k)
{ {
return k == 0 return k == 0

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services; using Managing.Application.Abstractions.Services;
using Managing.Application.Backtesting; using Managing.Application.Backtests;
using Managing.Application.Bots; using Managing.Application.Bots;
using Managing.Infrastructure.Databases; using Managing.Infrastructure.Databases;
using Managing.Infrastructure.Databases.InfluxDb; using Managing.Infrastructure.Databases.InfluxDb;

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.Bots;
using Managing.Domain.Users; using Managing.Domain.Trades;
using Managing.Domain.Workflows;
using static Managing.Common.Enums; using static Managing.Common.Enums;
namespace Managing.Application.Abstractions; namespace Managing.Application.Abstractions;
public interface IBotService public interface IBotService
{ {
Task SaveOrUpdateBotBackup(User user, string identifier, BotStatus status, TradingBotBackup data); Task<IEnumerable<Bot>> GetBotsAsync();
void AddSimpleBotToCache(IBot bot); Task<IEnumerable<Bot>> GetBotsByStatusAsync(BotStatus status);
void AddTradingBotToCache(ITradingBot bot); Task<BotStatus> StopBot(Guid identifier);
List<ITradingBot> GetActiveBots(); Task<BotStatus> RestartBot(Guid identifier);
Task<IEnumerable<BotBackup>> GetSavedBotsAsync(); Task<bool> DeleteBot(Guid identifier);
Task StartBotFromBackup(BotBackup backupBot); Task<bool> UpdateBotConfiguration(Guid identifier, TradingBotConfig newConfig);
Task<BotBackup> GetBotBackup(string identifier); 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> /// <summary>
/// Creates a trading bot using the unified TradingBot class /// Gets paginated bots with filtering and sorting
/// </summary> /// </summary>
/// <param name="config">The trading bot configuration</param> /// <param name="pageNumber">Page number (1-based)</param>
/// <returns>ITradingBot instance</returns> /// <param name="pageSize">Number of items per page</param>
Task<ITradingBot> CreateTradingBot(TradingBotConfig config); /// <param name="status">Filter by status (optional)</param>
/// <param name="name">Filter by name (partial match, case-insensitive)</param>
/// <summary> /// <param name="ticker">Filter by ticker (partial match, case-insensitive)</param>
/// Creates a trading bot for backtesting using the unified TradingBot class /// <param name="agentName">Filter by agent name (partial match, case-insensitive)</param>
/// </summary> /// <param name="sortBy">Sort field</param>
/// <param name="config">The trading bot configuration</param> /// <param name="sortDirection">Sort direction ("Asc" or "Desc")</param>
/// <returns>ITradingBot instance configured for backtesting</returns> /// <returns>Tuple containing the bots for the current page and total count</returns>
Task<ITradingBot> CreateBacktestTradingBot(TradingBotConfig config); Task<(IEnumerable<Bot> Bots, int TotalCount)> GetBotsPaginatedAsync(
int pageNumber,
IBot CreateSimpleBot(string botName, Workflow workflow); int pageSize,
Task<string> StopBot(string botName); BotStatus? status = null,
Task<bool> DeleteBot(string botName); string? name = null,
Task<string> RestartBot(string botName); string? ticker = null,
Task ToggleIsForWatchingOnly(string botName); string? agentName = null,
Task<bool> UpdateBotConfiguration(string identifier, TradingBotConfig newConfig); 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);
}

View File

@@ -8,19 +8,7 @@ namespace Managing.Application.Abstractions
public interface IScenarioService public interface IScenarioService
{ {
Task<Scenario> CreateScenario(string name, List<string> strategies, int? loopbackPeriod = 1); Task<Scenario> CreateScenario(string name, List<string> strategies, int? loopbackPeriod = 1);
Task<IEnumerable<Indicator>> GetIndicatorsAsync(); Task<IEnumerable<IndicatorBase>> GetIndicatorsAsync();
Task<Indicator> CreateStrategy(IndicatorType type,
string name,
int? period = null,
int? fastPeriods = null,
int? slowPeriods = null,
int? signalPeriods = null,
double? multiplier = null,
int? stochPeriods = null,
int? smoothPeriods = null,
int? cyclePeriods = null);
Task<bool> UpdateScenario(string name, List<string> strategies, int? loopbackPeriod); Task<bool> UpdateScenario(string name, List<string> strategies, int? loopbackPeriod);
Task<bool> UpdateStrategy(IndicatorType indicatorType, string name, int? period, int? fastPeriods, Task<bool> UpdateStrategy(IndicatorType indicatorType, string name, int? period, int? fastPeriods,
@@ -29,12 +17,12 @@ namespace Managing.Application.Abstractions
Task<IEnumerable<Scenario>> GetScenariosByUserAsync(User user); Task<IEnumerable<Scenario>> GetScenariosByUserAsync(User user);
Task<Scenario> CreateScenarioForUser(User user, string name, List<string> strategies, int? loopbackPeriod = 1); Task<Scenario> CreateScenarioForUser(User user, string name, List<string> strategies, int? loopbackPeriod = 1);
Task<IEnumerable<Indicator>> GetIndicatorsByUserAsync(User user); Task<IEnumerable<IndicatorBase>> GetIndicatorsByUserAsync(User user);
Task<bool> DeleteIndicatorByUser(User user, string name); Task<bool> DeleteIndicatorByUser(User user, string name);
Task<bool> DeleteScenarioByUser(User user, string name); Task<bool> DeleteScenarioByUser(User user, string name);
Task<Scenario> GetScenarioByUser(User user, string name); Task<Scenario> GetScenarioByUser(User user, string name);
Task<Indicator> CreateIndicatorForUser(User user, Task<IndicatorBase> CreateIndicatorForUser(User user,
IndicatorType type, IndicatorType type,
string name, string name,
int? period = null, int? period = null,

View File

@@ -1,38 +1,30 @@
using Managing.Core.FixedSizedQueue; using Managing.Domain.Accounts;
using Managing.Domain.Accounts;
using Managing.Domain.Bots; using Managing.Domain.Bots;
using Managing.Domain.Candles; using Managing.Domain.Candles;
using Managing.Domain.Scenarios; using Managing.Domain.Indicators;
using Managing.Domain.Strategies.Base;
using Managing.Domain.Trades; using Managing.Domain.Trades;
using static Managing.Common.Enums; using static Managing.Common.Enums;
namespace Managing.Application.Abstractions namespace Managing.Application.Abstractions
{ {
public interface ITradingBot : IBot public interface ITradingBot
{ {
TradingBotConfig Config { get; set; } TradingBotConfig Config { get; set; }
Account Account { get; set; } Account Account { get; set; }
FixedSizeQueue<Candle> OptimizedCandles { get; set; } Dictionary<string, LightSignal> Signals { get; set; }
HashSet<Candle> Candles { get; set; } Dictionary<Guid, Position> Positions { get; set; }
HashSet<LightSignal> Signals { get; set; }
List<Position> Positions { get; set; }
Dictionary<DateTime, decimal> WalletBalances { get; set; } Dictionary<DateTime, decimal> WalletBalances { get; set; }
Dictionary<IndicatorType, IndicatorsResultBase> IndicatorsValues { get; set; }
DateTime StartupTime { get; }
DateTime CreateDate { get; }
DateTime PreloadSince { get; set; } DateTime PreloadSince { get; set; }
int PreloadedCandlesCount { get; set; } int PreloadedCandlesCount { get; set; }
long ExecutionCount { get; set; }
Candle LastCandle { get; set; }
Task Run(); Task Run();
Task ToggleIsForWatchOnly();
int GetWinRate(); int GetWinRate();
decimal GetProfitAndLoss(); decimal GetProfitAndLoss();
decimal GetTotalFees(); decimal GetTotalFees();
void LoadScenario(Scenario scenario);
void UpdateIndicatorsValues();
Task LoadAccount(); Task LoadAccount();
Task LoadLastCandle();
Task<Position> OpenPositionManually(TradeDirection direction); Task<Position> OpenPositionManually(TradeDirection direction);
Task CloseTrade(LightSignal signal, Position position, Trade tradeToClose, decimal lastPrice, Task CloseTrade(LightSignal signal, Position position, Trade tradeToClose, decimal lastPrice,

View File

@@ -1,12 +0,0 @@
using Managing.Domain.Workflows;
using Managing.Domain.Workflows.Synthetics;
namespace Managing.Application.Abstractions;
public interface IWorkflowService
{
bool DeleteWorkflow(string name);
Task<IEnumerable<IFlow>> GetAvailableFlows();
IEnumerable<SyntheticWorkflow> GetWorkflows();
Task<Workflow> InsertOrUpdateWorkflow(SyntheticWorkflow workflowRequest);
}

View File

@@ -35,52 +35,52 @@ public class AccountService : IAccountService
_userRepository = userRepository; _userRepository = userRepository;
} }
public async Task<Account> CreateAccount(User user, Account request) public async Task<Account> CreateAccount(User user, Account account)
{ {
var account = await _accountRepository.GetAccountByNameAsync(request.Name); var a = await _accountRepository.GetAccountByNameAsync(account.Name);
if (account != null) if (a != null)
{ {
throw new Exception($"Account {request.Name} alreary exist"); throw new Exception($"Account {account.Name} alreary exist");
} }
else else
{ {
request.User = user; account.User = user;
if (request.Exchange == TradingExchanges.Evm if (account.Exchange == TradingExchanges.Evm
&& request.Type == AccountType.Trader) && account.Type == AccountType.Trader)
{ {
var keys = _evmManager.GenerateAddress(); var keys = _evmManager.GenerateAddress();
request.Key = keys.Key; account.Key = keys.Key;
request.Secret = keys.Secret; account.Secret = keys.Secret;
} }
else if (request.Exchange == TradingExchanges.Evm else if (account.Exchange == TradingExchanges.Evm
&& request.Type == AccountType.Privy) && account.Type == AccountType.Privy)
{ {
if (string.IsNullOrEmpty(request.Key)) if (string.IsNullOrEmpty(account.Key))
{ {
// No key provided, create new privy embedded wallet. // No key provided, create new privy embedded wallet.
// TODO : Fix it to create privy wallet // TODO : Fix it to create privy wallet
var privyClient = await _evmManager.CreatePrivyWallet(); var privyClient = await _evmManager.CreatePrivyWallet();
request.Key = privyClient.Address; account.Key = privyClient.Address;
request.Secret = privyClient.Id; account.Secret = privyClient.Id;
} }
else else
{ {
request.Key = request.Key; // Address account.Key = account.Key; // Address
request.Secret = request.Secret; // Privy wallet id account.Secret = account.Secret; // Privy wallet id
} }
} }
else else
{ {
request.Key = request.Key; account.Key = account.Key;
request.Secret = request.Secret; account.Secret = account.Secret;
} }
await _accountRepository.InsertAccountAsync(request); await _accountRepository.InsertAccountAsync(account);
} }
return request; return account;
} }
public bool DeleteAccount(User user, string name) public bool DeleteAccount(User user, string name)
@@ -106,7 +106,7 @@ public class AccountService : IAccountService
throw new ArgumentException($"Account '{name}' not found"); throw new ArgumentException($"Account '{name}' not found");
} }
ManageProperties(hideSecrets, getBalance, account); await ManagePropertiesAsync(hideSecrets, getBalance, account);
if (account.User == null && account.User != null) if (account.User == null && account.User != null)
{ {
@@ -119,7 +119,7 @@ public class AccountService : IAccountService
public async Task<Account> GetAccountByKey(string key, bool hideSecrets, bool getBalance) public async Task<Account> GetAccountByKey(string key, bool hideSecrets, bool getBalance)
{ {
var account = await _accountRepository.GetAccountByKeyAsync(key); var account = await _accountRepository.GetAccountByKeyAsync(key);
ManageProperties(hideSecrets, getBalance, account); await ManagePropertiesAsync(hideSecrets, getBalance, account);
return account; return account;
} }
@@ -127,7 +127,7 @@ public class AccountService : IAccountService
public async Task<Account> GetAccountByUser(User user, string name, bool hideSecrets, bool getBalance) public async Task<Account> GetAccountByUser(User user, string name, bool hideSecrets, bool getBalance)
{ {
var account = await _accountRepository.GetAccountByNameAsync(name); var account = await _accountRepository.GetAccountByNameAsync(name);
ManageProperties(hideSecrets, getBalance, account); await ManagePropertiesAsync(hideSecrets, getBalance, account);
return account; return account;
} }
@@ -139,7 +139,7 @@ public class AccountService : IAccountService
if (account != null) if (account != null)
{ {
ManageProperties(hideSecrets, getBalance, account); await ManagePropertiesAsync(hideSecrets, getBalance, account);
if (account.User != null) if (account.User != null)
{ {
account.User = await _userRepository.GetUserByNameAsync(account.User.Name); account.User = await _userRepository.GetUserByNameAsync(account.User.Name);
@@ -161,7 +161,7 @@ public class AccountService : IAccountService
foreach (var account in result) foreach (var account in result)
{ {
ManageProperties(hideSecrets, getBalance, account); await ManagePropertiesAsync(hideSecrets, getBalance, account);
accounts.Add(account); accounts.Add(account);
} }
@@ -173,13 +173,14 @@ public class AccountService : IAccountService
return GetAccountsByUserAsync(user, hideSecrets).Result; return GetAccountsByUserAsync(user, hideSecrets).Result;
} }
public async Task<IEnumerable<Account>> GetAccountsByUserAsync(User user, bool hideSecrets = true) public async Task<IEnumerable<Account>> GetAccountsByUserAsync(User user, bool hideSecrets = true,
bool getBalance = false)
{ {
var cacheKey = $"user-account-{user.Name}"; var cacheKey = $"user-account-{user.Name}";
// For now, we'll get fresh data since caching async operations requires more complex logic // For now, we'll get fresh data since caching async operations requires more complex logic
// This can be optimized later with proper async caching // This can be optimized later with proper async caching
return await GetAccountsAsync(user, hideSecrets, false); return await GetAccountsAsync(user, hideSecrets, getBalance);
} }
private async Task<IEnumerable<Account>> GetAccountsAsync(User user, bool hideSecrets, bool getBalance) private async Task<IEnumerable<Account>> GetAccountsAsync(User user, bool hideSecrets, bool getBalance)
@@ -189,7 +190,7 @@ public class AccountService : IAccountService
foreach (var account in result.Where(a => a.User.Name == user.Name)) foreach (var account in result.Where(a => a.User.Name == user.Name))
{ {
ManageProperties(hideSecrets, getBalance, account); await ManagePropertiesAsync(hideSecrets, getBalance, account);
accounts.Add(account); accounts.Add(account);
} }
@@ -332,7 +333,7 @@ public class AccountService : IAccountService
} }
} }
private void ManageProperties(bool hideSecrets, bool getBalance, Account account) private async Task ManagePropertiesAsync(bool hideSecrets, bool getBalance, Account account)
{ {
if (account != null) if (account != null)
{ {
@@ -340,7 +341,7 @@ public class AccountService : IAccountService
{ {
try try
{ {
account.Balances = _exchangeService.GetBalances(account).Result; account.Balances = await _exchangeService.GetBalances(account);
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -0,0 +1,123 @@
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using Managing.Domain.Statistics;
using Microsoft.Extensions.Logging;
namespace Managing.Application.Agents;
public class AgentService : IAgentService
{
private readonly IAgentBalanceRepository _agentBalanceRepository;
private readonly IAgentSummaryRepository _agentSummaryRepository;
private readonly ICacheService _cacheService;
private readonly ILogger<AgentService> _logger;
public AgentService(
IAgentBalanceRepository agentBalanceRepository,
IAgentSummaryRepository agentSummaryRepository,
ICacheService cacheService,
ILogger<AgentService> logger)
{
_agentBalanceRepository = agentBalanceRepository;
_agentSummaryRepository = agentSummaryRepository;
_cacheService = cacheService;
_logger = logger;
}
public async Task<AgentBalanceHistory> GetAgentBalances(string agentName, DateTime start,
DateTime? end = null)
{
var effectiveEnd = end ?? DateTime.UtcNow;
string cacheKey = $"AgentBalances_{agentName}_{start:yyyyMMdd}_{effectiveEnd:yyyyMMdd}";
// Check if the balances are already cached
var cachedBalances = _cacheService.GetValue<AgentBalanceHistory>(cacheKey);
if (cachedBalances != null)
{
return cachedBalances;
}
var balances = await _agentBalanceRepository.GetAgentBalances(agentName, start, end);
// Create a single AgentBalanceHistory with all balances
var result = new AgentBalanceHistory
{
AgentName = agentName,
AgentBalances = balances.OrderBy(b => b.Time).ToList()
};
// Cache the results for 5 minutes
_cacheService.SaveValue(cacheKey, result, TimeSpan.FromMinutes(5));
return result;
}
public async Task<(IList<AgentBalanceHistory> Agents, int TotalCount)> GetBestAgents(
DateTime start,
DateTime? end = null,
int page = 1,
int pageSize = 10)
{
var effectiveEnd = end ?? DateTime.UtcNow;
string cacheKey = $"BestAgents_{start:yyyyMMdd}_{effectiveEnd:yyyyMMdd}";
// Check if the results are already cached
var cachedResult = _cacheService.GetValue<(IList<AgentBalanceHistory>, int)>(cacheKey);
if (cachedResult != default)
{
// Apply pagination to cached results
var (cachedAgents, cachedTotalCount) = cachedResult;
var paginatedAgents = cachedAgents
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToList();
return (paginatedAgents, cachedTotalCount);
}
// Get all agents with their balance history
var (fetchedAgents, fetchedTotalCount) =
await _agentBalanceRepository.GetAllAgentBalancesWithHistory(start, end);
// Cache all results for 5 minutes
_cacheService.SaveValue(cacheKey, (fetchedAgents, fetchedTotalCount), TimeSpan.FromMinutes(5));
// Apply pagination
var result = fetchedAgents
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToList();
return (result, fetchedTotalCount);
}
public async Task SaveOrUpdateAgentSummary(AgentSummary agentSummary)
{
try
{
// Use the injected AgentSummaryRepository to save or update
await _agentSummaryRepository.SaveOrUpdateAsync(agentSummary);
_logger.LogInformation("AgentSummary saved/updated for user {UserId} with agent name {AgentName}",
agentSummary.UserId, agentSummary.AgentName);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error saving/updating AgentSummary for user {UserId} with agent name {AgentName}",
agentSummary.UserId, agentSummary.AgentName);
throw;
}
}
public async Task<IEnumerable<AgentSummary>> GetAllAgentSummaries()
{
return await _agentSummaryRepository.GetAllAsync();
}
public async Task<IEnumerable<string>> GetAllOnlineAgents()
{
var agentSummaries = await _agentSummaryRepository.GetAllAgentWithRunningBots();
return agentSummaries.Select(a => a.AgentName);
}
}

View File

@@ -14,14 +14,13 @@ using Microsoft.Extensions.Logging;
using static Managing.Common.Enums; using static Managing.Common.Enums;
using LightBacktestResponse = Managing.Domain.Backtests.LightBacktest; // Use the domain model for notification using LightBacktestResponse = Managing.Domain.Backtests.LightBacktest; // Use the domain model for notification
namespace Managing.Application.Backtesting namespace Managing.Application.Backtests
{ {
public class Backtester : IBacktester public class Backtester : IBacktester
{ {
private readonly IBacktestRepository _backtestRepository; private readonly IBacktestRepository _backtestRepository;
private readonly ILogger<Backtester> _logger; private readonly ILogger<Backtester> _logger;
private readonly IExchangeService _exchangeService; private readonly IExchangeService _exchangeService;
private readonly IBotFactory _botFactory;
private readonly IScenarioService _scenarioService; private readonly IScenarioService _scenarioService;
private readonly IAccountService _accountService; private readonly IAccountService _accountService;
private readonly IMessengerService _messengerService; private readonly IMessengerService _messengerService;
@@ -31,7 +30,6 @@ namespace Managing.Application.Backtesting
public Backtester( public Backtester(
IExchangeService exchangeService, IExchangeService exchangeService,
IBotFactory botFactory,
IBacktestRepository backtestRepository, IBacktestRepository backtestRepository,
ILogger<Backtester> logger, ILogger<Backtester> logger,
IScenarioService scenarioService, IScenarioService scenarioService,
@@ -42,7 +40,6 @@ namespace Managing.Application.Backtesting
IGrainFactory grainFactory) IGrainFactory grainFactory)
{ {
_exchangeService = exchangeService; _exchangeService = exchangeService;
_botFactory = botFactory;
_backtestRepository = backtestRepository; _backtestRepository = backtestRepository;
_logger = logger; _logger = logger;
_scenarioService = scenarioService; _scenarioService = scenarioService;
@@ -144,7 +141,7 @@ namespace Managing.Application.Backtesting
/// <returns>The lightweight backtest results</returns> /// <returns>The lightweight backtest results</returns>
public async Task<LightBacktestResponse> RunTradingBotBacktest( public async Task<LightBacktestResponse> RunTradingBotBacktest(
TradingBotConfig config, TradingBotConfig config,
List<Candle> candles, HashSet<Candle> candles,
User user = null, User user = null,
bool withCandles = false, bool withCandles = false,
string requestId = null, string requestId = null,
@@ -158,7 +155,7 @@ namespace Managing.Application.Backtesting
/// </summary> /// </summary>
private async Task<LightBacktestResponse> RunBacktestWithCandles( private async Task<LightBacktestResponse> RunBacktestWithCandles(
TradingBotConfig config, TradingBotConfig config,
List<Candle> candles, HashSet<Candle> candles,
User user = null, User user = null,
bool save = false, bool save = false,
bool withCandles = false, bool withCandles = false,
@@ -200,7 +197,7 @@ namespace Managing.Application.Backtesting
return await _accountService.GetAccountByAccountName(config.AccountName, false, false); return await _accountService.GetAccountByAccountName(config.AccountName, false, false);
} }
private List<Candle> GetCandles(Ticker ticker, Timeframe timeframe, private HashSet<Candle> GetCandles(Ticker ticker, Timeframe timeframe,
DateTime startDate, DateTime endDate) DateTime startDate, DateTime endDate)
{ {
var candles = _exchangeService.GetCandlesInflux(TradingExchanges.Evm, ticker, var candles = _exchangeService.GetCandlesInflux(TradingExchanges.Evm, ticker,

View File

@@ -1,49 +0,0 @@
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Services;
using Managing.Application.ManageBot;
using Managing.Domain.Bots;
using Microsoft.Extensions.Logging;
namespace Managing.Application.Bots.Base
{
public class BotFactory : IBotFactory
{
private readonly IExchangeService _exchangeService;
private readonly IMessengerService _messengerService;
private readonly IAccountService _accountService;
private readonly ILogger<TradingBotBase> _tradingBotLogger;
private readonly ITradingService _tradingService;
private readonly IBotService _botService;
private readonly IBackupBotService _backupBotService;
public BotFactory(
IExchangeService exchangeService,
ILogger<TradingBotBase> tradingBotLogger,
IMessengerService messengerService,
IAccountService accountService,
ITradingService tradingService,
IBotService botService,
IBackupBotService backupBotService)
{
_tradingBotLogger = tradingBotLogger;
_exchangeService = exchangeService;
_messengerService = messengerService;
_accountService = accountService;
_tradingService = tradingService;
_botService = botService;
_backupBotService = backupBotService;
}
public async Task<ITradingBot> CreateTradingBot(TradingBotConfig config)
{
// Delegate to BotService which handles scenario loading properly
return await _botService.CreateTradingBot(config);
}
public async Task<ITradingBot> CreateBacktestTradingBot(TradingBotConfig config)
{
// Delegate to BotService which handles scenario loading properly
return await _botService.CreateBacktestTradingBot(config);
}
}
}

View File

@@ -0,0 +1,214 @@
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Grains;
using Managing.Application.Abstractions.Services;
using Managing.Application.Bots.Models;
using Managing.Domain.Statistics;
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
namespace Managing.Application.Bots.Grains;
public class AgentGrain : Grain, IAgentGrain, IRemindable
{
private readonly IPersistentState<AgentGrainState> _state;
private readonly ILogger<AgentGrain> _logger;
private readonly IBotService _botService;
private readonly IAgentService _agentService;
private readonly IExchangeService _exchangeService;
private readonly IUserService _userService;
private readonly IAccountService _accountService;
private readonly ITradingService _tradingService;
private const string _updateSummaryReminderName = "UpdateAgentSummary";
public AgentGrain(
[PersistentState("agent-state", "agent-store")]
IPersistentState<AgentGrainState> state,
ILogger<AgentGrain> logger,
IBotService botService,
IAgentService agentService,
IExchangeService exchangeService,
IUserService userService,
IAccountService accountService,
ITradingService tradingService)
{
_state = state;
_logger = logger;
_botService = botService;
_agentService = agentService;
_exchangeService = exchangeService;
_userService = userService;
_accountService = accountService;
_tradingService = tradingService;
}
public override Task OnActivateAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("AgentGrain activated for user {UserId}", this.GetPrimaryKeyLong());
return base.OnActivateAsync(cancellationToken);
}
public async Task InitializeAsync(int userId, string agentName)
{
_state.State.AgentName = agentName;
await _state.WriteStateAsync();
await UpdateSummary();
_logger.LogInformation("Agent {UserId} initialized with name {AgentName}", userId, agentName);
await RegisterReminderAsync();
}
private async Task RegisterReminderAsync()
{
try
{
// Register a reminder that fires every 5 minutes
await this.RegisterOrUpdateReminder(_updateSummaryReminderName, TimeSpan.FromMinutes(5),
TimeSpan.FromMinutes(1));
_logger.LogInformation("Reminder registered for agent {UserId} to update summary every 5 minutes",
this.GetPrimaryKeyLong());
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to register reminder for agent {UserId}", this.GetPrimaryKeyLong());
}
}
public async Task ReceiveReminder(string reminderName, TickStatus status)
{
if (reminderName == _updateSummaryReminderName)
{
try
{
_logger.LogInformation("Reminder triggered for agent {UserId} to update summary",
this.GetPrimaryKeyLong());
await UpdateSummary();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error updating agent summary from reminder for user {UserId}",
this.GetPrimaryKeyLong());
}
}
}
public async Task UpdateSummary()
{
try
{
// Get all bots for this agent
var bots = await _botService.GetBotsByIdsAsync(_state.State.BotIds);
// Calculate aggregated statistics from bot data
var totalPnL = bots.Sum(b => b.Pnl);
var totalWins = bots.Sum(b => b.TradeWins);
var totalLosses = bots.Sum(b => b.TradeLosses);
// Calculate ROI based on total volume traded with proper division by zero handling
var totalVolume = bots.Sum(b => b.Volume);
decimal totalROI;
if (totalVolume > 0)
{
totalROI = (totalPnL / totalVolume) * 100;
}
else if (totalVolume == 0 && totalPnL == 0)
{
// No trading activity yet
totalROI = 0;
}
else if (totalVolume == 0 && totalPnL != 0)
{
// Edge case: PnL exists but no volume (shouldn't happen in normal cases)
_logger.LogWarning("Agent {UserId} has PnL {PnL} but zero volume", this.GetPrimaryKeyLong(), totalPnL);
totalROI = 0;
}
else
{
// Fallback for any other edge cases
totalROI = 0;
}
// Calculate Runtime based on the farthest date from bot startup times
DateTime? runtime = null;
if (bots.Any())
{
runtime = bots.Max(b => b.StartupTime);
}
// Calculate total balance (USDC + open positions value)
decimal totalBalance = 0;
try
{
var userId = (int)this.GetPrimaryKeyLong();
var user = await _userService.GetUserByIdAsync(userId);
if (user != null)
{
var userAccounts = await _accountService.GetAccountsByUserAsync(user, hideSecrets: true, true);
foreach (var account in userAccounts)
{
// Get USDC balance
var usdcBalances = await _exchangeService.GetBalances(account);
var usdcBalance = usdcBalances.FirstOrDefault(b => b.TokenName?.ToUpper() == "USDC")?.Amount ??
0;
totalBalance += usdcBalance;
}
// Get positions for all bots using their GUIDs as InitiatorIdentifier
var botPositions =
await _tradingService.GetPositionsByInitiatorIdentifiersAsync(_state.State.BotIds);
foreach (var position in botPositions.Where(p => !p.IsFinished()))
{
totalBalance += position.Open.Price * position.Open.Quantity;
totalBalance += position.ProfitAndLoss?.Realized ?? 0;
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error calculating total balance for agent {UserId}", this.GetPrimaryKeyLong());
totalBalance = 0; // Set to 0 if calculation fails
}
var summary = new AgentSummary
{
UserId = (int)this.GetPrimaryKeyLong(),
AgentName = _state.State.AgentName,
TotalPnL = totalPnL,
Wins = totalWins,
Losses = totalLosses,
TotalROI = totalROI,
Runtime = runtime,
ActiveStrategiesCount = bots.Count(b => b.Status == BotStatus.Running),
TotalVolume = totalVolume,
TotalBalance = totalBalance,
};
// Save summary to database
await _agentService.SaveOrUpdateAgentSummary(summary);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error calculating agent summary for user {UserId}", this.GetPrimaryKeyLong());
}
}
public async Task RegisterBotAsync(Guid botId)
{
if (_state.State.BotIds.Add(botId))
{
await _state.WriteStateAsync();
_logger.LogInformation("Bot {BotId} registered to Agent {UserId}", botId, this.GetPrimaryKeyLong());
}
}
public async Task UnregisterBotAsync(Guid botId)
{
if (_state.State.BotIds.Remove(botId))
{
await _state.WriteStateAsync();
_logger.LogInformation("Bot {BotId} unregistered from Agent {UserId}", botId, this.GetPrimaryKeyLong());
}
}
}

View File

@@ -1,7 +1,5 @@
using Managing.Application.Abstractions.Grains; using Managing.Application.Abstractions.Grains;
using Managing.Application.Abstractions.Models;
using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Repositories;
using Managing.Core.FixedSizedQueue;
using Managing.Domain.Backtests; using Managing.Domain.Backtests;
using Managing.Domain.Bots; using Managing.Domain.Bots;
using Managing.Domain.Candles; using Managing.Domain.Candles;
@@ -9,7 +7,6 @@ using Managing.Domain.Scenarios;
using Managing.Domain.Shared.Helpers; using Managing.Domain.Shared.Helpers;
using Managing.Domain.Strategies; using Managing.Domain.Strategies;
using Managing.Domain.Strategies.Base; using Managing.Domain.Strategies.Base;
using Managing.Domain.Trades;
using Managing.Domain.Users; using Managing.Domain.Users;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -52,7 +49,7 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
/// <returns>The complete backtest result</returns> /// <returns>The complete backtest result</returns>
public async Task<LightBacktest> RunBacktestAsync( public async Task<LightBacktest> RunBacktestAsync(
TradingBotConfig config, TradingBotConfig config,
List<Candle> candles, HashSet<Candle> candles,
User user = null, User user = null,
bool save = false, bool save = false,
bool withCandles = false, bool withCandles = false,
@@ -66,7 +63,7 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
// Create a fresh TradingBotBase instance for this backtest // Create a fresh TradingBotBase instance for this backtest
var tradingBot = await CreateTradingBotInstance(config); var tradingBot = await CreateTradingBotInstance(config);
tradingBot.Start(); tradingBot.Account = user.Accounts.First(a => a.Name == config.AccountName);
var totalCandles = candles.Count; var totalCandles = candles.Count;
var currentCandle = 0; var currentCandle = 0;
@@ -79,11 +76,15 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
tradingBot.WalletBalances.Clear(); tradingBot.WalletBalances.Clear();
tradingBot.WalletBalances.Add(candles.FirstOrDefault()!.Date, config.BotTradingBalance); tradingBot.WalletBalances.Add(candles.FirstOrDefault()!.Date, config.BotTradingBalance);
var fixedCandles = new HashSet<Candle>();
// Process all candles following the exact pattern from GetBacktestingResult // Process all candles following the exact pattern from GetBacktestingResult
foreach (var candle in candles) foreach (var candle in candles)
{ {
tradingBot.OptimizedCandles.Enqueue(candle); fixedCandles.Add(candle);
tradingBot.Candles.Add(candle); tradingBot.LastCandle = candle;
// Update signals manually only for backtesting
await tradingBot.UpdateSignals(fixedCandles);
await tradingBot.Run(); await tradingBot.Run();
currentCandle++; currentCandle++;
@@ -97,43 +98,16 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
currentWalletBalance, currentCandle, totalCandles, candle.Date.ToString("yyyy-MM-dd HH:mm")); currentWalletBalance, currentCandle, totalCandles, candle.Date.ToString("yyyy-MM-dd HH:mm"));
break; break;
} }
// Log progress every 10% or every 1000 candles, whichever comes first
var currentPercentage = (int)((double)currentCandle / totalCandles * 100);
var shouldLog = currentPercentage >= lastLoggedPercentage + 10 ||
currentCandle % 1000 == 0 ||
currentCandle == totalCandles;
if (shouldLog && currentPercentage > lastLoggedPercentage)
{
_logger.LogInformation(
"Backtest progress: {CurrentCandle}/{TotalCandles} ({Percentage}%) - Processing candle from {CandleDate}",
currentCandle, totalCandles, currentPercentage, candle.Date.ToString("yyyy-MM-dd HH:mm"));
lastLoggedPercentage = currentPercentage;
}
} }
_logger.LogInformation("Backtest processing completed. Calculating final results..."); _logger.LogInformation("Backtest processing completed. Calculating final results...");
// Set all candles for final calculations
tradingBot.Candles = new HashSet<Candle>(candles);
// Only calculate indicators values if withCandles is true
Dictionary<IndicatorType, IndicatorsResultBase> indicatorsValues = null;
if (withCandles)
{
// Convert LightScenario back to full Scenario for indicator calculations
var fullScenario = config.Scenario.ToScenario();
indicatorsValues = GetIndicatorsValues(fullScenario.Indicators, candles);
}
// Calculate final results following the exact pattern from GetBacktestingResult
var finalPnl = tradingBot.GetProfitAndLoss(); var finalPnl = tradingBot.GetProfitAndLoss();
var winRate = tradingBot.GetWinRate(); var winRate = tradingBot.GetWinRate();
var stats = TradingHelpers.GetStatistics(tradingBot.WalletBalances); var stats = TradingHelpers.GetStatistics(tradingBot.WalletBalances);
var growthPercentage = var growthPercentage =
TradingHelpers.GetGrowthFromInitalBalance(tradingBot.WalletBalances.FirstOrDefault().Value, finalPnl); TradingHelpers.GetGrowthFromInitalBalance(tradingBot.WalletBalances.FirstOrDefault().Value, finalPnl);
var hodlPercentage = TradingHelpers.GetHodlPercentage(candles[0], candles.Last()); var hodlPercentage = TradingHelpers.GetHodlPercentage(candles.First(), candles.Last());
var fees = tradingBot.GetTotalFees(); var fees = tradingBot.GetTotalFees();
var scoringParams = new BacktestScoringParams( var scoringParams = new BacktestScoringParams(
@@ -148,7 +122,7 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
maxDrawdown: stats.MaxDrawdown, maxDrawdown: stats.MaxDrawdown,
initialBalance: tradingBot.WalletBalances.FirstOrDefault().Value, initialBalance: tradingBot.WalletBalances.FirstOrDefault().Value,
tradingBalance: config.BotTradingBalance, tradingBalance: config.BotTradingBalance,
startDate: candles[0].Date, startDate: candles.First().Date,
endDate: candles.Last().Date, endDate: candles.Last().Date,
timeframe: config.Timeframe, timeframe: config.Timeframe,
moneyManagement: config.MoneyManagement moneyManagement: config.MoneyManagement
@@ -160,8 +134,8 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
var finalRequestId = requestId ?? Guid.NewGuid().ToString(); var finalRequestId = requestId ?? Guid.NewGuid().ToString();
// Create backtest result with conditional candles and indicators values // Create backtest result with conditional candles and indicators values
var result = new Backtest(config, tradingBot.Positions, tradingBot.Signals.ToList(), var result = new Backtest(config, tradingBot.Positions, tradingBot.Signals,
withCandles ? candles : new List<Candle>()) withCandles ? candles : new HashSet<Candle>())
{ {
FinalPnl = finalPnl, FinalPnl = finalPnl,
WinRate = winRate, WinRate = winRate,
@@ -170,9 +144,6 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
Fees = fees, Fees = fees,
WalletBalances = tradingBot.WalletBalances.ToList(), WalletBalances = tradingBot.WalletBalances.ToList(),
Statistics = stats, Statistics = stats,
IndicatorsValues = withCandles
? AggregateValues(indicatorsValues, tradingBot.IndicatorsValues)
: new Dictionary<IndicatorType, IndicatorsResultBase>(),
Score = scoringResult.Score, Score = scoringResult.Score,
ScoreMessage = scoringResult.GenerateSummaryMessage(), ScoreMessage = scoringResult.GenerateSummaryMessage(),
Id = Guid.NewGuid().ToString(), Id = Guid.NewGuid().ToString(),
@@ -190,9 +161,6 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
// Send notification if backtest meets criteria // Send notification if backtest meets criteria
await SendBacktestNotificationIfCriteriaMet(result); await SendBacktestNotificationIfCriteriaMet(result);
// Clean up the trading bot instance
tradingBot.Stop();
// Convert Backtest to LightBacktest for safe Orleans serialization // Convert Backtest to LightBacktest for safe Orleans serialization
return ConvertToLightBacktest(result); return ConvertToLightBacktest(result);
} }
@@ -241,13 +209,6 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
// Create the trading bot instance // Create the trading bot instance
var logger = _scopeFactory.CreateScope().ServiceProvider.GetRequiredService<ILogger<TradingBotBase>>(); var logger = _scopeFactory.CreateScope().ServiceProvider.GetRequiredService<ILogger<TradingBotBase>>();
var tradingBot = new TradingBotBase(logger, _scopeFactory, config); var tradingBot = new TradingBotBase(logger, _scopeFactory, config);
// Set the user if available
if (user != null)
{
tradingBot.User = user;
}
return tradingBot; return tradingBot;
} }
@@ -276,8 +237,7 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
/// Aggregates indicator values (following Backtester.cs pattern) /// Aggregates indicator values (following Backtester.cs pattern)
/// </summary> /// </summary>
private Dictionary<IndicatorType, IndicatorsResultBase> AggregateValues( private Dictionary<IndicatorType, IndicatorsResultBase> AggregateValues(
Dictionary<IndicatorType, IndicatorsResultBase> indicatorsValues, Dictionary<IndicatorType, IndicatorsResultBase> indicatorsValues)
Dictionary<IndicatorType, IndicatorsResultBase> botStrategiesValues)
{ {
var result = new Dictionary<IndicatorType, IndicatorsResultBase>(); var result = new Dictionary<IndicatorType, IndicatorsResultBase>();
foreach (var indicator in indicatorsValues) foreach (var indicator in indicatorsValues)
@@ -291,23 +251,17 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
/// <summary> /// <summary>
/// Gets indicators values (following Backtester.cs pattern) /// Gets indicators values (following Backtester.cs pattern)
/// </summary> /// </summary>
private Dictionary<IndicatorType, IndicatorsResultBase> GetIndicatorsValues(List<Indicator> indicators, private Dictionary<IndicatorType, IndicatorsResultBase> GetIndicatorsValues(List<LightIndicator> indicators,
List<Candle> candles) HashSet<Candle> candles)
{ {
var indicatorsValues = new Dictionary<IndicatorType, IndicatorsResultBase>(); var indicatorsValues = new Dictionary<IndicatorType, IndicatorsResultBase>();
var fixedCandles = new FixedSizeQueue<Candle>(10000);
foreach (var candle in candles)
{
fixedCandles.Enqueue(candle);
}
foreach (var indicator in indicators) foreach (var indicator in indicators)
{ {
try try
{ {
var s = ScenarioHelpers.BuildIndicator(indicator, 10000); var builtIndicator = ScenarioHelpers.BuildIndicator(indicator);
s.Candles = fixedCandles; indicatorsValues[indicator.Type] = builtIndicator.GetIndicatorValues(candles);
indicatorsValues[indicator.Type] = s.GetIndicatorValues();
} }
catch (Exception e) catch (Exception e)
{ {
@@ -325,79 +279,4 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
_isDisposed = true; _isDisposed = true;
} }
} }
public Task<BacktestProgress> GetBacktestProgressAsync()
{
throw new NotImplementedException();
}
public Task StartAsync()
{
throw new NotImplementedException();
}
public Task StopAsync()
{
throw new NotImplementedException();
}
public Task<BotStatus> GetStatusAsync()
{
throw new NotImplementedException();
}
public Task<TradingBotConfig> GetConfigurationAsync()
{
throw new NotImplementedException();
}
public Task<Position> OpenPositionManuallyAsync(TradeDirection direction)
{
throw new NotImplementedException();
}
public Task ToggleIsForWatchOnlyAsync()
{
throw new NotImplementedException();
}
public Task<TradingBotResponse> GetBotDataAsync()
{
throw new NotImplementedException();
}
public Task LoadBackupAsync(BotBackup backup)
{
throw new NotImplementedException();
}
public Task SaveBackupAsync()
{
throw new NotImplementedException();
}
public Task<decimal> GetProfitAndLossAsync()
{
throw new NotImplementedException();
}
public Task<int> GetWinRateAsync()
{
throw new NotImplementedException();
}
public Task<long> GetExecutionCountAsync()
{
throw new NotImplementedException();
}
public Task<DateTime> GetStartupTimeAsync()
{
throw new NotImplementedException();
}
public Task<DateTime> GetCreateDateAsync()
{
throw new NotImplementedException();
}
} }

View File

@@ -0,0 +1,205 @@
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Grains;
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
namespace Managing.Application.Bots.Grains;
/// <summary>
/// Orleans grain for LiveBotRegistry operations.
/// This grain acts as a central, durable directory for all LiveTradingBot grains.
/// It maintains a persistent, up-to-date list of all known bot IDs and their status.
/// </summary>
public class LiveBotRegistryGrain : Grain, ILiveBotRegistryGrain
{
private readonly IPersistentState<BotRegistryState> _state;
private readonly ILogger<LiveBotRegistryGrain> _logger;
private readonly IBotService _botService;
public LiveBotRegistryGrain(
[PersistentState("bot-registry", "registry-store")]
IPersistentState<BotRegistryState> state,
ILogger<LiveBotRegistryGrain> logger,
IBotService botService)
{
_state = state;
_logger = logger;
_botService = botService;
}
public override async Task OnActivateAsync(CancellationToken cancellationToken)
{
await base.OnActivateAsync(cancellationToken);
_logger.LogInformation("LiveBotRegistryGrain activated with {TotalBots} bots registered",
_state.State.TotalBotsCount);
}
public override async Task OnDeactivateAsync(DeactivationReason reason, CancellationToken cancellationToken)
{
_logger.LogInformation("LiveBotRegistryGrain deactivating. Reason: {Reason}. Total bots: {TotalBots}",
reason.Description, _state.State.TotalBotsCount);
await base.OnDeactivateAsync(reason, cancellationToken);
}
public async Task RegisterBot(Guid identifier, int userId)
{
try
{
if (_state.State.Bots.ContainsKey(identifier))
{
_logger.LogWarning("Bot {Identifier} is already registered in the registry", identifier);
return;
}
var entry = new BotRegistryEntry(identifier, userId);
_state.State.Bots[identifier] = entry;
// O(1) FIX: Increment the counters
_state.State.TotalBotsCount++;
_state.State.ActiveBotsCount++;
_state.State.LastUpdated = DateTime.UtcNow;
await _state.WriteStateAsync();
_logger.LogInformation(
"Bot {Identifier} registered successfully for user {UserId}. Total bots: {TotalBots}, Active bots: {ActiveBots}",
identifier, userId, _state.State.TotalBotsCount, _state.State.ActiveBotsCount);
// Notify platform summary grain about strategy count change
await NotifyPlatformSummaryAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to register bot {Identifier} for user {UserId}", identifier, userId);
throw;
}
}
public async Task UnregisterBot(Guid identifier)
{
try
{
if (!_state.State.Bots.TryGetValue(identifier, out var entryToRemove))
{
_logger.LogWarning("Bot {Identifier} is not registered in the registry", identifier);
return;
}
_state.State.Bots.Remove(identifier);
// O(1) FIX: Decrement the counters based on the removed entry's status
_state.State.TotalBotsCount--;
if (entryToRemove.Status == BotStatus.Running)
{
_state.State.ActiveBotsCount--;
}
_state.State.LastUpdated = DateTime.UtcNow;
await _state.WriteStateAsync();
_logger.LogInformation(
"Bot {Identifier} unregistered successfully from user {UserId}. Total bots: {TotalBots}",
identifier, entryToRemove.UserId, _state.State.TotalBotsCount);
// Notify platform summary grain about strategy count change
await NotifyPlatformSummaryAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to unregister bot {Identifier}", identifier);
throw;
}
}
public Task<List<BotRegistryEntry>> GetAllBots()
{
var bots = _state.State.Bots.Values.ToList();
_logger.LogDebug("Retrieved {Count} bots from registry", bots.Count);
return Task.FromResult(bots);
}
public Task<List<BotRegistryEntry>> GetBotsForUser(int userId)
{
var userBots = _state.State.Bots.Values
.Where(b => b.UserId == userId)
.ToList();
_logger.LogDebug("Retrieved {Count} bots for user {UserId}", userBots.Count, userId);
return Task.FromResult(userBots);
}
public async Task UpdateBotStatus(Guid identifier, BotStatus newStatus)
{
try
{
if (!_state.State.Bots.TryGetValue(identifier, out var entry))
{
_logger.LogWarning("Bot {Identifier} is not registered in the registry, cannot update status",
identifier);
return;
}
var previousStatus = entry.Status;
if (previousStatus == newStatus)
{
_logger.LogDebug("Bot {Identifier} status unchanged ({Status}), skipping state write", identifier,
newStatus);
return;
}
// O(1) FIX: Conditionally adjust the counter
if (newStatus == BotStatus.Running && previousStatus != BotStatus.Running)
{
_state.State.ActiveBotsCount++;
}
else if (newStatus != BotStatus.Running && previousStatus == BotStatus.Running)
{
_state.State.ActiveBotsCount--;
}
entry.Status = newStatus;
entry.LastStatusUpdate = DateTime.UtcNow;
_state.State.LastUpdated = DateTime.UtcNow;
await _state.WriteStateAsync();
_logger.LogInformation(
"Bot {Identifier} status updated from {PreviousStatus} to {NewStatus}. Active bots: {ActiveBots}",
identifier, previousStatus, newStatus, _state.State.ActiveBotsCount);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to update status for bot {Identifier} to {Status}", identifier, newStatus);
throw;
}
}
public Task<BotStatus> GetBotStatus(Guid identifier)
{
if (!_state.State.Bots.TryGetValue(identifier, out var entry))
{
_logger.LogWarning("Bot {Identifier} is not registered in the registry, returning None", identifier);
return Task.FromResult(BotStatus.Saved);
}
return Task.FromResult(entry.Status);
}
private async Task NotifyPlatformSummaryAsync()
{
try
{
var platformGrain = GrainFactory.GetGrain<IPlatformSummaryGrain>("platform-summary");
await platformGrain.UpdateActiveStrategyCountAsync(_state.State.ActiveBotsCount);
_logger.LogDebug("Notified platform summary about active strategy count change. New count: {ActiveCount}",
_state.State.ActiveBotsCount);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to notify platform summary about strategy count change");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
namespace Managing.Application.Bots.Models
{
public class AgentGrainState
{
public string AgentName { get; set; }
public HashSet<Guid> BotIds { get; set; } = new HashSet<Guid>();
}
}

View File

@@ -1,58 +0,0 @@
using Managing.Application.Abstractions;
using Managing.Application.ManageBot;
using Managing.Domain.Bots;
using Managing.Domain.Workflows;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace Managing.Application.Bots
{
public class SimpleBot : Bot
{
public readonly ILogger<TradingBotBase> Logger;
private readonly IBotService _botService;
private readonly IBackupBotService _backupBotService;
private Workflow _workflow;
public SimpleBot(string name, ILogger<TradingBotBase> logger, Workflow workflow, IBotService botService,
IBackupBotService backupBotService) :
base(name)
{
Logger = logger;
_botService = botService;
_backupBotService = backupBotService;
_workflow = workflow;
Interval = 100;
}
public override void Start()
{
Task.Run(() => InitWorker(Run));
base.Start();
}
public async Task Run()
{
await Task.Run(
async () =>
{
Logger.LogInformation(Identifier);
Logger.LogInformation(DateTime.Now.ToString());
await _workflow.Execute();
await SaveBackup();
Logger.LogInformation("__________________________________________________");
});
}
public override async Task SaveBackup()
{
var data = JsonConvert.SerializeObject(_workflow);
await _backupBotService.SaveOrUpdateBotBackup(User, Identifier, Status, new TradingBotBackup());
}
public override void LoadBackup(BotBackup backup)
{
_workflow = new Workflow();
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,8 @@
using Managing.Domain.Bots; using Managing.Domain.Bots;
using Managing.Domain.Candles;
using Managing.Domain.Indicators;
using Managing.Domain.Trades; using Managing.Domain.Trades;
using Managing.Domain.Users; using Managing.Domain.Users;
using static Managing.Common.Enums;
namespace Managing.Application.Bots; namespace Managing.Application.Bots;
@@ -23,13 +24,13 @@ public class TradingBotGrainState
/// Collection of trading signals generated by the bot /// Collection of trading signals generated by the bot
/// </summary> /// </summary>
[Id(1)] [Id(1)]
public HashSet<LightSignal> Signals { get; set; } = new(); public Dictionary<string, LightSignal> Signals { get; set; } = new();
/// <summary> /// <summary>
/// List of trading positions opened by the bot /// Dictionary of trading positions opened by the bot, keyed by position identifier
/// </summary> /// </summary>
[Id(2)] [Id(2)]
public List<Position> Positions { get; set; } = new(); public Dictionary<Guid, Position> Positions { get; set; } = new();
/// <summary> /// <summary>
/// Historical wallet balances tracked over time /// Historical wallet balances tracked over time
@@ -37,12 +38,6 @@ public class TradingBotGrainState
[Id(3)] [Id(3)]
public Dictionary<DateTime, decimal> WalletBalances { get; set; } = new(); public Dictionary<DateTime, decimal> WalletBalances { get; set; } = new();
/// <summary>
/// Current status of the bot (Running, Stopped, etc.)
/// </summary>
[Id(4)]
public BotStatus Status { get; set; } = BotStatus.Down;
/// <summary> /// <summary>
/// When the bot was started /// When the bot was started
/// </summary> /// </summary>
@@ -71,7 +66,7 @@ public class TradingBotGrainState
/// Bot identifier/name /// Bot identifier/name
/// </summary> /// </summary>
[Id(9)] [Id(9)]
public string Identifier { get; set; } = string.Empty; public Guid Identifier { get; set; } = Guid.Empty;
/// <summary> /// <summary>
/// Bot display name /// Bot display name
@@ -114,4 +109,16 @@ public class TradingBotGrainState
/// </summary> /// </summary>
[Id(16)] [Id(16)]
public DateTime LastBackupTime { get; set; } = DateTime.UtcNow; public DateTime LastBackupTime { get; set; } = DateTime.UtcNow;
/// <summary>
/// Last time a position was closed (for cooldown period tracking)
/// </summary>
[Id(17)]
public DateTime? LastPositionClosingTime { get; set; }
/// <summary>
/// The last candle data used for trading decisions
/// </summary>
[Id(18)]
public Candle? LastCandle { get; set; }
} }

View File

@@ -8,6 +8,7 @@ using Managing.Domain.Bots;
using Managing.Domain.MoneyManagements; using Managing.Domain.MoneyManagements;
using Managing.Domain.Risk; using Managing.Domain.Risk;
using Managing.Domain.Scenarios; using Managing.Domain.Scenarios;
using Managing.Domain.Strategies;
using Managing.Domain.Users; using Managing.Domain.Users;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -624,9 +625,9 @@ public class TradingBotChromosome : ChromosomeBase
return clone; return clone;
} }
public List<GeneticIndicator> GetSelectedIndicators() public List<LightIndicator> GetSelectedIndicators()
{ {
var selected = new List<GeneticIndicator>(); var selected = new List<LightIndicator>();
var genes = GetGenes(); var genes = GetGenes();
// Check all indicator selection slots (genes 5 to 5+N-1 where N is number of eligible indicators) // Check all indicator selection slots (genes 5 to 5+N-1 where N is number of eligible indicators)
@@ -634,7 +635,7 @@ public class TradingBotChromosome : ChromosomeBase
{ {
if (genes[5 + i].Value.ToString() == "1") if (genes[5 + i].Value.ToString() == "1")
{ {
var indicator = new GeneticIndicator var indicator = new LightIndicator(_eligibleIndicators[i].ToString(), _eligibleIndicators[i])
{ {
Type = _eligibleIndicators[i] Type = _eligibleIndicators[i]
}; };
@@ -721,31 +722,16 @@ public class TradingBotChromosome : ChromosomeBase
stopLoss = randomStopLoss; stopLoss = randomStopLoss;
// Log the generated values (for debugging) // Log the generated values (for debugging)
Console.WriteLine($"Generated: TP={takeProfit:F2}%, SL={stopLoss:F2}% (RR={takeProfit/stopLoss:F2}:1)"); Console.WriteLine($"Generated: TP={takeProfit:F2}%, SL={stopLoss:F2}% (RR={takeProfit / stopLoss:F2}:1)");
// Get loopback period from gene 4 // Get loopback period from gene 4
var loopbackPeriod = Convert.ToInt32(genes[4].Value); var loopbackPeriod = Convert.ToInt32(genes[4].Value);
// Build scenario using selected indicators // Build scenario using selected indicators
var scenario = new Scenario($"Genetic_{request.RequestId}_Scenario", loopbackPeriod); var scenario = new LightScenario($"Genetic_{request.RequestId}_Scenario", loopbackPeriod)
foreach (var geneticIndicator in selectedIndicators)
{ {
var indicator = ScenarioHelpers.BuildIndicator( Indicators = selectedIndicators
type: geneticIndicator.Type, };
name: $"Genetic_{geneticIndicator.Type}_{Guid.NewGuid():N}",
period: geneticIndicator.Period,
fastPeriods: geneticIndicator.FastPeriods,
slowPeriods: geneticIndicator.SlowPeriods,
signalPeriods: geneticIndicator.SignalPeriods,
multiplier: geneticIndicator.Multiplier,
stochPeriods: geneticIndicator.StochPeriods,
smoothPeriods: geneticIndicator.SmoothPeriods,
cyclePeriods: geneticIndicator.CyclePeriods
);
scenario.AddIndicator(indicator);
}
var mm = new MoneyManagement var mm = new MoneyManagement
{ {
@@ -776,7 +762,7 @@ public class TradingBotChromosome : ChromosomeBase
UseForPositionSizing = false, UseForPositionSizing = false,
UseForSignalFiltering = false, UseForSignalFiltering = false,
UseForDynamicStopLoss = false, UseForDynamicStopLoss = false,
Scenario = LightScenario.FromScenario(scenario), Scenario = scenario,
MoneyManagement = mm, MoneyManagement = mm,
RiskManagement = new RiskManagement RiskManagement = new RiskManagement
{ {
@@ -853,7 +839,7 @@ public class TradingBotChromosome : ChromosomeBase
ReplaceGene(1, new Gene(stopLoss)); ReplaceGene(1, new Gene(stopLoss));
// Log the initial values (for debugging) // Log the initial values (for debugging)
Console.WriteLine($"Initialized: TP={takeProfit:F2}%, SL={stopLoss:F2}% (RR={takeProfit/stopLoss:F2}:1)"); Console.WriteLine($"Initialized: TP={takeProfit:F2}%, SL={stopLoss:F2}% (RR={takeProfit / stopLoss:F2}:1)");
// Initialize remaining genes normally // Initialize remaining genes normally
for (int i = 2; i < Length; i++) for (int i = 2; i < Length; i++)
@@ -863,22 +849,6 @@ public class TradingBotChromosome : ChromosomeBase
} }
} }
/// <summary>
/// Genetic indicator with parameters
/// </summary>
public class GeneticIndicator
{
public IndicatorType Type { get; set; }
public int? Period { get; set; }
public int? FastPeriods { get; set; }
public int? SlowPeriods { get; set; }
public int? SignalPeriods { get; set; }
public double? Multiplier { get; set; }
public int? StochPeriods { get; set; }
public int? SmoothPeriods { get; set; }
public int? CyclePeriods { get; set; }
}
/// <summary> /// <summary>
/// Multi-objective fitness function for trading bot optimization /// Multi-objective fitness function for trading bot optimization
/// </summary> /// </summary>
@@ -889,7 +859,8 @@ public class TradingBotFitness : IFitness
private GeneticAlgorithm _geneticAlgorithm; private GeneticAlgorithm _geneticAlgorithm;
private readonly ILogger<GeneticService> _logger; private readonly ILogger<GeneticService> _logger;
public TradingBotFitness(IServiceScopeFactory serviceScopeFactory, GeneticRequest request, ILogger<GeneticService> logger) public TradingBotFitness(IServiceScopeFactory serviceScopeFactory, GeneticRequest request,
ILogger<GeneticService> logger)
{ {
_serviceScopeFactory = serviceScopeFactory; _serviceScopeFactory = serviceScopeFactory;
_request = request; _request = request;

View File

@@ -0,0 +1,432 @@
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Grains;
using Managing.Application.Abstractions.Models;
using Managing.Application.Abstractions.Services;
using Managing.Domain.Bots;
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
namespace Managing.Application.Grains;
/// <summary>
/// Grain for managing platform-wide summary metrics with real-time updates and periodic snapshots
/// </summary>
public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable
{
private readonly IPersistentState<PlatformSummaryGrainState> _state;
private readonly IBotService _botService;
private readonly IAgentService _agentService;
private readonly ITradingService _tradingService;
private readonly ILogger<PlatformSummaryGrain> _logger;
private const string _hourlySnapshotReminder = "HourlySnapshot";
private const string _dailySnapshotReminder = "DailySnapshot";
public PlatformSummaryGrain(
[PersistentState("platform-summary-state", "platform-summary-store")]
IPersistentState<PlatformSummaryGrainState> state,
IBotService botService,
IAgentService agentService,
ITradingService tradingService,
ILogger<PlatformSummaryGrain> logger)
{
_state = state;
_botService = botService;
_agentService = agentService;
_tradingService = tradingService;
_logger = logger;
}
public override async Task OnActivateAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Platform Summary Grain activated");
// Set up reminders for periodic snapshots
await this.RegisterOrUpdateReminder(_hourlySnapshotReminder,
TimeSpan.FromHours(1), TimeSpan.FromHours(1));
var now = DateTime.UtcNow;
var nextMidnight = now.Date.AddDays(1);
var timeUntilMidnight = nextMidnight - now;
await this.RegisterOrUpdateReminder(_dailySnapshotReminder,
timeUntilMidnight, TimeSpan.FromDays(1));
// Initial data load if state is empty
if (_state.State.LastUpdated == default)
{
await RefreshDataAsync();
}
}
public async Task<PlatformSummaryViewModel> GetPlatformSummaryAsync()
{
// If data is stale or has pending changes, refresh it
if (IsDataStale() || _state.State.HasPendingChanges)
{
await RefreshDataAsync();
}
return MapToViewModel(_state.State);
}
public async Task RefreshDataAsync()
{
try
{
_logger.LogInformation("Refreshing platform summary data");
var agents = await _agentService.GetAllAgentSummaries();
var strategies = await _botService.GetBotsAsync();
// Calculate totals
var totalAgents = agents.Count();
var totalActiveStrategies = strategies.Count(s => s.Status == BotStatus.Running);
// Calculate volume and PnL from strategies
var totalVolume = strategies.Sum(s => s.Volume);
var totalPnL = strategies.Sum(s => s.Pnl);
// Calculate real open interest and position count from actual positions
var (totalOpenInterest, totalPositionCount) = await CalculatePositionMetricsAsync();
// Update state
_state.State.TotalAgents = totalAgents;
_state.State.TotalActiveStrategies = totalActiveStrategies;
_state.State.TotalPlatformVolume = totalVolume;
_state.State.TotalPlatformPnL = totalPnL;
_state.State.TotalOpenInterest = totalOpenInterest;
_state.State.TotalPositionCount = totalPositionCount;
_state.State.LastUpdated = DateTime.UtcNow;
_state.State.HasPendingChanges = false;
// Update volume breakdown by asset
UpdateVolumeBreakdown(strategies);
// Update position count breakdown
UpdatePositionCountBreakdown(strategies);
await _state.WriteStateAsync();
_logger.LogInformation("Platform summary data refreshed successfully");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error refreshing platform summary data");
}
}
private void UpdateVolumeBreakdown(IEnumerable<Bot> strategies)
{
_state.State.VolumeByAsset.Clear();
// Group strategies by ticker and sum their volumes
var volumeByAsset = strategies
.Where(s => s.Volume > 0)
.GroupBy(s => s.Ticker)
.ToDictionary(g => g.Key, g => g.Sum(s => s.Volume));
foreach (var kvp in volumeByAsset)
{
_state.State.VolumeByAsset[kvp.Key] = kvp.Value;
}
_logger.LogDebug("Updated volume breakdown: {AssetCount} assets with total volume {TotalVolume}",
volumeByAsset.Count, volumeByAsset.Values.Sum());
}
private void UpdatePositionCountBreakdown(IEnumerable<Bot> strategies)
{
_state.State.PositionCountByAsset.Clear();
_state.State.PositionCountByDirection.Clear();
// Use position counts directly from bot statistics
var activeStrategies = strategies.Where(s => s.Status != BotStatus.Saved).ToList();
if (activeStrategies.Any())
{
// Group by asset and sum position counts per asset
var positionsByAsset = activeStrategies
.GroupBy(s => s.Ticker)
.ToDictionary(g => g.Key, g => g.Sum(b => b.LongPositionCount + b.ShortPositionCount));
// Sum long and short position counts across all bots
var totalLongPositions = activeStrategies.Sum(s => s.LongPositionCount);
var totalShortPositions = activeStrategies.Sum(s => s.ShortPositionCount);
// Update state
foreach (var kvp in positionsByAsset)
{
_state.State.PositionCountByAsset[kvp.Key] = kvp.Value;
}
_state.State.PositionCountByDirection[TradeDirection.Long] = totalLongPositions;
_state.State.PositionCountByDirection[TradeDirection.Short] = totalShortPositions;
_logger.LogDebug(
"Updated position breakdown from bot statistics: {AssetCount} assets, Long={LongPositions}, Short={ShortPositions}",
positionsByAsset.Count, totalLongPositions, totalShortPositions);
}
else
{
_logger.LogDebug("No active strategies found for position breakdown");
}
}
private async Task<(decimal totalOpenInterest, int totalPositionCount)> CalculatePositionMetricsAsync()
{
try
{
// Get all open positions from all accounts
// Get positions directly from database instead of exchange
var allPositions = await _tradingService.GetAllDatabasePositionsAsync();
var openPositions = allPositions?.Where(p => !p.IsFinished());
if (openPositions?.Any() == true)
{
var positionCount = openPositions.Count();
// Calculate open interest as the sum of leveraged position notional values
// Open interest = sum of (position size * price * leverage) for all open positions
var openInterest = openPositions
.Sum(p => (p.Open.Price * p.Open.Quantity) * p.Open.Leverage);
_logger.LogDebug(
"Calculated position metrics: {PositionCount} positions, {OpenInterest} leveraged open interest",
positionCount, openInterest);
return (openInterest, positionCount);
}
else
{
_logger.LogDebug("No open positions found for metrics calculation");
return (0m, 0);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to calculate position metrics, returning zero values");
return (0m, 0);
}
}
public Task<decimal> GetTotalVolumeAsync()
{
return Task.FromResult(_state.State.TotalPlatformVolume);
}
public Task<decimal> GetTotalPnLAsync()
{
return Task.FromResult(_state.State.TotalPlatformPnL);
}
public Task<decimal> GetTotalOpenInterest()
{
return Task.FromResult(_state.State.TotalOpenInterest);
}
public Task<int> GetTotalPositionCountAsync()
{
return Task.FromResult(_state.State.TotalPositionCount);
}
public Task<List<VolumeHistoryPoint>> GetVolumeHistoryAsync()
{
var historyPoints = _state.State.DailySnapshots
.OrderBy(s => s.Date)
.Select(s => new VolumeHistoryPoint
{
Date = s.Date,
Volume = s.TotalVolume
})
.ToList();
return Task.FromResult(historyPoints);
}
// Event handlers for immediate updates
public async Task UpdateActiveStrategyCountAsync(int newActiveCount)
{
_logger.LogInformation("Updating active strategies count to: {NewActiveCount}", newActiveCount);
_state.State.TotalActiveStrategies = newActiveCount;
_state.State.HasPendingChanges = true;
await _state.WriteStateAsync();
}
public async Task OnPositionOpenedAsync(PositionOpenedEvent evt)
{
_logger.LogInformation("Position opened: {PositionId} for {Ticker}", evt.PositionId, evt.Ticker);
_state.State.TotalPositionCount++;
_state.State.HasPendingChanges = true;
await _state.WriteStateAsync();
}
public async Task OnPositionClosedAsync(PositionClosedEvent evt)
{
_logger.LogInformation("Position closed: {PositionId} for {Ticker} with PnL: {PnL}",
evt.PositionId, evt.Ticker, evt.RealizedPnL);
_state.State.TotalPositionCount--;
_state.State.TotalPlatformVolume += evt.Volume;
_state.State.TotalPlatformPnL += evt.RealizedPnL;
// Update volume by asset
var asset = evt.Ticker;
if (!_state.State.VolumeByAsset.ContainsKey(asset))
{
_state.State.VolumeByAsset[asset] = 0;
}
_state.State.VolumeByAsset[asset] += evt.Volume;
_state.State.TotalOpenInterest -= evt.InitialVolume;
_state.State.HasPendingChanges = true;
await _state.WriteStateAsync();
}
public async Task OnTradeExecutedAsync(TradeExecutedEvent evt)
{
_logger.LogInformation("Trade executed: {TradeId} for {Ticker} with volume: {Volume}",
evt.TradeId, evt.Ticker, evt.Volume);
_state.State.TotalPlatformVolume += evt.Volume;
// Update volume by asset
var asset = evt.Ticker;
if (!_state.State.VolumeByAsset.ContainsKey(asset))
{
_state.State.VolumeByAsset[asset] = 0;
}
_state.State.VolumeByAsset[asset] += evt.Volume;
_state.State.HasPendingChanges = true;
await _state.WriteStateAsync();
}
// Reminder handlers for periodic snapshots
public async Task ReceiveReminder(string reminderName, TickStatus status)
{
_logger.LogInformation("Reminder received: {ReminderName}", reminderName);
switch (reminderName)
{
case _hourlySnapshotReminder:
await TakeHourlySnapshotAsync();
break;
case _dailySnapshotReminder:
await TakeDailySnapshotAsync();
break;
}
}
private async Task TakeHourlySnapshotAsync()
{
_logger.LogInformation("Taking hourly snapshot");
var snapshot = new HourlySnapshot
{
Timestamp = DateTime.UtcNow,
TotalAgents = _state.State.TotalAgents,
TotalStrategies = _state.State.TotalActiveStrategies,
TotalVolume = _state.State.TotalPlatformVolume,
TotalPnL = _state.State.TotalPlatformPnL,
TotalOpenInterest = _state.State.TotalOpenInterest,
TotalPositionCount = _state.State.TotalPositionCount
};
_state.State.HourlySnapshots.Add(snapshot);
// Keep only last 24 hours
var cutoff = DateTime.UtcNow.AddHours(-24);
_state.State.HourlySnapshots.RemoveAll(s => s.Timestamp < cutoff);
await _state.WriteStateAsync();
}
private async Task TakeDailySnapshotAsync()
{
_logger.LogInformation("Taking daily snapshot");
// Store 24-hour ago values for comparison
_state.State.TotalAgents24hAgo = _state.State.TotalAgents;
_state.State.TotalActiveStrategies24hAgo = _state.State.TotalActiveStrategies;
_state.State.TotalPlatformPnL24hAgo = _state.State.TotalPlatformPnL;
_state.State.TotalPlatformVolume24hAgo = _state.State.TotalPlatformVolume;
_state.State.TotalOpenInterest24hAgo = _state.State.TotalOpenInterest;
_state.State.TotalPositionCount24hAgo = _state.State.TotalPositionCount;
// Add daily snapshot
var dailySnapshot = new DailySnapshot
{
Date = DateTime.UtcNow.Date,
TotalAgents = _state.State.TotalAgents,
TotalStrategies = _state.State.TotalActiveStrategies,
TotalVolume = _state.State.TotalPlatformVolume,
TotalPnL = _state.State.TotalPlatformPnL,
TotalOpenInterest = _state.State.TotalOpenInterest,
TotalPositionCount = _state.State.TotalPositionCount
};
_state.State.DailySnapshots.Add(dailySnapshot);
// Keep only last 30 days
var cutoff = DateTime.UtcNow.AddDays(-30);
_state.State.DailySnapshots.RemoveAll(s => s.Date < cutoff);
_state.State.LastSnapshot = DateTime.UtcNow;
await _state.WriteStateAsync();
}
private bool IsDataStale()
{
var timeSinceLastUpdate = DateTime.UtcNow - _state.State.LastUpdated;
return timeSinceLastUpdate > TimeSpan.FromMinutes(5);
}
private PlatformSummaryViewModel MapToViewModel(PlatformSummaryGrainState state)
{
// Generate volume history from daily snapshots
var volumeHistory = state.DailySnapshots
.OrderBy(s => s.Date)
.Select(s => new VolumeHistoryPoint
{
Date = s.Date,
Volume = s.TotalVolume
})
.ToList();
return new PlatformSummaryViewModel
{
TotalAgents = state.TotalAgents,
TotalActiveStrategies = state.TotalActiveStrategies,
TotalPlatformPnL = state.TotalPlatformPnL,
TotalPlatformVolume = state.TotalPlatformVolume,
TotalPlatformVolumeLast24h = state.TotalPlatformVolume - state.TotalPlatformVolume24hAgo,
TotalOpenInterest = state.TotalOpenInterest,
TotalPositionCount = state.TotalPositionCount,
// 24-hour changes
AgentsChange24h = state.TotalAgents - state.TotalAgents24hAgo,
StrategiesChange24h = state.TotalActiveStrategies - state.TotalActiveStrategies24hAgo,
PnLChange24h = state.TotalPlatformPnL - state.TotalPlatformPnL24hAgo,
VolumeChange24h = state.TotalPlatformVolume - state.TotalPlatformVolume24hAgo,
OpenInterestChange24h = state.TotalOpenInterest - state.TotalOpenInterest24hAgo,
PositionCountChange24h = state.TotalPositionCount - state.TotalPositionCount24hAgo,
// Breakdowns
VolumeByAsset = state.VolumeByAsset ?? new Dictionary<Ticker, decimal>(),
PositionCountByAsset = state.PositionCountByAsset ?? new Dictionary<Ticker, int>(),
PositionCountByDirection = state.PositionCountByDirection ?? new Dictionary<TradeDirection, int>(),
// Volume history for charting (last 30 days)
VolumeHistory = volumeHistory,
// Metadata
LastUpdated = state.LastUpdated,
Last24HourSnapshot = state.LastSnapshot
};
}
}

View File

@@ -1,52 +0,0 @@
using Managing.Application.Abstractions.Repositories;
using Managing.Domain.Bots;
using Managing.Domain.Users;
using static Managing.Common.Enums;
namespace Managing.Application.ManageBot
{
public interface IBackupBotService
{
Task<BotBackup> GetBotBackup(string identifier);
Task SaveOrUpdateBotBackup(User user, string identifier, BotStatus status, TradingBotBackup data);
}
public class BackupBotService : IBackupBotService
{
private readonly IBotRepository _botRepository;
public BackupBotService(IBotRepository botRepository)
{
_botRepository = botRepository;
}
public async Task<BotBackup> GetBotBackup(string identifier)
{
return await _botRepository.GetBotByIdentifierAsync(identifier);
}
public async Task SaveOrUpdateBotBackup(User user, string identifier, BotStatus status, TradingBotBackup data)
{
var backup = await GetBotBackup(identifier);
if (backup != null)
{
backup.LastStatus = status;
backup.Data = data;
await _botRepository.UpdateBackupBot(backup);
}
else
{
var botBackup = new BotBackup
{
LastStatus = status,
User = user,
Identifier = identifier,
Data = data
};
await _botRepository.InsertBotAsync(botBackup);
}
}
}
}

View File

@@ -1,12 +1,13 @@
using System.Collections.Concurrent;
using Managing.Application.Abstractions; using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Grains;
using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services; using Managing.Application.Abstractions.Services;
using Managing.Application.Bots; using Managing.Application.Bots;
using Managing.Core;
using Managing.Domain.Bots; using Managing.Domain.Bots;
using Managing.Domain.Scenarios; using Managing.Domain.Scenarios;
using Managing.Domain.Users; using Managing.Domain.Shared.Helpers;
using Managing.Domain.Workflows; using Managing.Domain.Trades;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using static Managing.Common.Enums; using static Managing.Common.Enums;
@@ -16,301 +17,136 @@ namespace Managing.Application.ManageBot
public class BotService : IBotService public class BotService : IBotService
{ {
private readonly IBotRepository _botRepository; private readonly IBotRepository _botRepository;
private readonly IExchangeService _exchangeService;
private readonly IMessengerService _messengerService; private readonly IMessengerService _messengerService;
private readonly IAccountService _accountService;
private readonly ILogger<TradingBotBase> _tradingBotLogger; private readonly ILogger<TradingBotBase> _tradingBotLogger;
private readonly ITradingService _tradingService; private readonly ITradingService _tradingService;
private readonly IMoneyManagementService _moneyManagementService;
private readonly IUserService _userService;
private readonly IBackupBotService _backupBotService;
private readonly IServiceScopeFactory _scopeFactory;
private readonly IGrainFactory _grainFactory; private readonly IGrainFactory _grainFactory;
private readonly IServiceScopeFactory _scopeFactory;
private ConcurrentDictionary<string, BotTaskWrapper> _botTasks =
new ConcurrentDictionary<string, BotTaskWrapper>();
public BotService(IBotRepository botRepository, IExchangeService exchangeService, public BotService(IBotRepository botRepository,
IMessengerService messengerService, IAccountService accountService, ILogger<TradingBotBase> tradingBotLogger, IMessengerService messengerService, ILogger<TradingBotBase> tradingBotLogger,
ITradingService tradingService, IMoneyManagementService moneyManagementService, IUserService userService, ITradingService tradingService, IGrainFactory grainFactory, IServiceScopeFactory scopeFactory)
IBackupBotService backupBotService, IServiceScopeFactory scopeFactory, IGrainFactory grainFactory)
{ {
_botRepository = botRepository; _botRepository = botRepository;
_exchangeService = exchangeService;
_messengerService = messengerService; _messengerService = messengerService;
_accountService = accountService;
_tradingBotLogger = tradingBotLogger; _tradingBotLogger = tradingBotLogger;
_tradingService = tradingService; _tradingService = tradingService;
_moneyManagementService = moneyManagementService;
_userService = userService;
_backupBotService = backupBotService;
_scopeFactory = scopeFactory;
_grainFactory = grainFactory; _grainFactory = grainFactory;
_scopeFactory = scopeFactory;
} }
public class BotTaskWrapper public async Task<IEnumerable<Bot>> GetBotsAsync()
{
public Task Task { get; private set; }
public Type BotType { get; private set; }
public object BotInstance { get; private set; }
public BotTaskWrapper(Task task, Type botType, object botInstance)
{
Task = task;
BotType = botType;
BotInstance = botInstance;
}
}
public void AddSimpleBotToCache(IBot bot)
{
var botTask = new BotTaskWrapper(Task.Run(() => bot.Start()), bot.GetType(), bot);
_botTasks.AddOrUpdate(bot.Identifier, botTask, (key, existingVal) => botTask);
}
public void AddTradingBotToCache(ITradingBot bot)
{
var botTask = new BotTaskWrapper(Task.Run(() => bot.Start()), bot.GetType(), bot);
_botTasks.AddOrUpdate(bot.Identifier, botTask, (key, existingVal) => botTask);
}
private async Task InitBot(ITradingBot bot, BotBackup backupBot)
{
try
{
var user = await _userService.GetUser(backupBot.User.Name);
bot.User = user;
// Load backup data into the bot
bot.LoadBackup(backupBot);
// Only start the bot if the backup status is Up
if (backupBot.LastStatus == BotStatus.Up)
{
// Start the bot asynchronously without waiting for completion
_ = Task.Run(() => bot.Start());
}
else
{
// Keep the bot in Down status if it was originally Down
bot.Stop();
}
}
catch (Exception ex)
{
_tradingBotLogger.LogError(ex, "Error initializing bot {Identifier} from backup", backupBot.Identifier);
// Ensure the bot is stopped if initialization fails
bot.Stop();
throw;
}
}
public List<ITradingBot> GetActiveBots()
{
var bots = _botTasks.Values
.Where(wrapper => typeof(ITradingBot).IsAssignableFrom(wrapper.BotType))
.Select(wrapper => wrapper.BotInstance as ITradingBot)
.Where(bot => bot != null)
.ToList();
return bots;
}
public async Task<IEnumerable<BotBackup>> GetSavedBotsAsync()
{ {
return await _botRepository.GetBotsAsync(); return await _botRepository.GetBotsAsync();
} }
public async Task StartBotFromBackup(BotBackup backupBot) public async Task<IEnumerable<Bot>> GetBotsByStatusAsync(BotStatus status)
{ {
object bot = null; return await _botRepository.GetBotsByStatusAsync(status);
Task botTask = null;
var scalpingBotData = backupBot.Data;
// Get the config directly from the backup
var scalpingConfig = scalpingBotData.Config;
// Ensure the money management is properly loaded from database if needed
if (scalpingConfig.MoneyManagement != null &&
!string.IsNullOrEmpty(scalpingConfig.MoneyManagement.Name))
{
var moneyManagement = _moneyManagementService
.GetMoneyMangement(scalpingConfig.MoneyManagement.Name).Result;
if (moneyManagement != null)
{
scalpingConfig.MoneyManagement = moneyManagement;
}
} }
// Ensure the scenario is properly loaded from database if needed public async Task<BotStatus> StopBot(Guid identifier)
if (scalpingConfig.Scenario == null && !string.IsNullOrEmpty(scalpingConfig.ScenarioName))
{
var scenario = await _tradingService.GetScenarioByNameAsync(scalpingConfig.ScenarioName);
if (scenario != null)
{
scalpingConfig.Scenario = LightScenario.FromScenario(scenario);
}
else
{
throw new ArgumentException(
$"Scenario '{scalpingConfig.ScenarioName}' not found in database when loading backup");
}
}
if (scalpingConfig.Scenario == null)
{
throw new ArgumentException(
"Scenario object must be provided or ScenarioName must be valid when loading backup");
}
// Ensure critical properties are set correctly for restored bots
scalpingConfig.IsForBacktest = false;
// IMPORTANT: Save the backup to database BEFORE creating the Orleans grain
// This ensures the backup exists when the grain tries to serialize it
await SaveOrUpdateBotBackup(backupBot.User, backupBot.Identifier, backupBot.LastStatus, backupBot.Data);
bot = await CreateTradingBot(scalpingConfig);
botTask = Task.Run(() => InitBot((ITradingBot)bot, backupBot));
if (bot != null && botTask != null)
{
var botWrapper = new BotTaskWrapper(botTask, bot.GetType(), bot);
_botTasks.AddOrUpdate(backupBot.Identifier, botWrapper, (key, existingVal) => botWrapper);
}
}
public async Task<BotBackup> GetBotBackup(string identifier)
{
return await _botRepository.GetBotByIdentifierAsync(identifier);
}
public async Task SaveOrUpdateBotBackup(User user, string identifier, BotStatus status, TradingBotBackup data)
{
var backup = await GetBotBackup(identifier);
if (backup != null)
{
backup.LastStatus = status;
backup.Data = data;
await _botRepository.UpdateBackupBot(backup);
}
else
{
var botBackup = new BotBackup
{
LastStatus = status,
User = user,
Identifier = identifier,
Data = data
};
await _botRepository.InsertBotAsync(botBackup);
}
}
public IBot CreateSimpleBot(string botName, Workflow workflow)
{
return new SimpleBot(botName, _tradingBotLogger, workflow, this, _backupBotService);
}
public async Task<string> StopBot(string identifier)
{
if (_botTasks.TryGetValue(identifier, out var botWrapper))
{
if (botWrapper.BotInstance is IBot bot)
{
await Task.Run(() =>
bot.Stop());
var stopMessage = $"🛑 **Bot Stopped**\n\n" +
$"🎯 **Agent:** {bot.User.AgentName}\n" +
$"🤖 **Bot Name:** {bot.Name}\n" +
$"⏰ **Stopped At:** {DateTime.UtcNow:MMM dd, yyyy • HH:mm:ss} UTC\n\n" +
$"✅ **Bot has been safely stopped and is no longer active.**";
await _messengerService.SendTradeMessage(stopMessage, false, bot.User);
return bot.GetStatus();
}
}
return BotStatus.Down.ToString();
}
public async Task<bool> DeleteBot(string identifier)
{
if (_botTasks.TryRemove(identifier, out var botWrapper))
{ {
try try
{ {
if (botWrapper.BotInstance is IBot bot) var grain = _grainFactory.GetGrain<ILiveTradingBotGrain>(identifier);
await grain.StopAsync();
return BotStatus.Stopped;
}
catch (Exception e)
{ {
await Task.Run(() => _tradingBotLogger.LogError(e, "Error stopping bot {Identifier}", identifier);
bot.Stop()); return BotStatus.Stopped;
}
}
public async Task<bool> DeleteBot(Guid identifier)
{
var grain = _grainFactory.GetGrain<ILiveTradingBotGrain>(identifier);
try
{
var config = await grain.GetConfiguration();
var account = await grain.GetAccount();
await grain.StopAsync();
await _botRepository.DeleteBot(identifier);
await grain.DeleteAsync();
var deleteMessage = $"🗑️ **Bot Deleted**\n\n" + var deleteMessage = $"🗑️ **Bot Deleted**\n\n" +
$"🎯 **Agent:** {bot.User.AgentName}\n" + $"🎯 **Agent:** {account.User.AgentName}\n" +
$"🤖 **Bot Name:** {bot.Name}\n" + $"🤖 **Bot Name:** {config.Name}\n" +
$"⏰ **Deleted At:** {DateTime.UtcNow:MMM dd, yyyy • HH:mm:ss} UTC\n\n" + $"⏰ **Deleted At:** {DateTime.UtcNow:MMM dd, yyyy • HH:mm:ss} UTC\n\n" +
$"⚠️ **Bot has been permanently deleted and all data removed.**"; $"⚠️ **Bot has been permanently deleted and all data removed.**";
await _messengerService.SendTradeMessage(deleteMessage, false, bot.User); await _messengerService.SendTradeMessage(deleteMessage, false, account.User);
}
await _botRepository.DeleteBotBackup(identifier);
return true; return true;
} }
catch (Exception e) catch (Exception e)
{ {
Console.WriteLine(e); _tradingBotLogger.LogError(e, "Error deleting bot {Identifier}", identifier);
return false; return false;
} }
} }
return false; public async Task<BotStatus> RestartBot(Guid identifier)
{
try
{
var registryGrain = _grainFactory.GetGrain<ILiveBotRegistryGrain>(0);
var previousStatus = await registryGrain.GetBotStatus(identifier);
// If bot is already up, return the status directly
if (previousStatus == BotStatus.Running)
{
return BotStatus.Running;
} }
public async Task<string> RestartBot(string identifier) var botGrain = _grainFactory.GetGrain<ILiveTradingBotGrain>(identifier);
if (previousStatus == BotStatus.Saved)
{ {
if (_botTasks.TryGetValue(identifier, out var botWrapper)) // First time startup
await botGrain.StartAsync();
var grainState = await botGrain.GetBotDataAsync();
var account = await botGrain.GetAccount();
var startupMessage = $"🚀 **Bot Started**\n\n" +
$"🎯 **Agent:** {account.User.AgentName}\n" +
$"🤖 **Bot Name:** {grainState.Config.Name}\n" +
$"⏰ **Started At:** {DateTime.UtcNow:MMM dd, yyyy • HH:mm:ss} UTC\n" +
$"🕐 **Startup Time:** {grainState.StartupTime:MMM dd, yyyy • HH:mm:ss} UTC\n\n" +
$"✅ **Bot has been successfully started and is now active.**";
await _messengerService.SendTradeMessage(startupMessage, false, account.User);
}
else
{ {
if (botWrapper.BotInstance is IBot bot) // Restart (bot was previously down)
{ await botGrain.RestartAsync();
// Stop the bot first to ensure clean state var grainState = await botGrain.GetBotDataAsync();
bot.Stop(); var account = await botGrain.GetAccount();
// Small delay to ensure stop is complete
await Task.Delay(100);
// Restart the bot (this will update StartupTime)
bot.Restart();
// Start the bot asynchronously without waiting for completion
_ = Task.Run(() => bot.Start());
var restartMessage = $"🔄 **Bot Restarted**\n\n" + var restartMessage = $"🔄 **Bot Restarted**\n\n" +
$"🎯 **Agent:** {bot.User.AgentName}\n" + $"🎯 **Agent:** {account.User.AgentName}\n" +
$"🤖 **Bot Name:** {bot.Name}\n" + $"🤖 **Bot Name:** {grainState.Config.Name}\n" +
$"⏰ **Restarted At:** {DateTime.UtcNow:MMM dd, yyyy • HH:mm:ss} UTC\n" + $"⏰ **Restarted At:** {DateTime.UtcNow:MMM dd, yyyy • HH:mm:ss} UTC\n" +
$"🕐 **New Startup Time:** {bot.StartupTime:MMM dd, yyyy • HH:mm:ss} UTC\n\n" + $"🕐 **New Startup Time:** {grainState.StartupTime:MMM dd, yyyy • HH:mm:ss} UTC\n\n" +
$"🚀 **Bot has been successfully restarted and is now active.**"; $"🚀 **Bot has been successfully restarted and is now active.**";
await _messengerService.SendTradeMessage(restartMessage, false, bot.User); await _messengerService.SendTradeMessage(restartMessage, false, account.User);
return bot.GetStatus(); }
return BotStatus.Running;
}
catch (Exception e)
{
_tradingBotLogger.LogError(e, "Error restarting bot {Identifier}", identifier);
return BotStatus.Stopped;
} }
} }
return BotStatus.Down.ToString(); private async Task<Bot> GetBot(Guid identifier)
}
public async Task ToggleIsForWatchingOnly(string identifier)
{ {
if (_botTasks.TryGetValue(identifier, out var botTaskWrapper) && var bot = await _botRepository.GetBotByIdentifierAsync(identifier);
botTaskWrapper.BotInstance is ITradingBot tradingBot) return bot;
{
await tradingBot.ToggleIsForWatchOnly();
}
} }
/// <summary> /// <summary>
@@ -319,11 +155,9 @@ namespace Managing.Application.ManageBot
/// <param name="identifier">The bot identifier</param> /// <param name="identifier">The bot identifier</param>
/// <param name="newConfig">The new configuration to apply</param> /// <param name="newConfig">The new configuration to apply</param>
/// <returns>True if the configuration was successfully updated, false otherwise</returns> /// <returns>True if the configuration was successfully updated, false otherwise</returns>
public async Task<bool> UpdateBotConfiguration(string identifier, TradingBotConfig newConfig) public async Task<bool> UpdateBotConfiguration(Guid identifier, TradingBotConfig newConfig)
{
if (_botTasks.TryGetValue(identifier, out var botTaskWrapper) &&
botTaskWrapper.BotInstance is TradingBotBase tradingBot)
{ {
var grain = _grainFactory.GetGrain<ILiveTradingBotGrain>(identifier);
// Ensure the scenario is properly loaded from database if needed // Ensure the scenario is properly loaded from database if needed
if (newConfig.Scenario == null && !string.IsNullOrEmpty(newConfig.ScenarioName)) if (newConfig.Scenario == null && !string.IsNullOrEmpty(newConfig.ScenarioName))
{ {
@@ -345,102 +179,183 @@ namespace Managing.Application.ManageBot
"Scenario object must be provided or ScenarioName must be valid when updating configuration"); "Scenario object must be provided or ScenarioName must be valid when updating configuration");
} }
// Check if the bot name is changing return await grain.UpdateConfiguration(newConfig);
if (newConfig.Name != identifier && !string.IsNullOrEmpty(newConfig.Name))
{
// Check if new name already exists
if (_botTasks.ContainsKey(newConfig.Name))
{
return false; // New name already in use
} }
// Update the bot configuration first public async Task<TradingBotConfig> GetBotConfig(Guid identifier)
var updateResult = await tradingBot.UpdateConfiguration(newConfig, allowNameChange: true);
if (updateResult)
{ {
// Update the dictionary key var grain = _grainFactory.GetGrain<ILiveTradingBotGrain>(identifier);
if (_botTasks.TryRemove(identifier, out var removedWrapper)) return await grain.GetConfiguration();
{
_botTasks.TryAdd(newConfig.Name, removedWrapper);
// Update the backup with the new identifier
if (!newConfig.IsForBacktest)
{
// Delete old backup
await _botRepository.DeleteBotBackup(identifier);
// Save new backup will be handled by the bot's SaveBackup method
}
}
} }
return updateResult; public async Task<IEnumerable<string>> GetActiveBotsNamesAsync()
}
else
{ {
// No name change, just update configuration var bots = await _botRepository.GetBotsByStatusAsync(BotStatus.Running);
return await tradingBot.UpdateConfiguration(newConfig); return bots.Select(b => b.Name);
}
} }
public async Task<IEnumerable<Bot>> GetBotsByUser(int id)
{
return await _botRepository.GetBotsByUserIdAsync(id);
}
public async Task<IEnumerable<Bot>> GetBotsByIdsAsync(IEnumerable<Guid> botIds)
{
return await _botRepository.GetBotsByIdsAsync(botIds);
}
public async Task<Bot> GetBotByName(string name)
{
return await _botRepository.GetBotByNameAsync(name);
}
public async Task<Bot> GetBotByIdentifier(Guid identifier)
{
return await _botRepository.GetBotByIdentifierAsync(identifier);
}
public async Task<Position> OpenPositionManuallyAsync(Guid identifier, TradeDirection direction)
{
var grain = _grainFactory.GetGrain<ILiveTradingBotGrain>(identifier);
return await grain.OpenPositionManuallyAsync(direction);
}
public async Task<Position> ClosePositionAsync(Guid identifier, Guid positionId)
{
var grain = _grainFactory.GetGrain<ILiveTradingBotGrain>(identifier);
return await grain.ClosePositionAsync(positionId);
}
public async Task<bool> UpdateBotStatisticsAsync(Guid identifier)
{
try
{
var grain = _grainFactory.GetGrain<ILiveTradingBotGrain>(identifier);
var botData = await grain.GetBotDataAsync();
// Get the current bot from database
var existingBot = await _botRepository.GetBotByIdentifierAsync(identifier);
if (existingBot == null)
{
_tradingBotLogger.LogWarning("Bot {Identifier} not found in database for statistics update",
identifier);
return false; return false;
} }
public async Task<ITradingBot> CreateTradingBot(TradingBotConfig config) // Calculate statistics using TradingBox helpers
var (tradeWins, tradeLosses) = TradingBox.GetWinLossCount(botData.Positions);
var pnl = botData.ProfitAndLoss;
var fees = botData.Positions.Values.Sum(p =>
{ {
// Ensure the scenario is properly loaded from database if needed if (p.Open.Price > 0 && p.Open.Quantity > 0)
if (config.Scenario == null && !string.IsNullOrEmpty(config.ScenarioName))
{ {
var scenario = await _tradingService.GetScenarioByNameAsync(config.ScenarioName); var positionSizeUsd = (p.Open.Price * p.Open.Quantity) * p.Open.Leverage;
if (scenario != null) var uiFeeRate = 0.001m; // 0.1%
var uiFeeOpen = positionSizeUsd * uiFeeRate;
var networkFeeForOpening = 0.50m;
return uiFeeOpen + networkFeeForOpening;
}
return 0;
});
var volume = TradingBox.GetTotalVolumeTraded(botData.Positions);
// Calculate ROI based on total investment
var totalInvestment = botData.Positions.Values
.Where(p => p.IsFinished())
.Sum(p => p.Open.Quantity * p.Open.Price);
var roi = totalInvestment > 0 ? (pnl / totalInvestment) * 100 : 0;
// Update bot statistics
existingBot.TradeWins = tradeWins;
existingBot.TradeLosses = tradeLosses;
existingBot.Pnl = pnl;
existingBot.Roi = roi;
existingBot.Volume = volume;
existingBot.Fees = fees;
// Use the new SaveBotStatisticsAsync method
return await SaveBotStatisticsAsync(existingBot);
}
catch (Exception e)
{ {
config.Scenario = LightScenario.FromScenario(scenario); _tradingBotLogger.LogError(e, "Error updating bot statistics for {Identifier}", identifier);
return false;
}
}
public async Task<bool> SaveBotStatisticsAsync(Bot bot)
{
try
{
if (bot == null)
{
_tradingBotLogger.LogWarning("Cannot save bot statistics: bot object is null");
return false;
}
var existingBot = await _botRepository.GetBotByIdentifierAsync(bot.Identifier);
// Check if bot already exists in database
await ServiceScopeHelpers.WithScopedService<IBotRepository>(
_scopeFactory,
async repo =>
{
if (existingBot == null)
{
_tradingBotLogger.LogInformation("Updating existing bot statistics for bot {BotId}",
bot.Identifier);
// Insert new bot
await repo.InsertBotAsync(bot);
_tradingBotLogger.LogInformation(
"Created new bot statistics for bot {BotId}: Wins={Wins}, Losses={Losses}, PnL={PnL}, ROI={ROI}%, Volume={Volume}, Fees={Fees}",
bot.Identifier, bot.TradeWins, bot.TradeLosses, bot.Pnl, bot.Roi, bot.Volume, bot.Fees);
} }
else else
{ {
throw new ArgumentException($"Scenario '{config.ScenarioName}' not found in database"); _tradingBotLogger.LogInformation("Creating new bot statistics for bot {BotId}",
bot.Identifier);
// Update existing bot
await repo.UpdateBot(bot);
_tradingBotLogger.LogDebug(
"Updated bot statistics for bot {BotId}: Wins={Wins}, Losses={Losses}, PnL={PnL}, ROI={ROI}%, Volume={Volume}, Fees={Fees}",
bot.Identifier, bot.TradeWins, bot.TradeLosses, bot.Pnl, bot.Roi, bot.Volume, bot.Fees);
}
});
return true;
}
catch (Exception e)
{
_tradingBotLogger.LogError(e, "Error saving bot statistics for bot {BotId}", bot?.Identifier);
return false;
} }
} }
if (config.Scenario == null) public async 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")
{ {
throw new ArgumentException("Scenario object must be provided or ScenarioName must be valid"); return await ServiceScopeHelpers.WithScopedService<IBotRepository, (IEnumerable<Bot> Bots, int TotalCount)>(
} _scopeFactory,
async repo =>
// For now, use TradingBot for both live trading and backtesting
// TODO: Implement Orleans grain for live trading when ready
if (!config.IsForBacktest)
{ {
// Ensure critical properties are set correctly for live trading return await repo.GetBotsPaginatedAsync(
config.IsForBacktest = false; pageNumber,
} pageSize,
status,
return new TradingBotBase(_tradingBotLogger, _scopeFactory, config); name,
} ticker,
agentName,
public async Task<ITradingBot> CreateBacktestTradingBot(TradingBotConfig config) sortBy,
{ sortDirection);
// Ensure the scenario is properly loaded from database if needed });
if (config.Scenario == null && !string.IsNullOrEmpty(config.ScenarioName))
{
var scenario = await _tradingService.GetScenarioByNameAsync(config.ScenarioName);
if (scenario != null)
{
config.Scenario = LightScenario.FromScenario(scenario);
}
else
{
throw new ArgumentException($"Scenario '{config.ScenarioName}' not found in database");
}
}
if (config.Scenario == null)
{
throw new ArgumentException("Scenario object must be provided or ScenarioName must be valid");
}
config.IsForBacktest = true;
return new TradingBotBase(_tradingBotLogger, _scopeFactory, config);
} }
} }
} }

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