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>
82 lines
3.1 KiB
C#
82 lines
3.1 KiB
C#
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);
|
||
}
|