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 });
}