From 9aecdcc4547e1906eb08c2053513a2a206ccdbf6 Mon Sep 17 00:00:00 2001 From: PureSaber Date: Sun, 11 Oct 2026 10:50:48 +0800 Subject: [PATCH] Support exact bond terminal cash settlement --- src/quant_execution/ledger.py | 3 +- tests/test_bond_terminal.py | 70 +++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 tests/test_bond_terminal.py diff --git a/src/quant_execution/ledger.py b/src/quant_execution/ledger.py index 2038b5e..0c4559c 100644 --- a/src/quant_execution/ledger.py +++ b/src/quant_execution/ledger.py @@ -1727,7 +1727,8 @@ def _validate_corporate_action(self, event: CorporateActionEvent) -> None: spec = self._spec(event.instrument_id) if event.action_type == "terminal_cash": if ( - spec.asset_class not in {AssetClass.EQUITY, AssetClass.ETF, AssetClass.OPTION} + spec.asset_class + not in {AssetClass.EQUITY, AssetClass.ETF, AssetClass.OPTION, AssetClass.BOND} or event.ratio is None or event.ratio.units != 0 or event.cash_amount is None diff --git a/tests/test_bond_terminal.py b/tests/test_bond_terminal.py new file mode 100644 index 0000000..132044e --- /dev/null +++ b/tests/test_bond_terminal.py @@ -0,0 +1,70 @@ +from dataclasses import replace +from datetime import datetime, timezone +from decimal import Decimal + +import pytest +from quant_data_kit import AssetClass, CorporateActionEvent, MarkPriceEvent + +from quant_execution.contracts import Fill, Side +from quant_execution.derivatives import fp +from quant_execution.ledger import ExactAccountLedger +from quant_execution.us_cash import instrument + + +@pytest.mark.parametrize("payout", ["0", "40", "102.125"]) +def test_bond_redemption_and_default_balance_and_idempotency(payout): + at = datetime(2025, 1, 1, tzinfo=timezone.utc) + spec = replace( + instrument("BOND", "BOND", at), + asset_class=AssetClass.BOND, + product_type="convertible", + metadata={"research_delivery": "true"}, + ) + ledger = ExactAccountLedger( + account_id="test", + base_currency="USD", + instruments={"BOND": spec}, + initial_cash={"USD": fp(1000)}, + opened_at=at, + ) + fields = { + "instrument_id": "BOND", + "event_time": at, + "received_at": at, + "available_at": at, + "source": "fixture", + "trading_day": at.date(), + "session_id": "test", + "sequence": 0, + } + ledger.mark(MarkPriceEvent(**fields, event_id="mark", price=fp(100))) + ledger.apply( + Fill( + fill_id="fill", + order_id="order", + account_id="test", + strategy_id="test", + instrument_id="BOND", + side=Side.BUY, + quantity=fp(3), + price=fp(100), + event_time=at, + ) + ) + event = CorporateActionEvent( + **fields, + event_id="redemption", + action_type="terminal_cash", + effective_date=at.date(), + ratio=fp(0), + cash_amount=fp(payout), + currency="USD", + ) + ledger.apply(event) + count = len(ledger.transactions) + ledger.apply(event) + assert len(ledger.transactions) == count + assert ledger.snapshot().positions["BOND"].units == 0 + assert ledger.snapshot().nav.to_decimal() == 700 + 3 * Decimal(payout) + for tx in ledger.transactions: + assert sum(p.amount.to_decimal() for p in tx.postings) == 0