diff --git a/docs/user-guide/brokers.md b/docs/user-guide/brokers.md index 00f280b..84404c9 100644 --- a/docs/user-guide/brokers.md +++ b/docs/user-guide/brokers.md @@ -44,6 +44,7 @@ await broker.connect() - `7496` is the usual TWS live port - `4002` and `4001` are the usual paper/live IB Gateway ports - TWS or IB Gateway must be running with API access enabled before you connect +- Submitted orders use `DAY` time in force so TWS presets do not change their duration. ## Alpaca diff --git a/src/ml4t/live/brokers/ib.py b/src/ml4t/live/brokers/ib.py index b0d3f14..d500825 100644 --- a/src/ml4t/live/brokers/ib.py +++ b/src/ml4t/live/brokers/ib.py @@ -659,6 +659,8 @@ def _create_ib_order( else: raise ValueError(f"Unsupported order type: {order_type}") + # Send a concrete TIF so TWS presets cannot change it after submission. + order.tif = "DAY" # Honor the caller's extended-hours intent (no-op before this was set). order.outsideRth = outside_rth return order diff --git a/tests/unit/test_ib_broker.py b/tests/unit/test_ib_broker.py index 08eafe4..568e6e9 100644 --- a/tests/unit/test_ib_broker.py +++ b/tests/unit/test_ib_broker.py @@ -499,6 +499,8 @@ async def test_submit_limit_order(self, mock_ib_class): assert order.quantity == 50 assert order.order_type == OrderType.LIMIT assert order.limit_price == 150.0 + _, ib_order = mock_ib.placeOrder.call_args.args + assert ib_order.tif == "DAY" @pytest.mark.asyncio @patch("ml4t.live.brokers.ib.IB") @@ -713,6 +715,7 @@ async def test_submit_market_order_defaults_rth_only(self, mock_ib_class): _, ib_order = mock_ib.placeOrder.call_args.args assert ib_order.outsideRth is False + assert ib_order.tif == "DAY" @pytest.mark.asyncio @patch("ml4t.live.brokers.ib.IB") diff --git a/tests/unit/test_provider_contract.py b/tests/unit/test_provider_contract.py index fabe19f..cf9868e 100644 --- a/tests/unit/test_provider_contract.py +++ b/tests/unit/test_provider_contract.py @@ -226,7 +226,7 @@ def test_tampered_reported_contract_fails_closed( assert classification.reason == "reported contract does not match its evidence commit" -def test_pending_candidate_preserves_v012_ib_contract() -> None: +def test_ib_tif_change_invalidates_v012_evidence() -> None: repository = Path(__file__).resolve().parents[2] tag = subprocess.run( ["git", "cat-file", "-e", "v0.1.2^{commit}"], @@ -238,4 +238,5 @@ def test_pending_candidate_preserves_v012_ib_contract() -> None: pytest.skip("v0.1.2 history is required") classification = _classification(repository, "ib", "v0.1.2", "HEAD") - assert classification.reusable is True + assert classification.reusable is False + assert "src/ml4t/live/brokers/ib.py" in classification.changed_inputs