Phase 3: Trading-Kern (Risk, Execution, Portfolio) mit sicherem Broker-Default

- Core/Trading/TradingModels: Signal, Order(Request/Result), RiskContext/Decision,
  Account, Position, Quote, ExecutionResult, Enums (Side/OrderType/Mode)
- IBrokerClient + NullBrokerClient (sicherer Default, handelt NIE bis IBKR-Adapter verifiziert)
- RiskService (+IRiskService): Sizing nach MaxTrade%, Modul-Limit, Slippage; Buy/Sell
- PortfolioService (+IPortfolioService): core_position + core_trade_history + core_budget
- ExecutionService (+IExecutionService): Signal -> Kurs -> Konto -> Risiko -> Order -> Buchung
- TradingSettings in AppSettings (Paper/Live, TradingEnabled, Risikoparameter)
- CoreMigrations: core_position; DI-Registrierung der Trading-Services
- Tests: RiskService (11) + ExecutionService (6, NSubstitute) -> 38/38 gruen

Offen (bewusst gekapselt): echter IbkrBrokerClient gegen Client-Portal-Gateway (manuell verifizieren).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
This commit is contained in:
Richard
2026-07-27 11:33:59 +02:00
parent d0bc833235
commit 2ad4b55db1
15 changed files with 833 additions and 8 deletions
+124
View File
@@ -0,0 +1,124 @@
namespace IBKRTrader.Core.Trading;
/// <summary>Kauf oder Verkauf.</summary>
public enum TradeSide { Buy, Sell }
/// <summary>Order-Typ.</summary>
public enum OrderType { Market, Limit }
/// <summary>Handelsmodus Paper-Account (Test) oder Live.</summary>
public enum TradingMode { Paper, Live }
/// <summary>
/// Signal, das ein Modul an den <see cref="IExecutionService"/> übergibt.
/// Das Modul liefert nur die Absicht Sizing, Risiko und Ausführung macht der Core.
/// </summary>
public sealed record TradeSignal
{
/// <summary>Ticker-Symbol (z. B. "AAPL").</summary>
public required string Symbol { get; init; }
/// <summary>Kauf oder Verkauf.</summary>
public required TradeSide Side { get; init; }
/// <summary>Kürzel des auslösenden Moduls (z. B. "CT").</summary>
public required string SourceModule { get; init; }
/// <summary>Begründung des Signals (für Logging/Buchführung).</summary>
public string Reason { get; init; } = "";
/// <summary>Optionaler Limit-Preis. null = Market-Order.</summary>
public decimal? LimitPrice { get; init; }
/// <summary>Optionaler Nominalwert-Wunsch; sonst greift das Risiko-Sizing.</summary>
public decimal? SuggestedNotional { get; init; }
}
/// <summary>Konkrete Order-Anforderung an den Broker.</summary>
public sealed record OrderRequest
{
public required string Symbol { get; init; }
public required TradeSide Side { get; init; }
public required int Quantity { get; init; }
public required OrderType Type { get; init; }
public decimal? LimitPrice { get; init; }
}
/// <summary>Ergebnis einer Order-Platzierung.</summary>
public sealed record OrderResult
{
public bool Success { get; init; }
public string? OrderId { get; init; }
public int FilledQuantity { get; init; }
public decimal AvgFillPrice { get; init; }
public string? Error { get; init; }
public static OrderResult Filled(string orderId, int qty, decimal price) =>
new() { Success = true, OrderId = orderId, FilledQuantity = qty, AvgFillPrice = price };
public static OrderResult Fail(string error) =>
new() { Success = false, Error = error };
}
/// <summary>Momentaufnahme eines Kurses.</summary>
public sealed record Quote(string Symbol, decimal Last, decimal Bid, decimal Ask);
/// <summary>Kontostand-Momentaufnahme des Brokers.</summary>
public sealed record AccountState(decimal NetLiquidation, decimal AvailableFunds);
/// <summary>Offene Position eines Moduls.</summary>
public sealed record Position(string Module, string Symbol, int Quantity, decimal AvgPrice)
{
public decimal Notional => Quantity * AvgPrice;
}
/// <summary>Kontext für die Risikobewertung eines Signals.</summary>
public sealed record RiskContext
{
/// <summary>Aktueller Kurs des Symbols.</summary>
public required decimal Price { get; init; }
/// <summary>Netto-Liquidationswert des Kontos.</summary>
public required decimal NetLiquidation { get; init; }
/// <summary>Aktuell vom Modul gehaltener Nominalwert (Summe offener Positionen).</summary>
public decimal ModuleExposure { get; init; }
/// <summary>Bereits gehaltene Stückzahl für das Signal-Symbol.</summary>
public int ExistingQuantity { get; init; }
}
/// <summary>Aus den Settings abgeleitete Risiko-Parameter.</summary>
public sealed record RiskParameters(
decimal MaxTradePercent,
decimal MaxPositionPercentPerModule,
decimal MaxSlippagePercent);
/// <summary>Entscheidung der Risikoprüfung.</summary>
public sealed record RiskDecision
{
public bool Approved { get; init; }
public int Quantity { get; init; }
public string Reason { get; init; } = "";
public static RiskDecision Reject(string reason) =>
new() { Approved = false, Quantity = 0, Reason = reason };
public static RiskDecision Approve(int quantity, string reason = "OK") =>
new() { Approved = true, Quantity = quantity, Reason = reason };
}
/// <summary>Ergebnis einer Signal-Ausführung durch den <see cref="IExecutionService"/>.</summary>
public sealed record ExecutionResult
{
/// <summary>"EXECUTE", "SKIP" oder "ERROR".</summary>
public required string Action { get; init; }
public string Reason { get; init; } = "";
public OrderResult? Order { get; init; }
public bool Executed => Action == "EXECUTE";
public static ExecutionResult Skip(string reason) => new() { Action = "SKIP", Reason = reason };
public static ExecutionResult Error(string reason, OrderResult? order = null) => new() { Action = "ERROR", Reason = reason, Order = order };
public static ExecutionResult Execute(OrderResult order) => new() { Action = "EXECUTE", Reason = "OK", Order = order };
}