using IBKRTrader.Core.Budget; using IBKRTrader.Core.Database; using IBKRTrader.Core.Logging; namespace IBKRTrader.Core.Trading; /// /// DB-gestützte Buchführung über offene Positionen (core_position), /// Trade-Historie (core_trade_history) und Budget (core_budget). /// public sealed class PortfolioService : IPortfolioService { private readonly DatabaseService _db; private readonly TradeHistoryService _history; private readonly BudgetService _budget; private readonly LoggingService _logger; public PortfolioService( DatabaseService db, TradeHistoryService history, BudgetService budget, LoggingService logger) { _db = db; _history = history; _budget = budget; _logger = logger; } private sealed class PosDto { public int Quantity { get; set; } public decimal AvgPrice { get; set; } } public async Task GetModuleExposureAsync(string module, CancellationToken ct = default) { var sum = await _db.ExecuteScalarAsync( "SELECT SUM(quantity * avg_price) FROM `core_position` WHERE module = @module", new { module }); return sum ?? 0m; } public async Task GetPositionQuantityAsync(string module, string symbol, CancellationToken ct = default) { var row = await _db.QueryFirstOrDefaultAsync( "SELECT quantity AS Quantity, avg_price AS AvgPrice FROM `core_position` " + "WHERE module = @module AND symbol = @symbol", new { module, symbol }); return row?.Quantity ?? 0; } public async Task> GetPositionsAsync(string module, CancellationToken ct = default) { var rows = await _db.QueryAsync( "SELECT module AS Module, symbol AS Symbol, quantity AS Quantity, avg_price AS AvgPrice " + "FROM `core_position` WHERE module = @module AND quantity > 0", new { module }); return rows.ToList(); } public async Task RecordFillAsync( string module, string symbol, TradeSide side, int quantity, decimal price, string? orderId, CancellationToken ct = default) { if (quantity <= 0) return; var action = side == TradeSide.Buy ? "BUY" : "SELL"; await _history.RecordTradeAsync(module, symbol, action, quantity, price, orderId); var current = await _db.QueryFirstOrDefaultAsync( "SELECT quantity AS Quantity, avg_price AS AvgPrice FROM `core_position` " + "WHERE module = @module AND symbol = @symbol", new { module, symbol }); var oldQty = current?.Quantity ?? 0; var oldAvg = current?.AvgPrice ?? 0m; if (side == TradeSide.Buy) { var newQty = oldQty + quantity; var newAvg = oldQty > 0 ? (oldQty * oldAvg + quantity * price) / newQty : price; await UpsertPositionAsync(module, symbol, newQty, newAvg); await _budget.ReserveBudgetAsync(module, quantity * price); } else { var newQty = oldQty - quantity; if (newQty <= 0) await DeletePositionAsync(module, symbol); else await UpsertPositionAsync(module, symbol, newQty, oldAvg); await _budget.ReleaseBudgetAsync(module, quantity * price); } _logger.Info(module, $"Position gebucht: {action} {quantity}x {symbol} @ {price:F2}"); } private Task UpsertPositionAsync(string module, string symbol, int quantity, decimal avgPrice) => _db.ExecuteAsync(@" INSERT INTO `core_position` (module, symbol, quantity, avg_price) VALUES (@module, @symbol, @quantity, @avgPrice) ON DUPLICATE KEY UPDATE quantity = @quantity, avg_price = @avgPrice", new { module, symbol, quantity, avgPrice }); private Task DeletePositionAsync(string module, string symbol) => _db.ExecuteAsync( "DELETE FROM `core_position` WHERE module = @module AND symbol = @symbol", new { module, symbol }); }