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> @
125 lines
4.7 KiB
C#
125 lines
4.7 KiB
C#
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 };
|
||
}
|