From 459f5cc6b2c45db9ca4421cbf6de5f1b5cfc9022 Mon Sep 17 00:00:00 2001 From: PureSaber <88314620+PureSaber@users.noreply.github.com> Date: Sat, 26 Sep 2026 10:47:23 +0800 Subject: [PATCH 1/2] feat: integrate research factors and current data contracts --- pyproject.toml | 2 +- requirements.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 From e9bf4ca2db6e7fb632cf3b091227b4e61ff84bda Mon Sep 17 00:00:00 2001 From: PureSaber <88314620+PureSaber@users.noreply.github.com> Date: Sat, 26 Sep 2026 11:02:31 +0800 Subject: [PATCH 2/2] fix: omit zero-effect corporate action postings while preserving event identity --- src/quant_execution/ledger.py | 17 ++++++++++--- tests/test_unheld_corporate_action.py | 36 +++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 tests/test_unheld_corporate_action.py 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")))