Files
IBKRTrader/src/IBKRTrader.Core/Analytics/RealizedPnlEngine.cs
T
RichardandClaude Opus 4.8 2a312ca035 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>
2026-07-31 09:25:18 +02:00

82 lines
3.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using IBKRTrader.Core.Persistence.Entities;
namespace IBKRTrader.Core.Analytics;
/// <summary>
/// Ein geschlossener (realisierter) Teil-Trade: eine SELL-Menge, gegen ihre FIFO-gematchten BUY-Lots
/// abgerechnet. Aus den Fills der <c>core_trade_history</c> rein rechnerisch abgeleitet.
/// </summary>
public sealed record RealizedTrade(
string Module,
string Symbol,
decimal Quantity,
decimal BuyPrice,
decimal SellPrice,
DateTime OpenedAt,
DateTime ClosedAt)
{
/// <summary>Realisierte GuV dieser Menge (Erlös − Einstand); Fees sind hier nicht berücksichtigt.</summary>
public decimal RealizedPnl => (SellPrice - BuyPrice) * Quantity;
}
/// <summary>
/// Reines, seiteneffektfreies FIFO-Lot-Matching über Fills (BUY öffnet Lots, SELL realisiert gegen die
/// ältesten offenen Lots) — je (Modul, Symbol). Grundlage sowohl für die Supervisor-KPIs
/// (<see cref="TradeAnalytics"/>) als auch für die realisierte GuV im Accounting-Modul (kein Duplikat).
/// Geldkritisch → unit-getestet. Long-only-Sicht: SELL-Mengen ohne passendes offenes Lot werden
/// ignoriert (kein Leerverkauf modelliert).
/// </summary>
public static class RealizedPnlEngine
{
private sealed class Lot
{
public decimal Quantity;
public decimal Price;
public DateTime OpenedAt;
}
/// <summary>Matcht alle Fills zu realisierten Teil-Trades (chronologisch, FIFO je Modul+Symbol).</summary>
public static IReadOnlyList<RealizedTrade> Match(IEnumerable<CoreTrade> fills)
{
var result = new List<RealizedTrade>();
var groups = fills
.GroupBy(f => (f.Module, f.Symbol));
foreach (var g in groups)
{
var open = new Queue<Lot>();
foreach (var f in g.OrderBy(x => x.TradedAt).ThenBy(x => x.Id))
{
bool isBuy = string.Equals(f.Action, "BUY", StringComparison.OrdinalIgnoreCase);
if (isBuy)
{
open.Enqueue(new Lot { Quantity = f.Quantity, Price = f.Price, OpenedAt = f.TradedAt });
continue;
}
// SELL: gegen die ältesten offenen Lots abrechnen.
decimal remaining = f.Quantity;
while (remaining > 0 && open.Count > 0)
{
var lot = open.Peek();
decimal matched = Math.Min(remaining, lot.Quantity);
result.Add(new RealizedTrade(
g.Key.Module, g.Key.Symbol, matched, lot.Price, f.Price, lot.OpenedAt, f.TradedAt));
lot.Quantity -= matched;
remaining -= matched;
if (lot.Quantity <= 0) open.Dequeue();
}
// Überschüssige SELL-Menge ohne offenes Lot: ignoriert (kein Short modelliert).
}
}
return result;
}
/// <summary>Summe der realisierten GuV über alle gematchten Teil-Trades.</summary>
public static decimal TotalRealized(IEnumerable<CoreTrade> fills) =>
Match(fills).Sum(t => t.RealizedPnl);
}