|
9 | 9 | IBKRReconciliationReadError, |
10 | 10 | build_ibkr_order_key, |
11 | 11 | build_reconciliation_candidate, |
| 12 | + calculate_legacy_reconciliation_observation_sha256, |
12 | 13 | collect_read_only_reconciliation_observations, |
13 | 14 | ) |
| 15 | +from quant_platform_kit.common.broker_reconciliation import calculate_broker_observation_sha256 |
14 | 16 | from quant_platform_kit.common.live_continuity import runtime_target_fingerprint |
15 | 17 | from quant_platform_kit.common.runtime_target import build_runtime_target |
16 | 18 |
|
@@ -157,6 +159,33 @@ def test_manual_order_key_requires_perm_id() -> None: |
157 | 159 | build_ibkr_order_key(account_id="U123", order_id=0) |
158 | 160 |
|
159 | 161 |
|
| 162 | +def test_order_event_metadata_does_not_change_legacy_reconciliation_digest() -> None: |
| 163 | + legacy_open_order = { |
| 164 | + "account": "U123", |
| 165 | + "contract": {"symbol": "SOXL"}, |
| 166 | + "perm_id": "9001", |
| 167 | + "action": "BUY", |
| 168 | + "order_type": "LMT", |
| 169 | + "total_quantity": 2.0, |
| 170 | + "limit_price": 22.0, |
| 171 | + "aux_price": 0.0, |
| 172 | + "status": "Submitted", |
| 173 | + "filled": 0.0, |
| 174 | + "remaining": 2.0, |
| 175 | + } |
| 176 | + enriched_open_order = { |
| 177 | + **legacy_open_order, |
| 178 | + "order_key": "ibkr-order-v1-example", |
| 179 | + "order_identity": {"account_scope_sha256": "digest", "client_id": "7", "order_id": "456"}, |
| 180 | + "cumulative_filled_quantity": 0.0, |
| 181 | + "status_transitions": [{"from": "created", "to": "submitted"}], |
| 182 | + } |
| 183 | + |
| 184 | + assert calculate_legacy_reconciliation_observation_sha256((enriched_open_order,)) == ( |
| 185 | + calculate_broker_observation_sha256((legacy_open_order,)) |
| 186 | + ) |
| 187 | + |
| 188 | + |
160 | 189 | def test_cash_reconciliation_ignores_dynamic_margin_and_valuation_tags() -> None: |
161 | 190 | def fetch_snapshot(_ib, *, dynamic_net_liquidation: float, dynamic_available_funds: float, **_kwargs): |
162 | 191 | return SimpleNamespace( |
@@ -411,3 +440,99 @@ def configured_env(name, default=None): |
411 | 440 |
|
412 | 441 | assert candidate.permits_active_lkg is True |
413 | 442 | assert candidate.recovery_blockers == () |
| 443 | + |
| 444 | + |
| 445 | +def test_candidate_keeps_legacy_order_digests_after_order_event_wiring(tmp_path) -> None: |
| 446 | + target = _frozen_runtime_target() |
| 447 | + |
| 448 | + def empty_env(name, default=None): |
| 449 | + return str(tmp_path) if name == "IBKR_EXECUTION_STATE_DIR" else default |
| 450 | + |
| 451 | + legacy_observations = IBKRReconciliationObservations( |
| 452 | + account_scope={"account_ids": ["U123"]}, |
| 453 | + account_identity_match=True, |
| 454 | + positions=(), |
| 455 | + cash=(), |
| 456 | + open_orders=( |
| 457 | + { |
| 458 | + "account": "U123", |
| 459 | + "contract": {"symbol": "SOXL"}, |
| 460 | + "perm_id": "9001", |
| 461 | + "status": "Submitted", |
| 462 | + "filled": 0.0, |
| 463 | + "remaining": 2.0, |
| 464 | + }, |
| 465 | + ), |
| 466 | + recent_executions=( |
| 467 | + { |
| 468 | + "account": "U123", |
| 469 | + "contract": {"symbol": "SOXL"}, |
| 470 | + "order_id": "456", |
| 471 | + "execution_id": "exec-1", |
| 472 | + "shares": 1.0, |
| 473 | + "price": 21.5, |
| 474 | + }, |
| 475 | + ), |
| 476 | + ) |
| 477 | + seed = build_reconciliation_candidate( |
| 478 | + observations=legacy_observations, |
| 479 | + runtime_target=target, |
| 480 | + platform_id="ibkr", |
| 481 | + strategy_profile="soxl_soxx_trend_income", |
| 482 | + account_group="LIVE", |
| 483 | + project_id=None, |
| 484 | + env_reader=empty_env, |
| 485 | + ) |
| 486 | + expected = { |
| 487 | + key: seed.evidence.to_dict()[key] |
| 488 | + for key in ( |
| 489 | + "positions_sha256", |
| 490 | + "cash_sha256", |
| 491 | + "open_orders_sha256", |
| 492 | + "recent_executions_sha256", |
| 493 | + "local_execution_ledger_sha256", |
| 494 | + ) |
| 495 | + } |
| 496 | + enriched_observations = IBKRReconciliationObservations( |
| 497 | + account_scope=legacy_observations.account_scope, |
| 498 | + account_identity_match=legacy_observations.account_identity_match, |
| 499 | + positions=legacy_observations.positions, |
| 500 | + cash=legacy_observations.cash, |
| 501 | + open_orders=( |
| 502 | + { |
| 503 | + **legacy_observations.open_orders[0], |
| 504 | + "order_key": "ibkr-order-v1-example", |
| 505 | + "order_identity": {"account_scope_sha256": "digest", "client_id": "7", "order_id": "456"}, |
| 506 | + "cumulative_filled_quantity": 0.0, |
| 507 | + }, |
| 508 | + ), |
| 509 | + recent_executions=( |
| 510 | + { |
| 511 | + **legacy_observations.recent_executions[0], |
| 512 | + "order_key": "ibkr-order-v1-example", |
| 513 | + "order_identity": {"account_scope_sha256": "digest", "client_id": "7", "order_id": "456"}, |
| 514 | + "cumulative_filled_quantity": 1.0, |
| 515 | + }, |
| 516 | + ), |
| 517 | + ) |
| 518 | + |
| 519 | + def configured_env(name, default=None): |
| 520 | + if name == "IBKR_EXECUTION_STATE_DIR": |
| 521 | + return str(tmp_path) |
| 522 | + if name == "IBKR_RECONCILIATION_EXPECTED_DIGESTS_JSON": |
| 523 | + import json |
| 524 | + |
| 525 | + return json.dumps(expected) |
| 526 | + return default |
| 527 | + |
| 528 | + candidate = build_reconciliation_candidate( |
| 529 | + observations=enriched_observations, |
| 530 | + runtime_target=target, |
| 531 | + platform_id="ibkr", |
| 532 | + strategy_profile="soxl_soxx_trend_income", |
| 533 | + account_group="LIVE", |
| 534 | + project_id=None, |
| 535 | + env_reader=configured_env, |
| 536 | + ) |
| 537 | + |
| 538 | + assert candidate.permits_active_lkg is True |
0 commit comments