@
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:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user