diff --git a/src/IBKRTrader.Core/Capital/CapitalService.cs b/src/IBKRTrader.Core/Capital/CapitalService.cs index 135c764..ba2e5ac 100644 --- a/src/IBKRTrader.Core/Capital/CapitalService.cs +++ b/src/IBKRTrader.Core/Capital/CapitalService.cs @@ -214,8 +214,9 @@ public sealed class CapitalService : ICapitalService /// public Task SettleReservationAsync( - long reservationId, int filledQuantity, decimal actualAmount, CancellationToken ct = default) => - _reservations.SettleAsync(reservationId, filledQuantity, actualAmount, ct); + long reservationId, int filledQuantity, decimal actualAmount, string? brokerOrderId = null, + CancellationToken ct = default) => + _reservations.SettleAsync(reservationId, filledQuantity, actualAmount, brokerOrderId, ct); /// public Task CancelReservationAsync(long reservationId, string reason, CancellationToken ct = default) => diff --git a/src/IBKRTrader.Core/Capital/ICapitalService.cs b/src/IBKRTrader.Core/Capital/ICapitalService.cs index 32d9604..fa043b6 100644 --- a/src/IBKRTrader.Core/Capital/ICapitalService.cs +++ b/src/IBKRTrader.Core/Capital/ICapitalService.cs @@ -34,7 +34,8 @@ public interface ICapitalService /// (mit Puffer) und tatsächlichem Bedarf fließt zurück in den Pool. /// Task SettleReservationAsync( - long reservationId, int filledQuantity, decimal actualAmount, CancellationToken ct = default); + long reservationId, int filledQuantity, decimal actualAmount, string? brokerOrderId = null, + CancellationToken ct = default); /// Gibt eine Bindung frei, ohne dass ausgeführt wurde. Task CancelReservationAsync(long reservationId, string reason, CancellationToken ct = default); diff --git a/src/IBKRTrader.Core/Capital/ReservationService.cs b/src/IBKRTrader.Core/Capital/ReservationService.cs index 7dbc36e..b3e8840 100644 --- a/src/IBKRTrader.Core/Capital/ReservationService.cs +++ b/src/IBKRTrader.Core/Capital/ReservationService.cs @@ -189,7 +189,8 @@ public sealed class ReservationService /// Tatsächlich ausgeführte Menge; 0 = nichts ausgeführt. /// Tatsächlich benötigtes Kapital (bei Teilfüllung anteilig). public async Task SettleAsync( - long id, int filledQuantity, decimal actualAmount, CancellationToken ct = default) + long id, int filledQuantity, decimal actualAmount, string? brokerOrderId = null, + CancellationToken ct = default) { await using var db = await _dbf.CreateDbContextAsync(ct); var row = await db.Reservations.FindAsync([id], ct); @@ -197,6 +198,12 @@ public sealed class ReservationService row.UpdatedAt = DateTime.UtcNow; + // Die Order-ID spaetestens hier festhalten. Im synchronen Pfad gibt es keinen beobachtbaren + // Zeitpunkt zwischen Absenden und Endstatus, an dem MarkSubmittedAsync greifen koennte - + // ohne diese Zeile bliebe BrokerOrderId leer, und der Abgleich beim Neustart haette nichts, + // woran er eine Bindung einer Order zuordnen koennte. + if (!string.IsNullOrWhiteSpace(brokerOrderId)) row.BrokerOrderId = brokerOrderId; + if (filledQuantity <= 0) { row.State = ReservationState.Cancelled; diff --git a/src/IBKRTrader.Core/Persistence/Ef/CoreDbContext.cs b/src/IBKRTrader.Core/Persistence/Ef/CoreDbContext.cs index bbce4c3..8ed19df 100644 --- a/src/IBKRTrader.Core/Persistence/Ef/CoreDbContext.cs +++ b/src/IBKRTrader.Core/Persistence/Ef/CoreDbContext.cs @@ -56,7 +56,12 @@ public class CoreDbContext : DbContext e.ToTable("core_trade_history"); e.HasKey(x => x.Id); e.Property(x => x.Module).HasMaxLength(50); - e.Property(x => x.Symbol).HasMaxLength(20); + // 64 wie core_position.ContractKey: hier steht seit der Optionserweiterung der + // Instrumentenschluessel und nicht mehr das nackte Ticker-Symbol. Bei 20 Zeichen wuerde + // "MSFT|20260918|P|302.5" auf "MSFT|20260918|P|302." gekuerzt - und das laesst sich als + // Strike 302 lesen. Ein still verfaelschter Eintrag in der Handelshistorie ist + // schlimmer als gar keiner. + e.Property(x => x.Symbol).HasMaxLength(64); e.Property(x => x.Action).HasMaxLength(10); e.Property(x => x.SignalId).HasMaxLength(64); e.Property(x => x.IbkrOrderId).HasMaxLength(100); diff --git a/src/IBKRTrader.Core/Persistence/Ef/Migrations/20260904134622_WidenTradeHistorySymbol.Designer.cs b/src/IBKRTrader.Core/Persistence/Ef/Migrations/20260904134622_WidenTradeHistorySymbol.Designer.cs new file mode 100644 index 0000000..b28f1b2 --- /dev/null +++ b/src/IBKRTrader.Core/Persistence/Ef/Migrations/20260904134622_WidenTradeHistorySymbol.Designer.cs @@ -0,0 +1,598 @@ +// +using System; +using IBKRTrader.Core.Persistence.Ef; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace IBKRTrader.Core.Persistence.Ef.Migrations +{ + [DbContext(typeof(CoreDbContext))] + [Migration("20260904134622_WidenTradeHistorySymbol")] + partial class WidenTradeHistorySymbol + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.13") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("IBKRTrader.Core.IBKR.IBKRExternalIdentifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("InstrumentId") + .HasColumnType("bigint"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Ticker") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("Source", "Ticker"); + + b.HasIndex("InstrumentId", "Source", "Ticker") + .IsUnique(); + + b.ToTable("core_ibkr_external_identifiers", (string)null); + }); + + modelBuilder.Entity("IBKRTrader.Core.IBKR.IBKRInstrument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("CompanyName") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(5) + .HasColumnType("varchar(5)"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Exchange") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("IbkrConid") + .HasColumnType("bigint"); + + b.Property("Industry") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Isin") + .HasMaxLength(12) + .HasColumnType("varchar(12)"); + + b.Property("LastFetched") + .HasColumnType("datetime(6)"); + + b.Property("PrimaryExchange") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("SecType") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Sector") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Symbol") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Active"); + + b.HasIndex("IbkrConid") + .IsUnique(); + + b.HasIndex("Symbol"); + + b.ToTable("core_ibkr_instruments", (string)null); + }); + + modelBuilder.Entity("IBKRTrader.Core.IBKR.IBKRMarketBar", b => + { + b.Property("InstrumentId") + .HasColumnType("bigint"); + + b.Property("BarSize") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Timestamp") + .HasColumnType("datetime(6)"); + + b.Property("BarCount") + .HasColumnType("int"); + + b.Property("Close") + .HasPrecision(12, 6) + .HasColumnType("decimal(12,6)"); + + b.Property("High") + .HasPrecision(12, 6) + .HasColumnType("decimal(12,6)"); + + b.Property("Low") + .HasPrecision(12, 6) + .HasColumnType("decimal(12,6)"); + + b.Property("Open") + .HasPrecision(12, 6) + .HasColumnType("decimal(12,6)"); + + b.Property("Volume") + .HasColumnType("bigint"); + + b.Property("Wap") + .HasPrecision(12, 6) + .HasColumnType("decimal(12,6)"); + + b.HasKey("InstrumentId", "BarSize", "Timestamp"); + + b.HasIndex("Timestamp"); + + b.ToTable("core_ibkr_market_data", (string)null); + }); + + modelBuilder.Entity("IBKRTrader.Core.Persistence.Entities.CoreBudget", b => + { + b.Property("Module") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("MaxPerTrade") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("TotalBudget") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UsedBudget") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.HasKey("Module"); + + b.ToTable("core_budget", (string)null); + }); + + modelBuilder.Entity("IBKRTrader.Core.Persistence.Entities.CoreDecisionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ContextJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Decision") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("Module") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("Side") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("SignalId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("SignalPrice") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("Symbol") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("Timestamp") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("SignalId"); + + b.HasIndex("Module", "Timestamp"); + + b.ToTable("core_decision_journal", (string)null); + }); + + modelBuilder.Entity("IBKRTrader.Core.Persistence.Entities.CoreOrderEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("DetailsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("Module") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrderType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("Price") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("Response") + .IsRequired() + .HasColumnType("text"); + + b.Property("Side") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("SignalId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("Symbol") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("Timestamp") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("SignalId"); + + b.HasIndex("Symbol", "Timestamp"); + + b.ToTable("core_order_events", (string)null); + }); + + modelBuilder.Entity("IBKRTrader.Core.Persistence.Entities.CorePosition", b => + { + b.Property("Module") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ContractKey") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("AvgPrice") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(5) + .HasColumnType("varchar(5)"); + + b.Property("Expiry") + .HasColumnType("date"); + + b.Property("Kind") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Multiplier") + .HasColumnType("int"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("Right") + .HasMaxLength(1) + .HasColumnType("varchar(1)"); + + b.Property("Strike") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("Symbol") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Module", "ContractKey"); + + b.HasIndex("Symbol"); + + b.ToTable("core_position", (string)null); + }); + + modelBuilder.Entity("IBKRTrader.Core.Persistence.Entities.CoreReservation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("Book") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BrokerOrderId") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ContractKey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(5) + .HasColumnType("varchar(5)"); + + b.Property("ExpiresAt") + .HasColumnType("datetime(6)"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("Shares") + .HasColumnType("int"); + + b.Property("Side") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("SignalId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("State") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("Symbol") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("BrokerOrderId"); + + b.HasIndex("ExpiresAt"); + + b.HasIndex("SignalId", "ContractKey") + .IsUnique(); + + b.HasIndex("State", "Book"); + + b.ToTable("core_reservation", (string)null); + }); + + modelBuilder.Entity("IBKRTrader.Core.Persistence.Entities.CoreSetting", b => + { + b.Property("Key") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Key"); + + b.ToTable("core_settings", (string)null); + }); + + modelBuilder.Entity("IBKRTrader.Core.Persistence.Entities.CoreTrade", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Action") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IbkrOrderId") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Module") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Notes") + .HasColumnType("longtext"); + + b.Property("Price") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("Quantity") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("SignalId") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Symbol") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("TotalValue") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("TradedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Module"); + + b.HasIndex("SignalId"); + + b.HasIndex("Symbol"); + + b.ToTable("core_trade_history", (string)null); + }); + + modelBuilder.Entity("IBKRTrader.Core.Persistence.Entities.CoreWorkerLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("FinishedAt") + .HasColumnType("datetime(6)"); + + b.Property("Message") + .HasColumnType("longtext"); + + b.Property("Module") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("StartedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("WorkerName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("WorkerName", "StartedAt"); + + b.ToTable("core_worker_log", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/IBKRTrader.Core/Persistence/Ef/Migrations/20260904134622_WidenTradeHistorySymbol.cs b/src/IBKRTrader.Core/Persistence/Ef/Migrations/20260904134622_WidenTradeHistorySymbol.cs new file mode 100644 index 0000000..41dbfa0 --- /dev/null +++ b/src/IBKRTrader.Core/Persistence/Ef/Migrations/20260904134622_WidenTradeHistorySymbol.cs @@ -0,0 +1,42 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace IBKRTrader.Core.Persistence.Ef.Migrations +{ + /// + public partial class WidenTradeHistorySymbol : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Symbol", + table: "core_trade_history", + type: "varchar(64)", + maxLength: 64, + nullable: false, + oldClrType: typeof(string), + oldType: "varchar(20)", + oldMaxLength: 20) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Symbol", + table: "core_trade_history", + type: "varchar(20)", + maxLength: 20, + nullable: false, + oldClrType: typeof(string), + oldType: "varchar(64)", + oldMaxLength: 64) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + } + } +} diff --git a/src/IBKRTrader.Core/Persistence/Ef/Migrations/CoreDbContextModelSnapshot.cs b/src/IBKRTrader.Core/Persistence/Ef/Migrations/CoreDbContextModelSnapshot.cs index f260434..25a4103 100644 --- a/src/IBKRTrader.Core/Persistence/Ef/Migrations/CoreDbContextModelSnapshot.cs +++ b/src/IBKRTrader.Core/Persistence/Ef/Migrations/CoreDbContextModelSnapshot.cs @@ -530,8 +530,8 @@ namespace IBKRTrader.Core.Persistence.Ef.Migrations b.Property("Symbol") .IsRequired() - .HasMaxLength(20) - .HasColumnType("varchar(20)"); + .HasMaxLength(64) + .HasColumnType("varchar(64)"); b.Property("TotalValue") .HasPrecision(18, 4) diff --git a/src/IBKRTrader.Core/Trading/ExecutionService.cs b/src/IBKRTrader.Core/Trading/ExecutionService.cs index 8aa498a..c64dda5 100644 --- a/src/IBKRTrader.Core/Trading/ExecutionService.cs +++ b/src/IBKRTrader.Core/Trading/ExecutionService.cs @@ -155,7 +155,8 @@ public sealed class ExecutionService : IExecutionService // 9. Bindung abschließen. Reserviert wurde mit Puffer, ausgeführt zum echten Preis – die // Differenz fließt zurück in den Pool. var actualAmount = result.FilledQuantity * result.AvgFillPrice * signal.Instrument.Multiplier; - await _capital.SettleReservationAsync(reservationId, result.FilledQuantity, actualAmount, ct); + await _capital.SettleReservationAsync( + reservationId, result.FilledQuantity, actualAmount, result.OrderId, ct); Journal(signal, TradeDecision.Executed, DecisionReason.OrderPlaced, $"Ausgeführt: {sideStr} {result.FilledQuantity}x {signal.Instrument.Key} @ {result.AvgFillPrice:F2}", diff --git a/tests/IBKRTrader.Tests/Trading/ExecutionServiceTests.cs b/tests/IBKRTrader.Tests/Trading/ExecutionServiceTests.cs index c0db0ea..1ad68b6 100644 --- a/tests/IBKRTrader.Tests/Trading/ExecutionServiceTests.cs +++ b/tests/IBKRTrader.Tests/Trading/ExecutionServiceTests.cs @@ -243,8 +243,9 @@ public class ExecutionServiceTests await CreateSut().ExecuteAsync(BuySignal); // Reserviert wurde mit Puffer, ausgefuehrt zum echten Preis: 5 x 100 = 500 fliessen als - // tatsaechlicher Bedarf zurueck, der Rest wird freigegeben. - await _capital.Received(1).SettleReservationAsync(1, 5, 500m, Arg.Any()); + // tatsaechlicher Bedarf zurueck, der Rest wird freigegeben. Die Order-ID gehoert dazu - + // ohne sie kann der Abgleich beim Neustart keine Bindung einer Order zuordnen. + await _capital.Received(1).SettleReservationAsync(1, 5, 500m, "O1", Arg.Any()); } [Fact]