R1: Umbau auf Multi-Projekt-Struktur (Vorbild PolytraderSharp)

Kurskorrektur: Vorbild ist PolytraderSharp (C#), nicht die veraltete Python-Version.
Reiner Strukturumbau, Verhalten unveraendert.

- src/IBKRTrader.Core (classlib): Core-Code + UI-Contract (ModuleFormBase, WindowManager)
- src/IBKRTrader.Modules.CongressTrading (classlib, referenziert nur Core)
- Root: IBKRTrader.App (WinExe) referenziert Core + Modul
- tests/IBKRTrader.Tests: Referenzen auf Core + Modul
- Neue .slnx; ungenutztes HtmlAgilityPack entfernt
- docs/ARCHITECTURE.md: korrigierter Ziel-Stand + R-Phasenplan
- Build + 38/38 Tests gruen; App startet

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
This commit is contained in:
Richard
2026-07-27 20:26:25 +02:00
parent 2ad4b55db1
commit c20538c8d8
61 changed files with 171 additions and 129 deletions
@@ -0,0 +1,96 @@
using IBKRTrader.Core.Logging;
using IBKRTrader.Core.Settings;
namespace IBKRTrader.Core.Trading;
/// <summary>
/// Führt Modul-Signale aus: globaler Schalter → Kurs → Konto → Risiko → Order → Buchung.
/// Kennt kein Modul – Module rufen nur <see cref="ExecuteAsync"/> mit ihrem Signal auf.
/// </summary>
public sealed class ExecutionService : IExecutionService
{
private readonly IBrokerClient _broker;
private readonly IRiskService _risk;
private readonly IPortfolioService _portfolio;
private readonly SettingsService _settings;
private readonly LoggingService _logger;
public ExecutionService(
IBrokerClient broker,
IRiskService risk,
IPortfolioService portfolio,
SettingsService settings,
LoggingService logger)
{
_broker = broker;
_risk = risk;
_portfolio = portfolio;
_settings = settings;
_logger = logger;
}
public async Task<ExecutionResult> ExecuteAsync(TradeSignal signal, CancellationToken ct = default)
{
var trading = _settings.Settings.Trading;
var module = signal.SourceModule;
// 1. Globaler Hauptschalter
if (!trading.TradingEnabled)
return Log(module, ExecutionResult.Skip("Trading global deaktiviert."));
// 2. Kurs
var quote = await _broker.GetQuoteAsync(signal.Symbol, ct);
if (quote is null || quote.Last <= 0)
return Log(module, ExecutionResult.Skip($"Kein Kurs für {signal.Symbol} verfügbar."));
// 3. Konto + 4. bestehende Exposure/Position
var account = await _broker.GetAccountStateAsync(ct);
var exposure = await _portfolio.GetModuleExposureAsync(module, ct);
var existingQty = await _portfolio.GetPositionQuantityAsync(module, signal.Symbol, ct);
// 5. Risikoprüfung
var context = new RiskContext
{
Price = quote.Last,
NetLiquidation = account.NetLiquidation,
ModuleExposure = exposure,
ExistingQuantity = existingQty
};
var riskParams = new RiskParameters(
(decimal)trading.MaxTradePercent,
(decimal)trading.MaxPositionPercentPerModule,
(decimal)trading.MaxSlippagePercent);
var decision = _risk.Evaluate(signal, context, riskParams);
if (!decision.Approved)
return Log(module, ExecutionResult.Skip(decision.Reason));
// 6. Order platzieren
var order = new OrderRequest
{
Symbol = signal.Symbol,
Side = signal.Side,
Quantity = decision.Quantity,
Type = signal.LimitPrice.HasValue ? OrderType.Limit : OrderType.Market,
LimitPrice = signal.LimitPrice
};
var result = await _broker.PlaceOrderAsync(order, ct);
if (!result.Success)
return Log(module, ExecutionResult.Error(result.Error ?? "Order fehlgeschlagen.", result));
// 7. Buchung
await _portfolio.RecordFillAsync(
module, signal.Symbol, signal.Side,
result.FilledQuantity, result.AvgFillPrice, result.OrderId, ct);
return Log(module, ExecutionResult.Execute(result));
}
private ExecutionResult Log(string module, ExecutionResult result)
{
var text = $"[{result.Action}] {result.Reason}";
if (result.Action == "ERROR") _logger.Error(module, text);
else _logger.Info(module, text);
return result;
}
}