Instrumente sind waehrungsfaehig

Die Waehrung gehoert zur Identitaet eines Instruments und nicht zu
seiner Beschreibung: derselbe Ticker kann an mehreren Plaetzen in
verschiedenen Waehrungen notieren, und ein Kauf darf nur aus Bargeld
derselben Waehrung finanziert werden (Kapitalmodell §4 Schicht C). Zwei
Notierungen als eine Position zu fuehren, wuerde Bestand und Deckung
vermischen.

USD bleibt im Instrumentenschluessel IMPLIZIT ("AAPL", nicht
"AAPL:USD"). Andernfalls waere jede Bestandszeile ab sofort ein anderes
Instrument als vorher und aus einer Position wuerden zwei. Trennzeichen
ist der Doppelpunkt und nicht der Punkt - Ticker enthalten Punkte
(BRK.B).

Neu: Currencies (kanonische Schreibweise an allen vier Stellen, die sie
gleich haben muessen), Currency an OptionSpec, InstrumentRef,
TradeSignal, OrderRequest und core_position. CapitalService nimmt die
Waehrung ab jetzt aus dem Instrument statt vom Aufrufer - wer sie
uebergeben koennte, koennte die Deckungspruefung gegen den falschen Topf
laufen lassen. --verify-order kennt --currency.

Anlass: das Paper-Konto haelt EUR 91.832 und USD -3.751. Ein
USD-Nachweis waere ein USD-Kredit gewesen und wurde von Schicht C
zu Recht abgelehnt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-09-04 15:39:30 +02:00
co-authored by Claude Opus 5
parent bd04642735
commit 2a73172e27
16 changed files with 873 additions and 33 deletions
@@ -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]