Spaces:
Running
Running
| """Next-open execution, explicit costs, and accounting reconciliation.""" | |
| from __future__ import annotations | |
| from datetime import UTC, date, datetime, timedelta | |
| from decimal import Decimal | |
| from pathlib import Path | |
| from tradebench.data.query import PitQueryService | |
| from tradebench.data.sample_dataset import ASSET_LIQUID, SAMPLE_DATASET_VERSION | |
| from tradebench.episodes.models import EpisodeManifest | |
| from tradebench.execution.costs import ( | |
| CostModel, | |
| compute_fee_total, | |
| compute_slippage_bps, | |
| fill_price_from_open, | |
| ) | |
| from tradebench.execution.engine import ( | |
| OrderIntent, | |
| advance_trading_session, | |
| portfolio_market_value, | |
| ) | |
| from tradebench.ledger.events import ( | |
| DayAdvanced, | |
| DividendApplied, | |
| LedgerEvent, | |
| OrderFilled, | |
| ) | |
| from tradebench.ledger.projector import project | |
| def _manifest(sample_dataset_root: Path) -> EpisodeManifest: | |
| path = ( | |
| sample_dataset_root | |
| / "catalog" | |
| / SAMPLE_DATASET_VERSION | |
| / "episode_manifests" | |
| / "sample_ep_001.json" | |
| ) | |
| return EpisodeManifest.model_validate_json(path.read_text(encoding="utf-8")) | |
| def _last_close(pit: PitQueryService, asset_id: str, session_date: date) -> Decimal: | |
| df = pit.get_bars([asset_id], session_date, lookback_days=1) | |
| row = df.iloc[-1] | |
| return Decimal(str(row["close"])) | |
| def test_fills_use_next_session_open_not_decision_close( | |
| pit_query_service: PitQueryService, | |
| sample_dataset_root: Path, | |
| ) -> None: | |
| """Buy decided on ``t`` must fill at ``t+1`` open (adjusted), not at ``t`` close.""" | |
| pit = pit_query_service | |
| manifest = _manifest(sample_dataset_root) | |
| dec = date(2020, 1, 2) | |
| exe = date(2020, 1, 3) | |
| model = CostModel( | |
| commission_floor=Decimal("1"), | |
| fee_bps=Decimal("10"), | |
| slippage_bps_cap=Decimal("50"), | |
| ) | |
| prior: list[LedgerEvent] = [ | |
| DividendApplied( | |
| event_id="boot", | |
| episode_id="ep", | |
| event_time=datetime(2020, 1, 1, 12, 0, 0, tzinfo=UTC), | |
| asset_id=ASSET_LIQUID, | |
| cash_credited=Decimal("100000"), | |
| ), | |
| ] | |
| step = advance_trading_session( | |
| pit=pit, | |
| manifest=manifest, | |
| episode_id="ep", | |
| decision_date=dec, | |
| execution_date=exe, | |
| prior_events=prior, | |
| new_orders=[ | |
| OrderIntent( | |
| client_order_id="b1", | |
| asset_id=ASSET_LIQUID, | |
| side="buy", | |
| quantity=10, | |
| ), | |
| ], | |
| cost_model=model, | |
| base_event_time=datetime(2020, 1, 2, 16, 0, 0, tzinfo=UTC), | |
| ) | |
| fills = [e for e in step if isinstance(e, OrderFilled)] | |
| assert len(fills) == 1 | |
| fill = fills[0] | |
| dec_close = _last_close(pit, ASSET_LIQUID, dec) | |
| exe_open = Decimal(str(pit.get_bars([ASSET_LIQUID], exe, 1).iloc[-1]["open"])) | |
| assert dec_close == Decimal("100") | |
| assert exe_open == Decimal("99.75") | |
| bar_dec = pit.get_bars([ASSET_LIQUID], dec, 1).iloc[-1] | |
| dv = Decimal(str(bar_dec["dollar_volume"])) | |
| slip = compute_slippage_bps( | |
| model, | |
| order_notional=Decimal(fill.quantity) * exe_open, | |
| trailing_dollar_volume=dv, | |
| ) | |
| expected_px = fill_price_from_open(exe_open, "buy", slip) | |
| assert fill.avg_fill_price == expected_px | |
| assert fill.avg_fill_price != fill_price_from_open(dec_close, "buy", slip) | |
| def test_cash_debits_include_commission_and_bps_fees( | |
| pit_query_service: PitQueryService, | |
| sample_dataset_root: Path, | |
| ) -> None: | |
| pit = pit_query_service | |
| manifest = _manifest(sample_dataset_root) | |
| model = CostModel( | |
| commission_floor=Decimal("2"), | |
| fee_bps=Decimal("25"), | |
| slippage_bps_cap=Decimal("100"), | |
| ) | |
| prior: list[LedgerEvent] = [ | |
| DividendApplied( | |
| event_id="boot", | |
| episode_id="ep", | |
| event_time=datetime(2020, 1, 1, 12, 0, 0, tzinfo=UTC), | |
| asset_id=ASSET_LIQUID, | |
| cash_credited=Decimal("100000"), | |
| ), | |
| ] | |
| step = advance_trading_session( | |
| pit=pit, | |
| manifest=manifest, | |
| episode_id="ep", | |
| decision_date=date(2020, 1, 2), | |
| execution_date=date(2020, 1, 3), | |
| prior_events=prior, | |
| new_orders=[ | |
| OrderIntent( | |
| client_order_id="b1", | |
| asset_id=ASSET_LIQUID, | |
| side="buy", | |
| quantity=5, | |
| ), | |
| ], | |
| cost_model=model, | |
| base_event_time=datetime(2020, 1, 2, 16, 0, 0, tzinfo=UTC), | |
| ) | |
| fill = next(e for e in step if isinstance(e, OrderFilled)) | |
| fill_notional = fill.avg_fill_price * Decimal(fill.quantity) | |
| expected_fees = compute_fee_total(model, fill_notional=fill_notional) | |
| assert fill.fees == expected_fees | |
| st = project([*prior, *step]) | |
| assert st.cash >= Decimal("0") | |
| def test_scripted_episode_reconciles_after_each_day_advanced( | |
| pit_query_service: PitQueryService, | |
| sample_dataset_root: Path, | |
| ) -> None: | |
| """``DayAdvanced.portfolio_market_value`` matches cash + marks from projection.""" | |
| pit = pit_query_service | |
| manifest = _manifest(sample_dataset_root) | |
| model = CostModel( | |
| commission_floor=Decimal("1"), | |
| fee_bps=Decimal("10"), | |
| slippage_bps_cap=Decimal("30"), | |
| ) | |
| events: list[LedgerEvent] = [ | |
| DividendApplied( | |
| event_id="boot", | |
| episode_id="ep", | |
| event_time=datetime(2020, 1, 1, 12, 0, 0, tzinfo=UTC), | |
| asset_id=ASSET_LIQUID, | |
| cash_credited=Decimal("100000"), | |
| ), | |
| ] | |
| days = [ | |
| (date(2020, 1, 2), date(2020, 1, 3)), | |
| (date(2020, 1, 3), date(2020, 1, 6)), | |
| ] | |
| for dec, exe in days: | |
| step = advance_trading_session( | |
| pit=pit, | |
| manifest=manifest, | |
| episode_id="ep", | |
| decision_date=dec, | |
| execution_date=exe, | |
| prior_events=events, | |
| new_orders=[ | |
| OrderIntent( | |
| client_order_id=f"buy-{exe}", | |
| asset_id=ASSET_LIQUID, | |
| side="buy", | |
| quantity=1, | |
| ), | |
| ], | |
| cost_model=model, | |
| base_event_time=datetime.combine(dec, datetime.min.time()).replace( | |
| tzinfo=UTC, | |
| ) | |
| + timedelta(hours=16), | |
| ) | |
| events.extend(step) | |
| adv = next(e for e in step if isinstance(e, DayAdvanced)) | |
| st = project(events) | |
| closes = { | |
| ASSET_LIQUID: _last_close(pit, ASSET_LIQUID, exe), | |
| } | |
| recomputed = portfolio_market_value(st, closes) | |
| assert adv.portfolio_market_value == recomputed | |