Compare commits
6
Commits
084cd7ef77
...
b221e0447b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b221e0447b | ||
|
|
614392afc3 | ||
|
|
b517f7a637 | ||
|
|
9e0d3b229a | ||
|
|
2a73172e27 | ||
|
|
bd04642735 |
@@ -403,6 +403,52 @@ DebugTrading-Modul (#468) platzieren echte Orders. Ihre Positionen dürfen nicht
|
||||
eines Strategiemoduls landen – sie verfälschten dessen Auslastung, dessen GuV und dessen
|
||||
Konzentration, und zwar dauerhaft, weil die Position echt ist.
|
||||
|
||||
### [O/K] Währung gehört zur Identität eines Instruments
|
||||
|
||||
Am 2026-09-04 stellte sich beim Order-Nachweis heraus, dass das Paper-Konto sein Bargeld in **EUR**
|
||||
hält und in USD mit −3.751 im Minus steht. Ein USD-Kauf wäre damit ein USD-Kredit gewesen, und
|
||||
Schicht C hat ihn zu Recht abgelehnt. Die Entscheidung fiel gegen ein Aussetzen der Sicherung und
|
||||
für das, was das Problem an der Wurzel löst: **Instrumente sind währungsfähig.**
|
||||
|
||||
Die Währung steht dabei im Instrumentenschlüssel und nicht bloß als Attribut daneben. Derselbe
|
||||
Ticker notiert an mehreren Plätzen — SAP gibt es in Frankfurt in EUR und als ADR in New York in
|
||||
USD, zu deutlich verschiedenen Kursen. Zwei Notierungen als eine Position zu führen, würde Bestand
|
||||
und Deckung vermischen.
|
||||
|
||||
**USD bleibt implizit** (`AAPL`, nicht `AAPL:USD`). Andernfalls wäre jede Bestandszeile ab sofort
|
||||
ein anderes Instrument als vorher und aus einer Position würden zwei. Trennzeichen ist der
|
||||
Doppelpunkt, nicht der Punkt — Ticker enthalten Punkte (BRK.B).
|
||||
|
||||
### [X] #479 — Die Order lief auf die falsche Notierung
|
||||
|
||||
Der teuerste Fund des Tages, und einer, den kein Unit-Test hätte finden können. Geprüft, bepreist
|
||||
und reserviert wurde `SAP:EUR` zu 188,74 EUR. Ausgeführt wurde die SAP-ADR in New York zu 216,68
|
||||
USD; das USD-Guthaben rutschte um 434 ins Minus.
|
||||
|
||||
Damit wurde **genau der Margin-Kredit aufgenommen, den Schicht C verhindern soll** — an der
|
||||
Sicherung vorbei, weil die Sicherung ein anderes Instrument geprüft hatte als das, was gehandelt
|
||||
wurde. Ohne Fehlermeldung. Die Bücher wiesen anschließend eine EUR-Position zum USD-Preis aus, die
|
||||
es beim Broker nie gab.
|
||||
|
||||
Die Ursache war eine nicht gesetzte Eigenschaft: `ExecutionService` baute die `OrderRequest` aus
|
||||
Einzelfeldern, und `Currency` fiel still auf USD zurück. Die Lehre ist allgemeiner als der Fehler:
|
||||
**eine neue Dimension an der Identität eines Objekts macht jede Stelle, die dieses Objekt aus
|
||||
Teilen zusammensetzt, zu einer Fehlerquelle.** Deshalb wurde nicht die Zuweisung nachgetragen,
|
||||
sondern `OrderRequest.For(instrument, …)` eingeführt — im Orderpfad lässt sich ein Auftrag nicht
|
||||
mehr aus losen Feldern bauen.
|
||||
|
||||
### [K] #480 — Das Kapitalmodell rechnet Währungen zusammen
|
||||
|
||||
Solange jedes Instrument USD war, war die Frage gegenstandslos. Mit der ersten EUR-Position ist sie
|
||||
es nicht mehr: Pool, Konzentrationslimit und die Summe der offenen Reservierungen addieren Beträge
|
||||
verschiedener Währungen unverändert.
|
||||
|
||||
Bei ähnlichen Kursen fällt das nicht auf — **genau deshalb ist es gefährlich.** Und die Richtung
|
||||
des Fehlers ist nicht einheitlich: bei einer starken Fremdwährung wird die Auslastung
|
||||
*unterschätzt*, und ein Buch darf mehr aufmachen als es soll. Das Kapitalmodell sieht die Lösung in
|
||||
§7 bereits vor (Buch-Cash und Reservierungen je Währung); umgesetzt ist sie bisher nur für die
|
||||
Reservierungen und für Schicht C. Für die Pool-Rechnung fehlen Umrechnungskurse — die liefert #321.
|
||||
|
||||
### [H] #293 — Wie der Nachweis geführt wird
|
||||
|
||||
`--ibkr-check` liest nur. Für #293 gibt es deshalb `--verify-order SYMBOL` am Daemon: es geht durch
|
||||
@@ -415,6 +461,13 @@ Paper-Modus **und** außerhalb eines Paper-Ports (eines allein sagt nichts – e
|
||||
einem Live-Port bewegt echtes Geld); und den globalen Handelsschalter übergeht er nicht, sonst
|
||||
wäre der keiner.
|
||||
|
||||
**Geführt am 2026-09-04** gegen das Paper-Konto DUR371528: Kauf von 2 × `SAP:EUR` zu 185,86 EUR,
|
||||
alle fünf Tabellen stimmen. Der Nachweis hat dabei **vier Fehler gefunden**, drei davon für
|
||||
Unit-Tests unerreichbar — die falsche Notierung (#479), eine Bargeldaufschlüsselung, die gar nicht
|
||||
ankam, eine Kursabfrage ohne Währung, und zwei Spalten der Buchführung, die zu eng waren bzw. leer
|
||||
blieben. Das ist der eigentliche Ertrag: der Lauf gegen echte Gegenstellen prüft die Fugen zwischen
|
||||
den Bausteinen, und dort saßen alle vier.
|
||||
|
||||
### [W] #309/#310 — Was steht, und was der Handelsbetrieb noch braucht
|
||||
|
||||
Gerüst und reine Strategie-Logik stehen: `ow_`-Schema mit fünf Tabellen, Zustandsautomat,
|
||||
|
||||
@@ -151,10 +151,16 @@ public sealed class CapitalService : ICapitalService
|
||||
public async Task<ReservationService.ReserveResult> ReserveAsync(
|
||||
BookId book, TradeSignal signal, InstrumentRef instrument, TradeSide side,
|
||||
int quantity, int closingQuantity, decimal price, OrderType orderType,
|
||||
AccountState account, string currency = "USD", CancellationToken ct = default)
|
||||
AccountState account, string? currency = null, CancellationToken ct = default)
|
||||
{
|
||||
var opening = Math.Max(0, quantity - closingQuantity);
|
||||
|
||||
// Die Waehrung kommt aus dem Instrument, nicht vom Aufrufer: sie ist Teil der Identitaet
|
||||
// des Kontrakts, und die Deckungspruefung muss gegen das Bargeld GENAU dieser Waehrung
|
||||
// laufen. Ein Aufrufer, der hier etwas anderes uebergeben koennte, koennte die Pruefung
|
||||
// gegen den falschen Topf laufen lassen.
|
||||
currency = instrument.Currency;
|
||||
|
||||
return await _reservations.ReserveAsync(
|
||||
book, instrument, side, quantity, currency, signal.SignalId,
|
||||
load =>
|
||||
@@ -208,8 +214,9 @@ public sealed class CapitalService : ICapitalService
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task CancelReservationAsync(long reservationId, string reason, CancellationToken ct = default) =>
|
||||
|
||||
@@ -27,14 +27,15 @@ public interface ICapitalService
|
||||
Task<ReservationService.ReserveResult> ReserveAsync(
|
||||
BookId book, TradeSignal signal, InstrumentRef instrument, TradeSide side,
|
||||
int quantity, int closingQuantity, decimal price, OrderType orderType,
|
||||
AccountState account, string currency = "USD", CancellationToken ct = default);
|
||||
AccountState account, string? currency = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Schließt eine Bindung nach dem Ergebnis der Order ab. Die Differenz zwischen Reservierung
|
||||
/// (mit Puffer) und tatsächlichem Bedarf fließt zurück in den Pool.
|
||||
/// </summary>
|
||||
Task SettleReservationAsync(
|
||||
long reservationId, int filledQuantity, decimal actualAmount, CancellationToken ct = default);
|
||||
long reservationId, int filledQuantity, decimal actualAmount, string? brokerOrderId = null,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>Gibt eine Bindung frei, ohne dass ausgeführt wurde.</summary>
|
||||
Task CancelReservationAsync(long reservationId, string reason, CancellationToken ct = default);
|
||||
|
||||
@@ -189,7 +189,8 @@ public sealed class ReservationService
|
||||
/// <param name="filledQuantity">Tatsächlich ausgeführte Menge; 0 = nichts ausgeführt.</param>
|
||||
/// <param name="actualAmount">Tatsächlich benötigtes Kapital (bei Teilfüllung anteilig).</param>
|
||||
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;
|
||||
|
||||
@@ -42,6 +42,7 @@ public class CoreDbContext : DbContext
|
||||
e.Property(x => x.ContractKey).HasMaxLength(64);
|
||||
e.Property(x => x.Symbol).HasMaxLength(20);
|
||||
e.Property(x => x.Kind).HasMaxLength(10);
|
||||
e.Property(x => x.Currency).HasMaxLength(5);
|
||||
e.Property(x => x.Right).HasMaxLength(1);
|
||||
e.Property(x => x.Strike).HasPrecision(18, 4);
|
||||
e.Property(x => x.AvgPrice).HasPrecision(18, 4);
|
||||
@@ -55,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);
|
||||
|
||||
Generated
+598
@@ -0,0 +1,598 @@
|
||||
// <auto-generated />
|
||||
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("20260904133807_AddPositionCurrency")]
|
||||
partial class AddPositionCurrency
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<long>("InstrumentId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Source")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<string>("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<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("CompanyName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Currency")
|
||||
.IsRequired()
|
||||
.HasMaxLength(5)
|
||||
.HasColumnType("varchar(5)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Exchange")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("varchar(20)");
|
||||
|
||||
b.Property<long>("IbkrConid")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Industry")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("varchar(100)");
|
||||
|
||||
b.Property<string>("Isin")
|
||||
.HasMaxLength(12)
|
||||
.HasColumnType("varchar(12)");
|
||||
|
||||
b.Property<DateTime?>("LastFetched")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("PrimaryExchange")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("varchar(20)");
|
||||
|
||||
b.Property<string>("SecType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("varchar(10)");
|
||||
|
||||
b.Property<string>("Sector")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("varchar(100)");
|
||||
|
||||
b.Property<string>("Symbol")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("varchar(20)");
|
||||
|
||||
b.Property<DateTime>("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<long>("InstrumentId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("BarSize")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("varchar(10)");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<int?>("BarCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("Close")
|
||||
.HasPrecision(12, 6)
|
||||
.HasColumnType("decimal(12,6)");
|
||||
|
||||
b.Property<decimal>("High")
|
||||
.HasPrecision(12, 6)
|
||||
.HasColumnType("decimal(12,6)");
|
||||
|
||||
b.Property<decimal>("Low")
|
||||
.HasPrecision(12, 6)
|
||||
.HasColumnType("decimal(12,6)");
|
||||
|
||||
b.Property<decimal>("Open")
|
||||
.HasPrecision(12, 6)
|
||||
.HasColumnType("decimal(12,6)");
|
||||
|
||||
b.Property<long>("Volume")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<decimal?>("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<string>("Module")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<decimal>("MaxPerTrade")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<decimal>("TotalBudget")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<decimal>("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<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<string>("ContextJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Decision")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("varchar(20)");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Module")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("varchar(40)");
|
||||
|
||||
b.Property<string>("Side")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("varchar(10)");
|
||||
|
||||
b.Property<string>("SignalId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("varchar(64)");
|
||||
|
||||
b.Property<decimal>("SignalPrice")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<string>("Symbol")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("varchar(20)");
|
||||
|
||||
b.Property<DateTime>("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<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<string>("DetailsJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("EventType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("varchar(20)");
|
||||
|
||||
b.Property<string>("Module")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<string>("OrderType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("varchar(20)");
|
||||
|
||||
b.Property<decimal>("Price")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<int>("Quantity")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Response")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Side")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("varchar(10)");
|
||||
|
||||
b.Property<string>("SignalId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("varchar(64)");
|
||||
|
||||
b.Property<string>("Symbol")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("varchar(20)");
|
||||
|
||||
b.Property<DateTime>("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<string>("Module")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<string>("ContractKey")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("varchar(64)");
|
||||
|
||||
b.Property<decimal>("AvgPrice")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<string>("Currency")
|
||||
.IsRequired()
|
||||
.HasMaxLength(5)
|
||||
.HasColumnType("varchar(5)");
|
||||
|
||||
b.Property<DateOnly?>("Expiry")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<string>("Kind")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("varchar(10)");
|
||||
|
||||
b.Property<int>("Multiplier")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Quantity")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Right")
|
||||
.HasMaxLength(1)
|
||||
.HasColumnType("varchar(1)");
|
||||
|
||||
b.Property<decimal?>("Strike")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<string>("Symbol")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("varchar(20)");
|
||||
|
||||
b.Property<DateTime>("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<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<decimal>("Amount")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<string>("Book")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<string>("BrokerOrderId")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("varchar(100)");
|
||||
|
||||
b.Property<string>("ContractKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("varchar(64)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Currency")
|
||||
.IsRequired()
|
||||
.HasMaxLength(5)
|
||||
.HasColumnType("varchar(5)");
|
||||
|
||||
b.Property<DateTime>("ExpiresAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Note")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("Quantity")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Shares")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Side")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("varchar(10)");
|
||||
|
||||
b.Property<string>("SignalId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("varchar(64)");
|
||||
|
||||
b.Property<string>("State")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("varchar(20)");
|
||||
|
||||
b.Property<string>("Symbol")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("varchar(20)");
|
||||
|
||||
b.Property<DateTime>("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<string>("Key")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("varchar(100)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("core_settings", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IBKRTrader.Core.Persistence.Entities.CoreTrade", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<string>("Action")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("varchar(10)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("IbkrOrderId")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("varchar(100)");
|
||||
|
||||
b.Property<string>("Module")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<decimal>("Price")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<decimal>("Quantity")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<string>("SignalId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("varchar(64)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<string>("Symbol")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("varchar(20)");
|
||||
|
||||
b.Property<decimal>("TotalValue")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<DateTime>("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<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("Module")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("varchar(20)");
|
||||
|
||||
b.Property<string>("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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace IBKRTrader.Core.Persistence.Ef.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddPositionCurrency : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Currency",
|
||||
table: "core_position",
|
||||
type: "varchar(5)",
|
||||
maxLength: 5,
|
||||
nullable: false,
|
||||
// Bestandszeilen stammen aus der Zeit, in der jedes Instrument USD war - genau das
|
||||
// besagt auch ihr ContractKey, in dem USD implizit bleibt. Ein leerer Vorgabewert
|
||||
// wuerde sie zu Positionen ohne Waehrung machen und die Deckungspruefung ins Leere
|
||||
// laufen lassen.
|
||||
defaultValue: "USD")
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Currency",
|
||||
table: "core_position");
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+598
@@ -0,0 +1,598 @@
|
||||
// <auto-generated />
|
||||
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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<long>("InstrumentId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Source")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<string>("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<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("CompanyName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Currency")
|
||||
.IsRequired()
|
||||
.HasMaxLength(5)
|
||||
.HasColumnType("varchar(5)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Exchange")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("varchar(20)");
|
||||
|
||||
b.Property<long>("IbkrConid")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Industry")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("varchar(100)");
|
||||
|
||||
b.Property<string>("Isin")
|
||||
.HasMaxLength(12)
|
||||
.HasColumnType("varchar(12)");
|
||||
|
||||
b.Property<DateTime?>("LastFetched")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("PrimaryExchange")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("varchar(20)");
|
||||
|
||||
b.Property<string>("SecType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("varchar(10)");
|
||||
|
||||
b.Property<string>("Sector")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("varchar(100)");
|
||||
|
||||
b.Property<string>("Symbol")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("varchar(20)");
|
||||
|
||||
b.Property<DateTime>("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<long>("InstrumentId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("BarSize")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("varchar(10)");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<int?>("BarCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("Close")
|
||||
.HasPrecision(12, 6)
|
||||
.HasColumnType("decimal(12,6)");
|
||||
|
||||
b.Property<decimal>("High")
|
||||
.HasPrecision(12, 6)
|
||||
.HasColumnType("decimal(12,6)");
|
||||
|
||||
b.Property<decimal>("Low")
|
||||
.HasPrecision(12, 6)
|
||||
.HasColumnType("decimal(12,6)");
|
||||
|
||||
b.Property<decimal>("Open")
|
||||
.HasPrecision(12, 6)
|
||||
.HasColumnType("decimal(12,6)");
|
||||
|
||||
b.Property<long>("Volume")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<decimal?>("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<string>("Module")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<decimal>("MaxPerTrade")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<decimal>("TotalBudget")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<decimal>("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<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<string>("ContextJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Decision")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("varchar(20)");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Module")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("varchar(40)");
|
||||
|
||||
b.Property<string>("Side")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("varchar(10)");
|
||||
|
||||
b.Property<string>("SignalId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("varchar(64)");
|
||||
|
||||
b.Property<decimal>("SignalPrice")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<string>("Symbol")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("varchar(20)");
|
||||
|
||||
b.Property<DateTime>("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<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<string>("DetailsJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("EventType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("varchar(20)");
|
||||
|
||||
b.Property<string>("Module")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<string>("OrderType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("varchar(20)");
|
||||
|
||||
b.Property<decimal>("Price")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<int>("Quantity")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Response")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Side")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("varchar(10)");
|
||||
|
||||
b.Property<string>("SignalId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("varchar(64)");
|
||||
|
||||
b.Property<string>("Symbol")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("varchar(20)");
|
||||
|
||||
b.Property<DateTime>("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<string>("Module")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<string>("ContractKey")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("varchar(64)");
|
||||
|
||||
b.Property<decimal>("AvgPrice")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<string>("Currency")
|
||||
.IsRequired()
|
||||
.HasMaxLength(5)
|
||||
.HasColumnType("varchar(5)");
|
||||
|
||||
b.Property<DateOnly?>("Expiry")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<string>("Kind")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("varchar(10)");
|
||||
|
||||
b.Property<int>("Multiplier")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Quantity")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Right")
|
||||
.HasMaxLength(1)
|
||||
.HasColumnType("varchar(1)");
|
||||
|
||||
b.Property<decimal?>("Strike")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<string>("Symbol")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("varchar(20)");
|
||||
|
||||
b.Property<DateTime>("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<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<decimal>("Amount")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<string>("Book")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<string>("BrokerOrderId")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("varchar(100)");
|
||||
|
||||
b.Property<string>("ContractKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("varchar(64)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Currency")
|
||||
.IsRequired()
|
||||
.HasMaxLength(5)
|
||||
.HasColumnType("varchar(5)");
|
||||
|
||||
b.Property<DateTime>("ExpiresAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Note")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("Quantity")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Shares")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Side")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("varchar(10)");
|
||||
|
||||
b.Property<string>("SignalId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("varchar(64)");
|
||||
|
||||
b.Property<string>("State")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("varchar(20)");
|
||||
|
||||
b.Property<string>("Symbol")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("varchar(20)");
|
||||
|
||||
b.Property<DateTime>("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<string>("Key")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("varchar(100)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("core_settings", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IBKRTrader.Core.Persistence.Entities.CoreTrade", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<string>("Action")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("varchar(10)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("IbkrOrderId")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("varchar(100)");
|
||||
|
||||
b.Property<string>("Module")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<decimal>("Price")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<decimal>("Quantity")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<string>("SignalId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("varchar(64)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<string>("Symbol")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("varchar(64)");
|
||||
|
||||
b.Property<decimal>("TotalValue")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<DateTime>("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<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("Module")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("varchar(20)");
|
||||
|
||||
b.Property<string>("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
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace IBKRTrader.Core.Persistence.Ef.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class WidenTradeHistorySymbol : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
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");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -342,6 +342,11 @@ namespace IBKRTrader.Core.Persistence.Ef.Migrations
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<string>("Currency")
|
||||
.IsRequired()
|
||||
.HasMaxLength(5)
|
||||
.HasColumnType("varchar(5)");
|
||||
|
||||
b.Property<DateOnly?>("Expiry")
|
||||
.HasColumnType("date");
|
||||
|
||||
@@ -525,8 +530,8 @@ namespace IBKRTrader.Core.Persistence.Ef.Migrations
|
||||
|
||||
b.Property<string>("Symbol")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("varchar(20)");
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("varchar(64)");
|
||||
|
||||
b.Property<decimal>("TotalValue")
|
||||
.HasPrecision(18, 4)
|
||||
|
||||
@@ -33,6 +33,13 @@ public class CorePosition
|
||||
/// <summary>Kontraktgröße: 1 bei Aktien, bei US-Aktienoptionen 100.</summary>
|
||||
public int Multiplier { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Währung, in der die Position notiert. Steckt zwar auch im <see cref="ContractKey"/>, steht
|
||||
/// aber als eigene Spalte da: Bestand und Deckung werden je Währung ausgewertet, und das soll
|
||||
/// eine Abfrage können, ohne den Schlüssel zerlegen zu müssen.
|
||||
/// </summary>
|
||||
public string Currency { get; set; } = IBKRTrader.Core.Trading.Currencies.Default;
|
||||
|
||||
/// <summary>Vorzeichenbehaftet: positiv = long, negativ = short (verkaufte Option).</summary>
|
||||
public int Quantity { get; set; }
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
namespace IBKRTrader.Core.Trading;
|
||||
|
||||
/// <summary>
|
||||
/// Währungskennungen nach ISO 4217, wie sie TWS verwendet.
|
||||
///
|
||||
/// <para>Eine eigene kleine Einheit, weil die Währung an vier Stellen dieselbe Schreibweise haben
|
||||
/// muss: im Instrumentenschlüssel, im TWS-Kontrakt, in der Reservierung und im Bargeldbestand des
|
||||
/// Kontos. Weichen zwei davon ab – "eur" gegen "EUR" –, findet die Deckungsprüfung kein Geld und
|
||||
/// lehnt ab, ohne dass irgendwo ein Fehler auftaucht.</para>
|
||||
/// </summary>
|
||||
public static class Currencies
|
||||
{
|
||||
/// <summary>
|
||||
/// Vorgabewährung. Sie bleibt im Instrumentenschlüssel <b>implizit</b>, damit bestehende
|
||||
/// Schlüssel und Bestandszeilen unverändert gültig bleiben.
|
||||
/// </summary>
|
||||
public const string Default = "USD";
|
||||
|
||||
/// <summary>
|
||||
/// Bringt eine Angabe auf die kanonische Form. Leer ergibt <see cref="Default"/> – ein Aufrufer,
|
||||
/// der nichts angibt, meint den bisherigen Fall.
|
||||
/// </summary>
|
||||
public static string Normalize(string? currency)
|
||||
{
|
||||
var c = (currency ?? "").Trim().ToUpperInvariant();
|
||||
if (c.Length == 0) return Default;
|
||||
if (!IsValid(c))
|
||||
throw new ArgumentException(
|
||||
$"'{currency}' ist keine gültige Währungskennung (erwartet drei Buchstaben nach ISO 4217).",
|
||||
nameof(currency));
|
||||
return c;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drei Großbuchstaben. Bewusst keine Liste zulässiger Währungen: die müsste gepflegt werden,
|
||||
/// und eine fehlende Währung wäre ein Fehler an der falschen Stelle – TWS lehnt einen
|
||||
/// unbekannten Kontrakt ohnehin ab, und zwar mit einer Meldung, die den Grund nennt.
|
||||
/// </summary>
|
||||
public static bool IsValid(string? currency) =>
|
||||
currency is { Length: 3 } c && c.All(ch => ch is >= 'A' and <= 'Z');
|
||||
}
|
||||
@@ -60,7 +60,7 @@ public sealed class ExecutionService : IExecutionService
|
||||
}
|
||||
|
||||
// 2. Kurs
|
||||
var quote = await _broker.GetQuoteAsync(signal.Symbol, ct);
|
||||
var quote = await _broker.GetQuoteAsync(signal.Instrument, ct);
|
||||
if (quote is null || quote.Last <= 0)
|
||||
{
|
||||
Journal(signal, TradeDecision.Skipped, DecisionReason.NoQuote, $"Kein Kurs für {signal.Symbol} verfügbar.");
|
||||
@@ -129,15 +129,10 @@ public sealed class ExecutionService : IExecutionService
|
||||
var reservationId = reservation.Reservation!.Id;
|
||||
|
||||
// 7. Order platzieren
|
||||
var order = new OrderRequest
|
||||
{
|
||||
Symbol = signal.Symbol,
|
||||
Side = signal.Side,
|
||||
Quantity = decision.Quantity,
|
||||
Type = orderType,
|
||||
LimitPrice = signal.LimitPrice,
|
||||
Option = signal.Option
|
||||
};
|
||||
// Aus dem Instrument gebaut, nicht aus Einzelfeldern: sonst faellt die Waehrung still auf
|
||||
// USD zurueck und die Order trifft eine andere Notierung als die, die geprueft wurde.
|
||||
var order = OrderRequest.For(
|
||||
signal.Instrument, signal.Side, decision.Quantity, orderType, signal.LimitPrice);
|
||||
|
||||
var result = await _broker.PlaceOrderAsync(order, ct);
|
||||
if (!result.Success)
|
||||
@@ -160,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}",
|
||||
|
||||
@@ -6,8 +6,14 @@ namespace IBKRTrader.Core.Trading;
|
||||
/// </summary>
|
||||
public interface IBrokerClient
|
||||
{
|
||||
/// <summary>Aktuelle Kurs-Momentaufnahme oder null, wenn nicht verfügbar.</summary>
|
||||
Task<Quote?> GetQuoteAsync(string symbol, CancellationToken ct = default);
|
||||
/// <summary>
|
||||
/// Aktuelle Kurs-Momentaufnahme oder <c>null</c>, wenn nicht verfügbar.
|
||||
///
|
||||
/// <para>Nimmt das <b>Instrument</b> und nicht nur das Symbol: ohne Währung wird jeder Kontrakt
|
||||
/// als USD gesucht, und ein Wert, der nur in EUR notiert, ist dann schlicht "nicht auflösbar".
|
||||
/// Ohne Kontraktangaben gälte dasselbe für jede Option.</para>
|
||||
/// </summary>
|
||||
Task<Quote?> GetQuoteAsync(InstrumentRef instrument, CancellationToken ct = default);
|
||||
|
||||
/// <summary>Kontostand-Momentaufnahme.</summary>
|
||||
Task<AccountState> GetAccountStateAsync(CancellationToken ct = default);
|
||||
|
||||
@@ -53,14 +53,14 @@ public sealed class IbkrBrokerClient : IBrokerClient, IBrokerPortfolioReader, ID
|
||||
$"TWS-Broker aktiv: {ibkr.Host}:{ibkr.Port} (Client {ibkr.ClientId}), Modus {mode}.");
|
||||
}
|
||||
|
||||
public async Task<Quote?> GetQuoteAsync(string symbol, CancellationToken ct = default)
|
||||
public async Task<Quote?> GetQuoteAsync(InstrumentRef instrument, CancellationToken ct = default)
|
||||
{
|
||||
if (!await IsReadyAsync(ct).ConfigureAwait(false)) return null;
|
||||
|
||||
var contract = await _connection!.ResolveContractAsync(InstrumentRef.ForStock(symbol), _requestTimeout, ct).ConfigureAwait(false);
|
||||
var contract = await _connection!.ResolveContractAsync(instrument, _requestTimeout, ct).ConfigureAwait(false);
|
||||
if (contract is null) return null;
|
||||
|
||||
return await _connection.RequestQuoteAsync(contract, symbol, _requestTimeout, ct).ConfigureAwait(false);
|
||||
return await _connection.RequestQuoteAsync(contract, instrument.Symbol, _requestTimeout, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<AccountState> GetAccountStateAsync(CancellationToken ct = default)
|
||||
|
||||
@@ -286,7 +286,7 @@ internal sealed class IbkrConnection : DefaultEWrapper, IDisposable
|
||||
{
|
||||
var query = instrument.Option is { } option
|
||||
? IbkrMapping.Option(option)
|
||||
: IbkrMapping.Stock(instrument.Symbol);
|
||||
: IbkrMapping.Stock(instrument.Symbol, instrument.Currency);
|
||||
_socket.reqContractDetails(id, query);
|
||||
|
||||
if (!await WaitAsync(slot.Done.Task, timeout, ct).ConfigureAwait(false))
|
||||
@@ -346,11 +346,17 @@ internal sealed class IbkrConnection : DefaultEWrapper, IDisposable
|
||||
_accounts[id] = slot;
|
||||
try
|
||||
{
|
||||
// $LEDGER liefert zusaetzlich die Bargeldbestaende je Waehrung. Sie werden fuer die
|
||||
// $LEDGER:ALL liefert die Bargeldbestaende JE WAEHRUNG. Sie werden fuer die
|
||||
// Waehrungsdeckung gebraucht (Kapitalmodell §4 Schicht C): ein Kauf in X darf nur aus
|
||||
// Cash in X finanziert werden, sonst entsteht ein Sollsaldo in X - ein Margin-Kredit,
|
||||
// auch wenn das Konto in Summe positiv aussieht.
|
||||
_socket.reqAccountSummary(id, "All", "NetLiquidation,AvailableFunds,TotalCashValue,$LEDGER");
|
||||
//
|
||||
// ":ALL" ist noetig und nicht kosmetisch. Das nackte "$LEDGER" liefert nur die
|
||||
// Zusammenfassung in Kontobasiswaehrung, und die traegt als Waehrungskennung "BASE" -
|
||||
// also genau den Wert, den die Auswertung ueberspringen muss, damit dasselbe Geld nicht
|
||||
// zweimal zaehlt. Ergebnis war eine leere Aufschluesselung und damit eine Ablehnung
|
||||
// jedes Kaufs durch Schicht C (am 2026-09-04 gegen das Paper-Gateway beobachtet).
|
||||
_socket.reqAccountSummary(id, "All", "NetLiquidation,AvailableFunds,TotalCashValue,$LEDGER:ALL");
|
||||
|
||||
if (!await WaitAsync(slot.Done.Task, timeout, ct).ConfigureAwait(false))
|
||||
{
|
||||
@@ -363,6 +369,16 @@ internal sealed class IbkrConnection : DefaultEWrapper, IDisposable
|
||||
return null;
|
||||
}
|
||||
|
||||
// Was der Broker an Waehrungen gemeldet hat, gehoert ins Log: ohne diese Zeile ist eine
|
||||
// Ablehnung durch Schicht C ("verfuegbar 0") von aussen nicht von einem echten
|
||||
// Geldmangel zu unterscheiden.
|
||||
_logger.Info(LogModule, slot.Cash.Count == 0
|
||||
? "Konto: keine Bargeldaufschluesselung je Waehrung erhalten - Schicht C lehnt Kaeufe " +
|
||||
"deshalb ab. Antwortet das Gateway nicht auf $LEDGER:ALL?"
|
||||
: "Konto: Bargeld je Waehrung " + string.Join(", ",
|
||||
slot.Cash.Select(kv => string.Create(CultureInfo.InvariantCulture,
|
||||
$"{kv.Key} {kv.Value:N2}"))) + ".");
|
||||
|
||||
return new AccountState(
|
||||
ReadDecimal(slot.Values, "NetLiquidation"),
|
||||
ReadDecimal(slot.Values, "AvailableFunds"),
|
||||
|
||||
@@ -23,13 +23,16 @@ internal static class IbkrMapping
|
||||
|
||||
public static string? ValidatePort(int port, TradingMode mode) => BrokerPorts.ValidateMode(port, mode);
|
||||
|
||||
/// <summary>US-Aktie über die SMART-Route.</summary>
|
||||
public static Contract Stock(string symbol) => new()
|
||||
/// <summary>
|
||||
/// Aktie über die SMART-Route. Die Währung entscheidet mit, welche Notierung gemeint ist –
|
||||
/// derselbe Ticker kann an mehreren Plätzen in verschiedenen Währungen laufen.
|
||||
/// </summary>
|
||||
public static Contract Stock(string symbol, string currency = Currencies.Default) => new()
|
||||
{
|
||||
Symbol = symbol.Trim().ToUpperInvariant(),
|
||||
SecType = "STK",
|
||||
Exchange = "SMART",
|
||||
Currency = "USD"
|
||||
Currency = Currencies.Normalize(currency)
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
@@ -49,7 +52,7 @@ internal static class IbkrMapping
|
||||
Symbol = spec.Underlying.Trim().ToUpperInvariant(),
|
||||
SecType = "OPT",
|
||||
Exchange = "SMART",
|
||||
Currency = "USD",
|
||||
Currency = Currencies.Normalize(spec.Currency),
|
||||
LastTradeDateOrContractMonth = spec.ExpiryCode,
|
||||
Strike = (double)spec.Strike,
|
||||
Right = spec.RightCode,
|
||||
@@ -59,7 +62,7 @@ internal static class IbkrMapping
|
||||
|
||||
/// <summary>Baut den Kontrakt passend zum Instrument des Auftrags – Aktie oder Option.</summary>
|
||||
public static Contract ForRequest(OrderRequest request) =>
|
||||
request.Option is { } option ? Option(option) : Stock(request.Symbol);
|
||||
request.Option is { } option ? Option(option) : Stock(request.Symbol, request.Currency);
|
||||
|
||||
public static Order BuildOrder(OrderRequest request, int orderId, string? account)
|
||||
{
|
||||
|
||||
@@ -23,13 +23,18 @@ public enum OptionRight { Call, Put }
|
||||
/// Optional. Nötig, sobald ein Basiswert mehrere Klassen führt (nach Splits, bei Mini-Optionen) –
|
||||
/// dann ist das Symbol allein nicht mehr eindeutig und TWS liefert den falschen Kontrakt.
|
||||
/// </param>
|
||||
/// <param name="Currency">
|
||||
/// Währung des Kontrakts. Gehört zur Identität, nicht zur Beschreibung: der Strike ist eine Zahl
|
||||
/// in dieser Währung, und die Deckung wird gegen das Bargeld <i>dieser</i> Währung geprüft.
|
||||
/// </param>
|
||||
public sealed record OptionSpec(
|
||||
string Underlying,
|
||||
DateOnly Expiry,
|
||||
decimal Strike,
|
||||
OptionRight Right,
|
||||
int Multiplier = 100,
|
||||
string? TradingClass = null)
|
||||
string? TradingClass = null,
|
||||
string Currency = Currencies.Default)
|
||||
{
|
||||
/// <summary>Kontraktbezeichner "C"/"P", wie ihn die TWS-API erwartet.</summary>
|
||||
public string RightCode => Right == OptionRight.Call ? "C" : "P";
|
||||
@@ -67,12 +72,13 @@ public sealed record OptionSpec(
|
||||
/// </summary>
|
||||
public readonly record struct InstrumentRef
|
||||
{
|
||||
private InstrumentRef(string key, InstrumentKind kind, string symbol, OptionSpec? option)
|
||||
private InstrumentRef(string key, InstrumentKind kind, string symbol, string currency, OptionSpec? option)
|
||||
{
|
||||
Key = key;
|
||||
Kind = kind;
|
||||
Symbol = symbol;
|
||||
Option = option;
|
||||
Key = key;
|
||||
Kind = kind;
|
||||
Symbol = symbol;
|
||||
Currency = currency;
|
||||
Option = option;
|
||||
}
|
||||
|
||||
/// <summary>Der Schlüssel selbst – Primärschlüsselbestandteil und Fremdschlüssel.</summary>
|
||||
@@ -83,26 +89,56 @@ public readonly record struct InstrumentRef
|
||||
/// <summary>Ticker der Aktie bzw. des Basiswerts der Option. Nie leer.</summary>
|
||||
public string Symbol { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Währung, in der das Instrument notiert und abgerechnet wird.
|
||||
///
|
||||
/// <para>Sie gehört zur <b>Identität</b> und nicht bloß zur Beschreibung: derselbe Ticker kann
|
||||
/// an mehreren Plätzen in verschiedenen Währungen notieren, und ein Kauf darf nur aus Bargeld
|
||||
/// derselben Währung finanziert werden (Kapitalmodell §4 Schicht C). Zwei Notierungen als eine
|
||||
/// Position zu führen, würde Bestand und Deckung vermischen.</para>
|
||||
/// </summary>
|
||||
public string Currency { get; }
|
||||
|
||||
/// <summary>Kontraktangaben, wenn <see cref="Kind"/> = <see cref="InstrumentKind.Option"/>.</summary>
|
||||
public OptionSpec? Option { get; }
|
||||
|
||||
/// <summary>Kontraktgröße: 1 bei Aktien, sonst der Multiplikator der Option.</summary>
|
||||
public int Multiplier => Option?.Multiplier ?? 1;
|
||||
|
||||
public static InstrumentRef ForStock(string symbol)
|
||||
public static InstrumentRef ForStock(string symbol, string currency = Currencies.Default)
|
||||
{
|
||||
var s = Normalize(symbol);
|
||||
return new InstrumentRef(s, InstrumentKind.Stock, s, null);
|
||||
var c = Currencies.Normalize(currency);
|
||||
return new InstrumentRef(Qualify(s, c), InstrumentKind.Stock, s, c, null);
|
||||
}
|
||||
|
||||
public static InstrumentRef ForOption(OptionSpec spec)
|
||||
{
|
||||
var underlying = Normalize(spec.Underlying);
|
||||
var currency = Currencies.Normalize(spec.Currency);
|
||||
var key = string.Create(CultureInfo.InvariantCulture,
|
||||
$"{underlying}|{spec.ExpiryCode}|{spec.RightCode}|{FormatStrike(spec.Strike)}");
|
||||
return new InstrumentRef(key, InstrumentKind.Option, underlying, spec with { Underlying = underlying });
|
||||
$"{Qualify(underlying, currency)}|{spec.ExpiryCode}|{spec.RightCode}|{FormatStrike(spec.Strike)}");
|
||||
return new InstrumentRef(key, InstrumentKind.Option, underlying, currency,
|
||||
spec with { Underlying = underlying, Currency = currency });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Symbol mit Währung, aber nur wenn nötig: <c>USD</c> bleibt <b>implizit</b>.
|
||||
///
|
||||
/// <para>Das ist kein Schönheitsentscheid. Alle bestehenden Schlüssel und die bereits
|
||||
/// angewendete Migration schreiben für eine Aktie schlicht das Ticker-Symbol; würde die Währung
|
||||
/// immer angehängt, wäre jede Bestandszeile ab sofort ein anderes Instrument als vorher, und
|
||||
/// aus einer Position würden zwei.</para>
|
||||
/// </summary>
|
||||
private static string Qualify(string symbol, string currency) =>
|
||||
currency == Currencies.Default ? symbol : $"{symbol}{CurrencySeparator}{currency}";
|
||||
|
||||
/// <summary>
|
||||
/// Trennt Symbol und Währung. Bewusst <b>nicht</b> der Punkt: Ticker enthalten Punkte
|
||||
/// (BRK.B, RDS.A), ein Doppelpunkt kommt in ihnen nicht vor.
|
||||
/// </summary>
|
||||
private const char CurrencySeparator = ':';
|
||||
|
||||
/// <summary>
|
||||
/// Liest einen Schlüssel zurück. Liefert <c>false</c> bei allem, was nicht dem obigen Aufbau
|
||||
/// entspricht – ein unlesbarer Schlüssel wird nie geraten, sondern gemeldet.
|
||||
@@ -113,9 +149,11 @@ public readonly record struct InstrumentRef
|
||||
if (string.IsNullOrWhiteSpace(key)) return false;
|
||||
|
||||
var parts = key.Split('|');
|
||||
if (!TrySplitSymbol(parts[0], out var symbol, out var currency)) return false;
|
||||
|
||||
if (parts.Length == 1)
|
||||
{
|
||||
instrument = ForStock(parts[0]);
|
||||
instrument = ForStock(symbol, currency);
|
||||
return true;
|
||||
}
|
||||
if (parts.Length != 4) return false;
|
||||
@@ -135,12 +173,27 @@ public readonly record struct InstrumentRef
|
||||
if (!decimal.TryParse(parts[3], NumberStyles.Number, CultureInfo.InvariantCulture, out var strike))
|
||||
return false;
|
||||
|
||||
instrument = ForOption(new OptionSpec(parts[0], DateOnly.FromDateTime(expiry), strike, right.Value));
|
||||
instrument = ForOption(new OptionSpec(
|
||||
symbol, DateOnly.FromDateTime(expiry), strike, right.Value, Currency: currency));
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Zerlegt "MIGA:EUR" bzw. "AAPL" in Symbol und Währung.</summary>
|
||||
private static bool TrySplitSymbol(string raw, out string symbol, out string currency)
|
||||
{
|
||||
symbol = raw; currency = Currencies.Default;
|
||||
if (string.IsNullOrWhiteSpace(raw)) return false;
|
||||
|
||||
var i = raw.IndexOf(CurrencySeparator);
|
||||
if (i < 0) return true;
|
||||
|
||||
symbol = raw[..i];
|
||||
currency = raw[(i + 1)..];
|
||||
return symbol.Length > 0 && Currencies.IsValid(currency);
|
||||
}
|
||||
|
||||
/// <summary>Der Schlüssel des Basiswerts – bei einer Option die Aktie, auf die sie lautet.</summary>
|
||||
public InstrumentRef UnderlyingRef => Kind == InstrumentKind.Stock ? this : ForStock(Symbol);
|
||||
public InstrumentRef UnderlyingRef => Kind == InstrumentKind.Stock ? this : ForStock(Symbol, Currency);
|
||||
|
||||
public override string ToString() => Key;
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ public sealed class NullBrokerClient : IBrokerClient, IBrokerPortfolioReader
|
||||
public Task<IReadOnlyList<BrokerExecution>> GetExecutionsAsync(DateTime? since = null, CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<BrokerExecution>>(Array.Empty<BrokerExecution>());
|
||||
|
||||
public Task<Quote?> GetQuoteAsync(string symbol, CancellationToken ct = default)
|
||||
public Task<Quote?> GetQuoteAsync(InstrumentRef instrument, CancellationToken ct = default)
|
||||
=> Task.FromResult<Quote?>(null);
|
||||
|
||||
public Task<AccountState> GetAccountStateAsync(CancellationToken ct = default)
|
||||
|
||||
@@ -143,6 +143,7 @@ public sealed class PortfolioService : IPortfolioService
|
||||
Strike = instrument.Option?.Strike,
|
||||
Right = instrument.Option?.RightCode,
|
||||
Multiplier = instrument.Multiplier,
|
||||
Currency = instrument.Currency,
|
||||
Quantity = applied.Quantity,
|
||||
AvgPrice = applied.AvgPrice,
|
||||
UpdatedAt = DateTime.UtcNow
|
||||
|
||||
@@ -53,9 +53,15 @@ public sealed record TradeSignal
|
||||
/// </summary>
|
||||
public OptionSpec? Option { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Währung der Aktie. Wird bei einer Option ignoriert – dort steht sie im <see cref="Option"/>.
|
||||
/// Vorgabe USD, damit bestehende Module unverändert bleiben.
|
||||
/// </summary>
|
||||
public string Currency { get; init; } = Currencies.Default;
|
||||
|
||||
/// <summary>Das gemeinte Instrument – Aktie oder Kontrakt – als eindeutiger Schlüssel.</summary>
|
||||
public InstrumentRef Instrument =>
|
||||
Option is null ? InstrumentRef.ForStock(Symbol) : InstrumentRef.ForOption(Option);
|
||||
Option is null ? InstrumentRef.ForStock(Symbol, Currency) : InstrumentRef.ForOption(Option);
|
||||
}
|
||||
|
||||
/// <summary>Konkrete Order-Anforderung an den Broker.</summary>
|
||||
@@ -71,8 +77,36 @@ public sealed record OrderRequest
|
||||
/// <summary>Optionskontrakt, falls die Order eine Option meint. <c>null</c> = Aktie.</summary>
|
||||
public OptionSpec? Option { get; init; }
|
||||
|
||||
/// <summary>Währung der Aktie; bei einer Option steht sie im <see cref="Option"/>.</summary>
|
||||
public string Currency { get; init; } = Currencies.Default;
|
||||
|
||||
public InstrumentRef Instrument =>
|
||||
Option is null ? InstrumentRef.ForStock(Symbol) : InstrumentRef.ForOption(Option);
|
||||
Option is null ? InstrumentRef.ForStock(Symbol, Currency) : InstrumentRef.ForOption(Option);
|
||||
|
||||
/// <summary>
|
||||
/// Baut den Auftrag aus dem <b>Instrument</b> – der einzige Weg, der die Identität vollständig
|
||||
/// überträgt.
|
||||
///
|
||||
/// <para><b>Warum es diese Fabrik gibt.</b> Wer die Felder einzeln setzt, kann die Währung
|
||||
/// vergessen; sie fällt dann auf USD zurück. Am 2026-09-04 ist genau das passiert: geprüft und
|
||||
/// reserviert wurde <c>SAP:EUR</c>, gehandelt wurde die SAP-ADR in New York – Kurs 188,74 EUR
|
||||
/// gegen Ausführung zu 216,68 USD, und das USD-Guthaben rutschte um 434 ins Minus. Es gab keine
|
||||
/// Fehlermeldung; die Bücher wiesen eine EUR-Position aus, die es beim Broker nie gab. Ein
|
||||
/// Auftrag muss aus dem Instrument entstehen, sonst ist er ein zweites, halb ausgefülltes
|
||||
/// Instrument.</para>
|
||||
/// </summary>
|
||||
public static OrderRequest For(
|
||||
InstrumentRef instrument, TradeSide side, int quantity, OrderType type, decimal? limitPrice = null) =>
|
||||
new()
|
||||
{
|
||||
Symbol = instrument.Symbol,
|
||||
Side = side,
|
||||
Quantity = quantity,
|
||||
Type = type,
|
||||
LimitPrice = limitPrice,
|
||||
Option = instrument.Option,
|
||||
Currency = instrument.Currency
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Ergebnis einer Order-Platzierung.</summary>
|
||||
|
||||
@@ -79,8 +79,10 @@ internal static class Program
|
||||
return await RunIbkrCheckAsync(host.Services, SymbolArg(args) ?? "AAPL").ConfigureAwait(false);
|
||||
|
||||
if (verifyOrder is not null)
|
||||
return await VerifyOrder.RunAsync(host.Services, verifyOrder, HasFlag(args, "--confirm"))
|
||||
.ConfigureAwait(false);
|
||||
return await VerifyOrder.RunAsync(
|
||||
host.Services, verifyOrder,
|
||||
ArgAfter(args, "--currency") ?? IBKRTrader.Core.Trading.Currencies.Default,
|
||||
HasFlag(args, "--confirm")).ConfigureAwait(false);
|
||||
|
||||
logger.Info("Core", "=== IBKRTrader (kopflos) startet ===");
|
||||
logger.Info("Core", $"Version: {IBKRTrader.Hosting.BuildInfo.Summary} | .NET {Environment.Version} | {RuntimeDescription()}");
|
||||
@@ -180,7 +182,7 @@ internal static class Program
|
||||
foreach (var p in positions.Take(10))
|
||||
Console.WriteLine($" {p}");
|
||||
|
||||
var quote = await broker.GetQuoteAsync(symbol, cts.Token).ConfigureAwait(false);
|
||||
var quote = await broker.GetQuoteAsync(InstrumentRef.ForStock(symbol), cts.Token).ConfigureAwait(false);
|
||||
Console.WriteLine(quote is null
|
||||
? $"Kurs {symbol}: keiner geliefert (ohne Marktdaten-Abo bei geschlossener Börse normal)"
|
||||
: $"Kurs {symbol}: {quote}");
|
||||
|
||||
@@ -28,14 +28,17 @@ internal static class VerifyOrder
|
||||
/// <summary>Nominalwert der Prüforder. Klein genug, dass ein Fehlschlag nichts kostet.</summary>
|
||||
private const decimal Notional = 500m;
|
||||
|
||||
public static async Task<int> RunAsync(IServiceProvider services, string symbol, bool confirm)
|
||||
public static async Task<int> RunAsync(
|
||||
IServiceProvider services, string symbol, string currency, bool confirm)
|
||||
{
|
||||
var inv = CultureInfo.InvariantCulture;
|
||||
var settings = services.GetRequiredService<SettingsService>().Settings;
|
||||
var trading = settings.Trading;
|
||||
var ibkr = settings.IBKR;
|
||||
|
||||
Console.WriteLine($"Order-Nachweis (#293) für {symbol}: {ibkr.Host}:{ibkr.Port}, " +
|
||||
var instrument = InstrumentRef.ForStock(symbol, currency);
|
||||
|
||||
Console.WriteLine($"Order-Nachweis (#293) für {instrument}: {ibkr.Host}:{ibkr.Port}, " +
|
||||
$"Modus {trading.Mode}, Client-ID {ibkr.ClientId}.");
|
||||
|
||||
// ── Sicherungen ──────────────────────────────────────────────────────────
|
||||
@@ -81,11 +84,12 @@ internal static class VerifyOrder
|
||||
return 1;
|
||||
}
|
||||
|
||||
var quote = await broker.GetQuoteAsync(symbol, cts.Token).ConfigureAwait(false);
|
||||
var quote = await broker.GetQuoteAsync(instrument, cts.Token).ConfigureAwait(false);
|
||||
if (quote is null || quote.Last <= 0)
|
||||
{
|
||||
Console.Error.WriteLine($"ABBRUCH: kein Kurs für {symbol}. Bei geschlossener Börse und ohne " +
|
||||
$"Marktdaten-Abo ist das normal – dann später erneut versuchen.");
|
||||
Console.Error.WriteLine($"ABBRUCH: kein Kurs für {instrument}. Entweder ist der Kontrakt " +
|
||||
$"nicht auflösbar (Symbol/Währung prüfen – die Meldung des Adapters " +
|
||||
$"steht oben), oder die Börse ist zu und es liegt kein Marktdaten-Abo vor.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -107,14 +111,21 @@ internal static class VerifyOrder
|
||||
|
||||
var headroom = await capital.GetHeadroomAsync(book.Value, account, cts.Token).ConfigureAwait(false);
|
||||
|
||||
// Der haeufigste Grund fuer eine Ablehnung durch Schicht C ist nicht "kein Geld", sondern
|
||||
// "Geld in der falschen Waehrung". Deshalb steht die Aufschluesselung hier, bevor
|
||||
// irgendetwas passiert.
|
||||
var cashHere = account.CashIn(instrument.Currency);
|
||||
Console.WriteLine($"Bargeld: {instrument.Currency} " +
|
||||
(cashHere is { } c ? c.ToString("N2", inv) : "keine Auskunft"));
|
||||
|
||||
Console.WriteLine($"Konto: NetLiquidation {account.NetLiquidation.ToString("N2", inv)}, " +
|
||||
$"Bargeld {account.TotalCashValue.ToString("N2", inv)}");
|
||||
Console.WriteLine($"Kurs: {quote}");
|
||||
Console.WriteLine($"Buch: {book} – Cap {headroom.Cap.ToString("N0", inv)}, " +
|
||||
$"gebunden {headroom.Bound.ToString("N0", inv)}, " +
|
||||
$"frei {headroom.Available.ToString("N0", inv)}");
|
||||
Console.WriteLine($"Vorhaben: KAUF {quantity} x {symbol} zum Marktpreis " +
|
||||
$"(~{(quantity * quote.Last).ToString("N2", inv)} USD).");
|
||||
Console.WriteLine($"Vorhaben: KAUF {quantity} x {instrument} zum Marktpreis " +
|
||||
$"(~{(quantity * quote.Last).ToString("N2", inv)} {instrument.Currency}).");
|
||||
|
||||
if (!confirm)
|
||||
{
|
||||
@@ -134,7 +145,8 @@ internal static class VerifyOrder
|
||||
Side = TradeSide.Buy,
|
||||
SourceModule = BookId.Diagnostics.Value,
|
||||
Reason = "Verifikation der Orderstrecke (#293)",
|
||||
SuggestedNotional = Notional
|
||||
SuggestedNotional = Notional,
|
||||
Currency = instrument.Currency
|
||||
};
|
||||
|
||||
var result = await execution.ExecuteAsync(signal, cts.Token).ConfigureAwait(false);
|
||||
@@ -161,9 +173,9 @@ internal static class VerifyOrder
|
||||
$"frei {afterHeadroom.Available.ToString("N0", inv)}");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("=== Order-Nachweis OK ===");
|
||||
Console.WriteLine($"Zu prüfen bleibt von Hand: core_trade_history, core_position (Buch {book}), " +
|
||||
$"core_reservation (Zustand Filled) und core_order_events – " +
|
||||
$"Signal-ID {signal.SignalId}.");
|
||||
Console.WriteLine($"Zu prüfen bleibt von Hand: core_trade_history, core_position (Buch {book}, " +
|
||||
$"Instrument {instrument}), core_reservation (Zustand Filled) und " +
|
||||
$"core_order_events – Signal-ID {signal.SignalId}.");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ public class ExecutionServiceTests
|
||||
private void ArrangeHappyPath()
|
||||
{
|
||||
EnableTrading();
|
||||
_broker.GetQuoteAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
_broker.GetQuoteAsync(Arg.Any<InstrumentRef>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<Quote?>(new Quote("AAPL", 100m, 99m, 101m)));
|
||||
_broker.GetAccountStateAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult(new AccountState(10_000m, 10_000m)));
|
||||
@@ -87,7 +87,7 @@ public class ExecutionServiceTests
|
||||
public async Task NoQuote_Skips()
|
||||
{
|
||||
EnableTrading();
|
||||
_broker.GetQuoteAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
_broker.GetQuoteAsync(Arg.Any<InstrumentRef>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<Quote?>(null));
|
||||
|
||||
var result = await CreateSut().ExecuteAsync(BuySignal);
|
||||
@@ -100,7 +100,7 @@ public class ExecutionServiceTests
|
||||
public async Task RiskRejects_Skips_WithReason()
|
||||
{
|
||||
EnableTrading();
|
||||
_broker.GetQuoteAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
_broker.GetQuoteAsync(Arg.Any<InstrumentRef>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<Quote?>(new Quote("AAPL", 100m, 99m, 101m)));
|
||||
_broker.GetAccountStateAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult(new AccountState(10_000m, 10_000m)));
|
||||
@@ -243,8 +243,45 @@ 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<CancellationToken>());
|
||||
// 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<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TheOrderCarriesTheSignalsCurrency_NotTheDefault()
|
||||
{
|
||||
// Am 2026-09-04 gegen das Paper-Konto beobachtet: geprueft und reserviert wurde SAP:EUR,
|
||||
// gehandelt wurde die SAP-ADR in New York, weil die OrderRequest aus Einzelfeldern gebaut
|
||||
// wurde und die Waehrung dabei auf USD zurueckfiel. Ohne Fehlermeldung.
|
||||
ArrangeHappyPath();
|
||||
var signal = new TradeSignal
|
||||
{
|
||||
Symbol = "SAP", Side = TradeSide.Buy, SourceModule = "CT", Currency = "EUR"
|
||||
};
|
||||
|
||||
await CreateSut().ExecuteAsync(signal);
|
||||
|
||||
await _broker.Received(1).PlaceOrderAsync(
|
||||
Arg.Is<OrderRequest>(o => o.Currency == "EUR" && o.Instrument.Key == "SAP:EUR"),
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TheOrderCarriesTheOptionContract()
|
||||
{
|
||||
ArrangeHappyPath();
|
||||
var spec = new OptionSpec("AAPL", new DateOnly(2026, 9, 18), 150m, OptionRight.Put);
|
||||
var signal = new TradeSignal
|
||||
{
|
||||
Symbol = "AAPL", Side = TradeSide.Sell, SourceModule = "CT", Option = spec
|
||||
};
|
||||
|
||||
await CreateSut().ExecuteAsync(signal);
|
||||
|
||||
await _broker.Received(1).PlaceOrderAsync(
|
||||
Arg.Is<OrderRequest>(o => o.Option == spec && o.Instrument.Kind == InstrumentKind.Option),
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -87,6 +87,72 @@ public class InstrumentTests
|
||||
act.Should().Throw<ArgumentException>();
|
||||
}
|
||||
|
||||
// ─── Währung gehört zur Identität ────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void UsdStaysImplicitInTheKey()
|
||||
{
|
||||
// Sonst waere jede Bestandszeile ab sofort ein anderes Instrument als vorher, und aus
|
||||
// einer Position wuerden zwei.
|
||||
InstrumentRef.ForStock("AAPL").Key.Should().Be("AAPL");
|
||||
InstrumentRef.ForStock("AAPL", "USD").Key.Should().Be("AAPL");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnotherCurrency_BecomesPartOfTheKey()
|
||||
{
|
||||
InstrumentRef.ForStock("MIGA", "EUR").Key.Should().Be("MIGA:EUR");
|
||||
InstrumentRef.ForStock("MIGA", "eur").Currency.Should().Be("EUR");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TheSameTickerInTwoCurrencies_AreTwoInstruments()
|
||||
{
|
||||
// Derselbe Ticker kann an mehreren Plaetzen notieren. Zwei Notierungen als eine Position
|
||||
// zu fuehren, wuerde Bestand und Deckung vermischen.
|
||||
InstrumentRef.ForStock("XYZ", "USD").Key
|
||||
.Should().NotBe(InstrumentRef.ForStock("XYZ", "EUR").Key);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OptionKey_CarriesTheCurrencyToo()
|
||||
{
|
||||
var eur = Put150 with { Currency = "EUR" };
|
||||
|
||||
InstrumentRef.ForOption(eur).Key.Should().Be("AAPL:EUR|20260918|P|150");
|
||||
InstrumentRef.ForOption(Put150).Key.Should().Be("AAPL|20260918|P|150");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("MIGA:EUR")]
|
||||
[InlineData("AAPL:EUR|20260918|P|150")]
|
||||
[InlineData("BRK.B")]
|
||||
public void KeysWithCurrencyAndDottedTickers_RoundTrip(string key)
|
||||
{
|
||||
// Der Punkt ist als Trennzeichen ausgeschieden, weil Ticker ihn enthalten (BRK.B).
|
||||
InstrumentRef.TryParse(key, out var parsed).Should().BeTrue();
|
||||
parsed.Key.Should().Be(key);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnderlyingOfAForeignOption_KeepsTheCurrency()
|
||||
{
|
||||
var eur = InstrumentRef.ForOption(Put150 with { Currency = "EUR" });
|
||||
|
||||
eur.UnderlyingRef.Key.Should().Be("MIGA:EUR".Replace("MIGA", "AAPL"));
|
||||
eur.UnderlyingRef.Currency.Should().Be("EUR");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("EU")]
|
||||
[InlineData("EURO")]
|
||||
[InlineData("eu1")]
|
||||
public void AnInvalidCurrency_IsRefused(string currency)
|
||||
{
|
||||
var act = () => InstrumentRef.ForStock("AAPL", currency);
|
||||
act.Should().Throw<ArgumentException>();
|
||||
}
|
||||
|
||||
// ─── Bewertung ───────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user