R8: Accounting- + Supervisor-Modul + Core-Datenfundament (S-0)
Portierung der beiden fehlenden Grundbausteine aus PolytraderSharp (voller Ausbau). Core S-0 (Datenfundament fuer Analyse/Forensik): - core_decision_journal + core_order_events (+ ReasonCode/Decision/OrderEvent-Enums), IDecisionJournal/IOrderEventLog mit fehlertoleranten EF-Impls (Handel bricht nie). - SignalId-Durchreichung TradeSignal -> ExecutionService -> core_trade_history; ExecutionService schreibt an jeder Verzweigung Journal/Order-Events. - JSONL-Log-Sink (LogJson + Dual-Sink), pure Analytik: RealizedPnlEngine (FIFO), TradeAnalytics, DossierBuilder. Migration AddAnalysisFoundation. Accounting-Modul (acc_): unabhaengiger IBKR-Kontoauszug (Activity Flex Query) hinter Interfaces mit Offline-Null-Stubs -> append-only Ledger + Periodenabrechnung/BWA + FX (USD/EUR) + CSV/PDF (PDFsharp/MigraDoc). Steuerschicht bewusst offen (Platzhalter-Tab). Kein Handel. Migration InitialAccounting. Supervisor-Modul (sup_): read-only OpenRouter-Agent (Function-Calling-Loop) + read-only Tool-Registry (8 Tools) + Profile + Dossier-Browser + Counterfactual-Job (Stub) + Tagesbericht/MCP-Light (opt-in). Migration InitialSupervisor. Verdrahtung: Program.cs (beide Module + Icons), slnx/App/Tests-Referenzen, provision-db.ps1, AppSettings-Sektionen, docs/konzepte, README. Tests: 79 -> 117 gruen (FIFO/KPIs/Dossier/JSONL, Classifier/Engine/FX/Idempotenz, OpenRouter/Registry/Agent/MCP, STA-Konstruktion beider neuen Fenster). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
cbbedb2e0e
commit
2a312ca035
@@ -1,4 +1,7 @@
|
||||
using System.Text.Json;
|
||||
using IBKRTrader.Core.Logging;
|
||||
using IBKRTrader.Core.Persistence;
|
||||
using IBKRTrader.Core.Persistence.Entities;
|
||||
using IBKRTrader.Core.Settings;
|
||||
|
||||
namespace IBKRTrader.Core.Trading;
|
||||
@@ -6,42 +9,59 @@ 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.
|
||||
///
|
||||
/// Schreibt an jeder Verzweigung strukturiert ins Entscheidungsjournal (core_decision_journal) und bei
|
||||
/// Order-Aktionen ins Order-Lifecycle-Log (core_order_events) und reicht die <c>SignalId</c> bis in die
|
||||
/// Trade-Historie durch – Grundlage für die Supervisor-Forensik. Journal-Fehler brechen den Handel nie.
|
||||
/// </summary>
|
||||
public sealed class ExecutionService : IExecutionService
|
||||
{
|
||||
private readonly IBrokerClient _broker;
|
||||
private readonly IRiskService _risk;
|
||||
private readonly IBrokerClient _broker;
|
||||
private readonly IRiskService _risk;
|
||||
private readonly IPortfolioService _portfolio;
|
||||
private readonly SettingsService _settings;
|
||||
private readonly LoggingService _logger;
|
||||
private readonly SettingsService _settings;
|
||||
private readonly LoggingService _logger;
|
||||
private readonly IDecisionJournal _journal;
|
||||
private readonly IOrderEventLog _orderEvents;
|
||||
|
||||
public ExecutionService(
|
||||
IBrokerClient broker,
|
||||
IRiskService risk,
|
||||
IPortfolioService portfolio,
|
||||
SettingsService settings,
|
||||
LoggingService logger)
|
||||
LoggingService logger,
|
||||
IDecisionJournal journal,
|
||||
IOrderEventLog orderEvents)
|
||||
{
|
||||
_broker = broker;
|
||||
_risk = risk;
|
||||
_portfolio = portfolio;
|
||||
_settings = settings;
|
||||
_logger = logger;
|
||||
_broker = broker;
|
||||
_risk = risk;
|
||||
_portfolio = portfolio;
|
||||
_settings = settings;
|
||||
_logger = logger;
|
||||
_journal = journal;
|
||||
_orderEvents = orderEvents;
|
||||
}
|
||||
|
||||
public async Task<ExecutionResult> ExecuteAsync(TradeSignal signal, CancellationToken ct = default)
|
||||
{
|
||||
var trading = _settings.Settings.Trading;
|
||||
var module = signal.SourceModule;
|
||||
var sideStr = signal.Side == TradeSide.Buy ? "BUY" : "SELL";
|
||||
|
||||
// 1. Globaler Hauptschalter
|
||||
if (!trading.TradingEnabled)
|
||||
{
|
||||
Journal(signal, TradeDecision.Skipped, DecisionReason.TradingDisabled, "Trading global deaktiviert.");
|
||||
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)
|
||||
{
|
||||
Journal(signal, TradeDecision.Skipped, DecisionReason.NoQuote, $"Kein Kurs für {signal.Symbol} verfügbar.");
|
||||
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);
|
||||
@@ -62,7 +82,11 @@ public sealed class ExecutionService : IExecutionService
|
||||
(decimal)trading.MaxSlippagePercent);
|
||||
var decision = _risk.Evaluate(signal, context, riskParams);
|
||||
if (!decision.Approved)
|
||||
{
|
||||
Journal(signal, TradeDecision.Rejected, DecisionReason.RiskRejected, decision.Reason,
|
||||
new { price = quote.Last, netLiq = account.NetLiquidation, exposure, existingQty });
|
||||
return Log(module, ExecutionResult.Skip(decision.Reason));
|
||||
}
|
||||
|
||||
// 6. Order platzieren
|
||||
var order = new OrderRequest
|
||||
@@ -76,16 +100,62 @@ public sealed class ExecutionService : IExecutionService
|
||||
|
||||
var result = await _broker.PlaceOrderAsync(order, ct);
|
||||
if (!result.Success)
|
||||
{
|
||||
OrderEvent(signal, OrderEventType.PlaceFailed, sideStr, order, quote.Last, result.Error ?? "Order fehlgeschlagen.");
|
||||
Journal(signal, TradeDecision.Failed, DecisionReason.OrderFailed, result.Error ?? "Order fehlgeschlagen.");
|
||||
return Log(module, ExecutionResult.Error(result.Error ?? "Order fehlgeschlagen.", result));
|
||||
}
|
||||
|
||||
OrderEvent(signal, OrderEventType.Filled, sideStr, order, result.AvgFillPrice, result.OrderId ?? "OK",
|
||||
new { filled = result.FilledQuantity, avgPrice = result.AvgFillPrice });
|
||||
|
||||
// 7. Buchung
|
||||
await _portfolio.RecordFillAsync(
|
||||
module, signal.Symbol, signal.Side,
|
||||
result.FilledQuantity, result.AvgFillPrice, result.OrderId, ct);
|
||||
result.FilledQuantity, result.AvgFillPrice, result.OrderId, signal.SignalId, ct);
|
||||
|
||||
Journal(signal, TradeDecision.Executed, DecisionReason.OrderPlaced,
|
||||
$"Ausgeführt: {sideStr} {result.FilledQuantity}x {signal.Symbol} @ {result.AvgFillPrice:F2}",
|
||||
new { orderId = result.OrderId, filled = result.FilledQuantity, avgPrice = result.AvgFillPrice });
|
||||
|
||||
return Log(module, ExecutionResult.Execute(result));
|
||||
}
|
||||
|
||||
// ─── Journal-/Order-Event-Helfer (fehlertolerant über die EF-Implementierung) ────────────────
|
||||
|
||||
private void Journal(TradeSignal signal, TradeDecision decision, DecisionReason reason,
|
||||
string message, object? context = null) =>
|
||||
_journal.Write(new CoreDecisionRecord
|
||||
{
|
||||
Timestamp = DateTime.UtcNow,
|
||||
SignalId = signal.SignalId,
|
||||
Module = signal.SourceModule,
|
||||
Symbol = signal.Symbol,
|
||||
Side = signal.Side == TradeSide.Buy ? "BUY" : "SELL",
|
||||
SignalPrice = signal.LimitPrice ?? 0m,
|
||||
Decision = decision,
|
||||
Reason = reason,
|
||||
ContextJson = context is null ? "" : JsonSerializer.Serialize(context),
|
||||
Message = message
|
||||
});
|
||||
|
||||
private void OrderEvent(TradeSignal signal, OrderEventType type, string side, OrderRequest order,
|
||||
decimal price, string response, object? details = null) =>
|
||||
_orderEvents.Write(new CoreOrderEvent
|
||||
{
|
||||
Timestamp = DateTime.UtcNow,
|
||||
SignalId = signal.SignalId,
|
||||
Module = signal.SourceModule,
|
||||
Symbol = signal.Symbol,
|
||||
EventType = type,
|
||||
Side = side,
|
||||
Price = price,
|
||||
Quantity = order.Quantity,
|
||||
OrderType = order.Type.ToString(),
|
||||
Response = response,
|
||||
DetailsJson = details is null ? "" : JsonSerializer.Serialize(details)
|
||||
});
|
||||
|
||||
private ExecutionResult Log(string module, ExecutionResult result)
|
||||
{
|
||||
var text = $"[{result.Action}] {result.Reason}";
|
||||
|
||||
@@ -15,7 +15,8 @@ public interface IPortfolioService
|
||||
/// <summary>Verbucht einen Fill: aktualisiert Position, Budget und Trade-Historie.</summary>
|
||||
Task RecordFillAsync(
|
||||
string module, string symbol, TradeSide side,
|
||||
int quantity, decimal price, string? orderId, CancellationToken ct = default);
|
||||
int quantity, decimal price, string? orderId, string? signalId = null,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>Alle offenen Positionen eines Moduls.</summary>
|
||||
Task<IReadOnlyList<Position>> GetPositionsAsync(string module, CancellationToken ct = default);
|
||||
|
||||
@@ -54,12 +54,13 @@ public sealed class PortfolioService : IPortfolioService
|
||||
|
||||
public async Task RecordFillAsync(
|
||||
string module, string symbol, TradeSide side,
|
||||
int quantity, decimal price, string? orderId, CancellationToken ct = default)
|
||||
int quantity, decimal price, string? orderId, string? signalId = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (quantity <= 0) return;
|
||||
|
||||
var action = side == TradeSide.Buy ? "BUY" : "SELL";
|
||||
await _history.RecordTradeAsync(module, symbol, action, quantity, price, orderId);
|
||||
await _history.RecordTradeAsync(module, symbol, action, quantity, price, orderId, signalId: signalId);
|
||||
|
||||
await using (var db = await _dbf.CreateDbContextAsync(ct))
|
||||
{
|
||||
|
||||
@@ -19,7 +19,8 @@ public class TradeHistoryService
|
||||
|
||||
public async Task RecordTradeAsync(
|
||||
string module, string symbol, string action,
|
||||
decimal quantity, decimal price, string? ibkrOrderId = null, string? notes = null)
|
||||
decimal quantity, decimal price, string? ibkrOrderId = null, string? notes = null,
|
||||
string? signalId = null)
|
||||
{
|
||||
await using var db = await _dbf.CreateDbContextAsync();
|
||||
db.TradeHistory.Add(new CoreTrade
|
||||
@@ -31,6 +32,7 @@ public class TradeHistoryService
|
||||
Price = price,
|
||||
TotalValue = quantity * price,
|
||||
TradedAt = DateTime.UtcNow,
|
||||
SignalId = signalId,
|
||||
IbkrOrderId = ibkrOrderId,
|
||||
Status = "Executed",
|
||||
Notes = notes,
|
||||
|
||||
@@ -15,6 +15,12 @@ public enum TradingMode { Paper, Live }
|
||||
/// </summary>
|
||||
public sealed record TradeSignal
|
||||
{
|
||||
/// <summary>
|
||||
/// Korrelations-ID der Signal-Kette (Signal → Entscheidung(en) → Order(s) → Trade). Standardmäßig
|
||||
/// neu erzeugt; ein Modul kann eine eigene ID setzen, um mehrere Signale zu verknüpfen.
|
||||
/// </summary>
|
||||
public string SignalId { get; init; } = Guid.NewGuid().ToString("N");
|
||||
|
||||
/// <summary>Ticker-Symbol (z. B. "AAPL").</summary>
|
||||
public required string Symbol { get; init; }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user