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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ matching; the generic rule book rejects XHKG/HKEX/SEHK securities instead of
silently routing them to A-share rules. See `PureSaber/quant-hk-equity` for the
research recipe and evidence limitations.

`HKDailyExecution` also accepts the exact ledger's optional `dividend_execution_mode`,
`fx_valuation_mode`, and `entitlement_evidence_verifier` constructor arguments. Defaults
retain legacy behavior. An explicit dividend mode requires `EVIDENCED_PIT`; production
mode also requires a trusted verifier. The caller schedules lifecycle facts through
`account.ledger`; this constructor does not infer event times or certify source data.
Dividend receivables and foreign cash can affect NAV without becoming spendable HKD.

Deterministic execution, matching, risk hooks, and multi-currency ledger contracts for
PureSaber quantitative research, backtesting, and paper trading.

Expand Down
11 changes: 11 additions & 0 deletions src/quant_execution/hong_kong.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
from quant_data_kit.exceptions import ValidationError

from quant_execution.contracts import Fee, Fill, Side
from quant_execution.dividends import (
DividendExecutionMode,
EntitlementEvidenceVerifier,
FxValuationMode,
)
from quant_execution.ledger import ExactAccountLedger


Expand Down Expand Up @@ -93,6 +98,9 @@ def __init__(
opened_at: datetime,
fees: HKFeeSchedule,
settlement_days: list[date],
dividend_execution_mode: DividendExecutionMode | None = None,
fx_valuation_mode: FxValuationMode = FxValuationMode.LEGACY,
entitlement_evidence_verifier: EntitlementEvidenceVerifier | None = None,
):
if not instruments or initial_cash.to_decimal() <= 0:
raise ValueError("HK execution needs instruments and positive capital")
Expand All @@ -118,6 +126,9 @@ def __init__(
initial_cash={"HKD": initial_cash},
money_scale=8,
opened_at=opened_at,
dividend_execution_mode=dividend_execution_mode,
fx_valuation_mode=fx_valuation_mode,
entitlement_evidence_verifier=entitlement_evidence_verifier,
)
self.pending: list[tuple[date, Decimal]] = []
self.executed: dict[str, tuple[tuple, Fill, dict]] = {}
Expand Down
144 changes: 141 additions & 3 deletions tests/test_hong_kong.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,23 @@
from conftest import T0, fp, spec
from quant_data_kit import AssetClass
from quant_data_kit.exceptions import ValidationError
from quant_data_kit.financial import (
CurrencyReference,
DividendEntitlement,
DividendLifecycle,
EvidenceTiming,
PhaseEvidence,
PublishedAmount,
)

from quant_execution.contracts import Side
from quant_execution.dividends import (
DividendEntitlementBasis,
DividendExecutionMode,
DividendExecutionPhase,
DividendExecutionRequest,
FxValuationMode,
)
from quant_execution.hong_kong import HKDailyExecution, HKFeeSchedule
from quant_execution.rules import RuleBookRiskGate

Expand Down Expand Up @@ -46,7 +61,7 @@ def instrument():
)


def account():
def account(**ledger_options):
return HKDailyExecution(
{"00700": instrument()},
initial_cash=fp(100000),
Expand All @@ -60,6 +75,7 @@ def account():
date(2026, 1, 8),
date(2026, 1, 9),
],
**ledger_options,
)


Expand All @@ -81,8 +97,12 @@ def test_both_sides_have_rounded_stamp_and_component_fees():
schedule().charge(fp(10000), date(2024, 1, 1), stamp_exempt=False)


def test_same_day_sale_allowed_but_cash_locks_until_t_plus_two_close():
broker = account()
@pytest.mark.parametrize("mode", [None, DividendExecutionMode.SCENARIO_ONLY])
def test_same_day_sale_allowed_but_cash_locks_until_t_plus_two_close(mode):
broker = account(
dividend_execution_mode=mode,
fx_valuation_mode=FxValuationMode.EVIDENCED_PIT if mode else FxValuationMode.LEGACY,
)
_, buy_fee = order(broker, "buy", Side.BUY)
available = broker.available_cash()
_, sell_fee = order(broker, "sell", Side.SELL, at=T0 + timedelta(seconds=1))
Expand Down Expand Up @@ -151,3 +171,121 @@ def test_non_cash_assets_are_rejected():
fees=schedule(),
settlement_days=[T0.date()],
)


def test_default_constructor_keeps_legacy_ledger():
broker = account()
assert broker.ledger.dividend_execution_mode is None
assert broker.ledger.fx_valuation_mode is FxValuationMode.LEGACY


@pytest.mark.parametrize(
("options", "message"),
[
(
{"dividend_execution_mode": DividendExecutionMode.SCENARIO_ONLY},
"requires EVIDENCED_PIT",
),
(
{"fx_valuation_mode": FxValuationMode.EVIDENCED_PIT},
"requires an explicit dividend execution mode",
),
(
{
"dividend_execution_mode": DividendExecutionMode.PRODUCTION_CERTIFIED,
"fx_valuation_mode": FxValuationMode.EVIDENCED_PIT,
},
"requires a trusted verifier",
),
],
)
def test_constructor_preserves_ledger_mode_validation(options, message):
with pytest.raises(ValidationError, match=message):
account(**options)


@pytest.mark.parametrize("trusted", [True, False])
def test_hk_entitlement_uses_supplied_production_verifier(trusted):
calls = []

class Verifier:
def verify_entitlement_basis(self, *, basis, lifecycle):
calls.append((basis, lifecycle))
return trusted

def verify_payment_policy(self, *, policy, lifecycle):
raise AssertionError("Entitlement without rounding does not use payment policy")

def verify_dividend_payment(self, *, payment, lifecycle):
raise AssertionError("Entitlement does not verify a payment")

broker = account(
dividend_execution_mode=DividendExecutionMode.PRODUCTION_CERTIFIED,
fx_valuation_mode=FxValuationMode.EVIDENCED_PIT,
entitlement_evidence_verifier=Verifier(),
)
broker.mark("00700", fp(100), T0)
order(broker, "buy", Side.BUY)
ex_at = T0 + timedelta(seconds=1)
hkd = CurrencyReference(
source_label="HKD", calculation_currency="HKD", normalization_rule="identity"
)
lifecycle = DividendLifecycle(
dividend_id="hk-dividend",
instrument_id="00700",
entitlement=DividendEntitlement(
evidence=PhaseEvidence(
event_id="hk-entitlement",
source="synthetic-hk-fixture",
evidence_id="hk-entitlement-evidence",
timing=EvidenceTiming(
effective_at=ex_at.isoformat(),
available_at=T0.isoformat(),
captured_at=T0.isoformat(),
source_published_at=T0.isoformat(),
),
),
approved_amount=PublishedAmount(
amount_text="1.00",
source_unit_text="1",
source_unit_name="share",
published_decimal_places=2,
approximate=False,
),
declared_currency=hkd,
record_date="2026-01-03",
scheduled_payment_date="2026-01-10",
payment_currencies=(hkd,),
default_payment_currency="HKD",
),
)
basis = DividendEntitlementBasis(
account_id="hk-research",
dividend_id=lifecycle.dividend_id,
instrument_id="00700",
ex_at=ex_at,
entitled_quantity=fp(100, 0),
available_at=T0,
captured_at=T0,
evidence_id="hk-position-evidence",
evidence_source="synthetic-hk-fixture",
certification_ref="test-verifier",
)
request = DividendExecutionRequest(
lifecycle=lifecycle,
phase=DividendExecutionPhase.ENTITLEMENT,
cutoff=ex_at,
entitlement_basis=basis,
)
spendable = broker.available_cash()
before_nav = broker.ledger.snapshot(T0).nav.to_decimal()
before = broker.ledger.capture_state()
if trusted:
broker.ledger.apply_dividend_lifecycle(request)
assert broker.ledger.snapshot(ex_at).nav.to_decimal() == before_nav + 100
assert broker.available_cash() == spendable
else:
with pytest.raises(ValidationError, match="ENTITLEMENT_EVIDENCE_NOT_CERTIFIED"):
broker.ledger.apply_dividend_lifecycle(request)
assert broker.ledger.capture_state() == before
assert calls == [(basis, lifecycle)]
Loading