R3 Slice 2: Trading-Buchführung auf EF Core (+ InMemory-Tests)

- BudgetService, TradeHistoryService, PortfolioService von Dapper/DatabaseService auf
  IDbContextFactory<CoreDbContext> (EF Core) umgestellt
- Positionen/Budget/Trade-Historie ueber CoreDbContext (core_position/core_budget/core_trade_history)
- Tests: EF-InMemory-Provider im Testprojekt; 5 PortfolioService-Tests (Buy/Sell/Avg/Exposure/Isolation)
- 35/35 Tests + Build + smoke-ui gruen; WorkerBase-Log/Modul/Dapper-Entfernung folgen

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
This commit is contained in:
Richard
2026-07-28 10:45:54 +02:00
parent 261032f9f9
commit 7e3791b82d
6 changed files with 210 additions and 113 deletions
+29 -23
View File
@@ -1,48 +1,54 @@
using IBKRTrader.Core.Database;
using IBKRTrader.Core.Logging;
using IBKRTrader.Core.Persistence.Ef;
using IBKRTrader.Core.Persistence.Entities;
using Microsoft.EntityFrameworkCore;
namespace IBKRTrader.Core.Budget;
/// <summary>
/// Verwaltet das Budget pro Modul.
/// Liest/schreibt core_budget.
/// Verwaltet das Budget pro Modul (Tabelle core_budget) via EF Core.
/// </summary>
public class BudgetService
{
private readonly DatabaseService _db;
private readonly LoggingService _logger;
private readonly IDbContextFactory<CoreDbContext> _dbf;
private readonly LoggingService _logger;
public BudgetService(DatabaseService db, LoggingService logger)
public BudgetService(IDbContextFactory<CoreDbContext> dbf, LoggingService logger)
{
_db = db;
_dbf = dbf;
_logger = logger;
}
public async Task<decimal> GetAvailableBudgetAsync(string module)
{
var row = await _db.QueryFirstOrDefaultAsync<dynamic>(
"SELECT total_budget, used_budget FROM `core_budget` WHERE module = @module",
new { module });
if (row == null) return 0;
return (decimal)row.total_budget - (decimal)row.used_budget;
await using var db = await _dbf.CreateDbContextAsync();
var b = await db.Budgets.FindAsync(module);
return b is null ? 0m : b.TotalBudget - b.UsedBudget;
}
public async Task ReserveBudgetAsync(string module, decimal amount)
{
await _db.ExecuteAsync(
@"INSERT INTO `core_budget` (module, used_budget)
VALUES (@module, @amount)
ON DUPLICATE KEY UPDATE used_budget = used_budget + @amount",
new { module, amount });
await using var db = await _dbf.CreateDbContextAsync();
var b = await db.Budgets.FindAsync(module);
if (b is null)
{
db.Budgets.Add(new CoreBudget { Module = module, UsedBudget = amount, UpdatedAt = DateTime.UtcNow });
}
else
{
b.UsedBudget += amount;
b.UpdatedAt = DateTime.UtcNow;
}
await db.SaveChangesAsync();
}
public async Task ReleaseBudgetAsync(string module, decimal amount)
{
await _db.ExecuteAsync(
@"UPDATE `core_budget`
SET used_budget = GREATEST(0, used_budget - @amount)
WHERE module = @module",
new { module, amount });
await using var db = await _dbf.CreateDbContextAsync();
var b = await db.Budgets.FindAsync(module);
if (b is null) return;
b.UsedBudget = Math.Max(0m, b.UsedBudget - amount);
b.UpdatedAt = DateTime.UtcNow;
await db.SaveChangesAsync();
}
}