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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ editable-build requirements for Python3.10-3.12. Every registry package is fixed
version. The `dev` extra names Python3.10's conditional compatibility dependencies explicitly so
a lock compiled on Python3.12 remains complete for the whole matrix. The internal package is also
fixed to the reviewed research-data revision in this development branch:
`quant-data-kit@8fed47b8f62694c36830dec270cfa21759133f2f`, from
`quant-data-kit@7a8813b3d1e52f476f8fe51f05ea714be41a52bb`, from
`https://github.com/PureSaber/quant-data-kit.git`. The project declaration and lock
use the same immutable source as the daily A-share research stack, so a clean
use the same immutable source as the US research stack in this branch, so a clean
resolver does not combine incompatible direct URLs. Existing release tags remain unchanged.

Regenerate the lock only after reviewing dependency changes in `pyproject.toml`:
Expand Down
13 changes: 13 additions & 0 deletions docs/us-cash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# US cash research account

`quant_execution.us_cash.USCashAccount` uses `ExactAccountLedger` for fills, fees, corporate actions and valuation. Cash is booked on trade date; `buying_power` separately subtracts unsettled sale proceeds. This is a conservative cash account using settled funds, not a margin/PDT or live brokerage model.

Standard equity settlement uses T+2 before 2024-05-28 and T+1 from that date. Same-day resale is allowed for holdings bought using settled funds. Account units permit six decimal places for normalized-price and fractional-share research; no broker fillability claim is implied.

The daily model releases buying power on the settlement date, without intraday clearing or broker-specific holds. Account mutations and cash queries cannot precede the current ledger state; an identical already-applied trade remains idempotent. Instruments must be USD cash equities/ETFs with unit multiplier.

Explicit `us_equity`/`us_etf` product types select `USCashEquityRule`. Legacy A-share product types preserve their prior rules. Costs are supplied assumptions, not a fixed regulatory fee schedule.

`terminal_cash` corporate events retire all shares and remove book cost, posting a final known cash amount and realized P&L. They require a zero share ratio and nonnegative cash in the settlement currency. A zero recovery must be explicit upstream evidence, never a substitute for missing prices.

Fractional equity sells now quantize posted cash and removed book cost before computing P&L. This corrects a one-unit rounding imbalance caused by independently quantizing all three amounts.
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ requires-python = ">=3.10"
dependencies = [
"pyarrow>=14.0",
"jsonschema>=4.20",
"quant-data-kit @ git+https://github.com/PureSaber/quant-data-kit.git@104f1ef8a3b1278c0ea5420fadaa9d1d863ce726",
"quant-data-kit @ git+https://github.com/PureSaber/quant-data-kit.git@7a8813b3d1e52f476f8fe51f05ea714be41a52bb",
]

[project.optional-dependencies]
us-research = ["exchange-calendars>=4.5,<5"]
dev = [
"exchange-calendars>=4.5,<5",
"exceptiongroup>=1",
"pytest>=7.4",
"pytest-cov>=5.0",
Expand Down
32 changes: 20 additions & 12 deletions requirements.lock
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#
# This file is autogenerated by pip-compile with Python 3.10
# This file is autogenerated by pip-compile with Python 3.12
# by the following command:
#
# python -m piptools compile --extra dev --build-deps-for editable --allow-unsafe --strip-extras --resolver backtracking --index-url https://pypi.org/simple --constraint requirements-constraints.txt --output-file requirements.lock pyproject.toml
# pip-compile --allow-unsafe --build-deps-for=editable --constraint=requirements-constraints.txt --extra=dev --index-url=https://pypi.org/simple --no-emit-index-url --no-index --output-file=requirements.lock --strip-extras pyproject.toml
#
attrs==26.1.0
# via
Expand All @@ -15,9 +15,9 @@ coverage==7.16.0
duckdb==1.5.5
# via quant-data-kit
exceptiongroup==1.3.1
# via
# pytest
# quant-execution (pyproject.toml)
# via quant-execution (pyproject.toml)
exchange-calendars==4.13.2
# via quant-execution (pyproject.toml)
iniconfig==2.3.0
# via pytest
jsonschema==4.26.0
Expand All @@ -28,16 +28,21 @@ jsonschema-rs==0.52.1
# via quant-data-kit
jsonschema-specifications==2025.9.1
# via jsonschema
korean-lunar-calendar==0.4.0
# via exchange-calendars
numpy==2.2.6
# via
# exchange-calendars
# pandas
# quant-data-kit
orjson==3.12.0
# via quant-data-kit
packaging==26.3
# via pytest
pandas==2.3.3
# via quant-data-kit
# via
# exchange-calendars
# quant-data-kit
pluggy==1.6.0
# via
# pytest
Expand All @@ -48,6 +53,8 @@ pyarrow==25.0.1
# quant-execution (pyproject.toml)
pygments==2.21.0
# via pytest
pyluach==2.3.0
# via exchange-calendars
pytest==9.1.1
# via
# pytest-cov
Expand All @@ -60,7 +67,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@104f1ef8a3b1278c0ea5420fadaa9d1d863ce726
quant-data-kit @ git+https://github.com/PureSaber/quant-data-kit.git@7a8813b3d1e52f476f8fe51f05ea714be41a52bb
# via quant-execution (pyproject.toml)
referencing==0.37.0
# via
Expand All @@ -76,17 +83,18 @@ ruff==0.16.5
six==1.17.0
# via python-dateutil
tomli==2.4.1
# via
# coverage
# pytest
# quant-execution (pyproject.toml)
# via quant-execution (pyproject.toml)
toolz==1.1.0
# via exchange-calendars
typing-extensions==4.16.0
# via
# exceptiongroup
# quant-data-kit
# referencing
tzdata==2026.3
# via pandas
# via
# exchange-calendars
# pandas
websockets==15.0.1
# via quant-data-kit

Expand Down
64 changes: 63 additions & 1 deletion src/quant_execution/ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -1273,6 +1273,10 @@ def _prepare_lot_update(
return lots, prior_close, today_close

def _apply_split_state(self, event: CorporateActionEvent) -> None:
if event.action_type == "terminal_cash":
self._position_lots[event.instrument_id] = []
self._marks.pop(event.instrument_id, None)
return
ratio = decimal(event.ratio)
self._position_lots[event.instrument_id] = [
(day, quantity * ratio)
Expand Down Expand Up @@ -1312,6 +1316,19 @@ def _apply_dividend_state(self, event: CorporateActionEvent) -> None:

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}
or event.ratio is None
or event.ratio.units != 0
or event.cash_amount is None
or event.cash_amount.units < 0
or event.currency != spec.settlement_currency
):
raise ValidationError(
"terminal_cash requires a zero share ratio and nonnegative settlement cash"
)
return
if event.action_type in {"cash_dividend_entitlement", "cash_dividend_payment"}:
if event.cash_amount is None or event.currency is None:
raise ValidationError(f"{event.action_type} requires cash_amount and currency")
Expand Down Expand Up @@ -1474,7 +1491,11 @@ def _fill_transaction(self, fill_event: Fill) -> LedgerTransaction:
]
)
else:
cost_removed = average * quantity * multiplier
# Quantize cash and removed book cost first. Independently rounding
# their difference can leave a one-unit imbalance for fractional
# equity fills. P&L is the exact residual of the posted amounts.
notional = decimal(fixed(notional, self.money_scale))
cost_removed = decimal(fixed(average * quantity * multiplier, self.money_scale))
postings.extend(
[
self._posting("assets:cash", spec.settlement_currency, notional),
Expand Down Expand Up @@ -1606,6 +1627,47 @@ def _settlement_transaction(self, event: Settlement) -> LedgerTransaction:
def _corporate_action_transaction(self, event: CorporateActionEvent) -> LedgerTransaction:
spec = self._spec(event.instrument_id)
quantity = self._positions.get(event.instrument_id, Decimal(0))
if event.action_type == "terminal_cash":
cash = decimal(fixed(quantity * decimal(event.cash_amount), self.money_scale))
cost = self._position_cost(event.instrument_id, derivative=False)
postings = (
self._posting("assets:cash", spec.settlement_currency, cash),
self._posting(
"assets:position_cost",
spec.settlement_currency,
-cost,
instrument_id=event.instrument_id,
),
self._posting(
"income:realized_pnl",
spec.settlement_currency,
cost - cash,
instrument_id=event.instrument_id,
),
self._posting(
"assets:position",
spec.settlement_currency,
Decimal(0),
instrument_id=event.instrument_id,
quantity_delta=-quantity,
quantity_scale=spec.quantity_step.scale,
),
self._posting(
"memo:position_counter",
spec.settlement_currency,
Decimal(0),
instrument_id=event.instrument_id,
quantity_delta=quantity,
quantity_scale=spec.quantity_step.scale,
),
)
return self._make_transaction(
event_type=LedgerEventType.CORPORATE_ACTION,
reference_id=event.event_id,
idempotency_key=f"corporate_action:{event.event_id}",
event_time=event.available_at,
postings=postings,
)
postings: list[Posting] = []
if event.cash_amount is not None:
currency = str(event.currency)
Expand Down
31 changes: 31 additions & 0 deletions src/quant_execution/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,35 @@ def fee_rate(
return _metadata_decimal(spec, key, default="0")


class USCashEquityRule(_AssetRule):
"""Explicit US cash equity rule; settlement is separate from resale permission."""

code = "US_CASH_EQUITY"

def check(self, intent, snapshot, state, spec, ledger):
from quant_execution.us_cash import settled_cash

base = super().check(intent, snapshot, state, spec, ledger)
if not base.accepted:
return base
if spec.settlement_currency != "USD":
return RiskDecision(False, "US_CURRENCY", "US cash research requires USD")
quantity = decimal(intent.quantity)
if intent.side is Side.SELL and quantity > _value(
snapshot.positions.get(spec.instrument_id)
):
return RiskDecision(False, "US_NO_SHORT", "cash account cannot sell short")
if intent.side is Side.BUY:
cost = quantity * _intent_price(intent, state) * decimal(spec.contract_multiplier)
cost *= Decimal(1) + _metadata_decimal(spec, "commission_rate")
if cost > settled_cash(ledger, state.event.available_at):
return RiskDecision(False, "US_UNSETTLED_CASH", "insufficient settled USD")
return _ACCEPTED_DECISION

def fee_rate(self, fill, order, state, spec, ledger):
return _metadata_decimal(spec, "commission_rate")


class AShareRule(_AssetRule):
code = "A_SHARE"

Expand Down Expand Up @@ -956,6 +985,8 @@ def _reservation_requirement(
def _rule(spec: InstrumentSpec) -> _AssetRule:
product = spec.product_type.lower()
if spec.asset_class in {AssetClass.EQUITY, AssetClass.ETF}:
if product in {"us_equity", "us_etf"}:
return USCashEquityRule()
from quant_execution.hong_kong import reject_generic_hk_rule

reject_generic_hk_rule(spec)
Expand Down
Loading
Loading