- Introduced Redis configuration in appsettings.json to enable SignalR backplane functionality. - Updated Program.cs to conditionally configure SignalR with Redis if a connection string is provided. - Added Redis connection service registration in ApiBootstrap for distributed scenarios. - Included necessary package references for StackExchange.Redis and Microsoft.Extensions.Caching.StackExchangeRedis in project files. - Implemented password masking for Redis connection strings to enhance security.
655 lines
31 KiB
C#
655 lines
31 KiB
C#
using System.Net;
|
||
using System.Reflection;
|
||
using Discord.Commands;
|
||
using Discord.WebSocket;
|
||
using Flagsmith;
|
||
using FluentValidation;
|
||
using Managing.Application;
|
||
using Managing.Application.Abstractions;
|
||
using Managing.Application.Abstractions.Repositories;
|
||
using Managing.Application.Abstractions.Services;
|
||
using Managing.Application.Accounts;
|
||
using Managing.Application.Agents;
|
||
using Managing.Application.Backtests;
|
||
using Managing.Application.Grains;
|
||
using Managing.Application.ManageBot;
|
||
using Managing.Application.ManageBot.Commands;
|
||
using Managing.Application.MoneyManagements;
|
||
using Managing.Application.Orleans;
|
||
using Managing.Application.Scenarios;
|
||
using Managing.Application.Shared;
|
||
using Managing.Application.Shared.Behaviours;
|
||
using Managing.Application.Synth;
|
||
using Managing.Application.Trading;
|
||
using Managing.Application.Trading.Commands;
|
||
using Managing.Application.Trading.Handlers;
|
||
using Managing.Application.Users;
|
||
using Managing.Application.Whitelist;
|
||
using Managing.Application.Workers;
|
||
using Managing.Domain.Trades;
|
||
using Managing.Infrastructure.Databases;
|
||
using Managing.Infrastructure.Databases.InfluxDb;
|
||
using Managing.Infrastructure.Databases.InfluxDb.Abstractions;
|
||
using Managing.Infrastructure.Databases.InfluxDb.Models;
|
||
using Managing.Infrastructure.Databases.PostgreSql;
|
||
using Managing.Infrastructure.Databases.PostgreSql.Configurations;
|
||
using Managing.Infrastructure.Evm;
|
||
using Managing.Infrastructure.Evm.Services;
|
||
using Managing.Infrastructure.Evm.Subgraphs;
|
||
using Managing.Infrastructure.Exchanges;
|
||
using Managing.Infrastructure.Exchanges.Abstractions;
|
||
using Managing.Infrastructure.Exchanges.Exchanges;
|
||
using Managing.Infrastructure.Messengers.Discord;
|
||
using Managing.Infrastructure.Storage;
|
||
using MediatR;
|
||
using Microsoft.AspNetCore.Hosting;
|
||
using Microsoft.Extensions.Configuration;
|
||
using Microsoft.Extensions.DependencyInjection;
|
||
using Microsoft.Extensions.Hosting;
|
||
using Microsoft.Extensions.Logging;
|
||
using Microsoft.Extensions.Options;
|
||
using Orleans.Configuration;
|
||
|
||
namespace Managing.Bootstrap;
|
||
|
||
public static class ApiBootstrap
|
||
{
|
||
private static readonly Assembly ApplicationAssembly = typeof(StartBotCommand).GetTypeInfo().Assembly;
|
||
|
||
public static IServiceCollection RegisterApiDependencies(this IServiceCollection services,
|
||
IConfiguration configuration)
|
||
{
|
||
services.Configure<Web3ProxySettings>(configuration.GetSection("Web3Proxy"));
|
||
|
||
return services
|
||
.AddApplication()
|
||
.AddInfrastructure(configuration)
|
||
.AddWorkers(configuration)
|
||
.AddFluentValidation()
|
||
.AddMediatR()
|
||
;
|
||
}
|
||
|
||
public static IServiceCollection AddHostedServices(this IServiceCollection services)
|
||
{
|
||
services.AddHostedService<GrainInitializer>();
|
||
return services;
|
||
}
|
||
|
||
// Note: IClusterClient is automatically available in co-hosting scenarios
|
||
// through IGrainFactory. Services should inject IGrainFactory instead of IClusterClient
|
||
// to avoid circular dependency issues during DI container construction.
|
||
|
||
public static IHostBuilder ConfigureOrleans(this IHostBuilder hostBuilder, IConfiguration configuration,
|
||
bool isProduction)
|
||
{
|
||
// Check if Orleans grains should be active using root-level configuration
|
||
var runOrleansGrains = configuration.GetValue<bool>("RunOrleansGrains", true);
|
||
|
||
// Check environment variable as override
|
||
var runOrleansGrainsEnv = Environment.GetEnvironmentVariable("RUN_ORLEANS_GRAINS");
|
||
|
||
if (!string.IsNullOrEmpty(runOrleansGrainsEnv) &&
|
||
bool.TryParse(runOrleansGrainsEnv, out var runOrleansGrainsFromEnv))
|
||
{
|
||
runOrleansGrains = runOrleansGrainsFromEnv;
|
||
}
|
||
|
||
// Allow disabling Orleans clustering entirely in case of issues
|
||
var disableOrleansClusteringEnv = Environment.GetEnvironmentVariable("DISABLE_ORLEANS_CLUSTERING");
|
||
var disableOrleansClustering = !string.IsNullOrEmpty(disableOrleansClusteringEnv) &&
|
||
bool.TryParse(disableOrleansClusteringEnv, out var disabled) && disabled;
|
||
|
||
// Get TASK_SLOT for multi-instance configuration
|
||
var taskSlotEnv = Environment.GetEnvironmentVariable("TASK_SLOT");
|
||
var taskSlot = 1; // Default to 1 if not provided
|
||
if (!string.IsNullOrEmpty(taskSlotEnv) && int.TryParse(taskSlotEnv, out var parsedTaskSlot))
|
||
{
|
||
taskSlot = parsedTaskSlot;
|
||
}
|
||
|
||
// Calculate unique ports based on task slot
|
||
var siloPort = 11111 + (taskSlot - 1) * 10; // 11111, 11121, 11131, etc.
|
||
var gatewayPort = 30000 + (taskSlot - 1) * 10; // 30000, 30010, 30020, etc.
|
||
var dashboardPort = 9999 + (taskSlot - 1); // 9999, 10000, 10001, etc.
|
||
|
||
// Get hostname for clustering - prioritize external IP for multi-server setups
|
||
var hostname = Environment.GetEnvironmentVariable("CAPROVER_SERVER_IP") ?? // CapRover server IP
|
||
Environment.GetEnvironmentVariable("EXTERNAL_IP") ?? // Custom external IP
|
||
Environment.GetEnvironmentVariable("HOSTNAME") ?? // Container hostname
|
||
Environment.GetEnvironmentVariable("COMPUTERNAME") ?? // Windows hostname
|
||
"localhost";
|
||
|
||
// For Docker containers, always use localhost for same-server clustering
|
||
IPAddress advertisedIP = IPAddress.Loopback; // Advertise as localhost for same-server clustering
|
||
|
||
// Only use external IP if specifically provided for multi-server scenarios
|
||
var externalIP = Environment.GetEnvironmentVariable("CAPROVER_SERVER_IP") ??
|
||
Environment.GetEnvironmentVariable("EXTERNAL_IP");
|
||
|
||
if (!string.IsNullOrEmpty(externalIP) && IPAddress.TryParse(externalIP, out var parsedExternalIP))
|
||
{
|
||
advertisedIP = parsedExternalIP;
|
||
Console.WriteLine($"Using external IP for multi-server clustering: {advertisedIP}");
|
||
}
|
||
else
|
||
{
|
||
Console.WriteLine($"Using localhost for same-server clustering: {advertisedIP}");
|
||
}
|
||
|
||
var postgreSqlConnectionString = configuration.GetSection("PostgreSql")["Orleans"];
|
||
var siloRole = Environment.GetEnvironmentVariable("SILO_ROLE") ?? "Trading";
|
||
|
||
Console.WriteLine($"Task Slot: {taskSlot}");
|
||
Console.WriteLine($"Hostname: {hostname}");
|
||
Console.WriteLine($"Advertised IP: {advertisedIP}");
|
||
Console.WriteLine($"Role: {siloRole}");
|
||
Console.WriteLine($"Silo port: {siloPort}");
|
||
Console.WriteLine($"Gateway port: {gatewayPort}");
|
||
Console.WriteLine($"Dashboard port: {dashboardPort}");
|
||
|
||
return hostBuilder.UseOrleans(siloBuilder =>
|
||
{
|
||
// Configure clustering with improved networking or use localhost clustering if disabled
|
||
if (!disableOrleansClustering && !string.IsNullOrEmpty(postgreSqlConnectionString))
|
||
{
|
||
siloBuilder
|
||
.UseAdoNetClustering(options =>
|
||
{
|
||
options.ConnectionString = postgreSqlConnectionString;
|
||
options.Invariant = "Npgsql";
|
||
})
|
||
.Configure<EndpointOptions>(options =>
|
||
{
|
||
// Advertise the specific IP for clustering
|
||
options.AdvertisedIPAddress = advertisedIP;
|
||
options.SiloPort = siloPort;
|
||
options.GatewayPort = gatewayPort;
|
||
options.SiloListeningEndpoint = new IPEndPoint(IPAddress.Any, siloPort);
|
||
options.GatewayListeningEndpoint = new IPEndPoint(IPAddress.Any, gatewayPort);
|
||
})
|
||
.Configure<ClusterOptions>(options =>
|
||
{
|
||
options.ServiceId = "ManagingApp";
|
||
options.ClusterId = configuration["ASPNETCORE_ENVIRONMENT"] ?? "Development";
|
||
})
|
||
.Configure<SiloOptions>(options =>
|
||
{
|
||
// Configure silo address for multi-server clustering
|
||
options.SiloName = $"ManagingApi-{taskSlot}-{siloRole}";
|
||
Console.WriteLine($"Configuring silo with role: {siloRole}");
|
||
})
|
||
.Configure<MessagingOptions>(options =>
|
||
{
|
||
// Increase timeout for grain deactivation during shutdown
|
||
options.ResponseTimeout = TimeSpan.FromSeconds(30);
|
||
})
|
||
.Configure<GrainCollectionOptions>(options =>
|
||
{
|
||
// Configure grain collection timeouts
|
||
options.CollectionAge = TimeSpan.FromMinutes(10);
|
||
});
|
||
}
|
||
else
|
||
{
|
||
// Fallback to localhost clustering for testing or when database is unavailable
|
||
siloBuilder.UseLocalhostClustering(siloPort, gatewayPort)
|
||
.Configure<MessagingOptions>(options =>
|
||
{
|
||
// Increase timeout for grain deactivation during shutdown
|
||
options.ResponseTimeout = TimeSpan.FromSeconds(30);
|
||
})
|
||
.Configure<GrainCollectionOptions>(options =>
|
||
{
|
||
// Configure grain collection timeouts
|
||
options.CollectionAge = TimeSpan.FromMinutes(10);
|
||
});
|
||
}
|
||
|
||
// Conditionally configure reminder service based on flag
|
||
if (runOrleansGrains && !disableOrleansClustering && !string.IsNullOrEmpty(postgreSqlConnectionString))
|
||
{
|
||
siloBuilder.UseAdoNetReminderService(options =>
|
||
{
|
||
options.ConnectionString = postgreSqlConnectionString;
|
||
options.Invariant = "Npgsql";
|
||
});
|
||
}
|
||
|
||
// Configure networking - use specific IP for Docker containerized environments
|
||
if (disableOrleansClustering)
|
||
{
|
||
// Use localhost clustering when clustering is disabled
|
||
siloBuilder.ConfigureEndpoints(IPAddress.Loopback, siloPort, gatewayPort);
|
||
}
|
||
else if (string.IsNullOrEmpty(postgreSqlConnectionString))
|
||
{
|
||
// In Docker/containerized environments, use endpoint configuration
|
||
siloBuilder.Configure<EndpointOptions>(options =>
|
||
{
|
||
// Advertise the specific IP for clustering
|
||
options.AdvertisedIPAddress = advertisedIP;
|
||
options.SiloPort = siloPort;
|
||
options.GatewayPort = gatewayPort;
|
||
options.SiloListeningEndpoint = new IPEndPoint(IPAddress.Any, siloPort);
|
||
options.GatewayListeningEndpoint = new IPEndPoint(IPAddress.Any, gatewayPort);
|
||
});
|
||
}
|
||
|
||
siloBuilder
|
||
.Configure<MessagingOptions>(options =>
|
||
{
|
||
// Configure messaging for better reliability with increased timeouts
|
||
// Set to 2 hours to support long-running backtests that can take 47+ minutes
|
||
options.ResponseTimeout = TimeSpan.FromHours(2);
|
||
options.DropExpiredMessages = true;
|
||
})
|
||
.Configure<ClusterMembershipOptions>(options =>
|
||
{
|
||
// Configure cluster membership for better resilience
|
||
options.EnableIndirectProbes = true;
|
||
options.ProbeTimeout = TimeSpan.FromSeconds(10);
|
||
options.IAmAliveTablePublishTimeout = TimeSpan.FromSeconds(30);
|
||
options.MaxJoinAttemptTime = TimeSpan.FromSeconds(120);
|
||
|
||
// Improved settings for development environments with stale members
|
||
options.DefunctSiloCleanupPeriod = TimeSpan.FromMinutes(1);
|
||
options.DefunctSiloExpiration = TimeSpan.FromMinutes(2);
|
||
})
|
||
.Configure<GatewayOptions>(options =>
|
||
{
|
||
// Configure gateway with improved timeouts
|
||
options.GatewayListRefreshPeriod = TimeSpan.FromSeconds(60);
|
||
});
|
||
|
||
// Conditionally configure grain execution based on flag
|
||
if (runOrleansGrains)
|
||
{
|
||
siloBuilder.Configure<GrainCollectionOptions>(options =>
|
||
{
|
||
// Enable grain collection for active grains
|
||
// Set to 2.5 hours to allow long-running backtests (up to 2 hours) to complete
|
||
// without being collected prematurely
|
||
options.CollectionAge = TimeSpan.FromMinutes(150); // 2.5 hours
|
||
options.CollectionQuantum = TimeSpan.FromMinutes(1);
|
||
});
|
||
}
|
||
else
|
||
{
|
||
// Disable grain execution completely
|
||
siloBuilder.Configure<GrainCollectionOptions>(options =>
|
||
{
|
||
// Disable grain collection to prevent grains from running
|
||
options.CollectionAge = TimeSpan.FromDays(365); // Very long collection age
|
||
options.CollectionQuantum = TimeSpan.FromDays(1); // Very long quantum
|
||
});
|
||
}
|
||
|
||
siloBuilder
|
||
.ConfigureLogging(logging =>
|
||
{
|
||
logging.SetMinimumLevel(LogLevel.Information);
|
||
// Reduce verbosity of Orleans status update messages for long-running operations
|
||
// These are informational and can be very verbose during long backtests
|
||
logging.AddFilter("Orleans.Runtime.Messaging.IncomingMessageAcceptor", LogLevel.Warning);
|
||
logging.AddFilter("Orleans.Runtime.Messaging.MessageCenter", LogLevel.Warning);
|
||
// Keep important Orleans logs but reduce status update noise
|
||
logging.AddFilter("Microsoft.Orleans.Runtime.Messaging", LogLevel.Warning);
|
||
});
|
||
|
||
// Only enable dashboard in development to avoid shutdown issues
|
||
if (!isProduction)
|
||
{
|
||
siloBuilder.UseDashboard(options =>
|
||
{
|
||
// Configure dashboard with proper shutdown handling and unique ports per instance
|
||
options.Port = dashboardPort;
|
||
options.HostSelf = true;
|
||
options.CounterUpdateIntervalMs = 10000; // 10 seconds
|
||
options.HideTrace = true; // Hide trace to reduce dashboard overhead
|
||
options.Host = "0.0.0.0"; // Allow external connections
|
||
options.Username = "admin";
|
||
options.Password = "admin";
|
||
});
|
||
}
|
||
|
||
// Configure grain storage - use ADO.NET for production or memory for fallback
|
||
if (!disableOrleansClustering && !string.IsNullOrEmpty(postgreSqlConnectionString))
|
||
{
|
||
siloBuilder
|
||
.AddAdoNetGrainStorage("bot-store", options =>
|
||
{
|
||
options.ConnectionString = postgreSqlConnectionString;
|
||
options.Invariant = "Npgsql";
|
||
})
|
||
.AddAdoNetGrainStorage("registry-store", options =>
|
||
{
|
||
options.ConnectionString = postgreSqlConnectionString;
|
||
options.Invariant = "Npgsql";
|
||
})
|
||
.AddAdoNetGrainStorage("agent-store", options =>
|
||
{
|
||
options.ConnectionString = postgreSqlConnectionString;
|
||
options.Invariant = "Npgsql";
|
||
})
|
||
.AddAdoNetGrainStorage("platform-summary-store", options =>
|
||
{
|
||
options.ConnectionString = postgreSqlConnectionString;
|
||
options.Invariant = "Npgsql";
|
||
})
|
||
.AddAdoNetGrainStorage("candle-store", options =>
|
||
{
|
||
options.ConnectionString = postgreSqlConnectionString;
|
||
options.Invariant = "Npgsql";
|
||
});
|
||
}
|
||
else
|
||
{
|
||
// Fallback to memory storage when database is unavailable
|
||
siloBuilder
|
||
.AddMemoryGrainStorage("bot-store")
|
||
.AddMemoryGrainStorage("registry-store")
|
||
.AddMemoryGrainStorage("agent-store")
|
||
.AddMemoryGrainStorage("platform-summary-store")
|
||
.AddMemoryGrainStorage("candle-store");
|
||
}
|
||
|
||
// Configure Orleans Streams for price data distribution
|
||
siloBuilder.AddMemoryStreams("ManagingStreamProvider")
|
||
.AddMemoryGrainStorage("PubSubStore");
|
||
|
||
siloBuilder
|
||
.ConfigureServices(services =>
|
||
{
|
||
// Register custom placement directors for role-based placement
|
||
services.AddPlacementDirector<ComputePlacementStrategy, ComputePlacementDirector>();
|
||
services.AddPlacementDirector<TradingPlacementStrategy, TradingPlacementDirector>();
|
||
|
||
// Register existing services for Orleans DI
|
||
// These will be available to grains through dependency injection
|
||
services.AddTransient<IExchangeService, ExchangeService>();
|
||
services.AddTransient<IAccountService, AccountService>();
|
||
services.AddTransient<ITradingService, TradingService>();
|
||
services.AddTransient<IMessengerService, MessengerService>();
|
||
// Use Singleton for InfluxDB repositories to prevent connection disposal issues in Orleans grains
|
||
services.AddSingleton<IInfluxDbRepository, InfluxDbRepository>();
|
||
services.AddSingleton<ICandleRepository, CandleRepository>();
|
||
services.AddSingleton<IAgentBalanceRepository, AgentBalanceRepository>();
|
||
});
|
||
})
|
||
;
|
||
}
|
||
|
||
private static IServiceCollection AddApplication(this IServiceCollection services)
|
||
{
|
||
services.AddScoped<ITradingService, TradingService>();
|
||
services.AddScoped<IScenarioService, ScenarioService>();
|
||
services.AddScoped<IMoneyManagementService, MoneyManagementService>();
|
||
services.AddScoped<IAccountService, AccountService>();
|
||
services.AddScoped<IStatisticService, StatisticService>();
|
||
services.AddScoped<IAgentService, AgentService>();
|
||
services.AddScoped<ISettingsService, SettingsService>();
|
||
services.AddScoped<IUserService, UserService>();
|
||
services.AddScoped<IGeneticService, GeneticService>();
|
||
services.AddScoped<IBotService, BotService>();
|
||
services.AddScoped<IWorkerService, WorkerService>();
|
||
services.AddScoped<ISynthPredictionService, SynthPredictionService>();
|
||
services.AddScoped<ISynthApiClient, SynthApiClient>();
|
||
services.AddScoped<IPricesService, PricesService>();
|
||
services.AddScoped<IFlagsmithService, FlagsmithService>();
|
||
services.AddTransient<ICommandHandler<OpenPositionRequest, Position>, OpenPositionCommandHandler>();
|
||
services.AddTransient<ICommandHandler<OpenSpotPositionRequest, Position>, OpenSpotPositionCommandHandler>();
|
||
services.AddTransient<ICommandHandler<CloseBacktestFuturesPositionCommand, Position>, CloseBacktestFuturesPositionCommandHandler>();
|
||
services.AddTransient<ICommandHandler<CloseSpotPositionCommand, Position>, CloseSpotPositionCommandHandler>();
|
||
services.AddTransient<ICommandHandler<CloseFuturesPositionCommand, Position>, CloseFuturesPositionCommandHandler>();
|
||
// Keep old handler for backward compatibility
|
||
services.AddTransient<ICommandHandler<ClosePositionCommand, Position>, ClosePositionCommandHandler>();
|
||
|
||
// Processors
|
||
services.AddTransient<IBacktester, Backtester>();
|
||
services.AddTransient<JobService>();
|
||
services.AddTransient<IExchangeProcessor, EvmProcessor>();
|
||
|
||
services.AddTransient<ITradaoService, TradaoService>();
|
||
services.AddTransient<IExchangeService, ExchangeService>();
|
||
|
||
|
||
services.AddTransient<IWeb3ProxyService, Web3ProxyService>();
|
||
services.AddHttpClient<IWebhookService, WebhookService>();
|
||
services.AddTransient<IKaigenService, KaigenService>();
|
||
services.AddTransient<IWhitelistService, WhitelistService>();
|
||
|
||
services.AddSingleton<IMessengerService, MessengerService>();
|
||
services.AddSingleton<IDiscordService, DiscordService>();
|
||
|
||
// Admin services
|
||
services.AddSingleton<IAdminConfigurationService, AdminConfigurationService>();
|
||
|
||
// LLM and MCP services
|
||
services.AddScoped<ILlmService, Managing.Application.LLM.LlmService>();
|
||
services.AddScoped<IMcpService, Managing.Mcp.McpService>();
|
||
|
||
// MCP Tools (underlying implementations)
|
||
services.AddScoped<Managing.Mcp.Tools.BacktestTools>();
|
||
services.AddScoped<Managing.Mcp.Tools.DataTools>();
|
||
services.AddScoped<Managing.Mcp.Tools.BotTools>();
|
||
services.AddScoped<Managing.Mcp.Tools.IndicatorTools>();
|
||
|
||
// MCP Tool Wrappers (with tool definitions)
|
||
services.AddScoped<Managing.Mcp.McpTools.BacktestMcpTools>();
|
||
services.AddScoped<Managing.Mcp.McpTools.DataMcpTools>();
|
||
services.AddScoped<Managing.Mcp.McpTools.BotMcpTools>();
|
||
services.AddScoped<Managing.Mcp.McpTools.IndicatorMcpTools>();
|
||
|
||
return services;
|
||
}
|
||
|
||
private static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
|
||
{
|
||
// Database
|
||
services.AddSingleton<IPostgreSqlSettings>(sp =>
|
||
sp.GetRequiredService<IOptions<PostgreSqlSettings>>().Value);
|
||
|
||
services.AddSingleton<IInfluxDbSettings>(sp =>
|
||
sp.GetRequiredService<IOptions<InfluxDbSettings>>().Value);
|
||
|
||
services.Configure<KaigenSettings>(configuration.GetSection("Kaigen"));
|
||
services.Configure<FlagsmithSettings>(configuration.GetSection("Flagsmith"));
|
||
|
||
// Flagsmith - Register client as Singleton
|
||
services.AddSingleton<FlagsmithClient>(sp =>
|
||
{
|
||
var settings = sp.GetRequiredService<IOptions<FlagsmithSettings>>().Value;
|
||
if (string.IsNullOrWhiteSpace(settings.ApiKey))
|
||
{
|
||
throw new InvalidOperationException("Flagsmith ApiKey is not configured. Please set the Flagsmith:ApiKey configuration value.");
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(settings.ApiUrl))
|
||
{
|
||
throw new InvalidOperationException("Flagsmith ApiUrl is not configured. Please set the Flagsmith:ApiUrl configuration value.");
|
||
}
|
||
|
||
return new FlagsmithClient(settings.ApiKey, settings.ApiUrl);
|
||
});
|
||
|
||
// Evm
|
||
services.AddGbcFeed();
|
||
services.AddUniswapV2();
|
||
services.AddChainlink();
|
||
services.AddChainlinkGmx();
|
||
services.AddSingleton<IEvmManager, EvmManager>();
|
||
|
||
// PostgreSql Repositories
|
||
|
||
services.AddTransient<IAccountRepository, PostgreSqlAccountRepository>();
|
||
services.AddTransient<IBacktestRepository, PostgreSqlBacktestRepository>();
|
||
services.AddTransient<IJobRepository, PostgreSqlJobRepository>();
|
||
services.AddTransient<IGeneticRepository, PostgreSqlGeneticRepository>();
|
||
services.AddTransient<ITradingRepository, PostgreSqlTradingRepository>();
|
||
services.AddTransient<ISettingsRepository, PostgreSqlSettingsRepository>();
|
||
services.AddTransient<IUserRepository, PostgreSqlUserRepository>();
|
||
services.AddTransient<IAgentSummaryRepository, AgentSummaryRepository>();
|
||
services.AddTransient<IStatisticRepository, PostgreSqlStatisticRepository>();
|
||
services.AddTransient<IBotRepository, PostgreSqlBotRepository>();
|
||
services.AddTransient<IWorkerRepository, PostgreSqlWorkerRepository>();
|
||
services.AddTransient<ISynthRepository, PostgreSqlSynthRepository>();
|
||
services.AddTransient<IWhitelistRepository, PostgreSqlWhitelistRepository>();
|
||
|
||
// InfluxDb Repositories - Use Singleton for proper connection management in Orleans grains
|
||
services.AddSingleton<IInfluxDbRepository, InfluxDbRepository>();
|
||
services.AddSingleton<ICandleRepository, CandleRepository>();
|
||
services.AddSingleton<IAgentBalanceRepository, AgentBalanceRepository>();
|
||
|
||
// Cache
|
||
services.AddDistributedMemoryCache();
|
||
services.AddTransient<ICacheService, CacheService>();
|
||
services.AddSingleton<ITaskCache, TaskCache>();
|
||
|
||
// Services
|
||
services.AddTransient<ICacheService, CacheService>();
|
||
services.AddSingleton<ITaskCache, TaskCache>();
|
||
|
||
// Redis (for SignalR backplane and other distributed scenarios)
|
||
services.AddRedis(configuration);
|
||
|
||
return services;
|
||
}
|
||
|
||
private static IServiceCollection AddRedis(this IServiceCollection services, IConfiguration configuration)
|
||
{
|
||
// Check if Redis is configured
|
||
// Priority: ConnectionStrings:Redis (can be set via ConnectionStrings__Redis env var) > REDIS_URL env var
|
||
var redisConnectionString = configuration.GetConnectionString("Redis")
|
||
?? configuration["REDIS_URL"];
|
||
|
||
if (!string.IsNullOrWhiteSpace(redisConnectionString))
|
||
{
|
||
Console.WriteLine($"✅ Redis configured: {MaskRedisPassword(redisConnectionString)}");
|
||
|
||
// Register generic Redis connection service for various use cases
|
||
// (SignalR backplane, distributed caching, pub/sub, etc.)
|
||
services.AddSingleton<IRedisConnectionService, RedisConnectionService>();
|
||
}
|
||
else
|
||
{
|
||
Console.WriteLine("ℹ️ Redis not configured - running in single-instance mode");
|
||
|
||
// Register a no-op Redis service that returns null connections
|
||
services.AddSingleton<IRedisConnectionService>(sp =>
|
||
new RedisConnectionService(configuration, sp.GetRequiredService<ILogger<RedisConnectionService>>()));
|
||
}
|
||
|
||
return services;
|
||
}
|
||
|
||
private static string MaskRedisPassword(string connectionString)
|
||
{
|
||
if (connectionString.Contains("password=", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
var parts = connectionString.Split(',');
|
||
for (int i = 0; i < parts.Length; i++)
|
||
{
|
||
if (parts[i].Trim().StartsWith("password=", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
parts[i] = "password=***";
|
||
}
|
||
}
|
||
return string.Join(",", parts);
|
||
}
|
||
return connectionString;
|
||
}
|
||
|
||
private static IServiceCollection AddWorkers(this IServiceCollection services, IConfiguration configuration)
|
||
{
|
||
if (configuration.GetValue<bool>("WorkerNotifyBundleBacktest", false))
|
||
{
|
||
services.AddHostedService<NotifyBundleBacktestWorker>();
|
||
}
|
||
|
||
// Price Workers
|
||
if (configuration.GetValue<bool>("WorkerPricesFifteenMinutes", false))
|
||
{
|
||
services.AddHostedService<PricesFifteenMinutesWorker>();
|
||
}
|
||
|
||
if (configuration.GetValue<bool>("WorkerPricesOneHour", false))
|
||
{
|
||
services.AddHostedService<PricesOneHourWorker>();
|
||
}
|
||
|
||
if (configuration.GetValue<bool>("WorkerPricesFourHours", false))
|
||
{
|
||
services.AddHostedService<PricesFourHoursWorker>();
|
||
}
|
||
|
||
if (configuration.GetValue<bool>("WorkerPricesOneDay", false))
|
||
{
|
||
services.AddHostedService<PricesOneDayWorker>();
|
||
}
|
||
|
||
if (configuration.GetValue<bool>("WorkerPricesFiveMinutes", false))
|
||
{
|
||
services.AddHostedService<PricesFiveMinutesWorker>();
|
||
}
|
||
|
||
// Other Workers
|
||
if (configuration.GetValue<bool>("WorkerSpotlight", false))
|
||
{
|
||
services.AddHostedService<SpotlightWorker>();
|
||
}
|
||
|
||
if (configuration.GetValue<bool>("WorkerTraderWatcher", false))
|
||
{
|
||
services.AddHostedService<TraderWatcher>();
|
||
}
|
||
|
||
if (configuration.GetValue<bool>("WorkerLeaderboard", false))
|
||
{
|
||
services.AddHostedService<LeaderboardWorker>();
|
||
}
|
||
|
||
if (configuration.GetValue<bool>("WorkerFundingRatesWatcher", false))
|
||
{
|
||
services.AddHostedService<FundingRatesWatcher>();
|
||
}
|
||
|
||
if (configuration.GetValue<bool>("WorkerGeneticAlgorithm", false))
|
||
{
|
||
services.AddHostedService<GeneticAlgorithmWorker>();
|
||
}
|
||
|
||
// DEPRECATED: BundleBacktestWorker has been replaced by BundleBacktestGrain
|
||
// Bundle backtest processing is now handled by Orleans grain triggered directly from Backtester.cs
|
||
// if (configuration.GetValue<bool>("WorkerBundleBacktest", false))
|
||
// {
|
||
// services.AddHostedService<BundleBacktestWorker>();
|
||
// }
|
||
|
||
return services;
|
||
}
|
||
|
||
private static IServiceCollection AddMediatR(this IServiceCollection services)
|
||
{
|
||
return services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies(AppDomain.CurrentDomain.GetAssemblies()))
|
||
.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>))
|
||
.AddTransient(typeof(IUnhandledExceptionBehaviour<,>), typeof(UnhandledExceptionBehaviour<,>));
|
||
}
|
||
|
||
private static IServiceCollection AddFluentValidation(this IServiceCollection services)
|
||
{
|
||
return services.AddValidatorsFromAssembly(ApplicationAssembly);
|
||
}
|
||
|
||
public static IWebHostBuilder SetupDiscordBot(this IWebHostBuilder builder)
|
||
{
|
||
return builder.ConfigureServices(services =>
|
||
{
|
||
services
|
||
.AddSingleton<DiscordSettings>()
|
||
.AddSingleton<DiscordSocketClient>()
|
||
.AddSingleton<CommandService>()
|
||
.AddHostedService<DiscordService>();
|
||
});
|
||
}
|
||
} |