diff --git a/pyproject.toml b/pyproject.toml index e0fb06c..5471985 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ requires-python = ">=3.10" dependencies = [ "pyarrow>=14.0", "jsonschema>=4.20", - "quant-data-kit @ git+https://github.com/PureSaber/quant-data-kit.git@5b68af566ee1d47f62caa5449719912df6e17983", + "quant-data-kit @ git+https://github.com/PureSaber/quant-data-kit.git@fbbad913c9e592fdcbf8b473c4230e2da8594cef", ] [project.optional-dependencies] diff --git a/requirements.lock b/requirements.lock index 571f9d9..40a6ad3 100644 --- a/requirements.lock +++ b/requirements.lock @@ -60,7 +60,7 @@ pytz==2026.3.post1 # via pandas pyyaml==6.0.3 # via quant-data-kit -quant-data-kit @ git+https://github.com/PureSaber/quant-data-kit.git@5b68af566ee1d47f62caa5449719912df6e17983 +quant-data-kit @ git+https://github.com/PureSaber/quant-data-kit.git@fbbad913c9e592fdcbf8b473c4230e2da8594cef # via quant-execution (pyproject.toml) referencing==0.37.0 # via diff --git a/src/quant_execution/ledger.py b/src/quant_execution/ledger.py index 8fee699..55fb639 100644 --- a/src/quant_execution/ledger.py +++ b/src/quant_execution/ledger.py @@ -813,10 +813,19 @@ def _apply( transaction = self._translate(event) if local_rollback: undo = self._capture_apply_undo(event, transaction, reference_id) - if local_rollback: - self._post(transaction) - else: - self._post(transaction, local_rollback=False) + # Corporate-action announcements are market events even when this + # account holds no eligible shares. Preserve event identity, time and + # split state, but do not invent a zero-effect accounting transaction. + no_effect_action = isinstance(event, CorporateActionEvent) and not any( + posting.amount.units + or (posting.quantity_delta is not None and posting.quantity_delta.units) + for posting in transaction.postings + ) + if not no_effect_action: + if local_rollback: + self._post(transaction) + else: + self._post(transaction, local_rollback=False) if isinstance(event, Fill): self._fills[event.fill_id] = event self._fill_trading_days[event.fill_id] = trading_day diff --git a/tests/test_unheld_corporate_action.py b/tests/test_unheld_corporate_action.py new file mode 100644 index 0000000..63ee594 --- /dev/null +++ b/tests/test_unheld_corporate_action.py @@ -0,0 +1,36 @@ +from dataclasses import replace + +import pytest +from conftest import T0, event_fields, fp +from quant_data_kit import CorporateActionEvent +from quant_data_kit.exceptions import ValidationError +from test_ledger import STOCK, mark, stock_spec + +from quant_execution.ledger import ExactAccountLedger + + +@pytest.mark.parametrize("ratio", [None, "2"]) +def test_unheld_action_has_no_cash_transaction_but_retains_event_identity(ratio): + ledger = ExactAccountLedger( + account_id="account", + base_currency="CNY", + instruments={STOCK: stock_spec()}, + initial_cash={"CNY": fp("10000")}, + ) + ledger.mark(mark(STOCK, "10", 1)) + event = CorporateActionEvent( + **event_fields("unheld-action", STOCK, seconds=2), + action_type="split_and_dividend" if ratio else "cash_dividend", + effective_date=T0.date(), + cash_amount=fp("1"), + currency="CNY", + ratio=fp(ratio) if ratio else None, + ) + count = len(ledger.transactions) + after = ledger.apply(event) + assert after.cash_balances["CNY"].to_decimal() == 10000 + assert after.nav.to_decimal() == 10000 + assert len(ledger.transactions) == count + assert ledger.apply(event) == after + with pytest.raises(ValidationError, match="reused"): + ledger.apply(replace(event, cash_amount=fp("2")))