Initial commit: Predictalytics solution
Clean Architecture .NET 8 solution (Domain/Application/Infrastructure/Api/Worker/WinFormsHost) for analyzing Polymarket traders for copytrading/strategy-replication candidates. Includes EF Core InitialBaseline migration and DB secrets removed from source/config in preparation for version control.
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
using Predictalytics.Application.Interfaces;
|
||||
using Predictalytics.Application.Services;
|
||||
using Predictalytics.Domain.Interfaces;
|
||||
using Predictalytics.Infrastructure.Data;
|
||||
using Predictalytics.Infrastructure.Data.Repositories;
|
||||
using Predictalytics.Infrastructure.Providers.Azuro;
|
||||
using Predictalytics.Infrastructure.Providers.Limitless;
|
||||
using Predictalytics.Infrastructure.Providers.Polymarket;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Predictalytics.Infrastructure;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static IServiceCollection AddPredictalytics(this IServiceCollection services, IConfiguration configuration, string? connectionStringOverride = null, bool dbDebug = false)
|
||||
{
|
||||
if (dbDebug) Serilog.Log.Warning(">>> INFRASTRUCTURE: AddPredictalytics STARTING");
|
||||
|
||||
// MySQL / EF Core
|
||||
var connectionString = connectionStringOverride;
|
||||
if (string.IsNullOrWhiteSpace(connectionString))
|
||||
{
|
||||
connectionString = configuration.GetConnectionString("DefaultConnection")
|
||||
?? "Server=localhost;Database=Predictalytics_dev;User=root;Password=;";
|
||||
}
|
||||
|
||||
// Use MySqlConnectionStringBuilder to ensure valid format and parse components
|
||||
var csBuilder = new MySqlConnector.MySqlConnectionStringBuilder(connectionString);
|
||||
|
||||
// Final safety check
|
||||
if (string.IsNullOrWhiteSpace(csBuilder.Database))
|
||||
{
|
||||
Serilog.Log.Error("❌ INVALID CONNECTION STRING: Database name is empty! (Input length: {Length})", connectionString.Length);
|
||||
throw new InvalidOperationException("The connection string is missing a valid 'Database' parameter.");
|
||||
}
|
||||
|
||||
var maskedCs = csBuilder.ConnectionString.Replace(csBuilder.Password, "****");
|
||||
if (dbDebug)
|
||||
{
|
||||
Serilog.Log.Warning("🗄️ Initializing database connection: {ConnectionString}", maskedCs);
|
||||
Serilog.Log.Warning("🗄️ Target Server: {Server}, Database: {Database}", csBuilder.Server, csBuilder.Database);
|
||||
}
|
||||
|
||||
// Explicitly register the connection string so we can use it elsewhere if needed
|
||||
services.AddSingleton(csBuilder.ConnectionString);
|
||||
|
||||
services.AddDbContext<AppDbContext>(options =>
|
||||
{
|
||||
// Log exactly what is being used at the moment of configuration
|
||||
if (dbDebug) Serilog.Log.Warning("🛠️ EF: Configuring AppDbContext. Target DB: '{Database}'", csBuilder.Database);
|
||||
|
||||
options.UseMySql(csBuilder.ConnectionString, new MySqlServerVersion(new Version(8, 0, 31)),
|
||||
mysql => mysql.EnableRetryOnFailure(3, TimeSpan.FromSeconds(10), null));
|
||||
});
|
||||
|
||||
// Repositories
|
||||
services.AddScoped<ITraderRepository, TraderRepository>();
|
||||
services.AddScoped<ITradeRepository, TradeRepository>();
|
||||
services.AddScoped<IMarketRepository, MarketRepository>();
|
||||
services.AddScoped<IWatchlistRepository, WatchlistRepository>();
|
||||
services.AddScoped<IAlertRepository, AlertRepository>();
|
||||
|
||||
// Application Services
|
||||
services.AddScoped<IScoringService, ScoringService>();
|
||||
services.AddScoped<IDiscoveryService, DiscoveryService>();
|
||||
services.AddScoped<IAlertService, AlertService>();
|
||||
services.AddScoped<IAnalyticsService, AnalyticsService>();
|
||||
services.AddScoped<WatchlistService>();
|
||||
services.AddSingleton<IRateLimiter, RateLimiterService>();
|
||||
services.AddSingleton<IPlatformStatisticsService, PlatformStatisticsService>();
|
||||
|
||||
// Platform Providers
|
||||
services.AddHttpClient();
|
||||
services.AddHttpClient("LimitlessApi", c =>
|
||||
{
|
||||
c.BaseAddress = new Uri("https://api.limitless.exchange/");
|
||||
c.DefaultRequestHeaders.Add("Accept", "application/json");
|
||||
c.Timeout = TimeSpan.FromSeconds(60);
|
||||
});
|
||||
|
||||
services.AddSingleton<PolymarketApiClient>();
|
||||
services.AddSingleton<LimitlessApiClient>();
|
||||
services.AddSingleton<IPlatformProvider, PolymarketProvider>();
|
||||
services.AddSingleton<IPlatformProvider, LimitlessProvider>();
|
||||
services.AddSingleton<IPlatformProvider, AzuroProvider>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies pending EF Core migrations and seeds default platform rows.
|
||||
/// Requires the target database to already be "stamped" with the InitialBaseline
|
||||
/// migration in __EFMigrationsHistory (see UMSETZUNGSPLAN.md, section B1) if it was
|
||||
/// previously created via the old EnsureCreated + manual ALTER approach.
|
||||
/// </summary>
|
||||
public static async Task EnsureDatabaseAsync(IServiceProvider services, bool dbDebug = false)
|
||||
{
|
||||
using var scope = services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
|
||||
try
|
||||
{
|
||||
if (dbDebug)
|
||||
{
|
||||
var maskedConnStr = System.Text.RegularExpressions.Regex.Replace(
|
||||
db.Database.GetDbConnection().ConnectionString ?? "NULL", "Password=[^;]+", "Password=****");
|
||||
Serilog.Log.Warning("🔍 DEBUG: Applying EF Core migrations. ConnectionString: {CS}", maskedConnStr);
|
||||
}
|
||||
|
||||
await db.Database.MigrateAsync();
|
||||
|
||||
if (dbDebug) Serilog.Log.Warning("✅ DEBUG: Migrations applied successfully.");
|
||||
|
||||
// Seed default platform rows (idempotent)
|
||||
var conn = db.Database.GetDbConnection();
|
||||
if (conn.State != System.Data.ConnectionState.Open) await conn.OpenAsync();
|
||||
|
||||
using var seedPlatform = conn.CreateCommand();
|
||||
seedPlatform.CommandText = @"
|
||||
INSERT IGNORE INTO `PlatformConfigs` (`Id`, `Name`, `DisplayName`, `IsActive`, `CreatedAt`, `UpdatedAt`) VALUES
|
||||
(0, 'Unknown', 'Unknown Platform', 1, UTC_TIMESTAMP(), UTC_TIMESTAMP()),
|
||||
(1, 'Polymarket', 'Polymarket', 1, UTC_TIMESTAMP(), UTC_TIMESTAMP()),
|
||||
(2, 'Azuro', 'Azuro', 1, UTC_TIMESTAMP(), UTC_TIMESTAMP()),
|
||||
(3, 'Limitless', 'Limitless', 1, UTC_TIMESTAMP(), UTC_TIMESTAMP());";
|
||||
await seedPlatform.ExecuteNonQueryAsync();
|
||||
|
||||
await conn.CloseAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Warning("⚠️ Could not connect to database or apply migrations: {Message}. Background workers will retry connection automatically.", ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user