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
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
|||||||
LOGS_DIR="$SCRIPT_DIR/$LOGS_DIR_NAME"
|
LOGS_DIR="$SCRIPT_DIR/$LOGS_DIR_NAME"
|
||||||
mkdir -p "$LOGS_DIR" || { echo "Failed to create logs directory: $LOGS_DIR"; exit 1; }
|
mkdir -p "$LOGS_DIR" || { echo "Failed to create logs directory: $LOGS_DIR"; exit 1; }
|
||||||
|
|
||||||
LOG_FILE="./logs/migration_${ENVIRONMENT}_${TIMESTAMP}.log"
|
LOG_FILE="$SCRIPT_DIR/logs/migration_${ENVIRONMENT}_${TIMESTAMP}.log"
|
||||||
|
|
||||||
# Colors for output
|
# Colors for output
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
@@ -155,6 +155,12 @@ extract_connection_details() {
|
|||||||
log "📋 Extracted connection details: $DB_HOST:$DB_PORT/$DB_NAME (user: $DB_USER, password: $DB_PASSWORD)"
|
log "📋 Extracted connection details: $DB_HOST:$DB_PORT/$DB_NAME (user: $DB_USER, password: $DB_PASSWORD)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Helper function to get the first migration name
|
||||||
|
get_first_migration() {
|
||||||
|
local first_migration=$(cd "$DB_PROJECT_PATH" && dotnet ef migrations list --no-build --startup-project "$API_PROJECT_PATH" | head -1 | awk '{print $1}')
|
||||||
|
echo "$first_migration"
|
||||||
|
}
|
||||||
|
|
||||||
# Helper function to test PostgreSQL connectivity
|
# Helper function to test PostgreSQL connectivity
|
||||||
test_postgres_connectivity() {
|
test_postgres_connectivity() {
|
||||||
if ! command -v psql >/dev/null 2>&1; then
|
if ! command -v psql >/dev/null 2>&1; then
|
||||||
@@ -243,13 +249,6 @@ else
|
|||||||
error "❌ Failed to build Managing.Infrastructure.Database project"
|
error "❌ Failed to build Managing.Infrastructure.Database project"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log "🔧 Building Managing.Api project..."
|
|
||||||
if (cd "$API_PROJECT_PATH" && dotnet build); then
|
|
||||||
log "✅ Managing.Api project built successfully"
|
|
||||||
else
|
|
||||||
error "❌ Failed to build Managing.Api project"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Step 1: Check Database Connection and Create if Needed
|
# Step 1: Check Database Connection and Create if Needed
|
||||||
log "🔧 Step 1: Checking database connection and creating database if needed..."
|
log "🔧 Step 1: Checking database connection and creating database if needed..."
|
||||||
|
|
||||||
@@ -417,13 +416,23 @@ else
|
|||||||
error " This is critical. Please review the previous error messages and your connection string for '$ENVIRONMENT'."
|
error " This is critical. Please review the previous error messages and your connection string for '$ENVIRONMENT'."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Step 2: Create Backup
|
# Step 2: Create database backup (only if database exists)
|
||||||
log "📦 Step 2: Creating database backup using pg_dump..."
|
log "📦 Step 2: Checking if database backup is needed..."
|
||||||
|
|
||||||
# Define the actual backup file path (absolute)
|
# Check if the target database exists
|
||||||
BACKUP_FILE="$BACKUP_DIR/managing_${ENVIRONMENT}_backup_${TIMESTAMP}.sql"
|
DB_EXISTS=false
|
||||||
# Backup file display path (relative to script execution)
|
if PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "postgres" -c "SELECT 1 FROM pg_database WHERE datname='$DB_NAME';" 2>/dev/null | grep -q "1 row"; then
|
||||||
BACKUP_FILE_DISPLAY="$BACKUP_DIR_NAME/$ENVIRONMENT/managing_${ENVIRONMENT}_backup_${TIMESTAMP}.sql"
|
DB_EXISTS=true
|
||||||
|
log "✅ Target database '$DB_NAME' exists - proceeding with backup"
|
||||||
|
else
|
||||||
|
log "ℹ️ Target database '$DB_NAME' does not exist - skipping backup"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$DB_EXISTS" = "true" ]; then
|
||||||
|
# Define the actual backup file path (absolute)
|
||||||
|
BACKUP_FILE="$BACKUP_DIR/managing_${ENVIRONMENT}_backup_${TIMESTAMP}.sql"
|
||||||
|
# Backup file display path (relative to script execution)
|
||||||
|
BACKUP_FILE_DISPLAY="$BACKUP_DIR_NAME/$ENVIRONMENT/managing_${ENVIRONMENT}_backup_${TIMESTAMP}.sql"
|
||||||
|
|
||||||
# Create backup with retry logic
|
# Create backup with retry logic
|
||||||
BACKUP_SUCCESS=false
|
BACKUP_SUCCESS=false
|
||||||
@@ -439,11 +448,84 @@ for attempt in 1 2 3; do
|
|||||||
else
|
else
|
||||||
# If pg_dump fails, fall back to EF Core migration script
|
# If pg_dump fails, fall back to EF Core migration script
|
||||||
warn "⚠️ pg_dump failed, falling back to EF Core migration script..."
|
warn "⚠️ pg_dump failed, falling back to EF Core migration script..."
|
||||||
|
|
||||||
|
# Get the first migration name to generate complete script
|
||||||
|
FIRST_MIGRATION=$(get_first_migration)
|
||||||
|
|
||||||
|
if [ -n "$FIRST_MIGRATION" ]; then
|
||||||
|
log "📋 Generating complete backup script from initial migration: $FIRST_MIGRATION"
|
||||||
|
if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --from "$FIRST_MIGRATION" --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE"); then
|
||||||
|
log "✅ Complete EF Core Migration SQL Script generated: $BACKUP_FILE_DISPLAY"
|
||||||
|
BACKUP_SUCCESS=true
|
||||||
|
break
|
||||||
|
else
|
||||||
|
# Try fallback without specifying from migration
|
||||||
|
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE") 2>&1 || true)
|
||||||
|
if [ $attempt -lt 3 ]; then
|
||||||
|
warn "⚠️ Backup attempt $attempt failed. Retrying in 5 seconds..."
|
||||||
|
warn " EF CLI Output: $ERROR_OUTPUT"
|
||||||
|
sleep 5
|
||||||
|
else
|
||||||
|
error "❌ Database backup failed after 3 attempts."
|
||||||
|
error " EF CLI Output: $ERROR_OUTPUT"
|
||||||
|
error " Migration aborted for safety reasons."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Fallback: generate script without specifying from migration
|
||||||
|
if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE"); then
|
||||||
|
log "✅ EF Core Migration SQL Script generated: $BACKUP_FILE_DISPLAY"
|
||||||
|
BACKUP_SUCCESS=true
|
||||||
|
break
|
||||||
|
else
|
||||||
|
# Try fallback without specifying from migration
|
||||||
|
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE") 2>&1 || true)
|
||||||
|
if [ $attempt -lt 3 ]; then
|
||||||
|
warn "⚠️ Backup attempt $attempt failed. Retrying in 5 seconds..."
|
||||||
|
warn " EF CLI Output: $ERROR_OUTPUT"
|
||||||
|
sleep 5
|
||||||
|
else
|
||||||
|
error "❌ Database backup failed after 3 attempts."
|
||||||
|
error " EF CLI Output: $ERROR_OUTPUT"
|
||||||
|
error " Migration aborted for safety reasons."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# If pg_dump is not available, use EF Core migration script
|
||||||
|
warn "⚠️ pg_dump not available, using EF Core migration script for backup..."
|
||||||
|
|
||||||
|
# Get the first migration name to generate complete script
|
||||||
|
FIRST_MIGRATION=$(get_first_migration)
|
||||||
|
|
||||||
|
if [ -n "$FIRST_MIGRATION" ]; then
|
||||||
|
log "📋 Generating complete backup script from initial migration: $FIRST_MIGRATION"
|
||||||
|
if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --from "$FIRST_MIGRATION" --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE"); then
|
||||||
|
log "✅ Complete EF Core Migration SQL Script generated: $BACKUP_FILE_DISPLAY"
|
||||||
|
BACKUP_SUCCESS=true
|
||||||
|
break
|
||||||
|
else
|
||||||
|
# Try fallback without specifying from migration
|
||||||
|
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE") 2>&1 || true)
|
||||||
|
if [ $attempt -lt 3 ]; then
|
||||||
|
warn "⚠️ Backup attempt $attempt failed. Retrying in 5 seconds..."
|
||||||
|
warn " EF CLI Output: $ERROR_OUTPUT"
|
||||||
|
sleep 5
|
||||||
|
else
|
||||||
|
error "❌ Database backup failed after 3 attempts."
|
||||||
|
error " EF CLI Output: $ERROR_OUTPUT"
|
||||||
|
error " Migration aborted for safety reasons."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Fallback: generate script without specifying from migration
|
||||||
if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE"); then
|
if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE"); then
|
||||||
log "✅ EF Core Migration SQL Script generated: $BACKUP_FILE_DISPLAY"
|
log "✅ EF Core Migration SQL Script generated: $BACKUP_FILE_DISPLAY"
|
||||||
BACKUP_SUCCESS=true
|
BACKUP_SUCCESS=true
|
||||||
break
|
break
|
||||||
else
|
else
|
||||||
|
# Try fallback without specifying from migration
|
||||||
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE") 2>&1 || true)
|
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE") 2>&1 || true)
|
||||||
if [ $attempt -lt 3 ]; then
|
if [ $attempt -lt 3 ]; then
|
||||||
warn "⚠️ Backup attempt $attempt failed. Retrying in 5 seconds..."
|
warn "⚠️ Backup attempt $attempt failed. Retrying in 5 seconds..."
|
||||||
@@ -456,33 +538,69 @@ for attempt in 1 2 3; do
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
else
|
|
||||||
# If pg_dump is not available, use EF Core migration script
|
|
||||||
warn "⚠️ pg_dump not available, using EF Core migration script for backup..."
|
|
||||||
if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE"); then
|
|
||||||
log "✅ EF Core Migration SQL Script generated: $BACKUP_FILE_DISPLAY"
|
|
||||||
BACKUP_SUCCESS=true
|
|
||||||
break
|
|
||||||
else
|
|
||||||
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE") 2>&1 || true)
|
|
||||||
if [ $attempt -lt 3 ]; then
|
|
||||||
warn "⚠️ Backup attempt $attempt failed. Retrying in 5 seconds..."
|
|
||||||
warn " EF CLI Output: $ERROR_OUTPUT"
|
|
||||||
sleep 5
|
|
||||||
else
|
|
||||||
error "❌ Database backup failed after 3 attempts."
|
|
||||||
error " EF CLI Output: $ERROR_OUTPUT"
|
|
||||||
error " Migration aborted for safety reasons."
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# Check if backup was successful before proceeding
|
# Check if backup was successful before proceeding
|
||||||
if [ "$BACKUP_SUCCESS" != "true" ]; then
|
if [ "$BACKUP_SUCCESS" != "true" ]; then
|
||||||
error "❌ Database backup failed. Migration aborted for safety."
|
error "❌ Database backup failed. Migration aborted for safety."
|
||||||
error " Cannot proceed with migration without a valid backup."
|
error " Cannot proceed with migration without a valid backup."
|
||||||
error " Please resolve backup issues and try again."
|
error " Please resolve backup issues and try again."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 2.5: Check for pending model changes and create migrations if needed
|
||||||
|
log "🔍 Step 2.5: Checking for pending model changes..."
|
||||||
|
|
||||||
|
# Check if there are any pending model changes that need migrations
|
||||||
|
PENDING_CHANGES_OUTPUT=$( (cd "$DB_PROJECT_PATH" && dotnet ef migrations add --dry-run --startup-project "$API_PROJECT_PATH" --name "PendingChanges_${TIMESTAMP}") 2>&1 || true )
|
||||||
|
|
||||||
|
if echo "$PENDING_CHANGES_OUTPUT" | grep -q "No pending model changes"; then
|
||||||
|
log "✅ No pending model changes detected - existing migrations are up to date"
|
||||||
|
else
|
||||||
|
log "⚠️ Pending model changes detected that require new migrations"
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo "📋 PENDING MODEL CHANGES DETECTED"
|
||||||
|
echo "=========================================="
|
||||||
|
echo "The following changes require new migrations:"
|
||||||
|
echo "$PENDING_CHANGES_OUTPUT"
|
||||||
|
echo ""
|
||||||
|
echo "Would you like to create a new migration now?"
|
||||||
|
echo "=========================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
read -p "🔧 Create new migration? (y/n): " create_migration
|
||||||
|
|
||||||
|
if [[ "$create_migration" =~ ^[Yy]$ ]]; then
|
||||||
|
log "📝 Creating new migration..."
|
||||||
|
|
||||||
|
# Get migration name from user
|
||||||
|
read -p "📝 Enter migration name (or press Enter for auto-generated name): " migration_name
|
||||||
|
if [ -z "$migration_name" ]; then
|
||||||
|
migration_name="Migration_${TIMESTAMP}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create the migration
|
||||||
|
if (cd "$DB_PROJECT_PATH" && dotnet ef migrations add "$migration_name" --startup-project "$API_PROJECT_PATH"); then
|
||||||
|
log "✅ Migration '$migration_name' created successfully"
|
||||||
|
|
||||||
|
# Show the created migration file
|
||||||
|
LATEST_MIGRATION=$(find "$DB_PROJECT_PATH/Migrations" -name "*${migration_name}.cs" | head -1)
|
||||||
|
if [ -n "$LATEST_MIGRATION" ]; then
|
||||||
|
log "📄 Migration file created: $(basename "$LATEST_MIGRATION")"
|
||||||
|
log " Location: $LATEST_MIGRATION"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && dotnet ef migrations add "$migration_name" --startup-project "$API_PROJECT_PATH") 2>&1 || true )
|
||||||
|
error "❌ Failed to create migration '$migration_name'"
|
||||||
|
error " EF CLI Output: $ERROR_OUTPUT"
|
||||||
|
error " Please resolve the model issues and try again."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "⚠️ Skipping migration creation. Proceeding with existing migrations only."
|
||||||
|
log " Note: If there are pending changes, the migration may fail."
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Step 3: Run Migration (This effectively is a retry if previous "update" failed, or a final apply)
|
# Step 3: Run Migration (This effectively is a retry if previous "update" failed, or a final apply)
|
||||||
@@ -507,8 +625,88 @@ fi
|
|||||||
# Generate migration script first (Microsoft recommended approach)
|
# Generate migration script first (Microsoft recommended approach)
|
||||||
MIGRATION_SCRIPT="$BACKUP_DIR/migration_${ENVIRONMENT}_${TIMESTAMP}.sql"
|
MIGRATION_SCRIPT="$BACKUP_DIR/migration_${ENVIRONMENT}_${TIMESTAMP}.sql"
|
||||||
log "📝 Step 3b: Generating migration script for pending migrations..."
|
log "📝 Step 3b: Generating migration script for pending migrations..."
|
||||||
if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT"); then
|
|
||||||
log "✅ Migration script generated: $(basename "$MIGRATION_SCRIPT")"
|
# Check if database is empty (no tables) to determine the best approach
|
||||||
|
log "🔍 Checking if database has existing tables..."
|
||||||
|
DB_HAS_TABLES=false
|
||||||
|
if command -v psql >/dev/null 2>&1; then
|
||||||
|
TABLE_COUNT=$(PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public';" 2>/dev/null | tr -d ' ' || echo "0")
|
||||||
|
if [ "$TABLE_COUNT" -gt 0 ]; then
|
||||||
|
DB_HAS_TABLES=true
|
||||||
|
log "✅ Database has $TABLE_COUNT existing tables - using idempotent script generation"
|
||||||
|
else
|
||||||
|
log "⚠️ Database appears to be empty - using full migration script generation"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "⚠️ psql not available - assuming database has tables and using idempotent script generation"
|
||||||
|
DB_HAS_TABLES=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Generate migration script based on database state
|
||||||
|
if [ "$DB_HAS_TABLES" = "true" ]; then
|
||||||
|
# For databases with existing tables, we need to generate a complete script
|
||||||
|
# that includes all migrations from the beginning
|
||||||
|
log "📝 Generating complete migration script from initial migration..."
|
||||||
|
|
||||||
|
# Get the first migration name to generate script from the beginning
|
||||||
|
FIRST_MIGRATION=$(get_first_migration)
|
||||||
|
|
||||||
|
if [ -n "$FIRST_MIGRATION" ]; then
|
||||||
|
log "📋 Generating complete script for all migrations (idempotent)..."
|
||||||
|
if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT"); then
|
||||||
|
log "✅ Complete migration script generated (all migrations, idempotent): $(basename "$MIGRATION_SCRIPT")"
|
||||||
|
else
|
||||||
|
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT") 2>&1 || true )
|
||||||
|
error "❌ Failed to generate complete migration script."
|
||||||
|
error " EF CLI Output: $ERROR_OUTPUT"
|
||||||
|
error " Check the .NET project logs for detailed errors."
|
||||||
|
error " Backup script available at: $BACKUP_FILE_DISPLAY"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Fallback: generate script without specifying from migration
|
||||||
|
log "📝 Fallback: Generating migration script without specifying from migration..."
|
||||||
|
if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT"); then
|
||||||
|
log "✅ Migration script generated (idempotent): $(basename "$MIGRATION_SCRIPT")"
|
||||||
|
else
|
||||||
|
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT") 2>&1 || true )
|
||||||
|
error "❌ Failed to generate idempotent migration script."
|
||||||
|
error " EF CLI Output: $ERROR_OUTPUT"
|
||||||
|
error " Check the .NET project logs for detailed errors."
|
||||||
|
error " Backup script available at: $BACKUP_FILE_DISPLAY"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Use full script generation for empty databases (generate script from the very beginning)
|
||||||
|
log "📝 Generating full migration script for empty database..."
|
||||||
|
|
||||||
|
# Get the first migration name to generate script from the beginning
|
||||||
|
FIRST_MIGRATION=$(get_first_migration)
|
||||||
|
|
||||||
|
if [ -n "$FIRST_MIGRATION" ]; then
|
||||||
|
log "📋 Generating complete script for all migrations..."
|
||||||
|
if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT"); then
|
||||||
|
log "✅ Complete migration script generated (all migrations): $(basename "$MIGRATION_SCRIPT")"
|
||||||
|
else
|
||||||
|
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT") 2>&1 || true )
|
||||||
|
error "❌ Failed to generate complete migration script."
|
||||||
|
error " EF CLI Output: $ERROR_OUTPUT"
|
||||||
|
error " Check the .NET project logs for detailed errors."
|
||||||
|
error " Backup script available at: $BACKUP_FILE_DISPLAY"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Fallback: generate script without specifying from migration
|
||||||
|
log "📝 Fallback: Generating migration script without specifying from migration..."
|
||||||
|
if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT"); then
|
||||||
|
log "✅ Migration script generated (fallback): $(basename "$MIGRATION_SCRIPT")"
|
||||||
|
else
|
||||||
|
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT") 2>&1 || true )
|
||||||
|
error "❌ Failed to generate fallback migration script."
|
||||||
|
error " EF CLI Output: $ERROR_OUTPUT"
|
||||||
|
error " Check the .NET project logs for detailed errors."
|
||||||
|
error " Backup script available at: $BACKUP_FILE_DISPLAY"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Show the migration script path to the user for review
|
# Show the migration script path to the user for review
|
||||||
echo ""
|
echo ""
|
||||||
@@ -519,8 +717,26 @@ if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef mig
|
|||||||
echo "Environment: $ENVIRONMENT"
|
echo "Environment: $ENVIRONMENT"
|
||||||
echo "Database: $DB_HOST:$DB_PORT/$DB_NAME"
|
echo "Database: $DB_HOST:$DB_PORT/$DB_NAME"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
# Show a preview of the migration script content
|
||||||
|
if [ -f "$MIGRATION_SCRIPT" ]; then
|
||||||
|
SCRIPT_SIZE=$(wc -l < "$MIGRATION_SCRIPT")
|
||||||
|
echo "📄 Migration script contains $SCRIPT_SIZE lines"
|
||||||
|
|
||||||
|
# Show first 20 lines as preview
|
||||||
|
echo ""
|
||||||
|
echo "📋 PREVIEW (first 20 lines):"
|
||||||
|
echo "----------------------------------------"
|
||||||
|
head -20 "$MIGRATION_SCRIPT" | sed 's/^/ /'
|
||||||
|
if [ "$SCRIPT_SIZE" -gt 20 ]; then
|
||||||
|
echo " ... (showing first 20 lines of $SCRIPT_SIZE total)"
|
||||||
|
fi
|
||||||
|
echo "----------------------------------------"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
echo "⚠️ IMPORTANT: Please review the migration script before proceeding!"
|
echo "⚠️ IMPORTANT: Please review the migration script before proceeding!"
|
||||||
echo " You can examine the script with: cat $MIGRATION_SCRIPT"
|
echo " You can examine the full script with: cat $MIGRATION_SCRIPT"
|
||||||
echo " Or open it in your editor to review the changes."
|
echo " Or open it in your editor to review the changes."
|
||||||
echo ""
|
echo ""
|
||||||
echo "=========================================="
|
echo "=========================================="
|
||||||
@@ -564,16 +780,15 @@ if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef mig
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Clean up migration script after successful application
|
# Save a copy of the migration script for reference before cleaning up
|
||||||
rm -f "$MIGRATION_SCRIPT"
|
MIGRATION_SCRIPT_COPY="$BACKUP_DIR/migration_${ENVIRONMENT}_${TIMESTAMP}_applied.sql"
|
||||||
|
if [ -f "$MIGRATION_SCRIPT" ]; then
|
||||||
|
cp "$MIGRATION_SCRIPT" "$MIGRATION_SCRIPT_COPY"
|
||||||
|
log "📝 Migration script saved for reference: $(basename "$MIGRATION_SCRIPT_COPY")"
|
||||||
|
fi
|
||||||
|
|
||||||
else
|
# Clean up temporary migration script after successful application
|
||||||
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT") 2>&1 || true )
|
rm -f "$MIGRATION_SCRIPT"
|
||||||
error "❌ Failed to generate migration script."
|
|
||||||
error " EF CLI Output: $ERROR_OUTPUT"
|
|
||||||
error " Check the .NET project logs for detailed errors."
|
|
||||||
error " Backup script available at: $BACKUP_FILE_DISPLAY"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Step 4: Verify Migration
|
# Step 4: Verify Migration
|
||||||
log "🔍 Step 4: Verifying migration status..."
|
log "🔍 Step 4: Verifying migration status..."
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace Managing.Api.Authorization;
|
namespace Managing.Api.Authorization;
|
||||||
|
|
||||||
|
|
||||||
public class JwtMiddleware
|
public class JwtMiddleware
|
||||||
{
|
{
|
||||||
private readonly RequestDelegate _next;
|
private readonly RequestDelegate _next;
|
||||||
@@ -14,7 +13,21 @@ public class JwtMiddleware
|
|||||||
|
|
||||||
public async Task Invoke(HttpContext context, IUserService userService, IJwtUtils jwtUtils)
|
public async Task Invoke(HttpContext context, IUserService userService, IJwtUtils jwtUtils)
|
||||||
{
|
{
|
||||||
|
if (context.Request.Path.StartsWithSegments("/User/create-token") ||
|
||||||
|
context.Request.Path.StartsWithSegments("/swagger") ||
|
||||||
|
context.Request.Path.StartsWithSegments("/health"))
|
||||||
|
{
|
||||||
|
await _next(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
|
var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(token))
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException("Authorization token is missing");
|
||||||
|
}
|
||||||
|
|
||||||
var userId = jwtUtils.ValidateJwtToken(token);
|
var userId = jwtUtils.ValidateJwtToken(token);
|
||||||
if (userId != null)
|
if (userId != null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
@@ -1,6 +1,5 @@
|
|||||||
using Managing.Api.Models.Requests;
|
using Managing.Api.Models.Requests;
|
||||||
using Managing.Api.Models.Responses;
|
using Managing.Api.Models.Responses;
|
||||||
using Managing.Application.Abstractions;
|
|
||||||
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 +7,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;
|
||||||
@@ -244,7 +242,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 +288,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.Up));
|
||||||
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);
|
||||||
@@ -343,11 +341,11 @@ 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.Up));
|
||||||
|
|
||||||
// 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 })
|
||||||
.OrderByDescending(item => item.PnL)
|
.OrderByDescending(item => item.PnL)
|
||||||
.Take(3)
|
.Take(3)
|
||||||
.ToList();
|
.ToList();
|
||||||
@@ -441,55 +439,42 @@ public class DataController : ControllerBase
|
|||||||
/// </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 UserStrategyDetailsViewModel MapStrategyToViewModel(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;
|
||||||
|
|
||||||
return new UserStrategyDetailsViewModel
|
return new UserStrategyDetailsViewModel
|
||||||
{
|
{
|
||||||
Name = strategy.Name,
|
Name = strategy.Name,
|
||||||
ScenarioName = strategy.Config.ScenarioName,
|
State = strategy.Status.ToString(),
|
||||||
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 = new Dictionary<Guid, Position>(),
|
||||||
.ToList(), // Include sorted positions with most recent first
|
Identifier = strategy.Identifier.ToString(),
|
||||||
Identifier = strategy.Identifier,
|
WalletBalances = new Dictionary<DateTime, decimal>(),
|
||||||
WalletBalances = strategy.WalletBalances,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -544,18 +529,17 @@ public class DataController : ControllerBase
|
|||||||
continue; // Skip agents with no strategies
|
continue; // Skip agents with no strategies
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combine all positions from all strategies
|
// TODO: Add this calculation into repository for better performance
|
||||||
var allPositions = strategies.SelectMany<ITradingBot, Position>(s => s.Positions).ToList();
|
|
||||||
|
var globalPnL = strategies.Sum(s => s.Pnl);
|
||||||
|
var globalVolume = strategies.Sum(s => s.Volume);
|
||||||
|
var globalVolumeLast24h = strategies.Sum(s => s.Volume);
|
||||||
|
|
||||||
// Calculate agent metrics for platform totals
|
// 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
|
// Add to platform totals
|
||||||
totalPlatformPnL += totalPnL;
|
totalPlatformPnL += globalPnL;
|
||||||
totalPlatformVolume += totalVolume;
|
totalPlatformVolume += globalVolume;
|
||||||
totalPlatformVolumeLast24h += volumeLast24h;
|
totalPlatformVolumeLast24h += globalVolumeLast24h;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the platform totals
|
// Set the platform totals
|
||||||
@@ -569,128 +553,25 @@ public class DataController : ControllerBase
|
|||||||
return Ok(summary);
|
return Ok(summary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieves a list of agent summaries for the agent index page
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="timeFilter">Time filter to apply (24H, 3D, 1W, 1M, 1Y, Total)</param>
|
|
||||||
/// <returns>A list of agent summaries sorted by performance</returns>
|
|
||||||
[HttpGet("GetAgentIndex")]
|
|
||||||
public async Task<ActionResult<AgentIndexViewModel>> GetAgentIndex(string timeFilter = "Total")
|
|
||||||
{
|
|
||||||
// Validate time filter
|
|
||||||
var validTimeFilters = new[] { "24H", "3D", "1W", "1M", "1Y", "Total" };
|
|
||||||
if (!validTimeFilters.Contains(timeFilter))
|
|
||||||
{
|
|
||||||
timeFilter = "Total"; // Default to Total if invalid
|
|
||||||
}
|
|
||||||
|
|
||||||
string cacheKey = $"AgentIndex_{timeFilter}";
|
|
||||||
|
|
||||||
// Check if the agent index is already cached
|
|
||||||
var cachedIndex = _cacheService.GetValue<AgentIndexViewModel>(cacheKey);
|
|
||||||
|
|
||||||
if (cachedIndex != null)
|
|
||||||
{
|
|
||||||
return Ok(cachedIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all agents and their strategies
|
|
||||||
var agentsWithStrategies = await _mediator.Send(new GetAllAgentsCommand(timeFilter));
|
|
||||||
|
|
||||||
// Create the agent index response
|
|
||||||
var agentIndex = new AgentIndexViewModel
|
|
||||||
{
|
|
||||||
TimeFilter = timeFilter
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create summaries for each agent
|
|
||||||
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
|
|
||||||
var allPositions = strategies.SelectMany<ITradingBot, Position>(s => s.Positions).ToList();
|
|
||||||
|
|
||||||
// Calculate agent metrics
|
|
||||||
decimal totalPnL = TradingBox.GetPnLInTimeRange(allPositions, timeFilter);
|
|
||||||
decimal pnlLast24h = TradingBox.GetPnLInTimeRange(allPositions, "24H");
|
|
||||||
|
|
||||||
decimal totalROI = TradingBox.GetROIInTimeRange(allPositions, timeFilter);
|
|
||||||
decimal roiLast24h = TradingBox.GetROIInTimeRange(allPositions, "24H");
|
|
||||||
|
|
||||||
(int wins, int losses) = TradingBox.GetWinLossCountInTimeRange(allPositions, timeFilter);
|
|
||||||
|
|
||||||
// Calculate trading volumes
|
|
||||||
decimal totalVolume = TradingBox.GetTotalVolumeTraded(allPositions);
|
|
||||||
decimal volumeLast24h = TradingBox.GetLast24HVolumeTraded(allPositions);
|
|
||||||
|
|
||||||
// Calculate win rate
|
|
||||||
int averageWinRate = 0;
|
|
||||||
if (wins + losses > 0)
|
|
||||||
{
|
|
||||||
averageWinRate = (wins * 100) / (wins + losses);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to agent summaries
|
|
||||||
var agentSummary = new AgentSummaryViewModel
|
|
||||||
{
|
|
||||||
AgentName = user.AgentName,
|
|
||||||
TotalPnL = totalPnL,
|
|
||||||
PnLLast24h = pnlLast24h,
|
|
||||||
TotalROI = totalROI,
|
|
||||||
ROILast24h = roiLast24h,
|
|
||||||
Wins = wins,
|
|
||||||
Losses = losses,
|
|
||||||
AverageWinRate = averageWinRate,
|
|
||||||
ActiveStrategiesCount = strategies.Count,
|
|
||||||
TotalVolume = totalVolume,
|
|
||||||
VolumeLast24h = volumeLast24h
|
|
||||||
};
|
|
||||||
|
|
||||||
agentIndex.AgentSummaries.Add(agentSummary);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort agent summaries by total PnL (highest first)
|
|
||||||
agentIndex.AgentSummaries = agentIndex.AgentSummaries.OrderByDescending(a => a.TotalPnL).ToList();
|
|
||||||
|
|
||||||
// Cache the results for 5 minutes
|
|
||||||
_cacheService.SaveValue(cacheKey, agentIndex, TimeSpan.FromMinutes(5));
|
|
||||||
|
|
||||||
return Ok(agentIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves a paginated 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>
|
/// <param name="page">Page number (defaults to 1)</param>
|
||||||
/// <param name="pageSize">Number of items per page (defaults to 10, max 100)</param>
|
/// <param name="pageSize">Number of items per page (defaults to 10, max 100)</param>
|
||||||
/// <param name="sortBy">Field to sort by (TotalPnL, PnLLast24h, TotalROI, ROILast24h, Wins, Losses, AverageWinRate, ActiveStrategiesCount, TotalVolume, VolumeLast24h)</param>
|
/// <param name="sortBy">Field to sort by (TotalPnL, TotalROI, Wins, Losses, AgentName, CreatedAt, UpdatedAt)</param>
|
||||||
/// <param name="sortOrder">Sort order - "asc" or "desc" (defaults to "desc")</param>
|
/// <param name="sortOrder">Sort order - "asc" or "desc" (defaults to "desc")</param>
|
||||||
/// <param name="agentNames">Optional comma-separated list of agent names to filter by</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>
|
/// <returns>A paginated list of agent summaries sorted by the specified field</returns>
|
||||||
[HttpGet("GetAgentIndexPaginated")]
|
[HttpGet("GetAgentIndexPaginated")]
|
||||||
public async Task<ActionResult<PaginatedAgentIndexResponse>> GetAgentIndexPaginated(
|
public async Task<ActionResult<PaginatedAgentIndexResponse>> GetAgentIndexPaginated(
|
||||||
string timeFilter = "Total",
|
|
||||||
int page = 1,
|
int page = 1,
|
||||||
int pageSize = 10,
|
int pageSize = 10,
|
||||||
string sortBy = "TotalPnL",
|
SortableFields sortBy = SortableFields.TotalPnL,
|
||||||
string sortOrder = "desc",
|
string sortOrder = "desc",
|
||||||
string? agentNames = null)
|
string? agentNames = null)
|
||||||
{
|
{
|
||||||
// Validate time filter
|
|
||||||
var validTimeFilters = new[] { "24H", "3D", "1W", "1M", "1Y", "Total" };
|
|
||||||
if (!validTimeFilters.Contains(timeFilter))
|
|
||||||
{
|
|
||||||
timeFilter = "Total"; // Default to Total if invalid
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate pagination parameters
|
// Validate pagination parameters
|
||||||
if (page < 1)
|
if (page < 1)
|
||||||
{
|
{
|
||||||
@@ -708,177 +589,59 @@ public class DataController : ControllerBase
|
|||||||
return BadRequest("Sort order must be 'asc' or 'desc'");
|
return BadRequest("Sort order must be 'asc' or 'desc'");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate sort by field
|
// Parse agent names filter
|
||||||
var validSortFields = new[]
|
IEnumerable<string>? agentNamesList = null;
|
||||||
{
|
|
||||||
"TotalPnL", "PnLLast24h", "TotalROI", "ROILast24h", "Wins", "Losses", "AverageWinRate",
|
|
||||||
"ActiveStrategiesCount", "TotalVolume", "VolumeLast24h"
|
|
||||||
};
|
|
||||||
if (!validSortFields.Contains(sortBy))
|
|
||||||
{
|
|
||||||
sortBy = "TotalPnL"; // Default to TotalPnL if invalid
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create cache key that includes agent names filter
|
|
||||||
var agentNamesForCache = !string.IsNullOrWhiteSpace(agentNames)
|
|
||||||
? string.Join("_", agentNames.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
|
||||||
.Select(name => name.Trim())
|
|
||||||
.Where(name => !string.IsNullOrWhiteSpace(name))
|
|
||||||
.OrderBy(name => name))
|
|
||||||
: "all";
|
|
||||||
string cacheKey = $"AgentIndex_{timeFilter}_{agentNamesForCache}";
|
|
||||||
|
|
||||||
// Check if the agent index is already cached
|
|
||||||
var cachedIndex = _cacheService.GetValue<AgentIndexViewModel>(cacheKey);
|
|
||||||
|
|
||||||
List<AgentSummaryViewModel> allAgentSummaries;
|
|
||||||
|
|
||||||
if (cachedIndex != null)
|
|
||||||
{
|
|
||||||
allAgentSummaries = cachedIndex.AgentSummaries.ToList();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Get all agents and their strategies
|
|
||||||
var agentsWithStrategies = await _mediator.Send(new GetAllAgentsCommand(timeFilter));
|
|
||||||
|
|
||||||
allAgentSummaries = new List<AgentSummaryViewModel>();
|
|
||||||
|
|
||||||
// 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
|
|
||||||
var allPositions = strategies.SelectMany<ITradingBot, Position>(s => s.Positions).ToList();
|
|
||||||
|
|
||||||
// Calculate agent metrics
|
|
||||||
decimal totalPnL = TradingBox.GetPnLInTimeRange(allPositions, timeFilter);
|
|
||||||
decimal pnlLast24h = TradingBox.GetPnLInTimeRange(allPositions, "24H");
|
|
||||||
|
|
||||||
decimal totalROI = TradingBox.GetROIInTimeRange(allPositions, timeFilter);
|
|
||||||
decimal roiLast24h = TradingBox.GetROIInTimeRange(allPositions, "24H");
|
|
||||||
|
|
||||||
(int wins, int losses) = TradingBox.GetWinLossCountInTimeRange(allPositions, timeFilter);
|
|
||||||
|
|
||||||
// Calculate trading volumes
|
|
||||||
decimal totalVolume = TradingBox.GetTotalVolumeTraded(allPositions);
|
|
||||||
decimal volumeLast24h = TradingBox.GetLast24HVolumeTraded(allPositions);
|
|
||||||
|
|
||||||
// Calculate win rate
|
|
||||||
int averageWinRate = 0;
|
|
||||||
if (wins + losses > 0)
|
|
||||||
{
|
|
||||||
averageWinRate = (wins * 100) / (wins + losses);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to agent summaries
|
|
||||||
var agentSummary = new AgentSummaryViewModel
|
|
||||||
{
|
|
||||||
AgentName = user.AgentName,
|
|
||||||
TotalPnL = totalPnL,
|
|
||||||
PnLLast24h = pnlLast24h,
|
|
||||||
TotalROI = totalROI,
|
|
||||||
ROILast24h = roiLast24h,
|
|
||||||
Wins = wins,
|
|
||||||
Losses = losses,
|
|
||||||
AverageWinRate = averageWinRate,
|
|
||||||
ActiveStrategiesCount = strategies.Count,
|
|
||||||
TotalVolume = totalVolume,
|
|
||||||
VolumeLast24h = volumeLast24h
|
|
||||||
};
|
|
||||||
|
|
||||||
allAgentSummaries.Add(agentSummary);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache the results for 5 minutes
|
|
||||||
var agentIndex = new AgentIndexViewModel
|
|
||||||
{
|
|
||||||
TimeFilter = timeFilter,
|
|
||||||
AgentSummaries = allAgentSummaries
|
|
||||||
};
|
|
||||||
_cacheService.SaveValue(cacheKey, agentIndex, TimeSpan.FromMinutes(5));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply agent name filtering if specified
|
|
||||||
if (!string.IsNullOrWhiteSpace(agentNames))
|
if (!string.IsNullOrWhiteSpace(agentNames))
|
||||||
{
|
{
|
||||||
var agentNameList = agentNames.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
agentNamesList = agentNames.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||||
.Select(name => name.Trim())
|
.Select(name => name.Trim())
|
||||||
.Where(name => !string.IsNullOrWhiteSpace(name))
|
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
if (agentNameList.Any())
|
|
||||||
{
|
|
||||||
allAgentSummaries = allAgentSummaries
|
|
||||||
.Where(agent => agentNameList.Contains(agent.AgentName, StringComparer.OrdinalIgnoreCase))
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply sorting
|
// Get paginated results from database
|
||||||
var sortedSummaries = sortBy switch
|
var command = new GetPaginatedAgentSummariesCommand(page, pageSize, sortBy, sortOrder, agentNamesList);
|
||||||
|
var result = await _mediator.Send(command);
|
||||||
|
var agentSummaries = result.Results;
|
||||||
|
var totalCount = result.TotalCount;
|
||||||
|
|
||||||
|
// Map to view models
|
||||||
|
var agentSummaryViewModels = new List<AgentSummaryViewModel>();
|
||||||
|
foreach (var agentSummary in agentSummaries)
|
||||||
{
|
{
|
||||||
"TotalPnL" => sortOrder == "desc"
|
// Calculate win rate
|
||||||
? allAgentSummaries.OrderByDescending(a => a.TotalPnL)
|
int averageWinRate = 0;
|
||||||
: allAgentSummaries.OrderBy(a => a.TotalPnL),
|
if (agentSummary.Wins + agentSummary.Losses > 0)
|
||||||
"PnLLast24h" => sortOrder == "desc"
|
{
|
||||||
? allAgentSummaries.OrderByDescending(a => a.PnLLast24h)
|
averageWinRate = (agentSummary.Wins * 100) / (agentSummary.Wins + agentSummary.Losses);
|
||||||
: allAgentSummaries.OrderBy(a => a.PnLLast24h),
|
}
|
||||||
"TotalROI" => sortOrder == "desc"
|
|
||||||
? allAgentSummaries.OrderByDescending(a => a.TotalROI)
|
// Map to view model
|
||||||
: allAgentSummaries.OrderBy(a => a.TotalROI),
|
var agentSummaryViewModel = new AgentSummaryViewModel
|
||||||
"ROILast24h" => sortOrder == "desc"
|
{
|
||||||
? allAgentSummaries.OrderByDescending(a => a.ROILast24h)
|
AgentName = agentSummary.AgentName,
|
||||||
: allAgentSummaries.OrderBy(a => a.ROILast24h),
|
TotalPnL = agentSummary.TotalPnL,
|
||||||
"Wins" => sortOrder == "desc"
|
TotalROI = agentSummary.TotalROI,
|
||||||
? allAgentSummaries.OrderByDescending(a => a.Wins)
|
Wins = agentSummary.Wins,
|
||||||
: allAgentSummaries.OrderBy(a => a.Wins),
|
Losses = agentSummary.Losses,
|
||||||
"Losses" => sortOrder == "desc"
|
ActiveStrategiesCount = agentSummary.ActiveStrategiesCount,
|
||||||
? allAgentSummaries.OrderByDescending(a => a.Losses)
|
TotalVolume = agentSummary.TotalVolume,
|
||||||
: allAgentSummaries.OrderBy(a => a.Losses),
|
};
|
||||||
"AverageWinRate" => sortOrder == "desc"
|
|
||||||
? allAgentSummaries.OrderByDescending(a => a.AverageWinRate)
|
agentSummaryViewModels.Add(agentSummaryViewModel);
|
||||||
: allAgentSummaries.OrderBy(a => a.AverageWinRate),
|
}
|
||||||
"ActiveStrategiesCount" => sortOrder == "desc"
|
|
||||||
? allAgentSummaries.OrderByDescending(a => a.ActiveStrategiesCount)
|
|
||||||
: allAgentSummaries.OrderBy(a => a.ActiveStrategiesCount),
|
|
||||||
"TotalVolume" => sortOrder == "desc"
|
|
||||||
? allAgentSummaries.OrderByDescending(a => a.TotalVolume)
|
|
||||||
: allAgentSummaries.OrderBy(a => a.TotalVolume),
|
|
||||||
"VolumeLast24h" => sortOrder == "desc"
|
|
||||||
? allAgentSummaries.OrderByDescending(a => a.VolumeLast24h)
|
|
||||||
: allAgentSummaries.OrderBy(a => a.VolumeLast24h),
|
|
||||||
_ => sortOrder == "desc"
|
|
||||||
? allAgentSummaries.OrderByDescending(a => a.TotalPnL)
|
|
||||||
: allAgentSummaries.OrderBy(a => a.TotalPnL)
|
|
||||||
};
|
|
||||||
|
|
||||||
var totalCount = allAgentSummaries.Count;
|
|
||||||
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
|
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
|
||||||
|
|
||||||
// Apply pagination
|
|
||||||
var paginatedSummaries = sortedSummaries
|
|
||||||
.Skip((page - 1) * pageSize)
|
|
||||||
.Take(pageSize)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var response = new PaginatedAgentIndexResponse
|
var response = new PaginatedAgentIndexResponse
|
||||||
{
|
{
|
||||||
AgentSummaries = paginatedSummaries,
|
AgentSummaries = agentSummaryViewModels,
|
||||||
TotalCount = totalCount,
|
TotalCount = totalCount,
|
||||||
CurrentPage = page,
|
CurrentPage = page,
|
||||||
PageSize = pageSize,
|
PageSize = pageSize,
|
||||||
TotalPages = totalPages,
|
TotalPages = totalPages,
|
||||||
HasNextPage = page < totalPages,
|
HasNextPage = page < totalPages,
|
||||||
HasPreviousPage = page > 1,
|
HasPreviousPage = page > 1,
|
||||||
TimeFilter = timeFilter,
|
|
||||||
SortBy = sortBy,
|
SortBy = sortBy,
|
||||||
SortOrder = sortOrder,
|
SortOrder = sortOrder,
|
||||||
FilteredAgentNames = agentNames
|
FilteredAgentNames = agentNames
|
||||||
@@ -970,7 +733,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,
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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));
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ public class UserController : BaseController
|
|||||||
/// <param name="userService">Service for user-related operations.</param>
|
/// <param name="userService">Service for user-related operations.</param>
|
||||||
/// <param name="jwtUtils">Utility for JWT token operations.</param>
|
/// <param name="jwtUtils">Utility for JWT token operations.</param>
|
||||||
/// <param name="webhookService">Service for webhook operations.</param>
|
/// <param name="webhookService">Service for webhook operations.</param>
|
||||||
public UserController(IConfiguration config, IUserService userService, IJwtUtils jwtUtils, IWebhookService webhookService)
|
public UserController(IConfiguration config, IUserService userService, IJwtUtils jwtUtils,
|
||||||
|
IWebhookService webhookService)
|
||||||
: base(userService)
|
: base(userService)
|
||||||
{
|
{
|
||||||
_config = config;
|
_config = config;
|
||||||
@@ -40,7 +41,7 @@ public class UserController : BaseController
|
|||||||
/// <param name="login">The login request containing user credentials.</param>
|
/// <param name="login">The login request containing user credentials.</param>
|
||||||
/// <returns>A JWT token if authentication is successful; otherwise, an Unauthorized result.</returns>
|
/// <returns>A JWT token if authentication is successful; otherwise, an Unauthorized result.</returns>
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
[HttpPost]
|
[HttpPost("create-token")]
|
||||||
public async Task<ActionResult<string>> CreateToken([FromBody] LoginRequest login)
|
public async Task<ActionResult<string>> CreateToken([FromBody] LoginRequest login)
|
||||||
{
|
{
|
||||||
var user = await _userService.Authenticate(login.Name, login.Address, login.Message, login.Signature);
|
var user = await _userService.Authenticate(login.Name, login.Address, login.Message, login.Signature);
|
||||||
@@ -58,10 +59,12 @@ public class UserController : BaseController
|
|||||||
/// Gets the current user's information.
|
/// Gets the current user's information.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The current user's information.</returns>
|
/// <returns>The current user's information.</returns>
|
||||||
|
[Authorize]
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<ActionResult<User>> GetCurrentUser()
|
public async Task<ActionResult<User>> GetCurrentUser()
|
||||||
{
|
{
|
||||||
var user = await base.GetUser();
|
var user = await base.GetUser();
|
||||||
|
user = await _userService.GetUserByName(user.Name);
|
||||||
return Ok(user);
|
return Ok(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,6 +73,7 @@ public class UserController : BaseController
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="agentName">The new agent name to set.</param>
|
/// <param name="agentName">The new agent name to set.</param>
|
||||||
/// <returns>The updated user with the new agent name.</returns>
|
/// <returns>The updated user with the new agent name.</returns>
|
||||||
|
[Authorize]
|
||||||
[HttpPut("agent-name")]
|
[HttpPut("agent-name")]
|
||||||
public async Task<ActionResult<User>> UpdateAgentName([FromBody] string agentName)
|
public async Task<ActionResult<User>> UpdateAgentName([FromBody] string agentName)
|
||||||
{
|
{
|
||||||
@@ -83,6 +87,7 @@ public class UserController : BaseController
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="avatarUrl">The new avatar URL to set.</param>
|
/// <param name="avatarUrl">The new avatar URL to set.</param>
|
||||||
/// <returns>The updated user with the new avatar URL.</returns>
|
/// <returns>The updated user with the new avatar URL.</returns>
|
||||||
|
[Authorize]
|
||||||
[HttpPut("avatar")]
|
[HttpPut("avatar")]
|
||||||
public async Task<ActionResult<User>> UpdateAvatarUrl([FromBody] string avatarUrl)
|
public async Task<ActionResult<User>> UpdateAvatarUrl([FromBody] string avatarUrl)
|
||||||
{
|
{
|
||||||
@@ -96,6 +101,7 @@ public class UserController : BaseController
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="telegramChannel">The new Telegram channel to set.</param>
|
/// <param name="telegramChannel">The new Telegram channel to set.</param>
|
||||||
/// <returns>The updated user with the new Telegram channel.</returns>
|
/// <returns>The updated user with the new Telegram channel.</returns>
|
||||||
|
[Authorize]
|
||||||
[HttpPut("telegram-channel")]
|
[HttpPut("telegram-channel")]
|
||||||
public async Task<ActionResult<User>> UpdateTelegramChannel([FromBody] string telegramChannel)
|
public async Task<ActionResult<User>> UpdateTelegramChannel([FromBody] string telegramChannel)
|
||||||
{
|
{
|
||||||
@@ -108,20 +114,21 @@ public class UserController : BaseController
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var welcomeMessage = $"🎉 **Trading Bot - Welcome!**\n\n" +
|
var welcomeMessage = $"🎉 **Trading Bot - Welcome!**\n\n" +
|
||||||
$"🎯 **Agent:** {user.Name}\n" +
|
$"🎯 **Agent:** {user.Name}\n" +
|
||||||
$"📡 **Channel ID:** {telegramChannel}\n" +
|
$"📡 **Channel ID:** {telegramChannel}\n" +
|
||||||
$"⏰ **Setup Time:** {DateTime.UtcNow:MMM dd, yyyy • HH:mm:ss} UTC\n\n" +
|
$"⏰ **Setup Time:** {DateTime.UtcNow:MMM dd, yyyy • HH:mm:ss} UTC\n\n" +
|
||||||
$"🔔 **Notification Types:**\n" +
|
$"🔔 **Notification Types:**\n" +
|
||||||
$"• 📈 Position Opens & Closes\n" +
|
$"• 📈 Position Opens & Closes\n" +
|
||||||
$"• 🤖 Bot configuration changes\n\n" +
|
$"• 🤖 Bot configuration changes\n\n" +
|
||||||
$"🚀 **Welcome aboard!** Your trading notifications are now live.";
|
$"🚀 **Welcome aboard!** Your trading notifications are now live.";
|
||||||
|
|
||||||
await _webhookService.SendMessage(welcomeMessage, telegramChannel);
|
await _webhookService.SendMessage(welcomeMessage, telegramChannel);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Log the error but don't fail the update operation
|
// Log the error but don't fail the update operation
|
||||||
Console.WriteLine($"Failed to send welcome message to telegram channel {telegramChannel}: {ex.Message}");
|
Console.WriteLine(
|
||||||
|
$"Failed to send welcome message to telegram channel {telegramChannel}: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,6 +139,7 @@ public class UserController : BaseController
|
|||||||
/// Tests the Telegram channel configuration by sending a test message.
|
/// Tests the Telegram channel configuration by sending a test message.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>A message indicating the test result.</returns>
|
/// <returns>A message indicating the test result.</returns>
|
||||||
|
[Authorize]
|
||||||
[HttpPost("telegram-channel/test")]
|
[HttpPost("telegram-channel/test")]
|
||||||
public async Task<ActionResult<string>> TestTelegramChannel()
|
public async Task<ActionResult<string>> TestTelegramChannel()
|
||||||
{
|
{
|
||||||
@@ -144,7 +152,7 @@ public class UserController : BaseController
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var testMessage = $"🚀 **Trading Bot - Channel Test**\n\n" +
|
var testMessage = $"🚀 **Trading Bot - Channel Test**\n\n" +
|
||||||
$"🎯 **Agent:** {user.Name}\n" +
|
$"🎯 **Agent:** {user.Name}\n" +
|
||||||
$"📡 **Channel ID:** {user.TelegramChannel}\n" +
|
$"📡 **Channel ID:** {user.TelegramChannel}\n" +
|
||||||
$"⏰ **Test Time:** {DateTime.UtcNow:MMM dd, yyyy • HH:mm:ss} UTC\n\n" +
|
$"⏰ **Test Time:** {DateTime.UtcNow:MMM dd, yyyy • HH:mm:ss} UTC\n\n" +
|
||||||
@@ -155,7 +163,8 @@ public class UserController : BaseController
|
|||||||
|
|
||||||
await _webhookService.SendMessage(testMessage, user.TelegramChannel);
|
await _webhookService.SendMessage(testMessage, user.TelegramChannel);
|
||||||
|
|
||||||
return Ok($"Test message sent successfully to Telegram channel {user.TelegramChannel}. Please check your Telegram to verify delivery.");
|
return Ok(
|
||||||
|
$"Test message sent successfully to Telegram channel {user.TelegramChannel}. Please check your Telegram to verify delivery.");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -163,4 +172,3 @@ public class UserController : BaseController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
55
src/Managing.Api/Models/Requests/GetBotsPaginatedRequest.cs
Normal file
55
src/Managing.Api/Models/Requests/GetBotsPaginatedRequest.cs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
using static Managing.Common.Enums;
|
||||||
|
|
||||||
|
namespace Managing.Api.Models.Requests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request model for getting paginated bots with filtering and sorting
|
||||||
|
/// </summary>
|
||||||
|
public class GetBotsPaginatedRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Page number (1-based). Default is 1.
|
||||||
|
/// </summary>
|
||||||
|
public int PageNumber { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of items per page. Default is 10, maximum is 100.
|
||||||
|
/// </summary>
|
||||||
|
public int PageSize { get; set; } = 10;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filter by bot status. If null, returns bots of all statuses.
|
||||||
|
/// </summary>
|
||||||
|
public BotStatus? Status { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filter by user ID. If null, returns bots for all users.
|
||||||
|
/// </summary>
|
||||||
|
public int? UserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filter by bot name (partial match, case-insensitive). If null, no name filtering is applied.
|
||||||
|
/// </summary>
|
||||||
|
public string? Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filter by ticker (partial match, case-insensitive). If null, no ticker filtering is applied.
|
||||||
|
/// </summary>
|
||||||
|
public string? Ticker { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filter by agent name (partial match, case-insensitive). If null, no agent name filtering is applied.
|
||||||
|
/// </summary>
|
||||||
|
public string? AgentName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sort field. Valid values: "Name", "Ticker", "Status", "CreateDate", "StartupTime", "Pnl", "WinRate", "AgentName".
|
||||||
|
/// Default is "CreateDate".
|
||||||
|
/// </summary>
|
||||||
|
public string SortBy { get; set; } = "CreateDate";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sort direction. Default is "Desc" (descending).
|
||||||
|
/// </summary>
|
||||||
|
public string SortDirection { get; set; } = "Desc";
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ public class UpdateBotConfigRequest
|
|||||||
/// The unique identifier of the bot to update
|
/// 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
|
||||||
|
|||||||
@@ -15,21 +15,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 +30,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
|
||||||
@@ -54,11 +40,6 @@ namespace Managing.Api.Models.Responses
|
|||||||
/// Total volume traded by this agent in USD
|
/// Total volume traded by this agent in USD
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public decimal TotalVolume { get; set; }
|
public decimal TotalVolume { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Volume traded in the last 24 hours in USD
|
|
||||||
/// </summary>
|
|
||||||
public decimal VolumeLast24h { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -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>();
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using static Managing.Common.Enums;
|
||||||
|
|
||||||
namespace Managing.Api.Models.Responses;
|
namespace Managing.Api.Models.Responses;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -48,7 +50,7 @@ public class PaginatedAgentIndexResponse
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Field used for sorting
|
/// Field used for sorting
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string SortBy { get; set; } = "TotalPnL";
|
public SortableFields SortBy { get; set; } = SortableFields.TotalPnL;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sort order (asc or desc)
|
/// Sort order (asc or desc)
|
||||||
|
|||||||
43
src/Managing.Api/Models/Responses/PaginatedResponse.cs
Normal file
43
src/Managing.Api/Models/Responses/PaginatedResponse.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
namespace Managing.Api.Models.Responses;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generic pagination response model
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of items in the response</typeparam>
|
||||||
|
public class PaginatedResponse<T>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The items for the current page
|
||||||
|
/// </summary>
|
||||||
|
public List<T> Items { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Total number of items across all pages
|
||||||
|
/// </summary>
|
||||||
|
public int TotalCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current page number (1-based)
|
||||||
|
/// </summary>
|
||||||
|
public int PageNumber { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of items per page
|
||||||
|
/// </summary>
|
||||||
|
public int PageSize { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Total number of pages
|
||||||
|
/// </summary>
|
||||||
|
public int TotalPages { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether there is a previous page
|
||||||
|
/// </summary>
|
||||||
|
public bool HasPreviousPage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether there is a next page
|
||||||
|
/// </summary>
|
||||||
|
public bool HasNextPage { get; set; }
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,16 +63,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 Dictionary<Guid, Position> Positions { get; set; } = new Dictionary<Guid, Position>();
|
||||||
|
|
||||||
public string Identifier { get; set; }
|
public string Identifier { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
public Dictionary<DateTime, decimal> WalletBalances { get; set; } = new Dictionary<DateTime, decimal>();
|
||||||
/// Name of the scenario used by this strategy
|
|
||||||
/// </summary>
|
|
||||||
public string ScenarioName { get; set; }
|
|
||||||
public Dictionary<DateTime, decimal> WalletBalances { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,6 @@ using Managing.Api.Authorization;
|
|||||||
using Managing.Api.Filters;
|
using Managing.Api.Filters;
|
||||||
using Managing.Api.HealthChecks;
|
using Managing.Api.HealthChecks;
|
||||||
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;
|
||||||
@@ -170,7 +169,7 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJw
|
|||||||
ValidateIssuerSigningKey = true
|
ValidateIssuerSigningKey = true
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
builder.Services.AddAuthorization();
|
|
||||||
builder.Services.AddCors(o => o.AddPolicy("CorsPolicy", builder =>
|
builder.Services.AddCors(o => o.AddPolicy("CorsPolicy", builder =>
|
||||||
{
|
{
|
||||||
builder
|
builder
|
||||||
@@ -233,12 +232,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();
|
||||||
@@ -288,5 +281,4 @@ app.UseEndpoints(endpoints =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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/",
|
||||||
|
|||||||
@@ -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.None)
|
||||||
|
{
|
||||||
|
Identifier = identifier;
|
||||||
|
UserId = userId;
|
||||||
|
Status = status;
|
||||||
|
RegisteredAt = DateTime.UtcNow;
|
||||||
|
LastStatusUpdate = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
using Orleans;
|
||||||
|
|
||||||
|
namespace Managing.Application.Abstractions.Grains;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Orleans grain state for BotRegistry.
|
||||||
|
/// This class represents the persistent state of the bot registry grain.
|
||||||
|
/// All properties must be serializable for Orleans state management.
|
||||||
|
/// </summary>
|
||||||
|
[GenerateSerializer]
|
||||||
|
public class BotRegistryState
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Dictionary containing all registered bots. The key is the identifier.
|
||||||
|
/// </summary>
|
||||||
|
[Id(0)]
|
||||||
|
public Dictionary<Guid, BotRegistryEntry> Bots { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When the registry was last updated
|
||||||
|
/// </summary>
|
||||||
|
[Id(1)]
|
||||||
|
public DateTime LastUpdated { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Total number of bots currently registered
|
||||||
|
/// </summary>
|
||||||
|
[Id(2)]
|
||||||
|
public int TotalBotsCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of active bots (status = Up)
|
||||||
|
/// </summary>
|
||||||
|
[Id(3)]
|
||||||
|
public int ActiveBotsCount { get; set; }
|
||||||
|
}
|
||||||
@@ -23,23 +23,5 @@ public interface IBacktestTradingBotGrain : IGrainWithGuidKey
|
|||||||
/// <param name="requestId">The request ID to associate with this backtest</param>
|
/// <param name="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; }
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
using Orleans;
|
||||||
|
using static Managing.Common.Enums;
|
||||||
|
|
||||||
|
namespace Managing.Application.Abstractions.Grains;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Orleans grain interface for LiveBotRegistry operations.
|
||||||
|
/// This interface defines the distributed, async operations available for the bot registry.
|
||||||
|
/// The registry acts as a central, durable directory for all LiveTradingBot grains.
|
||||||
|
/// </summary>
|
||||||
|
public interface ILiveBotRegistryGrain : IGrainWithIntegerKey
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Registers a new bot with its user ID. This should be called by the LiveTradingBotGrain when it is first initialized.
|
||||||
|
/// The initial status will be BotStatus.Up.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="identifier">The unique identifier of the bot</param>
|
||||||
|
/// <param name="userId">The unique identifier of the user who owns the bot</param>
|
||||||
|
/// <returns>A task that represents the asynchronous operation</returns>
|
||||||
|
Task RegisterBot(Guid identifier, int userId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes a bot from the registry. This should be a full removal, perhaps called when a user permanently deletes a bot.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="identifier">The unique identifier of the bot to unregister</param>
|
||||||
|
/// <returns>A task that represents the asynchronous operation</returns>
|
||||||
|
Task UnregisterBot(Guid identifier);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a list of all bots in the registry. This is for a management dashboard to see all bots in the system.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A list of all BotRegistryEntry objects in the registry</returns>
|
||||||
|
Task<List<BotRegistryEntry>> GetAllBots();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a list of all bots associated with a specific user. This is the primary method for a user's watchlist.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">The unique identifier of the user</param>
|
||||||
|
/// <returns>A list of BotRegistryEntry objects for the specified user</returns>
|
||||||
|
Task<List<BotRegistryEntry>> GetBotsForUser(int userId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A dedicated method for updating only the bot's Status field (Up/Down).
|
||||||
|
/// This will be called by LiveTradingBot's StartAsync and StopAsync methods.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="identifier">The unique identifier of the bot</param>
|
||||||
|
/// <param name="status">The new status to set for the bot</param>
|
||||||
|
/// <returns>A task that represents the asynchronous operation</returns>
|
||||||
|
Task UpdateBotStatus(Guid identifier, BotStatus status);
|
||||||
|
Task<BotStatus> GetBotStatus(Guid identifier);
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using Managing.Application.Abstractions.Models;
|
||||||
|
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<TradingBotResponse> 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();
|
||||||
|
}
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using Managing.Domain.Bots;
|
using Managing.Domain.Bots;
|
||||||
|
using Managing.Domain.Indicators;
|
||||||
using Managing.Domain.Trades;
|
using Managing.Domain.Trades;
|
||||||
using Orleans;
|
using Orleans;
|
||||||
using static Managing.Common.Enums;
|
using static Managing.Common.Enums;
|
||||||
@@ -16,7 +17,7 @@ public class TradingBotResponse
|
|||||||
/// Bot identifier
|
/// Bot identifier
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Id(0)]
|
[Id(0)]
|
||||||
public string Identifier { get; set; } = string.Empty;
|
public Guid Identifier { get; set; } = Guid.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Bot display name
|
/// Bot display name
|
||||||
@@ -37,16 +38,16 @@ public class TradingBotResponse
|
|||||||
public TradingBotConfig Config { get; set; }
|
public TradingBotConfig Config { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Trading positions
|
/// Trading positions dictionary, keyed by position identifier
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Id(4)]
|
[Id(4)]
|
||||||
public List<Position> Positions { get; set; } = new();
|
public Dictionary<Guid, Position> Positions { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Trading signals
|
/// Trading signals dictionary, keyed by signal identifier
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Id(5)]
|
[Id(5)]
|
||||||
public List<LightSignal> Signals { get; set; } = new();
|
public Dictionary<string, LightSignal> Signals { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Wallet balance history
|
/// Wallet balance history
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
}
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -13,18 +13,20 @@ 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<IndicatorBase>> GetStrategiesAsync();
|
||||||
Task<IEnumerable<Indicator>> GetIndicatorsAsync();
|
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 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);
|
||||||
}
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -28,4 +28,6 @@ public interface IStatisticService
|
|||||||
Task UpdateTopVolumeTicker(Enums.TradingExchanges exchange, int top);
|
Task UpdateTopVolumeTicker(Enums.TradingExchanges exchange, int top);
|
||||||
Task UpdateFundingRates();
|
Task UpdateFundingRates();
|
||||||
Task<List<FundingRate>> GetFundingRates();
|
Task<List<FundingRate>> GetFundingRates();
|
||||||
|
Task SaveOrUpdateAgentSummary(AgentSummary agentSummary);
|
||||||
|
Task<IEnumerable<AgentSummary>> GetAllAgentSummaries();
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -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,20 +19,20 @@ 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<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<PrivyInitAddressResponse> InitPrivyWallet(string publicAddress);
|
Task<PrivyInitAddressResponse> InitPrivyWallet(string publicAddress);
|
||||||
|
|
||||||
@@ -43,7 +45,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 +55,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);
|
||||||
}
|
}
|
||||||
@@ -5,9 +5,10 @@ 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);
|
||||||
}
|
}
|
||||||
@@ -4,14 +4,14 @@ using Managing.Application.Abstractions;
|
|||||||
using Managing.Application.Abstractions.Repositories;
|
using Managing.Application.Abstractions.Repositories;
|
||||||
using Managing.Application.Abstractions.Services;
|
using Managing.Application.Abstractions.Services;
|
||||||
using Managing.Application.Backtesting;
|
using Managing.Application.Backtesting;
|
||||||
using Managing.Application.Bots.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
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -45,14 +45,14 @@ 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 handler = new ClosePositionCommandHandler(
|
var handler = new ClosePositionCommandHandler(
|
||||||
_exchangeService,
|
_exchangeService,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -226,24 +226,22 @@ public class BundleBacktestWorker : BaseWorker<BundleBacktestWorker>
|
|||||||
if (runBacktestRequest.Config.Scenario != null)
|
if (runBacktestRequest.Config.Scenario != null)
|
||||||
{
|
{
|
||||||
var sReq = runBacktestRequest.Config.Scenario;
|
var sReq = runBacktestRequest.Config.Scenario;
|
||||||
scenario = new LightScenario(sReq.Name, sReq.LoopbackPeriod);
|
scenario = new LightScenario(sReq.Name, sReq.LoopbackPeriod)
|
||||||
foreach (var indicatorRequest in sReq.Indicators)
|
|
||||||
{
|
{
|
||||||
var indicator = new LightIndicator(indicatorRequest.Name, indicatorRequest.Type)
|
Indicators = sReq.Indicators?.Select(i => new LightIndicator(i.Name, i.Type)
|
||||||
{
|
{
|
||||||
SignalType = indicatorRequest.SignalType,
|
SignalType = i.SignalType,
|
||||||
MinimumHistory = indicatorRequest.MinimumHistory,
|
MinimumHistory = i.MinimumHistory,
|
||||||
Period = indicatorRequest.Period,
|
Period = i.Period,
|
||||||
FastPeriods = indicatorRequest.FastPeriods,
|
FastPeriods = i.FastPeriods,
|
||||||
SlowPeriods = indicatorRequest.SlowPeriods,
|
SlowPeriods = i.SlowPeriods,
|
||||||
SignalPeriods = indicatorRequest.SignalPeriods,
|
SignalPeriods = i.SignalPeriods,
|
||||||
Multiplier = indicatorRequest.Multiplier,
|
Multiplier = i.Multiplier,
|
||||||
SmoothPeriods = indicatorRequest.SmoothPeriods,
|
SmoothPeriods = i.SmoothPeriods,
|
||||||
StochPeriods = indicatorRequest.StochPeriods,
|
StochPeriods = i.StochPeriods,
|
||||||
CyclePeriods = indicatorRequest.CyclePeriods,
|
CyclePeriods = i.CyclePeriods
|
||||||
};
|
}).ToList() ?? new List<LightIndicator>()
|
||||||
scenario.AddIndicator(indicator);
|
};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map TradingBotConfig
|
// Map TradingBotConfig
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using Managing.Application.Abstractions.Services;
|
using Managing.Application.Abstractions.Services;
|
||||||
using Managing.Domain.Accounts;
|
using Managing.Domain.Accounts;
|
||||||
using Managing.Domain.Bots;
|
using Managing.Domain.Bots;
|
||||||
|
using Managing.Domain.Indicators;
|
||||||
using Managing.Domain.MoneyManagements;
|
using Managing.Domain.MoneyManagements;
|
||||||
using Managing.Domain.Scenarios;
|
using Managing.Domain.Scenarios;
|
||||||
using Managing.Domain.Shared.Helpers;
|
using Managing.Domain.Shared.Helpers;
|
||||||
@@ -24,6 +25,7 @@ public class StatisticService : IStatisticService
|
|||||||
private readonly IMessengerService _messengerService;
|
private readonly IMessengerService _messengerService;
|
||||||
private readonly ICacheService _cacheService;
|
private readonly ICacheService _cacheService;
|
||||||
private readonly IAgentBalanceRepository _agentBalanceRepository;
|
private readonly IAgentBalanceRepository _agentBalanceRepository;
|
||||||
|
private readonly IAgentSummaryRepository _agentSummaryRepository;
|
||||||
private readonly ILogger<StatisticService> _logger;
|
private readonly ILogger<StatisticService> _logger;
|
||||||
|
|
||||||
public StatisticService(
|
public StatisticService(
|
||||||
@@ -37,7 +39,8 @@ public class StatisticService : IStatisticService
|
|||||||
ITradaoService tradaoService,
|
ITradaoService tradaoService,
|
||||||
IMessengerService messengerService,
|
IMessengerService messengerService,
|
||||||
ICacheService cacheService,
|
ICacheService cacheService,
|
||||||
IAgentBalanceRepository agentBalanceRepository)
|
IAgentBalanceRepository agentBalanceRepository,
|
||||||
|
IAgentSummaryRepository agentSummaryRepository)
|
||||||
{
|
{
|
||||||
_exchangeService = exchangeService;
|
_exchangeService = exchangeService;
|
||||||
_accountService = accountService;
|
_accountService = accountService;
|
||||||
@@ -50,6 +53,7 @@ public class StatisticService : IStatisticService
|
|||||||
_messengerService = messengerService;
|
_messengerService = messengerService;
|
||||||
_cacheService = cacheService;
|
_cacheService = cacheService;
|
||||||
_agentBalanceRepository = agentBalanceRepository;
|
_agentBalanceRepository = agentBalanceRepository;
|
||||||
|
_agentSummaryRepository = agentSummaryRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdateTopVolumeTicker(TradingExchanges exchange, int top)
|
public async Task UpdateTopVolumeTicker(TradingExchanges exchange, int top)
|
||||||
@@ -497,4 +501,27 @@ public class StatisticService : IStatisticService
|
|||||||
|
|
||||||
return (result, fetchedTotalCount);
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
27
src/Managing.Application/Abstractions/Grains/IAgentGrain.cs
Normal file
27
src/Managing.Application/Abstractions/Grains/IAgentGrain.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
namespace Managing.Application.Abstractions.Grains
|
||||||
|
{
|
||||||
|
public interface IAgentGrain : IGrainWithIntegerKey
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the agent grain with user-specific data.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">The ID of the user (used as grain key).</param>
|
||||||
|
/// <param name="agentName">The display name of the agent.</param>
|
||||||
|
Task InitializeAsync(int userId, string agentName);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates a summary of the agent's stats for the AgentRegistryGrain.
|
||||||
|
/// </summary>
|
||||||
|
Task UpdateSummary();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers a new bot with this agent.
|
||||||
|
/// </summary>
|
||||||
|
Task RegisterBotAsync(Guid botId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unregisters a bot from this agent.
|
||||||
|
/// </summary>
|
||||||
|
Task UnregisterBotAsync(Guid botId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using Managing.Domain.Bots;
|
||||||
|
using Managing.Domain.Candles;
|
||||||
|
using Managing.Domain.Indicators;
|
||||||
|
|
||||||
|
namespace Managing.Application.Abstractions.Grains;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Orleans grain interface for scenario execution and signal generation.
|
||||||
|
/// This stateless grain handles candle management and signal generation for live trading.
|
||||||
|
/// </summary>
|
||||||
|
public interface IScenarioRunnerGrain : IGrainWithGuidKey
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Generates signals based on the current candles and scenario
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="config">The trading bot configuration</param>
|
||||||
|
/// <param name="previousSignals">Previous signals to consider</param>
|
||||||
|
/// <param name="startDate">Start date</param>
|
||||||
|
/// <returns>The generated signal or null if no signal</returns>
|
||||||
|
Task<LightSignal> GetSignals(TradingBotConfig config, Dictionary<string, LightSignal> previousSignals, DateTime startDate,
|
||||||
|
Candle candle);
|
||||||
|
}
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
using Managing.Domain.Bots;
|
|
||||||
|
|
||||||
namespace Managing.Application.Abstractions
|
|
||||||
{
|
|
||||||
public interface IBotFactory
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a trading bot using the unified TradingBot class
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="config">The trading bot configuration</param>
|
|
||||||
/// <returns>ITradingBot instance</returns>
|
|
||||||
Task<ITradingBot> CreateTradingBot(TradingBotConfig config);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a trading bot for backtesting using the unified TradingBot class
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="config">The trading bot configuration</param>
|
|
||||||
/// <returns>ITradingBot instance configured for backtesting</returns>
|
|
||||||
Task<ITradingBot> CreateBacktestTradingBot(TradingBotConfig config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +1,47 @@
|
|||||||
using Managing.Domain.Bots;
|
using Managing.Domain.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");
|
||||||
}
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -1,37 +1,28 @@
|
|||||||
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<Position> OpenPositionManually(TradeDirection direction);
|
Task<Position> OpenPositionManually(TradeDirection direction);
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ namespace Managing.Application.Backtesting
|
|||||||
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;
|
||||||
@@ -99,7 +96,6 @@ namespace Managing.Application.Backtesting
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var candles = GetCandles(config.Ticker, config.Timeframe, startDate, endDate);
|
var candles = GetCandles(config.Ticker, config.Timeframe, startDate, endDate);
|
||||||
throw new Exception();
|
|
||||||
return await RunBacktestWithCandles(config, candles, user, save, withCandles, requestId, metadata);
|
return await RunBacktestWithCandles(config, candles, user, save, withCandles, requestId, metadata);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -145,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,
|
||||||
@@ -159,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,
|
||||||
@@ -201,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,
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
163
src/Managing.Application/Bots/Grains/AgentGrain.cs
Normal file
163
src/Managing.Application/Bots/Grains/AgentGrain.cs
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
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 IStatisticService _statisticService;
|
||||||
|
private const string _updateSummaryReminderName = "UpdateAgentSummary";
|
||||||
|
|
||||||
|
public AgentGrain(
|
||||||
|
[PersistentState("agent-state", "agent-store")]
|
||||||
|
IPersistentState<AgentGrainState> state,
|
||||||
|
ILogger<AgentGrain> logger,
|
||||||
|
IBotService botService,
|
||||||
|
IStatisticService statisticService)
|
||||||
|
{
|
||||||
|
_state = state;
|
||||||
|
_logger = logger;
|
||||||
|
_botService = botService;
|
||||||
|
_statisticService = statisticService;
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
_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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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.Up),
|
||||||
|
TotalVolume = totalVolume,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save summary to database
|
||||||
|
await _statisticService.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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
179
src/Managing.Application/Bots/Grains/LiveBotRegistryGrain.cs
Normal file
179
src/Managing.Application/Bots/Grains/LiveBotRegistryGrain.cs
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
public LiveBotRegistryGrain(
|
||||||
|
[PersistentState("bot-registry", "registry-store")]
|
||||||
|
IPersistentState<BotRegistryState> state,
|
||||||
|
ILogger<LiveBotRegistryGrain> logger)
|
||||||
|
{
|
||||||
|
_state = state;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
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.Up)
|
||||||
|
{
|
||||||
|
_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);
|
||||||
|
}
|
||||||
|
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.Up && previousStatus != BotStatus.Up)
|
||||||
|
{
|
||||||
|
_state.State.ActiveBotsCount++;
|
||||||
|
}
|
||||||
|
else if (newStatus != BotStatus.Up && previousStatus == BotStatus.Up)
|
||||||
|
{
|
||||||
|
_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.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(entry.Status);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,12 @@
|
|||||||
|
using Managing.Application.Abstractions;
|
||||||
using Managing.Application.Abstractions.Grains;
|
using Managing.Application.Abstractions.Grains;
|
||||||
using Managing.Application.Abstractions.Models;
|
using Managing.Application.Abstractions.Models;
|
||||||
|
using Managing.Core;
|
||||||
|
using Managing.Domain.Accounts;
|
||||||
using Managing.Domain.Bots;
|
using Managing.Domain.Bots;
|
||||||
|
using Managing.Domain.Shared.Helpers;
|
||||||
using Managing.Domain.Trades;
|
using Managing.Domain.Trades;
|
||||||
|
using Managing.Domain.Users;
|
||||||
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;
|
||||||
@@ -13,37 +18,31 @@ namespace Managing.Application.Bots.Grains;
|
|||||||
/// Uses composition with TradingBotBase to maintain separation of concerns.
|
/// Uses composition with TradingBotBase to maintain separation of concerns.
|
||||||
/// This grain handles live trading scenarios with real-time market data and execution.
|
/// This grain handles live trading scenarios with real-time market data and execution.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class LiveTradingBotGrain : Grain<TradingBotGrainState>, ITradingBotGrain
|
public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
|
||||||
{
|
{
|
||||||
|
private readonly IPersistentState<TradingBotGrainState> _state;
|
||||||
private readonly ILogger<LiveTradingBotGrain> _logger;
|
private readonly ILogger<LiveTradingBotGrain> _logger;
|
||||||
private readonly IServiceScopeFactory _scopeFactory;
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
private TradingBotBase? _tradingBot;
|
private TradingBotBase? _tradingBot;
|
||||||
private IDisposable? _timer;
|
private IDisposable? _timer;
|
||||||
private bool _isDisposed = false;
|
private string _reminderName = "RebootReminder";
|
||||||
|
|
||||||
public LiveTradingBotGrain(
|
public LiveTradingBotGrain(
|
||||||
|
[PersistentState("live-trading-bot", "bot-store")]
|
||||||
|
IPersistentState<TradingBotGrainState> state,
|
||||||
ILogger<LiveTradingBotGrain> logger,
|
ILogger<LiveTradingBotGrain> logger,
|
||||||
IServiceScopeFactory scopeFactory)
|
IServiceScopeFactory scopeFactory)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_scopeFactory = scopeFactory;
|
_scopeFactory = scopeFactory;
|
||||||
|
_state = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task OnActivateAsync(CancellationToken cancellationToken)
|
public override async Task OnActivateAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await base.OnActivateAsync(cancellationToken);
|
|
||||||
|
|
||||||
_logger.LogInformation("LiveTradingBotGrain {GrainId} activated", this.GetPrimaryKey());
|
_logger.LogInformation("LiveTradingBotGrain {GrainId} activated", this.GetPrimaryKey());
|
||||||
|
await base.OnActivateAsync(cancellationToken);
|
||||||
// Initialize the grain state if not already done
|
await ResumeBotIfRequiredAsync();
|
||||||
if (!State.IsInitialized)
|
|
||||||
{
|
|
||||||
State.Identifier = this.GetPrimaryKey().ToString();
|
|
||||||
State.CreateDate = DateTime.UtcNow;
|
|
||||||
State.Status = BotStatus.Down;
|
|
||||||
State.IsInitialized = true;
|
|
||||||
await WriteStateAsync();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task OnDeactivateAsync(DeactivationReason reason, CancellationToken cancellationToken)
|
public override async Task OnDeactivateAsync(DeactivationReason reason, CancellationToken cancellationToken)
|
||||||
@@ -51,87 +50,168 @@ public class LiveTradingBotGrain : Grain<TradingBotGrainState>, ITradingBotGrain
|
|||||||
_logger.LogInformation("LiveTradingBotGrain {GrainId} deactivating. Reason: {Reason}",
|
_logger.LogInformation("LiveTradingBotGrain {GrainId} deactivating. Reason: {Reason}",
|
||||||
this.GetPrimaryKey(), reason.Description);
|
this.GetPrimaryKey(), reason.Description);
|
||||||
|
|
||||||
// Stop the timer and trading bot
|
StopAndDisposeTimer();
|
||||||
await StopAsync();
|
|
||||||
|
|
||||||
await base.OnDeactivateAsync(reason, cancellationToken);
|
await base.OnDeactivateAsync(reason, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task CreateAsync(TradingBotConfig config, User user)
|
||||||
|
{
|
||||||
|
if (config == null || string.IsNullOrEmpty(config.Name))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Bot configuration is not properly initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.IsForBacktest)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("LiveTradingBotGrain cannot be used for backtesting");
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a new bot, so we can assume it's not registered or active.
|
||||||
|
_state.State.Config = config;
|
||||||
|
_state.State.User = user;
|
||||||
|
_state.State.CreateDate = DateTime.UtcNow;
|
||||||
|
_state.State.Identifier = this.GetPrimaryKey();
|
||||||
|
await _state.WriteStateAsync();
|
||||||
|
|
||||||
|
var botRegistry = GrainFactory.GetGrain<ILiveBotRegistryGrain>(0);
|
||||||
|
await botRegistry.RegisterBot(_state.State.Identifier, user.Id);
|
||||||
|
|
||||||
|
// Register the bot with the user's agent
|
||||||
|
var agentGrain = GrainFactory.GetGrain<IAgentGrain>(user.Id);
|
||||||
|
await agentGrain.RegisterBotAsync(_state.State.Identifier);
|
||||||
|
|
||||||
|
await SaveBotAsync(BotStatus.None);
|
||||||
|
|
||||||
|
_logger.LogInformation("LiveTradingBotGrain {GrainId} created successfully", this.GetPrimaryKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ResumeBotIfRequiredAsync()
|
||||||
|
{
|
||||||
|
// Make the network call to the registry to get the source of truth
|
||||||
|
var botRegistry = GrainFactory.GetGrain<ILiveBotRegistryGrain>(0);
|
||||||
|
var botId = this.GetPrimaryKey();
|
||||||
|
var botStatus = await botRegistry.GetBotStatus(botId);
|
||||||
|
|
||||||
|
_logger.LogInformation("LiveTradingBotGrain {GrainId} activated. Registry status: {Status}",
|
||||||
|
botId, botStatus);
|
||||||
|
|
||||||
|
if (botStatus == BotStatus.Up && _tradingBot == null)
|
||||||
|
{
|
||||||
|
// Now, we can proceed with resuming the bot.
|
||||||
|
await ResumeBotInternalAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ResumeBotInternalAsync()
|
||||||
|
{
|
||||||
|
// The core of this method remains idempotent thanks to the _tradingBot null check
|
||||||
|
if (_tradingBot != null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Load state from persisted grain state
|
||||||
|
_tradingBot = CreateTradingBotInstance(_state.State.Config);
|
||||||
|
LoadStateIntoBase();
|
||||||
|
await _tradingBot.Start();
|
||||||
|
|
||||||
|
// Start the in-memory timer and persistent reminder
|
||||||
|
RegisterAndStartTimer();
|
||||||
|
await RegisterReminder();
|
||||||
|
await SaveBotAsync(BotStatus.Up);
|
||||||
|
_logger.LogInformation("LiveTradingBotGrain {GrainId} resumed successfully", this.GetPrimaryKey());
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to resume bot {GrainId}", this.GetPrimaryKey());
|
||||||
|
// If resume fails, update the status to Down via the registry and stop
|
||||||
|
await UpdateBotRegistryStatus(BotStatus.Down);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task StartAsync()
|
public async Task StartAsync()
|
||||||
{
|
{
|
||||||
|
var botRegistry = GrainFactory.GetGrain<ILiveBotRegistryGrain>(0);
|
||||||
|
var botId = this.GetPrimaryKey();
|
||||||
|
var status = await botRegistry.GetBotStatus(botId);
|
||||||
|
|
||||||
|
// This is the new idempotency check, using the registry as the source of truth
|
||||||
|
if (status == BotStatus.Up && _tradingBot != null)
|
||||||
|
{
|
||||||
|
await RegisterReminder();
|
||||||
|
_logger.LogInformation("LiveTradingBotGrain {GrainId} is already running", this.GetPrimaryKey());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (State.Status == BotStatus.Up)
|
// Resume the bot using the internal logic
|
||||||
{
|
await ResumeBotInternalAsync();
|
||||||
_logger.LogWarning("Bot {GrainId} is already running", this.GetPrimaryKey());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (State.Config == null || string.IsNullOrEmpty(State.Config.Name))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Bot configuration is not properly initialized");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure this is not a backtest configuration
|
|
||||||
if (State.Config.IsForBacktest)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("LiveTradingBotGrain cannot be used for backtesting");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the TradingBotBase instance using composition
|
|
||||||
_tradingBot = await CreateTradingBotInstance();
|
|
||||||
|
|
||||||
// Load backup if available
|
|
||||||
if (State.User != null)
|
|
||||||
{
|
|
||||||
await LoadBackupFromState();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start the trading bot
|
|
||||||
_tradingBot.Start();
|
|
||||||
|
|
||||||
// Update state
|
|
||||||
State.Status = BotStatus.Up;
|
|
||||||
State.StartupTime = DateTime.UtcNow;
|
|
||||||
await WriteStateAsync();
|
|
||||||
|
|
||||||
// Start Orleans timer for periodic execution
|
|
||||||
StartTimer();
|
|
||||||
|
|
||||||
|
// Update registry status (if it was previously 'Down')
|
||||||
|
await UpdateBotRegistryStatus(BotStatus.Up);
|
||||||
_logger.LogInformation("LiveTradingBotGrain {GrainId} started successfully", this.GetPrimaryKey());
|
_logger.LogInformation("LiveTradingBotGrain {GrainId} started successfully", this.GetPrimaryKey());
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Failed to start LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
|
_logger.LogError(ex, "Failed to start LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
|
||||||
State.Status = BotStatus.Down;
|
// Ensure registry status is correct even on failure
|
||||||
await WriteStateAsync();
|
await UpdateBotRegistryStatus(BotStatus.Down);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task RegisterReminder()
|
||||||
|
{
|
||||||
|
var reminderPeriod = TimeSpan.FromMinutes(2);
|
||||||
|
await this.RegisterOrUpdateReminder(_reminderName, reminderPeriod, reminderPeriod);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Starts the Orleans timer for periodic bot execution
|
||||||
|
/// </summary>
|
||||||
|
private void RegisterAndStartTimer()
|
||||||
|
{
|
||||||
|
if (_tradingBot == null) return;
|
||||||
|
|
||||||
|
if (_timer != null) return;
|
||||||
|
|
||||||
|
_timer = this.RegisterGrainTimer(
|
||||||
|
async _ => await ExecuteBotCycle(),
|
||||||
|
new GrainTimerCreationOptions
|
||||||
|
{
|
||||||
|
Period = TimeSpan.FromMinutes(1),
|
||||||
|
DueTime = TimeSpan.FromMinutes(1),
|
||||||
|
KeepAlive = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public async Task StopAsync()
|
public async Task StopAsync()
|
||||||
{
|
{
|
||||||
|
// The check is now against the registry status
|
||||||
|
var botRegistry = GrainFactory.GetGrain<ILiveBotRegistryGrain>(0);
|
||||||
|
var botStatus = await botRegistry.GetBotStatus(this.GetPrimaryKey());
|
||||||
|
if (botStatus == BotStatus.Down)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Bot {GrainId} is already stopped", this.GetPrimaryKey());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Stop the timer
|
StopAndDisposeTimer();
|
||||||
_timer?.Dispose();
|
await UnregisterReminder();
|
||||||
_timer = null;
|
|
||||||
|
|
||||||
// Stop the trading bot
|
// Sync state from the volatile TradingBotBase before destroying it
|
||||||
if (_tradingBot != null)
|
SyncStateFromBase();
|
||||||
{
|
await _state.WriteStateAsync();
|
||||||
_tradingBot.Stop();
|
await SaveBotAsync(BotStatus.Down);
|
||||||
|
_tradingBot = null;
|
||||||
// Save backup before stopping
|
|
||||||
await SaveBackupToState();
|
|
||||||
|
|
||||||
_tradingBot = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update state
|
|
||||||
State.Status = BotStatus.Down;
|
|
||||||
await WriteStateAsync();
|
|
||||||
|
|
||||||
|
await UpdateBotRegistryStatus(BotStatus.Down);
|
||||||
_logger.LogInformation("LiveTradingBotGrain {GrainId} stopped successfully", this.GetPrimaryKey());
|
_logger.LogInformation("LiveTradingBotGrain {GrainId} stopped successfully", this.GetPrimaryKey());
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -141,50 +221,88 @@ public class LiveTradingBotGrain : Grain<TradingBotGrainState>, ITradingBotGrain
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<BotStatus> GetStatusAsync()
|
private void StopAndDisposeTimer()
|
||||||
{
|
{
|
||||||
return Task.FromResult(State.Status);
|
if (_timer != null)
|
||||||
|
{
|
||||||
|
// Stop the timer
|
||||||
|
_timer?.Dispose();
|
||||||
|
_timer = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<TradingBotConfig> GetConfigurationAsync()
|
private async Task UnregisterReminder()
|
||||||
{
|
{
|
||||||
return Task.FromResult(State.Config);
|
var reminder = await this.GetReminder(_reminderName);
|
||||||
|
if (reminder != null)
|
||||||
|
{
|
||||||
|
await this.UnregisterReminder(reminder);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> UpdateConfigurationAsync(TradingBotConfig newConfig)
|
/// <summary>
|
||||||
|
/// Creates a TradingBotBase instance using composition
|
||||||
|
/// </summary>
|
||||||
|
private TradingBotBase CreateTradingBotInstance(TradingBotConfig config)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(config.AccountName))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Account name is required for live trading");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the trading bot instance
|
||||||
|
var logger = _scopeFactory.CreateScope().ServiceProvider.GetRequiredService<ILogger<TradingBotBase>>();
|
||||||
|
var tradingBot = new TradingBotBase(logger, _scopeFactory, config);
|
||||||
|
|
||||||
|
// Restore state from grain state
|
||||||
|
tradingBot.Signals = _state.State.Signals;
|
||||||
|
tradingBot.Positions = _state.State.Positions;
|
||||||
|
tradingBot.WalletBalances = _state.State.WalletBalances;
|
||||||
|
tradingBot.PreloadedCandlesCount = _state.State.PreloadedCandlesCount;
|
||||||
|
tradingBot.ExecutionCount = _state.State.ExecutionCount;
|
||||||
|
tradingBot.Identifier = _state.State.Identifier;
|
||||||
|
tradingBot.LastPositionClosingTime = _state.State.LastPositionClosingTime;
|
||||||
|
|
||||||
|
return tradingBot;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes one cycle of the trading bot
|
||||||
|
/// </summary>
|
||||||
|
private async Task ExecuteBotCycle()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (_tradingBot == null)
|
if (_tradingBot == null)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Bot is not running");
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure this is not a backtest configuration
|
// Execute the bot's Run method
|
||||||
if (newConfig.IsForBacktest)
|
await _tradingBot.Run();
|
||||||
{
|
SyncStateFromBase();
|
||||||
throw new InvalidOperationException("LiveTradingBotGrain cannot be used for backtesting");
|
await _state.WriteStateAsync();
|
||||||
}
|
|
||||||
|
|
||||||
// Update the configuration in the trading bot
|
// Save bot statistics to database
|
||||||
var success = await _tradingBot.UpdateConfiguration(newConfig);
|
await SaveBotAsync(BotStatus.Up);
|
||||||
|
}
|
||||||
if (success)
|
catch (ObjectDisposedException)
|
||||||
{
|
{
|
||||||
// Update the state
|
// Gracefully handle disposed service provider during shutdown
|
||||||
State.Config = newConfig;
|
_logger.LogInformation("Service provider disposed during shutdown for LiveTradingBotGrain {GrainId}",
|
||||||
await WriteStateAsync();
|
this.GetPrimaryKey());
|
||||||
}
|
return;
|
||||||
|
|
||||||
return success;
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Failed to update configuration for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
|
// TODO : Turn off the bot if an error occurs
|
||||||
return false;
|
_logger.LogError(ex, "Error during bot execution cycle for LiveTradingBotGrain {GrainId}",
|
||||||
|
this.GetPrimaryKey());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task<Position> OpenPositionManuallyAsync(TradeDirection direction)
|
public async Task<Position> OpenPositionManuallyAsync(TradeDirection direction)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -198,12 +316,14 @@ public class LiveTradingBotGrain : Grain<TradingBotGrainState>, ITradingBotGrain
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Failed to open manual position for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
|
_logger.LogError(ex, "Failed to open manual position for LiveTradingBotGrain {GrainId}",
|
||||||
|
this.GetPrimaryKey());
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ToggleIsForWatchOnlyAsync()
|
|
||||||
|
public Task<TradingBotResponse> GetBotDataAsync()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -212,39 +332,20 @@ public class LiveTradingBotGrain : Grain<TradingBotGrainState>, ITradingBotGrain
|
|||||||
throw new InvalidOperationException("Bot is not running");
|
throw new InvalidOperationException("Bot is not running");
|
||||||
}
|
}
|
||||||
|
|
||||||
await _tradingBot.ToggleIsForWatchOnly();
|
return Task.FromResult(new TradingBotResponse
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to toggle watch-only mode for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<TradingBotResponse> GetBotDataAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (_tradingBot == null)
|
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Bot is not running");
|
Identifier = _state.State.Identifier,
|
||||||
}
|
Name = _state.State.Name,
|
||||||
|
Config = _state.State.Config,
|
||||||
return new TradingBotResponse
|
|
||||||
{
|
|
||||||
Identifier = State.Identifier,
|
|
||||||
Name = State.Name,
|
|
||||||
Status = State.Status,
|
|
||||||
Config = State.Config,
|
|
||||||
Positions = _tradingBot.Positions,
|
Positions = _tradingBot.Positions,
|
||||||
Signals = _tradingBot.Signals.ToList(),
|
Signals = _tradingBot.Signals,
|
||||||
WalletBalances = _tradingBot.WalletBalances,
|
WalletBalances = _tradingBot.WalletBalances,
|
||||||
ProfitAndLoss = _tradingBot.GetProfitAndLoss(),
|
ProfitAndLoss = _tradingBot.GetProfitAndLoss(),
|
||||||
WinRate = _tradingBot.GetWinRate(),
|
WinRate = _tradingBot.GetWinRate(),
|
||||||
ExecutionCount = _tradingBot.ExecutionCount,
|
ExecutionCount = _state.State.ExecutionCount,
|
||||||
StartupTime = State.StartupTime,
|
StartupTime = _state.State.StartupTime,
|
||||||
CreateDate = State.CreateDate
|
CreateDate = _state.State.CreateDate
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -253,244 +354,236 @@ public class LiveTradingBotGrain : Grain<TradingBotGrainState>, ITradingBotGrain
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task LoadBackupAsync(BotBackup backup)
|
private void LoadStateIntoBase()
|
||||||
{
|
{
|
||||||
try
|
if (_tradingBot == null)
|
||||||
{
|
_tradingBot = CreateTradingBotInstance(_state.State.Config);
|
||||||
if (_tradingBot == null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Bot is not running");
|
|
||||||
}
|
|
||||||
|
|
||||||
_tradingBot.LoadBackup(backup);
|
if (_tradingBot == null) throw new InvalidOperationException("TradingBotBase instance could not be created");
|
||||||
|
|
||||||
// Update state from backup
|
_tradingBot.Signals = _state.State.Signals;
|
||||||
State.User = backup.User;
|
_tradingBot.Positions = _state.State.Positions;
|
||||||
State.Identifier = backup.Identifier;
|
_tradingBot.WalletBalances = _state.State.WalletBalances;
|
||||||
State.Status = backup.LastStatus;
|
_tradingBot.PreloadedCandlesCount = _state.State.PreloadedCandlesCount;
|
||||||
State.CreateDate = backup.Data.CreateDate;
|
_tradingBot.ExecutionCount = _state.State.ExecutionCount;
|
||||||
State.StartupTime = backup.Data.StartupTime;
|
_tradingBot.Identifier = _state.State.Identifier;
|
||||||
await WriteStateAsync();
|
_tradingBot.LastPositionClosingTime = _state.State.LastPositionClosingTime;
|
||||||
|
|
||||||
_logger.LogInformation("Backup loaded successfully for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to load backup for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SaveBackupAsync()
|
private void SyncStateFromBase()
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (_tradingBot == null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Bot is not running");
|
|
||||||
}
|
|
||||||
|
|
||||||
await _tradingBot.SaveBackup();
|
|
||||||
await SaveBackupToState();
|
|
||||||
|
|
||||||
_logger.LogInformation("Backup saved successfully for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to save backup for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<decimal> GetProfitAndLossAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (_tradingBot == null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Bot is not running");
|
|
||||||
}
|
|
||||||
|
|
||||||
return _tradingBot.GetProfitAndLoss();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to get P&L for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<int> GetWinRateAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (_tradingBot == null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Bot is not running");
|
|
||||||
}
|
|
||||||
|
|
||||||
return _tradingBot.GetWinRate();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to get win rate for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<long> GetExecutionCountAsync()
|
|
||||||
{
|
|
||||||
return Task.FromResult(State.ExecutionCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<DateTime> GetStartupTimeAsync()
|
|
||||||
{
|
|
||||||
return Task.FromResult(State.StartupTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<DateTime> GetCreateDateAsync()
|
|
||||||
{
|
|
||||||
return Task.FromResult(State.CreateDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a TradingBotBase instance using composition
|
|
||||||
/// </summary>
|
|
||||||
private async Task<TradingBotBase> CreateTradingBotInstance()
|
|
||||||
{
|
|
||||||
// Validate configuration for live trading
|
|
||||||
if (State.Config == null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Bot configuration is not initialized");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (State.Config.IsForBacktest)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("LiveTradingBotGrain cannot be used for backtesting");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(State.Config.AccountName))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Account name is required for live trading");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the trading bot instance
|
|
||||||
var logger = _scopeFactory.CreateScope().ServiceProvider.GetRequiredService<ILogger<TradingBotBase>>();
|
|
||||||
var tradingBot = new TradingBotBase(logger, _scopeFactory, State.Config);
|
|
||||||
|
|
||||||
// Set the user if available
|
|
||||||
if (State.User != null)
|
|
||||||
{
|
|
||||||
tradingBot.User = State.User;
|
|
||||||
}
|
|
||||||
|
|
||||||
return tradingBot;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Starts the Orleans timer for periodic bot execution
|
|
||||||
/// </summary>
|
|
||||||
private void StartTimer()
|
|
||||||
{
|
{
|
||||||
if (_tradingBot == null) return;
|
if (_tradingBot == null) return;
|
||||||
|
_state.State.Signals = _tradingBot.Signals;
|
||||||
var interval = _tradingBot.Interval;
|
_state.State.Positions = _tradingBot.Positions;
|
||||||
_timer = RegisterTimer(
|
_state.State.WalletBalances = _tradingBot.WalletBalances;
|
||||||
async _ => await ExecuteBotCycle(),
|
_state.State.PreloadedCandlesCount = _tradingBot.PreloadedCandlesCount;
|
||||||
null,
|
_state.State.ExecutionCount = _tradingBot.ExecutionCount;
|
||||||
TimeSpan.FromMilliseconds(interval),
|
_state.State.Identifier = _tradingBot.Identifier;
|
||||||
TimeSpan.FromMilliseconds(interval));
|
_state.State.LastPositionClosingTime = _tradingBot.LastPositionClosingTime;
|
||||||
|
_state.State.Config = _tradingBot.Config;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public async Task<bool> UpdateConfiguration(TradingBotConfig newConfig)
|
||||||
/// Executes one cycle of the trading bot
|
{
|
||||||
/// </summary>
|
if (_tradingBot == null)
|
||||||
private async Task ExecuteBotCycle()
|
LoadStateIntoBase();
|
||||||
|
|
||||||
|
var result = await _tradingBot!.UpdateConfiguration(newConfig);
|
||||||
|
|
||||||
|
if (result)
|
||||||
|
{
|
||||||
|
var botRegistry = GrainFactory.GetGrain<ILiveBotRegistryGrain>(0);
|
||||||
|
var botId = this.GetPrimaryKey();
|
||||||
|
var status = await botRegistry.GetBotStatus(botId);
|
||||||
|
_state.State.Config = newConfig;
|
||||||
|
await _state.WriteStateAsync();
|
||||||
|
await SaveBotAsync(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Account> GetAccount()
|
||||||
|
{
|
||||||
|
return Task.FromResult(_tradingBot.Account);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<TradingBotConfig> GetConfiguration()
|
||||||
|
{
|
||||||
|
return Task.FromResult(_state.State.Config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Position> ClosePositionAsync(Guid positionId)
|
||||||
|
{
|
||||||
|
if (_tradingBot == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Bot is not running");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_tradingBot.Positions.TryGetValue(positionId, out var position))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Position with ID {positionId} not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
var signal = _tradingBot.Signals.TryGetValue(position.SignalIdentifier, out var foundSignal)
|
||||||
|
? foundSignal
|
||||||
|
: null;
|
||||||
|
if (signal == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Signal with ID {position.SignalIdentifier} not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
await _tradingBot.CloseTrade(signal, position, position.Open, _tradingBot.LastCandle.Close, true);
|
||||||
|
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RestartAsync()
|
||||||
|
{
|
||||||
|
await StopAsync();
|
||||||
|
await StartAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (_tradingBot == null || State.Status != BotStatus.Up || _isDisposed)
|
// Stop the bot first if it's running
|
||||||
|
await StopAsync();
|
||||||
|
|
||||||
|
// Unregister from the bot registry
|
||||||
|
var botRegistry = GrainFactory.GetGrain<ILiveBotRegistryGrain>(0);
|
||||||
|
await botRegistry.UnregisterBot(_state.State.Identifier);
|
||||||
|
|
||||||
|
// Unregister from the user's agent
|
||||||
|
if (_state.State.User != null)
|
||||||
{
|
{
|
||||||
return;
|
var agentGrain = GrainFactory.GetGrain<IAgentGrain>(_state.State.User.Id);
|
||||||
|
await agentGrain.UnregisterBotAsync(_state.State.Identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute the bot's Run method
|
// Clear the state
|
||||||
await _tradingBot.Run();
|
_tradingBot = null;
|
||||||
|
await _state.ClearStateAsync();
|
||||||
|
|
||||||
// Update execution count
|
_logger.LogInformation("LiveTradingBotGrain {GrainId} deleted successfully", this.GetPrimaryKey());
|
||||||
State.ExecutionCount++;
|
|
||||||
|
|
||||||
await SaveBackupToState();
|
|
||||||
}
|
|
||||||
catch (ObjectDisposedException)
|
|
||||||
{
|
|
||||||
// Gracefully handle disposed service provider during shutdown
|
|
||||||
_logger.LogInformation("Service provider disposed during shutdown for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error during bot execution cycle for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
|
_logger.LogError(ex, "Failed to delete LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Saves the current bot state to Orleans state storage
|
/// Updates the bot status in the central BotRegistry
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task SaveBackupToState()
|
private async Task UpdateBotRegistryStatus(BotStatus status)
|
||||||
{
|
{
|
||||||
if (_tradingBot == null) return;
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Sync state from TradingBotBase
|
var botRegistry = GrainFactory.GetGrain<ILiveBotRegistryGrain>(0);
|
||||||
State.Config = _tradingBot.Config;
|
var botId = this.GetPrimaryKey();
|
||||||
State.Signals = _tradingBot.Signals;
|
await botRegistry.UpdateBotStatus(botId, status);
|
||||||
State.Positions = _tradingBot.Positions;
|
_logger.LogDebug("Bot {BotId} status updated to {Status} in BotRegistry", botId, status);
|
||||||
State.WalletBalances = _tradingBot.WalletBalances;
|
|
||||||
State.PreloadSince = _tradingBot.PreloadSince;
|
|
||||||
State.PreloadedCandlesCount = _tradingBot.PreloadedCandlesCount;
|
|
||||||
State.Interval = _tradingBot.Interval;
|
|
||||||
State.MaxSignals = _tradingBot._maxSignals;
|
|
||||||
State.LastBackupTime = DateTime.UtcNow;
|
|
||||||
|
|
||||||
await WriteStateAsync();
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Failed to save state for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
|
_logger.LogError(ex, "Failed to update bot {BotId} status to {Status} in BotRegistry", this.GetPrimaryKey(),
|
||||||
|
status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ReceiveReminder(string reminderName, TickStatus status)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Reminder '{ReminderName}' received for grain {GrainId}.", reminderName,
|
||||||
|
this.GetPrimaryKey());
|
||||||
|
|
||||||
|
if (reminderName == _reminderName)
|
||||||
|
{
|
||||||
|
// Now a single, clean call to the method that handles all the logic
|
||||||
|
await ResumeBotIfRequiredAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Loads bot state from Orleans state storage
|
/// Saves the current bot statistics to the database using BotService
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task LoadBackupFromState()
|
private async Task SaveBotAsync(BotStatus status)
|
||||||
{
|
{
|
||||||
if (_tradingBot == null) return;
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Sync state to TradingBotBase
|
Bot bot = null;
|
||||||
_tradingBot.Signals = State.Signals;
|
if (_tradingBot == null || _state.State.User == null)
|
||||||
_tradingBot.Positions = State.Positions;
|
{
|
||||||
_tradingBot.WalletBalances = State.WalletBalances;
|
// Save bot statistics for saved bots
|
||||||
_tradingBot.PreloadSince = State.PreloadSince;
|
bot = new Bot
|
||||||
_tradingBot.PreloadedCandlesCount = State.PreloadedCandlesCount;
|
{
|
||||||
_tradingBot.Config = State.Config;
|
Identifier = _state.State.Identifier,
|
||||||
|
Name = _state.State.Config.Name,
|
||||||
|
Ticker = _state.State.Config.Ticker,
|
||||||
|
User = _state.State.User,
|
||||||
|
Status = status,
|
||||||
|
CreateDate = _state.State.CreateDate,
|
||||||
|
StartupTime = _state.State.StartupTime,
|
||||||
|
TradeWins = 0,
|
||||||
|
TradeLosses = 0,
|
||||||
|
Pnl = 0,
|
||||||
|
Roi = 0,
|
||||||
|
Volume = 0,
|
||||||
|
Fees = 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Calculate statistics using TradingBox helpers
|
||||||
|
var (tradeWins, tradeLosses) = TradingBox.GetWinLossCount(_tradingBot.Positions);
|
||||||
|
var pnl = _tradingBot.GetProfitAndLoss();
|
||||||
|
var fees = _tradingBot.GetTotalFees();
|
||||||
|
var volume = TradingBox.GetTotalVolumeTraded(_tradingBot.Positions);
|
||||||
|
|
||||||
|
// Calculate ROI based on total investment
|
||||||
|
var totalInvestment = _tradingBot.Positions.Values
|
||||||
|
.Sum(p => p.Open.Quantity * p.Open.Price);
|
||||||
|
var roi = totalInvestment > 0 ? (pnl / totalInvestment) * 100 : 0;
|
||||||
|
|
||||||
|
// Create complete Bot object with all statistics
|
||||||
|
bot = new Bot
|
||||||
|
{
|
||||||
|
Identifier = _state.State.Identifier,
|
||||||
|
Name = _state.State.Config.Name,
|
||||||
|
Ticker = _state.State.Config.Ticker,
|
||||||
|
User = _state.State.User,
|
||||||
|
Status = status,
|
||||||
|
StartupTime = _state.State.StartupTime,
|
||||||
|
CreateDate = _state.State.CreateDate,
|
||||||
|
TradeWins = tradeWins,
|
||||||
|
TradeLosses = tradeLosses,
|
||||||
|
Pnl = pnl,
|
||||||
|
Roi = roi,
|
||||||
|
Volume = volume,
|
||||||
|
Fees = fees
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass the complete Bot object to BotService for saving
|
||||||
|
var success = await ServiceScopeHelpers.WithScopedService<IBotService, bool>(_scopeFactory,
|
||||||
|
async (botService) => { return await botService.SaveBotStatisticsAsync(bot); });
|
||||||
|
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(
|
||||||
|
"Successfully saved bot statistics for bot {BotId}: Wins={Wins}, Losses={Losses}, PnL={PnL}, ROI={ROI}%, Volume={Volume}, Fees={Fees}",
|
||||||
|
_state.State.Identifier, bot.TradeWins, bot.TradeLosses, bot.Pnl, bot.Roi, bot.Volume, bot.Fees);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Failed to save bot statistics for bot {BotId}", _state.State.Identifier);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Failed to load state for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
|
_logger.LogError(ex, "Failed to save bot statistics for bot {BotId}", _state.State.Identifier);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
if (!_isDisposed)
|
|
||||||
{
|
|
||||||
_timer?.Dispose();
|
|
||||||
_isDisposed = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
8
src/Managing.Application/Bots/Models/AgentGrainState.cs
Normal file
8
src/Managing.Application/Bots/Models/AgentGrainState.cs
Normal 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>();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -1,7 +1,7 @@
|
|||||||
using Managing.Domain.Bots;
|
using Managing.Domain.Bots;
|
||||||
|
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 +23,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 +37,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 +65,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 +108,10 @@ 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; }
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
public async Task<BotStatus> StopBot(Guid identifier)
|
||||||
|
{
|
||||||
// Get the config directly from the backup
|
try
|
||||||
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
|
var grain = _grainFactory.GetGrain<ILiveTradingBotGrain>(identifier);
|
||||||
.GetMoneyMangement(scalpingConfig.MoneyManagement.Name).Result;
|
await grain.StopAsync();
|
||||||
if (moneyManagement != null)
|
return BotStatus.Down;
|
||||||
{
|
|
||||||
scalpingConfig.MoneyManagement = moneyManagement;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
catch (Exception e)
|
||||||
// Ensure the scenario is properly loaded from database if needed
|
|
||||||
if (scalpingConfig.Scenario == null && !string.IsNullOrEmpty(scalpingConfig.ScenarioName))
|
|
||||||
{
|
{
|
||||||
var scenario = await _tradingService.GetScenarioByNameAsync(scalpingConfig.ScenarioName);
|
_tradingBotLogger.LogError(e, "Error stopping bot {Identifier}", identifier);
|
||||||
if (scenario != null)
|
return BotStatus.Down;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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" +
|
||||||
|
$"🎯 **Agent:** {account.User.AgentName}\n" +
|
||||||
|
$"🤖 **Bot Name:** {config.Name}\n" +
|
||||||
|
$"⏰ **Deleted At:** {DateTime.UtcNow:MMM dd, yyyy • HH:mm:ss} UTC\n\n" +
|
||||||
|
$"⚠️ **Bot has been permanently deleted and all data removed.**";
|
||||||
|
|
||||||
|
await _messengerService.SendTradeMessage(deleteMessage, false, account.User);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_tradingBotLogger.LogError(e, "Error deleting bot {Identifier}", identifier);
|
||||||
|
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.Up)
|
||||||
{
|
{
|
||||||
scalpingConfig.Scenario = LightScenario.FromScenario(scenario);
|
return BotStatus.Up;
|
||||||
|
}
|
||||||
|
|
||||||
|
var botGrain = _grainFactory.GetGrain<ILiveTradingBotGrain>(identifier);
|
||||||
|
if (previousStatus == BotStatus.None)
|
||||||
|
{
|
||||||
|
// 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
|
else
|
||||||
{
|
{
|
||||||
throw new ArgumentException(
|
// Restart (bot was previously down)
|
||||||
$"Scenario '{scalpingConfig.ScenarioName}' not found in database when loading backup");
|
await botGrain.RestartAsync();
|
||||||
}
|
var grainState = await botGrain.GetBotDataAsync();
|
||||||
}
|
var account = await botGrain.GetAccount();
|
||||||
|
|
||||||
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
|
|
||||||
{
|
|
||||||
if (botWrapper.BotInstance is IBot bot)
|
|
||||||
{
|
|
||||||
await Task.Run(() =>
|
|
||||||
bot.Stop());
|
|
||||||
|
|
||||||
var deleteMessage = $"🗑️ **Bot Deleted**\n\n" +
|
|
||||||
$"🎯 **Agent:** {bot.User.AgentName}\n" +
|
|
||||||
$"🤖 **Bot Name:** {bot.Name}\n" +
|
|
||||||
$"⏰ **Deleted At:** {DateTime.UtcNow:MMM dd, yyyy • HH:mm:ss} UTC\n\n" +
|
|
||||||
$"⚠️ **Bot has been permanently deleted and all data removed.**";
|
|
||||||
|
|
||||||
await _messengerService.SendTradeMessage(deleteMessage, false, bot.User);
|
|
||||||
}
|
|
||||||
|
|
||||||
await _botRepository.DeleteBotBackup(identifier);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Console.WriteLine(e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<string> RestartBot(string identifier)
|
|
||||||
{
|
|
||||||
if (_botTasks.TryGetValue(identifier, out var botWrapper))
|
|
||||||
{
|
|
||||||
if (botWrapper.BotInstance is IBot bot)
|
|
||||||
{
|
|
||||||
// Stop the bot first to ensure clean state
|
|
||||||
bot.Stop();
|
|
||||||
|
|
||||||
// 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.Down.ToString();
|
return BotStatus.Up;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_tradingBotLogger.LogError(e, "Error restarting bot {Identifier}", identifier);
|
||||||
|
return BotStatus.Down;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ToggleIsForWatchingOnly(string identifier)
|
private async Task<Bot> GetBot(Guid 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,128 +155,198 @@ 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) &&
|
var grain = _grainFactory.GetGrain<ILiveTradingBotGrain>(identifier);
|
||||||
botTaskWrapper.BotInstance is TradingBotBase tradingBot)
|
// Ensure the scenario is properly loaded from database if needed
|
||||||
|
if (newConfig.Scenario == null && !string.IsNullOrEmpty(newConfig.ScenarioName))
|
||||||
{
|
{
|
||||||
// Ensure the scenario is properly loaded from database if needed
|
var scenario = await _tradingService.GetScenarioByNameAsync(newConfig.ScenarioName);
|
||||||
if (newConfig.Scenario == null && !string.IsNullOrEmpty(newConfig.ScenarioName))
|
if (scenario != null)
|
||||||
{
|
{
|
||||||
var scenario = await _tradingService.GetScenarioByNameAsync(newConfig.ScenarioName);
|
newConfig.Scenario = LightScenario.FromScenario(scenario);
|
||||||
if (scenario != null)
|
|
||||||
{
|
|
||||||
newConfig.Scenario = LightScenario.FromScenario(scenario);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new ArgumentException(
|
|
||||||
$"Scenario '{newConfig.ScenarioName}' not found in database when updating configuration");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
if (newConfig.Scenario == null)
|
|
||||||
{
|
{
|
||||||
throw new ArgumentException(
|
throw new ArgumentException(
|
||||||
"Scenario object must be provided or ScenarioName must be valid when updating configuration");
|
$"Scenario '{newConfig.ScenarioName}' not found in database when updating configuration");
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the bot name is changing
|
|
||||||
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
|
|
||||||
var updateResult = await tradingBot.UpdateConfiguration(newConfig, allowNameChange: true);
|
|
||||||
|
|
||||||
if (updateResult)
|
|
||||||
{
|
|
||||||
// Update the dictionary key
|
|
||||||
if (_botTasks.TryRemove(identifier, out var removedWrapper))
|
|
||||||
{
|
|
||||||
_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;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// No name change, just update configuration
|
|
||||||
return await tradingBot.UpdateConfiguration(newConfig);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
if (newConfig.Scenario == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentException(
|
||||||
|
"Scenario object must be provided or ScenarioName must be valid when updating configuration");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await grain.UpdateConfiguration(newConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ITradingBot> CreateTradingBot(TradingBotConfig config)
|
public async Task<TradingBotConfig> GetBotConfig(Guid identifier)
|
||||||
{
|
{
|
||||||
// Ensure the scenario is properly loaded from database if needed
|
var grain = _grainFactory.GetGrain<ILiveTradingBotGrain>(identifier);
|
||||||
if (config.Scenario == null && !string.IsNullOrEmpty(config.ScenarioName))
|
return await grain.GetConfiguration();
|
||||||
{
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
|
|
||||||
// For now, use TradingBot for both live trading and backtesting
|
|
||||||
// TODO: Implement Orleans grain for live trading when ready
|
|
||||||
if (!config.IsForBacktest)
|
|
||||||
{
|
|
||||||
// Ensure critical properties are set correctly for live trading
|
|
||||||
config.IsForBacktest = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new TradingBotBase(_tradingBotLogger, _scopeFactory, config);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ITradingBot> CreateBacktestTradingBot(TradingBotConfig config)
|
public async Task<IEnumerable<string>> GetActiveBotsNamesAsync()
|
||||||
{
|
{
|
||||||
// Ensure the scenario is properly loaded from database if needed
|
var bots = await _botRepository.GetBotsByStatusAsync(BotStatus.Up);
|
||||||
if (config.Scenario == null && !string.IsNullOrEmpty(config.ScenarioName))
|
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 scenario = await _tradingService.GetScenarioByNameAsync(config.ScenarioName);
|
var grain = _grainFactory.GetGrain<ILiveTradingBotGrain>(identifier);
|
||||||
if (scenario != null)
|
var botData = await grain.GetBotDataAsync();
|
||||||
|
|
||||||
|
// Get the current bot from database
|
||||||
|
var existingBot = await _botRepository.GetBotByIdentifierAsync(identifier);
|
||||||
|
if (existingBot == null)
|
||||||
{
|
{
|
||||||
config.Scenario = LightScenario.FromScenario(scenario);
|
_tradingBotLogger.LogWarning("Bot {Identifier} not found in database for statistics update",
|
||||||
|
identifier);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate statistics using TradingBox helpers
|
||||||
|
var (tradeWins, tradeLosses) = TradingBox.GetWinLossCount(botData.Positions);
|
||||||
|
var pnl = botData.ProfitAndLoss;
|
||||||
|
var fees = botData.Positions.Values.Sum(p =>
|
||||||
|
{
|
||||||
|
if (p.Open.Price > 0 && p.Open.Quantity > 0)
|
||||||
|
{
|
||||||
|
var positionSizeUsd = (p.Open.Price * p.Open.Quantity) * p.Open.Leverage;
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
_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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if bot already exists in database
|
||||||
|
var existingBot = await _botRepository.GetBotByIdentifierAsync(bot.Identifier);
|
||||||
|
|
||||||
|
if (existingBot != null)
|
||||||
|
{
|
||||||
|
// Update existing bot
|
||||||
|
await _botRepository.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);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
throw new ArgumentException($"Scenario '{config.ScenarioName}' not found in database");
|
// Insert new bot
|
||||||
|
await _botRepository.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);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (config.Scenario == null)
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
throw new ArgumentException("Scenario object must be provided or ScenarioName must be valid");
|
_tradingBotLogger.LogError(e, "Error saving bot statistics for bot {BotId}", bot?.Identifier);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
config.IsForBacktest = true;
|
public async Task<(IEnumerable<Bot> Bots, int TotalCount)> GetBotsPaginatedAsync(
|
||||||
return new TradingBotBase(_tradingBotLogger, _scopeFactory, config);
|
int pageNumber,
|
||||||
|
int pageSize,
|
||||||
|
BotStatus? status = null,
|
||||||
|
string? name = null,
|
||||||
|
string? ticker = null,
|
||||||
|
string? agentName = null,
|
||||||
|
string sortBy = "CreateDate",
|
||||||
|
string sortDirection = "Desc")
|
||||||
|
{
|
||||||
|
return await ServiceScopeHelpers.WithScopedService<IBotRepository, (IEnumerable<Bot> Bots, int TotalCount)>(
|
||||||
|
_scopeFactory,
|
||||||
|
async repo =>
|
||||||
|
{
|
||||||
|
return await repo.GetBotsPaginatedAsync(
|
||||||
|
pageNumber,
|
||||||
|
pageSize,
|
||||||
|
status,
|
||||||
|
name,
|
||||||
|
ticker,
|
||||||
|
agentName,
|
||||||
|
sortBy,
|
||||||
|
sortDirection);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,10 +4,10 @@ namespace Managing.Application.ManageBot.Commands;
|
|||||||
|
|
||||||
public class DeleteBotCommand : IRequest<bool>
|
public class DeleteBotCommand : IRequest<bool>
|
||||||
{
|
{
|
||||||
public string Name { get; }
|
public Guid Identifier { get; }
|
||||||
|
|
||||||
public DeleteBotCommand(string name)
|
public DeleteBotCommand(Guid identifier)
|
||||||
{
|
{
|
||||||
Name = name;
|
Identifier = identifier;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
using Managing.Application.Abstractions;
|
using Managing.Domain.Bots;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
using static Managing.Common.Enums;
|
||||||
|
|
||||||
namespace Managing.Application.ManageBot.Commands
|
namespace Managing.Application.ManageBot.Commands
|
||||||
{
|
{
|
||||||
public class GetActiveBotsCommand : IRequest<List<ITradingBot>>
|
public class GetBotsByStatusCommand : IRequest<IEnumerable<Bot>>
|
||||||
{
|
{
|
||||||
public GetActiveBotsCommand()
|
public BotStatus Status { get; }
|
||||||
|
|
||||||
|
public GetBotsByStatusCommand(BotStatus status)
|
||||||
{
|
{
|
||||||
|
Status = status;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using Managing.Domain.Statistics;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace Managing.Application.ManageBot.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Command to retrieve all agent summaries with complete data
|
||||||
|
/// </summary>
|
||||||
|
public class GetAllAgentSummariesCommand : IRequest<IEnumerable<AgentSummary>>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Optional time filter to apply (24H, 3D, 1W, 1M, 1Y, Total)
|
||||||
|
/// </summary>
|
||||||
|
public string TimeFilter { get; }
|
||||||
|
|
||||||
|
public GetAllAgentSummariesCommand(string timeFilter = "Total")
|
||||||
|
{
|
||||||
|
TimeFilter = timeFilter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using Managing.Application.Abstractions;
|
using Managing.Domain.Bots;
|
||||||
using Managing.Domain.Users;
|
using Managing.Domain.Users;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@ namespace Managing.Application.ManageBot.Commands
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Command to retrieve all active agents and their strategies
|
/// Command to retrieve all active agents and their strategies
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class GetAllAgentsCommand : IRequest<Dictionary<User, List<ITradingBot>>>
|
public class GetAllAgentsCommand : IRequest<Dictionary<User, List<Bot>>>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Optional time filter to apply (24H, 3D, 1W, 1M, 1Y, Total)
|
/// Optional time filter to apply (24H, 3D, 1W, 1M, 1Y, Total)
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using Managing.Domain.Bots;
|
||||||
|
using MediatR;
|
||||||
|
using static Managing.Common.Enums;
|
||||||
|
|
||||||
|
namespace Managing.Application.ManageBot.Commands
|
||||||
|
{
|
||||||
|
public class GetBotsByUserAndStatusCommand : IRequest<IEnumerable<Bot>>
|
||||||
|
{
|
||||||
|
public int UserId { get; }
|
||||||
|
public BotStatus Status { get; }
|
||||||
|
|
||||||
|
public GetBotsByUserAndStatusCommand(int userId, BotStatus status)
|
||||||
|
{
|
||||||
|
UserId = userId;
|
||||||
|
Status = status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
using Managing.Domain.Statistics;
|
||||||
|
using MediatR;
|
||||||
|
using static Managing.Common.Enums;
|
||||||
|
|
||||||
|
namespace Managing.Application.ManageBot.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Command to retrieve paginated agent summaries with sorting and filtering
|
||||||
|
/// </summary>
|
||||||
|
public class GetPaginatedAgentSummariesCommand : IRequest<(IEnumerable<AgentSummary> Results, int TotalCount)>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Page number (1-based)
|
||||||
|
/// </summary>
|
||||||
|
public int Page { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of items per page
|
||||||
|
/// </summary>
|
||||||
|
public int PageSize { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Field to sort by
|
||||||
|
/// </summary>
|
||||||
|
public SortableFields SortBy { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sort order (asc or desc)
|
||||||
|
/// </summary>
|
||||||
|
public string SortOrder { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optional list of agent names to filter by
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<string>? AgentNames { get; }
|
||||||
|
|
||||||
|
public GetPaginatedAgentSummariesCommand(
|
||||||
|
int page = 1,
|
||||||
|
int pageSize = 10,
|
||||||
|
SortableFields sortBy = SortableFields.TotalPnL,
|
||||||
|
string sortOrder = "desc",
|
||||||
|
IEnumerable<string>? agentNames = null)
|
||||||
|
{
|
||||||
|
Page = page;
|
||||||
|
PageSize = pageSize;
|
||||||
|
SortBy = sortBy;
|
||||||
|
SortOrder = sortOrder;
|
||||||
|
AgentNames = agentNames;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using Managing.Application.Abstractions;
|
using Managing.Domain.Bots;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
|
||||||
namespace Managing.Application.ManageBot.Commands
|
namespace Managing.Application.ManageBot.Commands
|
||||||
@@ -6,13 +6,13 @@ namespace Managing.Application.ManageBot.Commands
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Command to retrieve all strategies owned by a specific user
|
/// Command to retrieve all strategies owned by a specific user
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class GetUserStrategiesCommand : IRequest<List<ITradingBot>>
|
public class GetUserStrategiesCommand : IRequest<List<Bot>>
|
||||||
{
|
{
|
||||||
public string UserName { get; }
|
public string AgentName { get; }
|
||||||
|
|
||||||
public GetUserStrategiesCommand(string userName)
|
public GetUserStrategiesCommand(string agentName)
|
||||||
{
|
{
|
||||||
UserName = userName;
|
AgentName = agentName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using Managing.Application.Abstractions;
|
using Managing.Domain.Bots;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
|
||||||
namespace Managing.Application.ManageBot.Commands
|
namespace Managing.Application.ManageBot.Commands
|
||||||
@@ -6,7 +6,7 @@ namespace Managing.Application.ManageBot.Commands
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Command to retrieve a specific strategy owned by a user
|
/// Command to retrieve a specific strategy owned by a user
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class GetUserStrategyCommand : IRequest<ITradingBot>
|
public class GetUserStrategyCommand : IRequest<Bot>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The username of the agent/user that owns the strategy
|
/// The username of the agent/user that owns the strategy
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using Managing.Domain.Trades;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace Managing.Application.ManageBot.Commands;
|
||||||
|
|
||||||
|
public class ManualPositionCommand : IRequest<Position>
|
||||||
|
{
|
||||||
|
public Guid PositionId { get; set; }
|
||||||
|
public Guid Identifier { get; set; }
|
||||||
|
|
||||||
|
public ManualPositionCommand(Guid identifier, Guid positionId)
|
||||||
|
{
|
||||||
|
Identifier = identifier;
|
||||||
|
PositionId = positionId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,15 +3,13 @@ using static Managing.Common.Enums;
|
|||||||
|
|
||||||
namespace Managing.Application.ManageBot.Commands
|
namespace Managing.Application.ManageBot.Commands
|
||||||
{
|
{
|
||||||
public class RestartBotCommand : IRequest<string>
|
public class RestartBotCommand : IRequest<BotStatus>
|
||||||
{
|
{
|
||||||
public string Name { get; }
|
public Guid Identifier { get; }
|
||||||
public BotType BotType { get; }
|
|
||||||
|
|
||||||
public RestartBotCommand(BotType botType, string name)
|
public RestartBotCommand(Guid identifier)
|
||||||
{
|
{
|
||||||
BotType = botType;
|
Identifier = identifier;
|
||||||
Name = name;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
using Managing.Domain.Bots;
|
using Managing.Domain.Bots;
|
||||||
using Managing.Domain.Users;
|
using Managing.Domain.Users;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
using static Managing.Common.Enums;
|
||||||
|
|
||||||
namespace Managing.Application.ManageBot.Commands
|
namespace Managing.Application.ManageBot.Commands
|
||||||
{
|
{
|
||||||
public class StartBotCommand : IRequest<string>
|
public class StartBotCommand : IRequest<BotStatus>
|
||||||
{
|
{
|
||||||
public string Name { get; }
|
|
||||||
public TradingBotConfig Config { get; }
|
public TradingBotConfig Config { get; }
|
||||||
public User User { get; }
|
public User User { get; internal set; }
|
||||||
|
public bool CreateOnly { get; }
|
||||||
|
|
||||||
public StartBotCommand(TradingBotConfig config, string name, User user)
|
public StartBotCommand(TradingBotConfig config, User user, bool createOnly = false)
|
||||||
{
|
{
|
||||||
Config = config;
|
Config = config;
|
||||||
Name = name;
|
|
||||||
User = user;
|
User = user;
|
||||||
|
CreateOnly = createOnly;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
|
using static Managing.Common.Enums;
|
||||||
|
|
||||||
namespace Managing.Application.ManageBot.Commands
|
namespace Managing.Application.ManageBot.Commands
|
||||||
{
|
{
|
||||||
public class StopBotCommand : IRequest<string>
|
public class StopBotCommand : IRequest<BotStatus>
|
||||||
{
|
{
|
||||||
public string Identifier { get; }
|
public Guid Identifier { get; }
|
||||||
|
|
||||||
public StopBotCommand(string identifier)
|
public StopBotCommand(Guid identifier)
|
||||||
{
|
{
|
||||||
Identifier = identifier;
|
Identifier = identifier;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
using MediatR;
|
|
||||||
|
|
||||||
namespace Managing.Application.ManageBot.Commands
|
|
||||||
{
|
|
||||||
public class ToggleIsForWatchingCommand : IRequest<string>
|
|
||||||
{
|
|
||||||
public string Name { get; }
|
|
||||||
|
|
||||||
public ToggleIsForWatchingCommand(string name)
|
|
||||||
{
|
|
||||||
Name = name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,12 +6,12 @@ namespace Managing.Application.ManageBot.Commands
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Command to update the configuration of a running trading bot
|
/// Command to update the configuration of a running trading bot
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class UpdateBotConfigCommand : IRequest<string>
|
public class UpdateBotConfigCommand : IRequest<bool>
|
||||||
{
|
{
|
||||||
public string Identifier { get; }
|
public Guid Identifier { get; }
|
||||||
public TradingBotConfig NewConfig { get; }
|
public TradingBotConfig NewConfig { get; }
|
||||||
|
|
||||||
public UpdateBotConfigCommand(string identifier, TradingBotConfig newConfig)
|
public UpdateBotConfigCommand(Guid identifier, TradingBotConfig newConfig)
|
||||||
{
|
{
|
||||||
Identifier = identifier;
|
Identifier = identifier;
|
||||||
NewConfig = newConfig;
|
NewConfig = newConfig;
|
||||||
|
|||||||
@@ -18,6 +18,6 @@ public class DeleteBotCommandHandler : IRequestHandler<DeleteBotCommand, bool>
|
|||||||
|
|
||||||
public Task<bool> Handle(DeleteBotCommand request, CancellationToken cancellationToken)
|
public Task<bool> Handle(DeleteBotCommand request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return _botService.DeleteBot(request.Name);
|
return _botService.DeleteBot(request.Identifier);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
using Managing.Application.Abstractions;
|
using Managing.Application.Abstractions;
|
||||||
using Managing.Application.ManageBot.Commands;
|
using Managing.Application.ManageBot.Commands;
|
||||||
|
using Managing.Domain.Bots;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
|
||||||
namespace Managing.Application.ManageBot
|
namespace Managing.Application.ManageBot
|
||||||
{
|
{
|
||||||
public class GetActiveBotsCommandHandler(IBotService botService)
|
public class GetBotsByStatusCommandHandler(IBotService botService)
|
||||||
: IRequestHandler<GetActiveBotsCommand, List<ITradingBot>>
|
: IRequestHandler<GetBotsByStatusCommand, IEnumerable<Bot>>
|
||||||
{
|
{
|
||||||
public Task<List<ITradingBot>> Handle(GetActiveBotsCommand request, CancellationToken cancellationToken)
|
public async Task<IEnumerable<Bot>> Handle(GetBotsByStatusCommand request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return Task.FromResult(botService.GetActiveBots());
|
return await botService.GetBotsByStatusAsync(request.Status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -20,11 +20,11 @@ namespace Managing.Application.ManageBot
|
|||||||
_accountService = accountService;
|
_accountService = accountService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<List<AgentStatusResponse>> Handle(GetAgentStatusesCommand request,
|
public async Task<List<AgentStatusResponse>> Handle(GetAgentStatusesCommand request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var result = new List<AgentStatusResponse>();
|
var result = new List<AgentStatusResponse>();
|
||||||
var allActiveBots = _botService.GetActiveBots();
|
var allActiveBots = await _botService.GetBotsByStatusAsync(BotStatus.Up);
|
||||||
|
|
||||||
// Group bots by user and determine status
|
// Group bots by user and determine status
|
||||||
var agentGroups = allActiveBots
|
var agentGroups = allActiveBots
|
||||||
@@ -38,7 +38,9 @@ namespace Managing.Application.ManageBot
|
|||||||
var bots = agentGroup.ToList();
|
var bots = agentGroup.ToList();
|
||||||
|
|
||||||
// Determine agent status: Online if at least one strategy is running, Offline otherwise
|
// Determine agent status: Online if at least one strategy is running, Offline otherwise
|
||||||
var agentStatus = bots.Any(bot => bot.GetStatus() == BotStatus.Up.ToString()) ? AgentStatus.Online : AgentStatus.Offline;
|
var agentStatus = bots.Any(bot => bot.Status == BotStatus.Up)
|
||||||
|
? AgentStatus.Online
|
||||||
|
: AgentStatus.Offline;
|
||||||
|
|
||||||
result.Add(new AgentStatusResponse
|
result.Add(new AgentStatusResponse
|
||||||
{
|
{
|
||||||
@@ -47,7 +49,7 @@ namespace Managing.Application.ManageBot
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.FromResult(result);
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
using Managing.Application.Abstractions.Services;
|
||||||
|
using Managing.Application.ManageBot.Commands;
|
||||||
|
using Managing.Domain.Statistics;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace Managing.Application.ManageBot
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Handler for retrieving all agent summaries with complete data
|
||||||
|
/// </summary>
|
||||||
|
public class GetAllAgentSummariesCommandHandler : IRequestHandler<GetAllAgentSummariesCommand,
|
||||||
|
IEnumerable<AgentSummary>>
|
||||||
|
{
|
||||||
|
private readonly IStatisticService _statisticService;
|
||||||
|
|
||||||
|
public GetAllAgentSummariesCommandHandler(IStatisticService statisticService)
|
||||||
|
{
|
||||||
|
_statisticService = statisticService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<AgentSummary>> Handle(GetAllAgentSummariesCommand request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Get all agent summaries from the database
|
||||||
|
var allAgentSummaries = await _statisticService.GetAllAgentSummaries();
|
||||||
|
|
||||||
|
if (request.TimeFilter != "Total")
|
||||||
|
{
|
||||||
|
var cutoffDate = GetCutoffDate(request.TimeFilter);
|
||||||
|
allAgentSummaries = allAgentSummaries.Where(a =>
|
||||||
|
a.UpdatedAt >= cutoffDate ||
|
||||||
|
(a.Runtime.HasValue && a.Runtime.Value >= cutoffDate));
|
||||||
|
}
|
||||||
|
|
||||||
|
return allAgentSummaries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the cutoff date based on the time filter
|
||||||
|
/// </summary>
|
||||||
|
private DateTime GetCutoffDate(string timeFilter)
|
||||||
|
{
|
||||||
|
return timeFilter switch
|
||||||
|
{
|
||||||
|
"24H" => DateTime.UtcNow.AddHours(-24),
|
||||||
|
"3D" => DateTime.UtcNow.AddDays(-3),
|
||||||
|
"1W" => DateTime.UtcNow.AddDays(-7),
|
||||||
|
"1M" => DateTime.UtcNow.AddMonths(-1),
|
||||||
|
"1Y" => DateTime.UtcNow.AddYears(-1),
|
||||||
|
_ => DateTime.MinValue // Default to include all data
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
using Managing.Application.Abstractions;
|
|
||||||
using Managing.Application.Abstractions.Services;
|
|
||||||
using Managing.Application.ManageBot.Commands;
|
|
||||||
using Managing.Common;
|
|
||||||
using Managing.Domain.Users;
|
|
||||||
using MediatR;
|
|
||||||
|
|
||||||
namespace Managing.Application.ManageBot
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Handler for retrieving all agents and their strategies
|
|
||||||
/// </summary>
|
|
||||||
public class GetAllAgentsCommandHandler : IRequestHandler<GetAllAgentsCommand, Dictionary<User, List<ITradingBot>>>
|
|
||||||
{
|
|
||||||
private readonly IBotService _botService;
|
|
||||||
private readonly IAccountService _accountService;
|
|
||||||
|
|
||||||
public GetAllAgentsCommandHandler(IBotService botService, IAccountService accountService)
|
|
||||||
{
|
|
||||||
_botService = botService;
|
|
||||||
_accountService = accountService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<Dictionary<User, List<ITradingBot>>> Handle(GetAllAgentsCommand request,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var result = new Dictionary<User, List<ITradingBot>>();
|
|
||||||
var allActiveBots = _botService.GetActiveBots();
|
|
||||||
|
|
||||||
// Group bots by user
|
|
||||||
foreach (var bot in allActiveBots)
|
|
||||||
{
|
|
||||||
if (bot.User == null)
|
|
||||||
{
|
|
||||||
// Skip bots without a user (this shouldn't happen, but just to be safe)
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply time filtering if needed (except for "Total")
|
|
||||||
if (request.TimeFilter != "Total")
|
|
||||||
{
|
|
||||||
// Check if this bot had activity within the specified time range
|
|
||||||
if (!BotHasActivityInTimeRange(bot, request.TimeFilter))
|
|
||||||
{
|
|
||||||
continue; // Skip this bot if it doesn't have activity in the time range
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the bot to the user's list
|
|
||||||
if (!result.ContainsKey(bot.User))
|
|
||||||
{
|
|
||||||
result[bot.User] = new List<ITradingBot>();
|
|
||||||
}
|
|
||||||
|
|
||||||
result[bot.User].Add(bot);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.FromResult(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks if a bot has had trading activity within the specified time range
|
|
||||||
/// </summary>
|
|
||||||
private bool BotHasActivityInTimeRange(ITradingBot bot, string timeFilter)
|
|
||||||
{
|
|
||||||
// Convert time filter to a DateTime
|
|
||||||
DateTime cutoffDate = DateTime.UtcNow;
|
|
||||||
|
|
||||||
switch (timeFilter)
|
|
||||||
{
|
|
||||||
case "24H":
|
|
||||||
cutoffDate = DateTime.UtcNow.AddHours(-24);
|
|
||||||
break;
|
|
||||||
case "3D":
|
|
||||||
cutoffDate = DateTime.UtcNow.AddDays(-3);
|
|
||||||
break;
|
|
||||||
case "1W":
|
|
||||||
cutoffDate = DateTime.UtcNow.AddDays(-7);
|
|
||||||
break;
|
|
||||||
case "1M":
|
|
||||||
cutoffDate = DateTime.UtcNow.AddMonths(-1);
|
|
||||||
break;
|
|
||||||
case "1Y":
|
|
||||||
cutoffDate = DateTime.UtcNow.AddYears(-1);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// Default to "Total" (no filtering)
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if there are any positions with activity after the cutoff date
|
|
||||||
return bot.Positions.Any(p =>
|
|
||||||
p.Date >= cutoffDate ||
|
|
||||||
(p.Open.Date >= cutoffDate) ||
|
|
||||||
(p.StopLoss.Status == Enums.TradeStatus.Filled && p.StopLoss.Date >= cutoffDate) ||
|
|
||||||
(p.TakeProfit1.Status == Enums.TradeStatus.Filled && p.TakeProfit1.Date >= cutoffDate) ||
|
|
||||||
(p.TakeProfit2 != null && p.TakeProfit2.Status == Enums.TradeStatus.Filled &&
|
|
||||||
p.TakeProfit2.Date >= cutoffDate));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using Managing.Application.Abstractions;
|
||||||
|
using Managing.Application.ManageBot.Commands;
|
||||||
|
using Managing.Domain.Bots;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace Managing.Application.ManageBot
|
||||||
|
{
|
||||||
|
public class GetBotsByUserAndStatusCommandHandler(IBotService botService)
|
||||||
|
: IRequestHandler<GetBotsByUserAndStatusCommand, IEnumerable<Bot>>
|
||||||
|
{
|
||||||
|
public async Task<IEnumerable<Bot>> Handle(GetBotsByUserAndStatusCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Get all bots for the user
|
||||||
|
var userBots = await botService.GetBotsByUser(request.UserId);
|
||||||
|
|
||||||
|
// Filter by status
|
||||||
|
return userBots.Where(bot => bot.Status == request.Status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,52 +1,25 @@
|
|||||||
using Managing.Application.Abstractions;
|
using Managing.Application.Abstractions;
|
||||||
using Managing.Application.Abstractions.Services;
|
|
||||||
using Managing.Application.ManageBot.Commands;
|
using Managing.Application.ManageBot.Commands;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using static Managing.Common.Enums;
|
|
||||||
|
|
||||||
namespace Managing.Application.ManageBot
|
namespace Managing.Application.ManageBot
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handler for retrieving only online agent names
|
/// Handler for retrieving only online agent names
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class GetOnlineAgentNamesCommandHandler : IRequestHandler<GetOnlineAgentNamesCommand, List<string>>
|
public class GetOnlineAgentNamesCommandHandler : IRequestHandler<GetOnlineAgentNamesCommand, IEnumerable<string>>
|
||||||
{
|
{
|
||||||
private readonly IBotService _botService;
|
private readonly IBotService _botService;
|
||||||
private readonly IAccountService _accountService;
|
|
||||||
|
|
||||||
public GetOnlineAgentNamesCommandHandler(IBotService botService, IAccountService accountService)
|
public GetOnlineAgentNamesCommandHandler(IBotService botService)
|
||||||
{
|
{
|
||||||
_botService = botService;
|
_botService = botService;
|
||||||
_accountService = accountService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<List<string>> Handle(GetOnlineAgentNamesCommand request,
|
public async Task<IEnumerable<string>> Handle(GetOnlineAgentNamesCommand request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var onlineAgentNames = new List<string>();
|
return await _botService.GetActiveBotsNamesAsync();
|
||||||
var allActiveBots = _botService.GetActiveBots();
|
|
||||||
|
|
||||||
// Group bots by user and determine status
|
|
||||||
var agentGroups = allActiveBots
|
|
||||||
.Where(bot => bot.User != null)
|
|
||||||
.GroupBy(bot => bot.User)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
foreach (var agentGroup in agentGroups)
|
|
||||||
{
|
|
||||||
var user = agentGroup.Key;
|
|
||||||
var bots = agentGroup.ToList();
|
|
||||||
|
|
||||||
// Only include agents that have at least one strategy running (Online status)
|
|
||||||
var isOnline = bots.Any(bot => bot.GetStatus() == BotStatus.Up.ToString());
|
|
||||||
|
|
||||||
if (isOnline)
|
|
||||||
{
|
|
||||||
onlineAgentNames.Add(user.AgentName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.FromResult(onlineAgentNames);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using Managing.Application.Abstractions.Repositories;
|
||||||
|
using Managing.Application.ManageBot.Commands;
|
||||||
|
using Managing.Domain.Statistics;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace Managing.Application.ManageBot
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Handler for retrieving paginated agent summaries with sorting and filtering
|
||||||
|
/// </summary>
|
||||||
|
public class GetPaginatedAgentSummariesCommandHandler : IRequestHandler<GetPaginatedAgentSummariesCommand,
|
||||||
|
(IEnumerable<AgentSummary> Results, int TotalCount)>
|
||||||
|
{
|
||||||
|
private readonly IAgentSummaryRepository _agentSummaryRepository;
|
||||||
|
|
||||||
|
public GetPaginatedAgentSummariesCommandHandler(IAgentSummaryRepository agentSummaryRepository)
|
||||||
|
{
|
||||||
|
_agentSummaryRepository = agentSummaryRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(IEnumerable<AgentSummary> Results, int TotalCount)> Handle(
|
||||||
|
GetPaginatedAgentSummariesCommand request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return await _agentSummaryRepository.GetPaginatedAsync(
|
||||||
|
request.Page,
|
||||||
|
request.PageSize,
|
||||||
|
request.SortBy,
|
||||||
|
request.SortOrder,
|
||||||
|
request.AgentNames);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,26 +1,26 @@
|
|||||||
using Managing.Application.Abstractions;
|
using Managing.Application.Abstractions;
|
||||||
|
using Managing.Application.Abstractions.Services;
|
||||||
using Managing.Application.ManageBot.Commands;
|
using Managing.Application.ManageBot.Commands;
|
||||||
|
using Managing.Domain.Bots;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
|
||||||
namespace Managing.Application.ManageBot
|
namespace Managing.Application.ManageBot
|
||||||
{
|
{
|
||||||
public class GetUserStrategiesCommandHandler : IRequestHandler<GetUserStrategiesCommand, List<ITradingBot>>
|
public class GetUserStrategiesCommandHandler : IRequestHandler<GetUserStrategiesCommand, IEnumerable<Bot>>
|
||||||
{
|
{
|
||||||
private readonly IBotService _botService;
|
private readonly IBotService _botService;
|
||||||
|
private readonly IUserService _userService;
|
||||||
|
|
||||||
public GetUserStrategiesCommandHandler(IBotService botService)
|
public GetUserStrategiesCommandHandler(IBotService botService, IUserService userService)
|
||||||
{
|
{
|
||||||
_botService = botService;
|
_botService = botService;
|
||||||
|
_userService = userService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<List<ITradingBot>> Handle(GetUserStrategiesCommand request, CancellationToken cancellationToken)
|
public async Task<IEnumerable<Bot>> Handle(GetUserStrategiesCommand request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var allActiveBots = _botService.GetActiveBots();
|
var user = await _userService.GetUserByAgentName(request.AgentName);
|
||||||
var userBots = allActiveBots
|
return await _botService.GetBotsByUser(user.Id);
|
||||||
.Where(bot => bot.User != null && bot.User.AgentName == request.UserName)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
return Task.FromResult(userBots);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using Managing.Application.Abstractions;
|
using Managing.Application.Abstractions;
|
||||||
using Managing.Application.ManageBot.Commands;
|
using Managing.Application.ManageBot.Commands;
|
||||||
|
using Managing.Domain.Bots;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
|
||||||
namespace Managing.Application.ManageBot
|
namespace Managing.Application.ManageBot
|
||||||
@@ -7,7 +8,7 @@ namespace Managing.Application.ManageBot
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handler for retrieving a specific strategy owned by a user
|
/// Handler for retrieving a specific strategy owned by a user
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class GetUserStrategyCommandHandler : IRequestHandler<GetUserStrategyCommand, ITradingBot>
|
public class GetUserStrategyCommandHandler : IRequestHandler<GetUserStrategyCommand, Bot>
|
||||||
{
|
{
|
||||||
private readonly IBotService _botService;
|
private readonly IBotService _botService;
|
||||||
|
|
||||||
@@ -16,17 +17,14 @@ namespace Managing.Application.ManageBot
|
|||||||
_botService = botService;
|
_botService = botService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<ITradingBot> Handle(GetUserStrategyCommand request, CancellationToken cancellationToken)
|
public async Task<Bot> Handle(GetUserStrategyCommand request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var allActiveBots = _botService.GetActiveBots();
|
var strategy = await _botService.GetBotByName(request.StrategyName);
|
||||||
|
if (strategy == null)
|
||||||
// Find the specific strategy that matches both user and strategy name
|
{
|
||||||
var strategy = allActiveBots
|
throw new Exception($"Strategy with name {request.StrategyName} not found");
|
||||||
.FirstOrDefault(bot =>
|
}
|
||||||
bot.User.AgentName == request.AgentName &&
|
return strategy;
|
||||||
bot.Identifier == request.StrategyName);
|
|
||||||
|
|
||||||
return Task.FromResult(strategy);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
using Managing.Application.Abstractions;
|
|
||||||
using Managing.Core;
|
|
||||||
using MediatR;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using static Managing.Common.Enums;
|
|
||||||
|
|
||||||
namespace Managing.Application.ManageBot;
|
|
||||||
|
|
||||||
public class LoadBackupBotCommandHandler : IRequestHandler<LoadBackupBotCommand, string>
|
|
||||||
{
|
|
||||||
private readonly IBotService _botService;
|
|
||||||
private readonly ILogger<LoadBackupBotCommandHandler> _logger;
|
|
||||||
|
|
||||||
public LoadBackupBotCommandHandler(
|
|
||||||
ILogger<LoadBackupBotCommandHandler> logger, IBotService botService)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_botService = botService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<string> Handle(LoadBackupBotCommand request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var backupBots = (await _botService.GetSavedBotsAsync()).ToList();
|
|
||||||
_logger.LogInformation("Loading {Count} backup bots.", backupBots.Count);
|
|
||||||
|
|
||||||
var result = new Dictionary<string, BotStatus>();
|
|
||||||
bool anyBackupStarted = false;
|
|
||||||
bool anyBotActive = false;
|
|
||||||
|
|
||||||
foreach (var backupBot in backupBots)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var activeBot = _botService.GetActiveBots().FirstOrDefault(b => b.Identifier == backupBot.Identifier);
|
|
||||||
|
|
||||||
if (activeBot == null)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("No active instance found for bot {Identifier}. Starting backup...",
|
|
||||||
backupBot.Identifier);
|
|
||||||
|
|
||||||
// Start the bot from backup
|
|
||||||
_botService.StartBotFromBackup(backupBot);
|
|
||||||
|
|
||||||
// Wait a short time to allow the bot to initialize
|
|
||||||
await Task.Delay(1000, cancellationToken);
|
|
||||||
|
|
||||||
// Try to get the active bot multiple times to ensure it's properly started
|
|
||||||
int attempts = 0;
|
|
||||||
const int maxAttempts = 2;
|
|
||||||
|
|
||||||
while (attempts < maxAttempts)
|
|
||||||
{
|
|
||||||
activeBot = _botService.GetActiveBots()
|
|
||||||
.FirstOrDefault(b => b.Identifier == backupBot.Identifier);
|
|
||||||
if (activeBot != null)
|
|
||||||
{
|
|
||||||
// Check if the bot was originally Down
|
|
||||||
if (backupBot.LastStatus == BotStatus.Down)
|
|
||||||
{
|
|
||||||
result[activeBot.Identifier] = BotStatus.Down;
|
|
||||||
_logger.LogInformation(
|
|
||||||
"Backup bot {Identifier} loaded but kept in Down status as it was originally Down.",
|
|
||||||
backupBot.Identifier);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result[activeBot.Identifier] = BotStatus.Up;
|
|
||||||
anyBackupStarted = true;
|
|
||||||
_logger.LogInformation("Backup bot {Identifier} started successfully.",
|
|
||||||
backupBot.Identifier);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
attempts++;
|
|
||||||
if (attempts < maxAttempts)
|
|
||||||
{
|
|
||||||
await Task.Delay(1000, cancellationToken); // Wait another second before next attempt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeBot == null)
|
|
||||||
{
|
|
||||||
result[backupBot.Identifier] = BotStatus.Down;
|
|
||||||
_logger.LogWarning("Backup bot {Identifier} failed to start after {MaxAttempts} attempts.",
|
|
||||||
backupBot.Identifier, maxAttempts);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var status = MiscExtensions.ParseEnum<BotStatus>(activeBot.GetStatus());
|
|
||||||
result[activeBot.Identifier] = status;
|
|
||||||
anyBotActive = true;
|
|
||||||
_logger.LogInformation("Bot {Identifier} is already active with status {Status}.",
|
|
||||||
activeBot.Identifier,
|
|
||||||
status);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error loading bot {Identifier}. Deleting its backup.", backupBot.Identifier);
|
|
||||||
result[backupBot.Identifier] = BotStatus.Down;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var summary = string.Join(", ", result.Select(b => $"{b.Key}: {b.Value}"));
|
|
||||||
_logger.LogInformation("Bot loading completed. Summary: {Summary}", summary);
|
|
||||||
|
|
||||||
// Determine final status
|
|
||||||
BotStatus finalStatus = anyBackupStarted
|
|
||||||
? BotStatus.Backup
|
|
||||||
: anyBotActive
|
|
||||||
? BotStatus.Up
|
|
||||||
: BotStatus.Down;
|
|
||||||
|
|
||||||
_logger.LogInformation("Final aggregate bot status: {FinalStatus}", finalStatus);
|
|
||||||
|
|
||||||
return finalStatus.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class LoadBackupBotCommand : IRequest<string>
|
|
||||||
{
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using Managing.Application.Abstractions;
|
||||||
|
using Managing.Application.ManageBot.Commands;
|
||||||
|
using Managing.Domain.Trades;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace Managing.Application.ManageBot;
|
||||||
|
|
||||||
|
public class ManualPositionCommandHandler : IRequestHandler<ManualPositionCommand, Position>
|
||||||
|
{
|
||||||
|
private readonly IBotService _botService;
|
||||||
|
|
||||||
|
public ManualPositionCommandHandler(IBotService botService)
|
||||||
|
{
|
||||||
|
_botService = botService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Position> Handle(ManualPositionCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var bot = await _botService.GetBotByIdentifier(request.Identifier);
|
||||||
|
if (bot == null)
|
||||||
|
{
|
||||||
|
throw new Exception($"Bot with identifier {request.Identifier} not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await _botService.ClosePositionAsync(request.Identifier, request.PositionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
using Managing.Application.Abstractions;
|
using Managing.Application.Abstractions;
|
||||||
using Managing.Application.ManageBot.Commands;
|
using Managing.Application.ManageBot.Commands;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
using static Managing.Common.Enums;
|
||||||
|
|
||||||
namespace Managing.Application.ManageBot
|
namespace Managing.Application.ManageBot
|
||||||
{
|
{
|
||||||
public class RestartBotCommandHandler : IRequestHandler<RestartBotCommand, string>
|
public class RestartBotCommandHandler : IRequestHandler<RestartBotCommand, BotStatus>
|
||||||
{
|
{
|
||||||
private readonly IBotService _botService;
|
private readonly IBotService _botService;
|
||||||
|
|
||||||
@@ -13,9 +14,9 @@ namespace Managing.Application.ManageBot
|
|||||||
_botService = botService;
|
_botService = botService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<string> Handle(RestartBotCommand request, CancellationToken cancellationToken)
|
public async Task<BotStatus> Handle(RestartBotCommand request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return _botService.RestartBot(request.Name);
|
return await _botService.RestartBot(request.Identifier);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,42 +1,38 @@
|
|||||||
using Managing.Application.Abstractions;
|
using Managing.Application.Abstractions.Grains;
|
||||||
using Managing.Application.Abstractions.Services;
|
using Managing.Application.Abstractions.Services;
|
||||||
using Managing.Application.ManageBot.Commands;
|
using Managing.Application.ManageBot.Commands;
|
||||||
using Managing.Common;
|
using Managing.Common;
|
||||||
using Managing.Domain.Bots;
|
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using static Managing.Common.Enums;
|
using static Managing.Common.Enums;
|
||||||
|
|
||||||
namespace Managing.Application.ManageBot
|
namespace Managing.Application.ManageBot
|
||||||
{
|
{
|
||||||
public class StartBotCommandHandler : IRequestHandler<StartBotCommand, string>
|
public class StartBotCommandHandler : IRequestHandler<StartBotCommand, BotStatus>
|
||||||
{
|
{
|
||||||
private readonly IBotFactory _botFactory;
|
|
||||||
private readonly IBotService _botService;
|
|
||||||
private readonly IMoneyManagementService _moneyManagementService;
|
|
||||||
private readonly IExchangeService _exchangeService;
|
|
||||||
private readonly IAccountService _accountService;
|
private readonly IAccountService _accountService;
|
||||||
|
private readonly IGrainFactory _grainFactory;
|
||||||
|
|
||||||
public StartBotCommandHandler(IBotFactory botFactory, IBotService botService,
|
public StartBotCommandHandler(
|
||||||
IMoneyManagementService moneyManagementService, IExchangeService exchangeService,
|
IAccountService accountService, IGrainFactory grainFactory)
|
||||||
IAccountService accountService)
|
|
||||||
{
|
{
|
||||||
_botFactory = botFactory;
|
|
||||||
_botService = botService;
|
|
||||||
_moneyManagementService = moneyManagementService;
|
|
||||||
_exchangeService = exchangeService;
|
|
||||||
_accountService = accountService;
|
_accountService = accountService;
|
||||||
|
_grainFactory = grainFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> Handle(StartBotCommand request, CancellationToken cancellationToken)
|
public async Task<BotStatus> Handle(StartBotCommand request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
BotStatus botStatus = BotStatus.Down;
|
|
||||||
|
|
||||||
// Validate the configuration
|
// Validate the configuration
|
||||||
if (request.Config == null)
|
if (request.Config == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentException("Bot configuration is required");
|
throw new ArgumentException("Bot configuration is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (request.Config.Scenario == null || !request.Config.Scenario.Indicators.Any())
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Scenario or indicators not loaded properly in constructor. This indicates a configuration error.");
|
||||||
|
}
|
||||||
|
|
||||||
if (request.Config.BotTradingBalance <= Constants.GMX.Config.MinimumPositionAmount)
|
if (request.Config.BotTradingBalance <= Constants.GMX.Config.MinimumPositionAmount)
|
||||||
{
|
{
|
||||||
throw new ArgumentException(
|
throw new ArgumentException(
|
||||||
@@ -59,68 +55,23 @@ namespace Managing.Application.ManageBot
|
|||||||
throw new Exception($"Account {request.Config.AccountName} has no USDC balance or not enough balance");
|
throw new Exception($"Account {request.Config.AccountName} has no USDC balance or not enough balance");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure essential configuration values are properly set
|
|
||||||
var configToUse = new TradingBotConfig
|
|
||||||
{
|
|
||||||
AccountName = request.Config.AccountName,
|
|
||||||
MoneyManagement = request.Config.MoneyManagement,
|
|
||||||
Ticker = request.Config.Ticker,
|
|
||||||
ScenarioName = request.Config.ScenarioName,
|
|
||||||
Scenario = request.Config.Scenario,
|
|
||||||
Timeframe = request.Config.Timeframe,
|
|
||||||
IsForWatchingOnly = request.Config.IsForWatchingOnly,
|
|
||||||
BotTradingBalance = request.Config.BotTradingBalance,
|
|
||||||
IsForBacktest = request.Config.IsForBacktest,
|
|
||||||
CooldownPeriod =
|
|
||||||
request.Config.CooldownPeriod > 0 ? request.Config.CooldownPeriod : 1, // Default to 1 if not set
|
|
||||||
MaxLossStreak = request.Config.MaxLossStreak,
|
|
||||||
MaxPositionTimeHours = request.Config.MaxPositionTimeHours, // Properly handle nullable value
|
|
||||||
FlipOnlyWhenInProfit = request.Config.FlipOnlyWhenInProfit,
|
|
||||||
FlipPosition = request.Config.FlipPosition, // Set FlipPosition
|
|
||||||
Name = request.Config.Name ?? request.Name,
|
|
||||||
CloseEarlyWhenProfitable = request.Config.CloseEarlyWhenProfitable
|
|
||||||
};
|
|
||||||
|
|
||||||
var tradingBot = await _botFactory.CreateTradingBot(configToUse);
|
|
||||||
tradingBot.User = request.User;
|
|
||||||
|
|
||||||
// Log the configuration being used
|
|
||||||
LogBotConfigurationAsync(tradingBot, $"{configToUse.Name} created");
|
|
||||||
|
|
||||||
_botService.AddTradingBotToCache(tradingBot);
|
|
||||||
return tradingBot.GetStatus();
|
|
||||||
|
|
||||||
return botStatus.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Logs the bot configuration for debugging and audit purposes
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="bot">The trading bot instance</param>
|
|
||||||
/// <param name="context">Context information for the log</param>
|
|
||||||
private void LogBotConfigurationAsync(ITradingBot bot, string context)
|
|
||||||
{
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var config = bot.GetConfiguration();
|
var botGrain = _grainFactory.GetGrain<ILiveTradingBotGrain>(Guid.NewGuid());
|
||||||
var logMessage = $"{context} - Bot: {config.Name}, " +
|
await botGrain.CreateAsync(request.Config, request.User);
|
||||||
$"Account: {config.AccountName}, " +
|
|
||||||
$"Ticker: {config.Ticker}, " +
|
|
||||||
$"Balance: {config.BotTradingBalance}, " +
|
|
||||||
$"MaxTime: {config.MaxPositionTimeHours?.ToString() ?? "Disabled"}, " +
|
|
||||||
$"FlipOnlyProfit: {config.FlipOnlyWhenInProfit}, " +
|
|
||||||
$"FlipPosition: {config.FlipPosition}, " +
|
|
||||||
$"Cooldown: {config.CooldownPeriod}, " +
|
|
||||||
$"MaxLoss: {config.MaxLossStreak}";
|
|
||||||
|
|
||||||
// Log through the bot's logger (this will use the bot's logging mechanism)
|
// Only start the bot if createOnly is false
|
||||||
// For now, we'll just add a comment that this could be enhanced with actual logging
|
if (!request.CreateOnly)
|
||||||
// Console.WriteLine(logMessage); // Could be replaced with proper logging
|
{
|
||||||
|
await botGrain.StartAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Ignore logging errors to not affect bot creation
|
throw new Exception($"Failed to start bot: {ex.Message}, {ex.StackTrace}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return request.CreateOnly ? BotStatus.None : BotStatus.Up;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
using Managing.Application.Abstractions;
|
using Managing.Application.Abstractions;
|
||||||
using Managing.Application.ManageBot.Commands;
|
using Managing.Application.ManageBot.Commands;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
using static Managing.Common.Enums;
|
||||||
|
|
||||||
namespace Managing.Application.ManageBot
|
namespace Managing.Application.ManageBot
|
||||||
{
|
{
|
||||||
public class StopBotCommandHandler : IRequestHandler<StopBotCommand, string>
|
public class StopBotCommandHandler : IRequestHandler<StopBotCommand, BotStatus>
|
||||||
{
|
{
|
||||||
private readonly IBotService _botService;
|
private readonly IBotService _botService;
|
||||||
|
|
||||||
@@ -13,9 +14,9 @@ namespace Managing.Application.ManageBot
|
|||||||
_botService = botService;
|
_botService = botService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<string> Handle(StopBotCommand request, CancellationToken cancellationToken)
|
public async Task<BotStatus> Handle(StopBotCommand request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return _botService.StopBot(request.Identifier);
|
return await _botService.StopBot(request.Identifier);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
using Managing.Application.Abstractions;
|
|
||||||
using Managing.Application.ManageBot.Commands;
|
|
||||||
using MediatR;
|
|
||||||
|
|
||||||
namespace Managing.Application.ManageBot
|
|
||||||
{
|
|
||||||
public class ToggleIsForWatchingCommandHandler : IRequestHandler<ToggleIsForWatchingCommand, string>
|
|
||||||
{
|
|
||||||
private readonly IBotService _botService;
|
|
||||||
|
|
||||||
public ToggleIsForWatchingCommandHandler(IBotService botService)
|
|
||||||
{
|
|
||||||
_botService = botService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<string> Handle(ToggleIsForWatchingCommand request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
_botService.ToggleIsForWatchingOnly(request.Name);
|
|
||||||
var bot = _botService.GetActiveBots().FirstOrDefault(b => b.Name == request.Name);
|
|
||||||
return Task.FromResult(bot?.Config.IsForWatchingOnly.ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,7 @@ namespace Managing.Application.ManageBot
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handler for updating trading bot configurations
|
/// Handler for updating trading bot configurations
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class UpdateBotConfigCommandHandler : IRequestHandler<UpdateBotConfigCommand, string>
|
public class UpdateBotConfigCommandHandler : IRequestHandler<UpdateBotConfigCommand, bool>
|
||||||
{
|
{
|
||||||
private readonly IBotService _botService;
|
private readonly IBotService _botService;
|
||||||
|
|
||||||
@@ -16,44 +16,27 @@ namespace Managing.Application.ManageBot
|
|||||||
_botService = botService;
|
_botService = botService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> Handle(UpdateBotConfigCommand request, CancellationToken cancellationToken)
|
public async Task<bool> Handle(UpdateBotConfigCommand request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(request.Identifier))
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Bot identifier is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.NewConfig == null)
|
if (request.NewConfig == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentException("New configuration is required");
|
throw new ArgumentException("New configuration is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the bot from active bots
|
var bot = await _botService.GetBotByIdentifier(request.Identifier);
|
||||||
var activeBots = _botService.GetActiveBots();
|
|
||||||
var bot = activeBots.FirstOrDefault(b => b.Identifier == request.Identifier);
|
|
||||||
|
|
||||||
if (bot == null)
|
if (bot == null)
|
||||||
{
|
{
|
||||||
return $"Bot with identifier {request.Identifier} not found or is not running";
|
throw new Exception($"Bot with identifier {request.Identifier} not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the bot configuration
|
return await _botService.UpdateBotConfiguration(request.Identifier, request.NewConfig);
|
||||||
var updateResult = await bot.UpdateConfiguration(request.NewConfig);
|
|
||||||
|
|
||||||
if (updateResult)
|
|
||||||
{
|
|
||||||
return $"Bot configuration updated successfully for {request.Identifier}";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return $"Failed to update bot configuration for {request.Identifier}";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
return $"Error updating bot configuration: {ex.Message}";
|
throw new Exception($"Error updating bot configuration: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,31 +7,33 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Remove="MoneyManagements\Abstractions\**" />
|
<Compile Remove="MoneyManagements\Abstractions\**"/>
|
||||||
<EmbeddedResource Remove="MoneyManagements\Abstractions\**" />
|
<EmbeddedResource Remove="MoneyManagements\Abstractions\**"/>
|
||||||
<None Remove="MoneyManagements\Abstractions\**" />
|
<None Remove="MoneyManagements\Abstractions\**"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="FluentValidation" Version="11.9.1" />
|
<PackageReference Include="FluentValidation" Version="11.9.1"/>
|
||||||
<PackageReference Include="GeneticSharp" Version="3.1.4" />
|
<PackageReference Include="GeneticSharp" Version="3.1.4"/>
|
||||||
<PackageReference Include="MediatR" Version="12.2.0" />
|
<PackageReference Include="MediatR" Version="12.2.0"/>
|
||||||
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.1.0" />
|
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.1.0"/>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Core" Version="1.1.0" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Core" Version="1.1.0"/>
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0"/>
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0"/>
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1"/>
|
||||||
<PackageReference Include="Microsoft.Orleans.Client" Version="9.2.1" />
|
<PackageReference Include="Microsoft.Orleans.Client" Version="9.2.1"/>
|
||||||
<PackageReference Include="Microsoft.Orleans.Core.Abstractions" Version="9.2.1" />
|
<PackageReference Include="Microsoft.Orleans.Core.Abstractions" Version="9.2.1"/>
|
||||||
<PackageReference Include="Polly" Version="8.4.0" />
|
<PackageReference Include="Microsoft.Orleans.Reminders" Version="9.2.1"/>
|
||||||
<PackageReference Include="Skender.Stock.Indicators" Version="2.5.0" />
|
<PackageReference Include="Microsoft.Orleans.Runtime" Version="9.2.1"/>
|
||||||
|
<PackageReference Include="Polly" Version="8.4.0"/>
|
||||||
|
<PackageReference Include="Skender.Stock.Indicators" Version="2.5.0"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Managing.Application.Abstractions\Managing.Application.Abstractions.csproj" />
|
<ProjectReference Include="..\Managing.Application.Abstractions\Managing.Application.Abstractions.csproj"/>
|
||||||
<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"/>
|
||||||
<ProjectReference Include="..\Managing.Infrastructure.Database\Managing.Infrastructure.Databases.csproj" />
|
<ProjectReference Include="..\Managing.Infrastructure.Database\Managing.Infrastructure.Databases.csproj"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
99
src/Managing.Application/Scenarios/ScenarioRunnerGrain.cs
Normal file
99
src/Managing.Application/Scenarios/ScenarioRunnerGrain.cs
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
using Managing.Application.Abstractions.Grains;
|
||||||
|
using Managing.Application.Abstractions.Services;
|
||||||
|
using Managing.Core;
|
||||||
|
using Managing.Domain.Bots;
|
||||||
|
using Managing.Domain.Candles;
|
||||||
|
using Managing.Domain.Indicators;
|
||||||
|
using Managing.Domain.Shared.Helpers;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Orleans.Concurrency;
|
||||||
|
using static Managing.Common.Enums;
|
||||||
|
|
||||||
|
namespace Managing.Application.Scenarios;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Orleans grain for scenario execution and signal generation.
|
||||||
|
/// This stateless grain handles candle management and signal generation for live trading.
|
||||||
|
/// </summary>
|
||||||
|
[StatelessWorker]
|
||||||
|
public class ScenarioRunnerGrain : Grain, IScenarioRunnerGrain
|
||||||
|
{
|
||||||
|
private readonly ILogger<ScenarioRunnerGrain> _logger;
|
||||||
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
|
|
||||||
|
public ScenarioRunnerGrain(
|
||||||
|
ILogger<ScenarioRunnerGrain> logger,
|
||||||
|
IServiceScopeFactory scopeFactory)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_scopeFactory = scopeFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<HashSet<Candle>> GetCandlesAsync(TradingBotConfig config, DateTime startDate)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var newCandles = await ServiceScopeHelpers.WithScopedService<IExchangeService, HashSet<Candle>>(
|
||||||
|
_scopeFactory, async exchangeService =>
|
||||||
|
{
|
||||||
|
return await exchangeService.GetCandlesInflux(
|
||||||
|
TradingExchanges.Evm,
|
||||||
|
config.Ticker,
|
||||||
|
startDate,
|
||||||
|
config.Timeframe,
|
||||||
|
500);
|
||||||
|
});
|
||||||
|
|
||||||
|
_logger.LogInformation($"Updated {newCandles.Count} candles for {config.Ticker}");
|
||||||
|
return newCandles;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to update candles for {Ticker}", config.Ticker);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<LightSignal> GetSignals(TradingBotConfig config, Dictionary<string, LightSignal> previousSignals,
|
||||||
|
DateTime startDate, Candle candle)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// return new LightSignal(config.Ticker, TradeDirection.Long, Confidence.High,
|
||||||
|
// candle, candle.Date, TradingExchanges.Evm, IndicatorType.Composite,
|
||||||
|
// SignalType.Signal, "Generated Signal");
|
||||||
|
|
||||||
|
var candlesHashSet = await GetCandlesAsync(config, startDate);
|
||||||
|
if (candlesHashSet.LastOrDefault()!.Date <= candle.Date)
|
||||||
|
{
|
||||||
|
return null; // No new candles, no need to generate a signal
|
||||||
|
}
|
||||||
|
|
||||||
|
var signal = TradingBox.GetSignal(
|
||||||
|
candlesHashSet,
|
||||||
|
config.Scenario,
|
||||||
|
previousSignals,
|
||||||
|
config.Scenario?.LoopbackPeriod ?? 1);
|
||||||
|
|
||||||
|
if (signal != null && signal.Date >= candle.Date)
|
||||||
|
{
|
||||||
|
_logger.LogInformation(
|
||||||
|
$"Generated signal for {config.Ticker}: {signal.Direction} with confidence {signal.Confidence}");
|
||||||
|
|
||||||
|
return new LightSignal(signal.Ticker, signal.Direction, Confidence.High,
|
||||||
|
candle, candle.Date, signal.Exchange, signal.IndicatorType,
|
||||||
|
signal.SignalType, signal.IndicatorName);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return null; // No signal generated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to update signals for {Ticker}", config.Ticker);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using Managing.Application.Abstractions;
|
using System.Data;
|
||||||
|
using Managing.Application.Abstractions;
|
||||||
using Managing.Application.Abstractions.Services;
|
using Managing.Application.Abstractions.Services;
|
||||||
using Managing.Domain.Scenarios;
|
using Managing.Domain.Scenarios;
|
||||||
using Managing.Domain.Strategies;
|
using Managing.Domain.Strategies;
|
||||||
@@ -25,7 +26,7 @@ namespace Managing.Application.Scenarios
|
|||||||
|
|
||||||
foreach (var strategy in strategies)
|
foreach (var strategy in strategies)
|
||||||
{
|
{
|
||||||
scenario.AddIndicator(await _tradingService.GetStrategyByNameAsync(strategy));
|
scenario.AddIndicator(await _tradingService.GetIndicatorByNameAsync(strategy));
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -41,41 +42,14 @@ namespace Managing.Application.Scenarios
|
|||||||
return scenario;
|
return scenario;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async 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)
|
|
||||||
{
|
|
||||||
var strategy = ScenarioHelpers.BuildIndicator(
|
|
||||||
type,
|
|
||||||
name,
|
|
||||||
period,
|
|
||||||
fastPeriods,
|
|
||||||
slowPeriods,
|
|
||||||
signalPeriods,
|
|
||||||
multiplier,
|
|
||||||
stochPeriods,
|
|
||||||
smoothPeriods,
|
|
||||||
cyclePeriods);
|
|
||||||
await _tradingService.InsertStrategyAsync(strategy);
|
|
||||||
return strategy;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<Scenario>> GetScenariosAsync()
|
public async Task<IEnumerable<Scenario>> GetScenariosAsync()
|
||||||
{
|
{
|
||||||
return await _tradingService.GetScenariosAsync();
|
return await _tradingService.GetScenariosAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<Indicator>> GetIndicatorsAsync()
|
public async Task<IEnumerable<IndicatorBase>> GetIndicatorsAsync()
|
||||||
{
|
{
|
||||||
return await _tradingService.GetStrategiesAsync();
|
return await _tradingService.GetIndicatorsAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> DeleteScenarioAsync(string name)
|
public async Task<bool> DeleteScenarioAsync(string name)
|
||||||
@@ -100,7 +74,7 @@ namespace Managing.Application.Scenarios
|
|||||||
scenario.Indicators.Clear();
|
scenario.Indicators.Clear();
|
||||||
foreach (var strategy in strategies)
|
foreach (var strategy in strategies)
|
||||||
{
|
{
|
||||||
scenario.AddIndicator(await _tradingService.GetStrategyByNameAsync(strategy));
|
scenario.AddIndicator(await _tradingService.GetIndicatorByNameAsync(strategy));
|
||||||
}
|
}
|
||||||
|
|
||||||
scenario.LoopbackPeriod = loopbackPeriod ?? 1;
|
scenario.LoopbackPeriod = loopbackPeriod ?? 1;
|
||||||
@@ -120,7 +94,7 @@ namespace Managing.Application.Scenarios
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var strategy = await _tradingService.GetStrategyByNameAsync(name);
|
var strategy = await _tradingService.GetIndicatorByNameAsync(name);
|
||||||
strategy.Type = indicatorType;
|
strategy.Type = indicatorType;
|
||||||
strategy.Period = period;
|
strategy.Period = period;
|
||||||
strategy.FastPeriods = fastPeriods;
|
strategy.FastPeriods = fastPeriods;
|
||||||
@@ -130,7 +104,7 @@ namespace Managing.Application.Scenarios
|
|||||||
strategy.StochPeriods = stochPeriods;
|
strategy.StochPeriods = stochPeriods;
|
||||||
strategy.SmoothPeriods = smoothPeriods;
|
strategy.SmoothPeriods = smoothPeriods;
|
||||||
strategy.CyclePeriods = cyclePeriods;
|
strategy.CyclePeriods = cyclePeriods;
|
||||||
await _tradingService.UpdateStrategyAsync(strategy);
|
await _tradingService.UpdateIndicatorAsync(strategy);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
@@ -156,7 +130,7 @@ namespace Managing.Application.Scenarios
|
|||||||
|
|
||||||
foreach (var strategyName in strategies)
|
foreach (var strategyName in strategies)
|
||||||
{
|
{
|
||||||
var strategy = await _tradingService.GetStrategyByNameAsync(strategyName);
|
var strategy = await _tradingService.GetIndicatorByNameAsync(strategyName);
|
||||||
if (strategy != null && strategy.User?.Name == user.Name)
|
if (strategy != null && strategy.User?.Name == user.Name)
|
||||||
{
|
{
|
||||||
scenario.AddIndicator(strategy);
|
scenario.AddIndicator(strategy);
|
||||||
@@ -167,7 +141,7 @@ namespace Managing.Application.Scenarios
|
|||||||
return scenario;
|
return scenario;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<Indicator>> GetIndicatorsByUserAsync(User user)
|
public async Task<IEnumerable<IndicatorBase>> GetIndicatorsByUserAsync(User user)
|
||||||
{
|
{
|
||||||
var indicators = await GetIndicatorsAsync();
|
var indicators = await GetIndicatorsAsync();
|
||||||
return indicators.Where(s => s.User?.Name == user.Name);
|
return indicators.Where(s => s.User?.Name == user.Name);
|
||||||
@@ -175,10 +149,10 @@ namespace Managing.Application.Scenarios
|
|||||||
|
|
||||||
public async Task<bool> DeleteIndicatorByUser(User user, string name)
|
public async Task<bool> DeleteIndicatorByUser(User user, string name)
|
||||||
{
|
{
|
||||||
var strategy = await _tradingService.GetStrategyByNameAsync(name);
|
var strategy = await _tradingService.GetIndicatorByNameAsync(name);
|
||||||
if (strategy != null && strategy.User?.Name == user.Name)
|
if (strategy != null && strategy.User?.Name == user.Name)
|
||||||
{
|
{
|
||||||
await _tradingService.DeleteStrategyAsync(strategy.Name);
|
await _tradingService.DeleteIndicatorAsync(strategy.Name);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,23 +203,35 @@ namespace Managing.Application.Scenarios
|
|||||||
return scenario != null && scenario.User?.Name == user.Name ? scenario : null;
|
return scenario != null && scenario.User?.Name == user.Name ? scenario : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Indicator> CreateIndicatorForUser(User user, IndicatorType type, string name,
|
public async Task<IndicatorBase> CreateIndicatorForUser(User user, IndicatorType type, string name,
|
||||||
int? period = null,
|
int? period = null,
|
||||||
int? fastPeriods = null, int? slowPeriods = null, int? signalPeriods = null,
|
int? fastPeriods = null, int? slowPeriods = null, int? signalPeriods = null,
|
||||||
double? multiplier = null, int? stochPeriods = null, int? smoothPeriods = null,
|
double? multiplier = null, int? stochPeriods = null, int? smoothPeriods = null,
|
||||||
int? cyclePeriods = null)
|
int? cyclePeriods = null)
|
||||||
{
|
{
|
||||||
// Create a new strategy using the existing implementation
|
var existingIndicator = await _tradingService.GetIndicatorByNameUserAsync(name, user);
|
||||||
var strategy = await CreateStrategy(type, name, period, fastPeriods, slowPeriods, signalPeriods,
|
|
||||||
multiplier, stochPeriods, smoothPeriods, cyclePeriods);
|
|
||||||
|
|
||||||
// Set the user
|
if (existingIndicator != null)
|
||||||
strategy.User = user;
|
{
|
||||||
|
throw new DuplicateNameException("An indicator with this name already exists for the user.");
|
||||||
// Update the strategy to save the user property
|
}
|
||||||
await _tradingService.UpdateStrategyAsync(strategy);
|
else
|
||||||
|
{
|
||||||
return strategy;
|
var indicator = new IndicatorBase(name, type)
|
||||||
|
{
|
||||||
|
Period = period,
|
||||||
|
FastPeriods = fastPeriods,
|
||||||
|
SlowPeriods = slowPeriods,
|
||||||
|
SignalPeriods = signalPeriods,
|
||||||
|
Multiplier = multiplier,
|
||||||
|
StochPeriods = stochPeriods,
|
||||||
|
SmoothPeriods = smoothPeriods,
|
||||||
|
CyclePeriods = cyclePeriods,
|
||||||
|
User = user
|
||||||
|
};
|
||||||
|
await _tradingService.InsertIndicatorAsync(indicator);
|
||||||
|
return indicator;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> DeleteStrategiesByUser(User user)
|
public async Task<bool> DeleteStrategiesByUser(User user)
|
||||||
@@ -255,7 +241,7 @@ namespace Managing.Application.Scenarios
|
|||||||
var strategies = await GetIndicatorsByUserAsync(user);
|
var strategies = await GetIndicatorsByUserAsync(user);
|
||||||
foreach (var strategy in strategies)
|
foreach (var strategy in strategies)
|
||||||
{
|
{
|
||||||
await _tradingService.DeleteStrategyAsync(strategy.Name);
|
await _tradingService.DeleteIndicatorAsync(strategy.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -281,7 +267,7 @@ namespace Managing.Application.Scenarios
|
|||||||
|
|
||||||
foreach (var strategyName in strategies)
|
foreach (var strategyName in strategies)
|
||||||
{
|
{
|
||||||
var strategy = await _tradingService.GetStrategyByNameAsync(strategyName);
|
var strategy = await _tradingService.GetIndicatorByNameAsync(strategyName);
|
||||||
if (strategy != null && strategy.User?.Name == user.Name)
|
if (strategy != null && strategy.User?.Name == user.Name)
|
||||||
{
|
{
|
||||||
scenario.AddIndicator(strategy);
|
scenario.AddIndicator(strategy);
|
||||||
@@ -296,7 +282,7 @@ namespace Managing.Application.Scenarios
|
|||||||
int? fastPeriods, int? slowPeriods, int? signalPeriods, double? multiplier,
|
int? fastPeriods, int? slowPeriods, int? signalPeriods, double? multiplier,
|
||||||
int? stochPeriods, int? smoothPeriods, int? cyclePeriods)
|
int? stochPeriods, int? smoothPeriods, int? cyclePeriods)
|
||||||
{
|
{
|
||||||
var strategy = await _tradingService.GetStrategyByNameAsync(name);
|
var strategy = await _tradingService.GetIndicatorByNameAsync(name);
|
||||||
if (strategy == null || strategy.User?.Name != user.Name)
|
if (strategy == null || strategy.User?.Name != user.Name)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -311,7 +297,7 @@ namespace Managing.Application.Scenarios
|
|||||||
|
|
||||||
public async Task<Scenario> GetScenarioByNameAndUserAsync(string scenarioName, User user)
|
public async Task<Scenario> GetScenarioByNameAndUserAsync(string scenarioName, User user)
|
||||||
{
|
{
|
||||||
var scenario = await _tradingService.GetScenarioByNameAsync(scenarioName);
|
var scenario = await _tradingService.GetScenarioByNameUserAsync(scenarioName, user);
|
||||||
if (scenario == null)
|
if (scenario == null)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"Scenario {scenarioName} not found for user {user.Name}");
|
throw new InvalidOperationException($"Scenario {scenarioName} not found for user {user.Name}");
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ public class SettingsService : ISettingsService
|
|||||||
private async Task SetupStochSTCTrend()
|
private async Task SetupStochSTCTrend()
|
||||||
{
|
{
|
||||||
var name = "STCTrend";
|
var name = "STCTrend";
|
||||||
var strategy = await _scenarioService.CreateStrategy(IndicatorType.Stc,
|
var strategy = await _scenarioService.CreateIndicatorForUser(null, IndicatorType.Stc,
|
||||||
name,
|
name,
|
||||||
fastPeriods: 23,
|
fastPeriods: 23,
|
||||||
slowPeriods: 50,
|
slowPeriods: 50,
|
||||||
@@ -101,7 +101,7 @@ public class SettingsService : ISettingsService
|
|||||||
private async Task SetupMacd()
|
private async Task SetupMacd()
|
||||||
{
|
{
|
||||||
var name = "MacdCross";
|
var name = "MacdCross";
|
||||||
var strategy = await _scenarioService.CreateStrategy(IndicatorType.MacdCross,
|
var strategy = await _scenarioService.CreateIndicatorForUser(null, IndicatorType.MacdCross,
|
||||||
name,
|
name,
|
||||||
fastPeriods: 12,
|
fastPeriods: 12,
|
||||||
slowPeriods: 26,
|
slowPeriods: 26,
|
||||||
@@ -112,7 +112,7 @@ public class SettingsService : ISettingsService
|
|||||||
private async Task SetupRsiDiv()
|
private async Task SetupRsiDiv()
|
||||||
{
|
{
|
||||||
var name = "RsiDiv6";
|
var name = "RsiDiv6";
|
||||||
var strategy = await _scenarioService.CreateStrategy(IndicatorType.RsiDivergence,
|
var strategy = await _scenarioService.CreateIndicatorForUser(null, IndicatorType.RsiDivergence,
|
||||||
name,
|
name,
|
||||||
period: 6);
|
period: 6);
|
||||||
await _scenarioService.CreateScenario(name, new List<string> { strategy.Name });
|
await _scenarioService.CreateScenario(name, new List<string> { strategy.Name });
|
||||||
@@ -121,7 +121,7 @@ public class SettingsService : ISettingsService
|
|||||||
private async Task SetupRsiDivConfirm()
|
private async Task SetupRsiDivConfirm()
|
||||||
{
|
{
|
||||||
var name = "RsiDivConfirm6";
|
var name = "RsiDivConfirm6";
|
||||||
var strategy = await _scenarioService.CreateStrategy(IndicatorType.RsiDivergenceConfirm,
|
var strategy = await _scenarioService.CreateIndicatorForUser(null, IndicatorType.RsiDivergenceConfirm,
|
||||||
name,
|
name,
|
||||||
period: 6);
|
period: 6);
|
||||||
await _scenarioService.CreateScenario(name, new List<string> { strategy.Name });
|
await _scenarioService.CreateScenario(name, new List<string> { strategy.Name });
|
||||||
@@ -130,7 +130,7 @@ public class SettingsService : ISettingsService
|
|||||||
private async Task SetupSuperTrend()
|
private async Task SetupSuperTrend()
|
||||||
{
|
{
|
||||||
var name = "SuperTrend";
|
var name = "SuperTrend";
|
||||||
var strategy = await _scenarioService.CreateStrategy(IndicatorType.SuperTrend,
|
var strategy = await _scenarioService.CreateIndicatorForUser(null, IndicatorType.SuperTrend,
|
||||||
name,
|
name,
|
||||||
period: 10,
|
period: 10,
|
||||||
multiplier: 3);
|
multiplier: 3);
|
||||||
@@ -140,7 +140,7 @@ public class SettingsService : ISettingsService
|
|||||||
private async Task SetupChandelierExit()
|
private async Task SetupChandelierExit()
|
||||||
{
|
{
|
||||||
var name = "ChandelierExit";
|
var name = "ChandelierExit";
|
||||||
var strategy = await _scenarioService.CreateStrategy(IndicatorType.ChandelierExit,
|
var strategy = await _scenarioService.CreateIndicatorForUser(null, IndicatorType.ChandelierExit,
|
||||||
name,
|
name,
|
||||||
period: 22,
|
period: 22,
|
||||||
multiplier: 3);
|
multiplier: 3);
|
||||||
@@ -150,7 +150,7 @@ public class SettingsService : ISettingsService
|
|||||||
private async Task SetupStochRsiTrend()
|
private async Task SetupStochRsiTrend()
|
||||||
{
|
{
|
||||||
var name = "StochRsiTrend";
|
var name = "StochRsiTrend";
|
||||||
var strategy = await _scenarioService.CreateStrategy(IndicatorType.StochRsiTrend,
|
var strategy = await _scenarioService.CreateIndicatorForUser(null, IndicatorType.StochRsiTrend,
|
||||||
name,
|
name,
|
||||||
period: 14,
|
period: 14,
|
||||||
stochPeriods: 14,
|
stochPeriods: 14,
|
||||||
@@ -162,7 +162,7 @@ public class SettingsService : ISettingsService
|
|||||||
private async Task SetupEmaTrend()
|
private async Task SetupEmaTrend()
|
||||||
{
|
{
|
||||||
var name = "Ema200Trend";
|
var name = "Ema200Trend";
|
||||||
var strategy = await _scenarioService.CreateStrategy(IndicatorType.EmaTrend,
|
var strategy = await _scenarioService.CreateIndicatorForUser(null, IndicatorType.EmaTrend,
|
||||||
name,
|
name,
|
||||||
period: 200);
|
period: 200);
|
||||||
await _scenarioService.CreateScenario(name, new List<string> { strategy.Name });
|
await _scenarioService.CreateScenario(name, new List<string> { strategy.Name });
|
||||||
@@ -171,7 +171,7 @@ public class SettingsService : ISettingsService
|
|||||||
private async Task SetupEmaCross()
|
private async Task SetupEmaCross()
|
||||||
{
|
{
|
||||||
var name = "Ema200Cross";
|
var name = "Ema200Cross";
|
||||||
var strategy = await _scenarioService.CreateStrategy(IndicatorType.EmaCross,
|
var strategy = await _scenarioService.CreateIndicatorForUser(null, IndicatorType.EmaCross,
|
||||||
name,
|
name,
|
||||||
period: 200);
|
period: 200);
|
||||||
await _scenarioService.CreateScenario(name, new List<string> { strategy.Name });
|
await _scenarioService.CreateScenario(name, new List<string> { strategy.Name });
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user