Files
PolyTraderSharp/Program.cs
T
RichardandClaude Opus 4.8 b8d2c8094b Feinschliff: restliche Copytrading-Services ins Modul + EF-Logging gedrosselt
- MasterTraderAnalyticsJob + PersistenceService von services/ nach
  src/PolyTrader.Modules.CopyTrading/Services/ verschoben und in
  CopyTradingModule.RegisterServices registriert (reiner Move). Beide sind
  copytrading-spezifisch (Master-Trader-Historie bzw. Persistenz der
  geschlossenen Copytrades ueber den Modul-Channel; Dual-Write in Core-Log bleibt).
- Program.cs: die zwei Hosted-Service-Registrierungen entfernt.
- appsettings.json: Logging-Sektion ergaenzt -> EF-Core-Command-Logging auf
  Warning (kein SQL-Spam mehr; --smoke-ui zeigt 0 "Executed DbCommand"-Zeilen).

Damit liegt die gesamte Copytrading-Logik im Modul; App/Core enthalten nur noch
generische Infrastruktur + Startup-Hydration + CLI-Tools.

Build gruen, 48 Tests gruen, --smoke-ui gruen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 19:47:15 +02:00

371 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Linq;
using System.Net.Http;
using System.Threading.Channels;
using System.Windows.Forms;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using PolyTrader.Core.Configuration;
using PolyTrader.Core.DependencyInjection;
using PolyTrader.Core.Modularity;
using PolyTrader.Modules.CopyTrading;
using PolyTrader.Modules.CopyTrading.Persistence;
using PolyTraderSharp.Models;
using PolyTraderSharp.Services;
namespace PolyTraderSharp;
internal static class Program
{
public static IHost? AppHost { get; private set; }
[STAThread]
private static void Main(string[] args)
{
// Config-Migration aus mongoexport-JSON (Ordner, Default "MongoDB") -> MySQL.
if (args.Length > 0 && string.Equals(args[0], "--migrate-json", StringComparison.OrdinalIgnoreCase))
{
var folder = args.Length > 1 ? args[1] : "MongoDB";
RunConfigMigrationFromJson(folder);
return;
}
// Readback-Verifikation der migrierten Config aus MySQL. Kein UI-Start.
if (args.Length > 0 && string.Equals(args[0], "--verify-mysql", StringComparison.OrdinalIgnoreCase))
{
RunVerifyMySql();
return;
}
// Headless-Smoke-Test der UI: konstruiert jede View + den Launcher (ohne Message-Loop
// und ohne Trading-/WSS-Services). Fängt Laufzeit-Konstruktionsfehler ab. Kein Fenster.
if (args.Length > 0 && string.Equals(args[0], "--smoke-ui", StringComparison.OrdinalIgnoreCase))
{
Environment.ExitCode = RunSmokeUi();
return;
}
// Diagnose: gibt die MySQL-Serverversion aus (für das ServerVersion-Pinning). Kein UI.
if (args.Length > 0 && string.Equals(args[0], "--db-version", StringComparison.OrdinalIgnoreCase))
{
RunDbVersion();
return;
}
ApplicationConfiguration.Initialize();
var modules = new System.Collections.Generic.List<IPolyTraderModule>
{
new CopyTradingModule()
};
AppHost = Host.CreateDefaultBuilder()
// Config immer neben der EXE suchen (nicht im Arbeitsverzeichnis), damit die App
// auch beim Start aus bin/ oder per Doppelklick ihre appsettings findet.
.UseContentRoot(AppContext.BaseDirectory)
// Host.CreateDefaultBuilder lädt appsettings.Local.json NICHT (nur appsettings.json
// + appsettings.{Environment}.json). Die gitignorierte Local-Datei hält aber die
// MySQL-Connection daher hier explizit ergänzen, sonst bleibt sie leer.
.ConfigureAppConfiguration((context, config) =>
config.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: false))
.ConfigureServices(delegate(HostBuilderContext context, IServiceCollection services)
{
var databaseOptions = new DatabaseOptions
{
MySqlConnectionString = context.Configuration["Database:MySqlConnectionString"] ?? string.Empty
};
services.Configure<DatabaseOptions>(context.Configuration.GetSection(DatabaseOptions.SectionName));
services.AddCorePersistence(databaseOptions);
services.AddSingleton((IServiceProvider sp) => ServerSettings.Load("server_settings.xml"));
services.AddSingleton<TradingState>();
services.AddSingleton(delegate(IServiceProvider sp)
{
TerminalLogger requiredService2 = sp.GetRequiredService<TerminalLogger>();
var httpHandler = new SocketsHttpHandler { PooledConnectionLifetime = TimeSpan.FromMinutes(2), MaxConnectionsPerServer = 100 };
return new PolymarketApiService(requiredService2, new HttpClient(httpHandler)
{
DefaultRequestHeaders =
{
{ "User-Agent", "py_clob_client" },
{ "Accept", "*/*" }
}
});
});
services.AddSingleton(delegate(IServiceProvider sp)
{
TerminalLogger requiredService2 = sp.GetRequiredService<TerminalLogger>();
var httpHandler = new SocketsHttpHandler { PooledConnectionLifetime = TimeSpan.FromMinutes(2), MaxConnectionsPerServer = 100 };
return new PolymarketClobClient(requiredService2, new HttpClient(httpHandler)
{
DefaultRequestHeaders =
{
{ "User-Agent", "py_clob_client" },
{ "Accept", "*/*" }
}
});
});
services.AddSingleton<TerminalLogger>();
services.AddSingleton<MullvadVpnService>();
services.AddSingleton<ThreemaService>();
services.AddSingleton<JobManager>();
// MUSS als erster HostedService laufen: hydriert den State, bevor die
// Trading-Services gegen einen leeren State anlaufen.
services.AddHostedService<StartupHydrationService>();
// Module registrieren ihre eigenen Services/Channels/State selbst.
foreach (var module in modules)
{
services.AddSingleton(module);
module.RegisterServices(services, context.Configuration);
}
services.AddHostedService<MarketSyncService>();
services.AddHostedService((IServiceProvider sp) => sp.GetRequiredService<MullvadVpnService>());
services.AddHostedService((IServiceProvider sp) => sp.GetRequiredService<ThreemaService>());
services.AddSingleton<PolyTraderSharp.Ui.ShellUiHost>();
services.AddTransient<PolyTraderSharp.Ui.LauncherForm>();
}).Build();
try
{
// Trade-Nummerierung fortsetzen: höchste bestehende TradeId aus dem Log lesen.
var copyState = AppHost.Services.GetRequiredService<CopyTradingState>();
var tradeLog = AppHost.Services.GetRequiredService<ICopyTradeLogRepository>();
var allTrades = tradeLog.Find(_ => true);
if (allTrades.Count > 0)
copyState.TotalCopyTrades = allTrades.Max(t => t.TradeId);
}
catch { }
AppHost.Start();
// Shell-Views registrieren (Core-App-Views; Module folgen via module.RegisterUi).
var uiHost = AppHost.Services.GetRequiredService<PolyTraderSharp.Ui.ShellUiHost>();
var viewServices = AppHost.Services;
uiHost.RegisterView(new PolyTrader.Core.Modularity.ModuleView
{
Id = "core.terminal",
Title = "Terminal",
Group = "Core",
Order = 10,
CreateForm = () =>
{
var view = new PolyTraderSharp.Ui.Views.TerminalView();
view.Initialize(viewServices.GetRequiredService<TerminalLogger>());
return view;
}
});
uiHost.RegisterView(new PolyTrader.Core.Modularity.ModuleView
{
Id = "core.settings",
Title = "Server Settings",
Group = "Core",
Order = 20,
CreateForm = () =>
{
var view = new PolyTraderSharp.Ui.Views.SettingsView();
view.Initialize(
viewServices.GetRequiredService<ThreemaService>(),
viewServices.GetRequiredService<MullvadVpnService>(),
viewServices.GetRequiredService<TerminalLogger>(),
viewServices.GetRequiredService<PolyTrader.Core.Persistence.IAccountRepository>(),
viewServices.GetRequiredService<TradingState>());
return view;
}
});
uiHost.RegisterView(new PolyTrader.Core.Modularity.ModuleView
{
Id = "core.jobs",
Title = "Server Jobs",
Group = "Core",
Order = 30,
CreateForm = () =>
{
var view = new PolyTraderSharp.Ui.Views.JobsView();
view.Initialize(viewServices.GetRequiredService<JobManager>());
return view;
}
});
uiHost.RegisterView(new PolyTrader.Core.Modularity.ModuleView
{
Id = "core.dashboard",
Title = "Dashboard",
Group = "Core",
Order = 5,
CreateForm = () =>
{
var view = new PolyTraderSharp.Ui.Views.DashboardView();
view.Initialize(
viewServices.GetRequiredService<PolyTrader.Core.Persistence.ITradeLogRepository>(),
viewServices.GetRequiredService<TradingState>());
return view;
}
});
// Modul-UI registrieren (Module steuern ihre Views selbst bei).
foreach (var module in modules)
{
module.RegisterUi(uiHost, viewServices);
}
var launcher = AppHost.Services.GetRequiredService<PolyTraderSharp.Ui.LauncherForm>();
Application.Run(launcher);
AppHost.StopAsync().GetAwaiter().GetResult();
}
private static void RunConfigMigrationFromJson(string folder)
{
var config = new Microsoft.Extensions.Configuration.ConfigurationBuilder()
.SetBasePath(System.IO.Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true)
.AddJsonFile("appsettings.Local.json", optional: true)
.AddEnvironmentVariables()
.Build();
var mySql = config["Database:MySqlConnectionString"] ?? string.Empty;
if (string.IsNullOrWhiteSpace(mySql))
{
Console.WriteLine("FEHLER: Database:MySqlConnectionString fehlt (appsettings.Local.json).");
return;
}
if (!System.IO.Path.IsPathRooted(folder))
folder = System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), folder);
Services.ConfigMigrator.RunFromJson(folder, mySql);
}
private static void RunVerifyMySql()
{
var config = new Microsoft.Extensions.Configuration.ConfigurationBuilder()
.SetBasePath(System.IO.Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true)
.AddJsonFile("appsettings.Local.json", optional: true)
.AddEnvironmentVariables()
.Build();
var mySql = config["Database:MySqlConnectionString"] ?? string.Empty;
if (string.IsNullOrWhiteSpace(mySql))
{
Console.WriteLine("FEHLER: Database:MySqlConnectionString fehlt (appsettings.Local.json).");
return;
}
Services.ConfigMigrator.VerifyMySql(mySql);
}
private static void RunDbVersion()
{
var config = new Microsoft.Extensions.Configuration.ConfigurationBuilder()
.SetBasePath(System.IO.Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true)
.AddJsonFile("appsettings.Local.json", optional: true)
.AddEnvironmentVariables()
.Build();
var conn = config["Database:MySqlConnectionString"] ?? string.Empty;
if (string.IsNullOrWhiteSpace(conn))
{
Console.WriteLine("FEHLER: Database:MySqlConnectionString fehlt (appsettings.Local.json).");
return;
}
try
{
using var c = new MySqlConnector.MySqlConnection(conn);
c.Open();
Console.WriteLine($"ServerVersion: {c.ServerVersion}");
}
catch (Exception ex)
{
Console.WriteLine($"FEHLER: {ex.GetType().Name}: {ex.Message}");
}
}
/// <summary>
/// Headless-Smoke-Test: baut einen minimalen Host (Persistenz + State + Modul-Registrierung
/// + Shell/Launcher), hydriert den State aus MySQL und konstruiert jede registrierte View
/// sowie den Launcher ohne Message-Loop und ohne Trading-/WSS-Services zu starten.
/// Gibt 0 zurück, wenn alles fehlerfrei konstruiert, sonst die Fehleranzahl.
/// </summary>
private static int RunSmokeUi()
{
ApplicationConfiguration.Initialize();
var modules = new System.Collections.Generic.List<IPolyTraderModule> { new CopyTradingModule() };
using var host = Host.CreateDefaultBuilder()
.UseContentRoot(AppContext.BaseDirectory)
.ConfigureAppConfiguration((context, config) =>
config.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: false))
.ConfigureServices((context, services) =>
{
var databaseOptions = new DatabaseOptions
{
MySqlConnectionString = context.Configuration["Database:MySqlConnectionString"] ?? string.Empty
};
services.Configure<DatabaseOptions>(context.Configuration.GetSection(DatabaseOptions.SectionName));
services.AddCorePersistence(databaseOptions);
services.AddSingleton<TerminalLogger>();
services.AddSingleton<TradingState>();
services.AddSingleton<StartupHydrationService>();
foreach (var module in modules)
{
services.AddSingleton(module);
module.RegisterServices(services, context.Configuration);
}
services.AddSingleton<PolyTraderSharp.Ui.ShellUiHost>();
services.AddTransient<PolyTraderSharp.Ui.LauncherForm>();
}).Build();
// State aus MySQL laden (Accounts/Trader/Settings), ohne die BackgroundServices zu starten.
try
{
host.Services.GetRequiredService<StartupHydrationService>()
.StartAsync(default).GetAwaiter().GetResult();
}
catch (Exception ex)
{
Console.WriteLine($"[WARN] Hydration übersprungen: {ex.Message}");
}
var uiHost = host.Services.GetRequiredService<PolyTraderSharp.Ui.ShellUiHost>();
foreach (var module in modules)
module.RegisterUi(uiHost, host.Services);
int failures = 0;
Console.WriteLine("=== Smoke-UI: View-Konstruktion ===");
foreach (var view in uiHost.Views)
{
try
{
using var form = view.CreateForm();
Console.WriteLine($"[OK] {view.Id} ({view.Title})");
}
catch (Exception ex)
{
failures++;
Console.WriteLine($"[FEHLER] {view.Id}: {ex.GetType().Name}: {ex.Message}");
}
}
try
{
using var launcher = host.Services.GetRequiredService<PolyTraderSharp.Ui.LauncherForm>();
Console.WriteLine("[OK] LauncherForm konstruiert");
}
catch (Exception ex)
{
failures++;
Console.WriteLine($"[FEHLER] LauncherForm: {ex.GetType().Name}: {ex.Message}");
}
Console.WriteLine(failures == 0 ? "=== Smoke-UI OK ===" : $"=== Smoke-UI: {failures} Fehler ===");
return failures;
}
}