Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion requirements.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 13 additions & 4 deletions src/quant_execution/ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 36 additions & 0 deletions tests/test_unheld_corporate_action.py
Original file line number Diff line number Diff line change
@@ -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")))
Loading