"""Execution engine end-to-end with a FAKE data provider + FAKE broker (no network, no IBKR). Proves the multi-strategy rebalance: netting → reconcile vs broker → whole-share orders → gates.""" from __future__ import annotations import numpy as np from fxhnt.application import ExecutionService from fxhnt.config import ExecutionSettings, Settings from fxhnt.domain.models import AssetClass, Market, PriceSeries, StrategySpec from fxhnt.domain.portfolio import Book, StrategyAllocation from fxhnt.ports.broker import AccountState, Order SPY = Market(symbol="SPY", asset_class=AssetClass.ETF) class FakeData: name = "fake" def fetch(self, market: Market, start=None, end=None) -> PriceSeries: close = 100.0 * np.cumprod(1.0 + np.full(400, 0.001)) # uptrend -> trend long return PriceSeries(market=market, dates=tuple(str(i) for i in range(400)), close=close) class FakeBroker: name = "fake" def __init__(self, state: AccountState) -> None: self.state = state self.placed: list[Order] = [] def account_state(self) -> AccountState: return self.state def place_order(self, order: Order) -> str: self.placed.append(order) return "Filled" def _book() -> Book: return Book(name="b", max_gross_leverage=1.0, allocations=[ StrategyAllocation(spec=StrategySpec(kind="trend", params={"window": 50}), market=SPY, weight=1.0), ]) def _settings() -> Settings: return Settings(execution=ExecutionSettings(allow_live=False)) def test_rebalance_from_flat_places_buy() -> None: broker = FakeBroker(AccountState(nlv=100_000, cash=100_000, positions={}, is_paper=True)) svc = ExecutionService(FakeData(), broker, _settings()) plan = svc.rebalance(_book(), execute=True) assert not plan.blocked assert plan.executed and len(broker.placed) == 1 o = broker.placed[0] assert o.symbol == "SPY" and o.side == "BUY" # ~100% of 100k at price ~149 (100*1.001^399) -> ~670 shares assert 600 < o.quantity < 750 def test_in_band_when_already_on_target_no_orders() -> None: # already holding ~full target -> hysteresis says do nothing broker = FakeBroker(AccountState(nlv=100_000, cash=0, positions={"SPY": 670}, is_paper=True)) svc = ExecutionService(FakeData(), broker, _settings()) plan = svc.rebalance(_book(), execute=True) assert not plan.executed and not broker.placed assert any("in band" in n for n in plan.notes) def test_live_account_blocked_without_allow_live() -> None: broker = FakeBroker(AccountState(nlv=100_000, cash=100_000, positions={}, is_paper=False)) svc = ExecutionService(FakeData(), broker, _settings()) plan = svc.rebalance(_book(), execute=True) assert plan.blocked and not broker.placed def test_inconsistent_account_data_blocks() -> None: # NLV says 100k but cash+positions says 60k -> 40% gap > data_tol -> refuse broker = FakeBroker(AccountState(nlv=100_000, cash=10_000, positions={}, is_paper=True, gross_position_value=50_000)) svc = ExecutionService(FakeData(), broker, _settings()) plan = svc.rebalance(_book(), execute=True) assert plan.blocked and not broker.placed class FractionalFakeBroker(FakeBroker): supports_fractional = True # Alpaca-style: the execution layer must size fractional shares def test_fractional_broker_places_precise_fractional_quantity() -> None: # A broker that supports fractional shares gets the EXACT target quantity (not whole-share rounded), # so the book implements its weights precisely — the whole point of the Alpaca leg. broker = FractionalFakeBroker(AccountState(nlv=100_000, cash=100_000, positions={}, is_paper=True)) svc = ExecutionService(FakeData(), broker, _settings()) plan = svc.rebalance(_book(), execute=True) assert plan.executed and len(broker.placed) == 1 q = broker.placed[0].quantity assert 600.0 < q < 750.0 assert q != int(q) # FRACTIONAL — has a decimal part, not the whole-share integer assert q == round(q, 6) # sized to the fractional precision, not arbitrary float noise def test_rebalance_weights_places_from_precomputed_targets() -> None: # The multistrat bridge: rebalance to PRE-COMPUTED weights + caller-supplied prices (no book/data fetch), # reusing the same gates + fractional sizing. 60/40 of 100k at $100 → 600 / 400 shares. broker = FractionalFakeBroker(AccountState(nlv=100_000, cash=100_000, positions={}, is_paper=True)) svc = ExecutionService(FakeData(), broker, _settings()) plan = svc.rebalance_weights("multistrat", {"SPY": 0.6, "IEF": 0.4}, {"SPY": 100.0, "IEF": 100.0}, max_gross=1.5, execute=True) assert not plan.blocked and plan.executed placed = {o.symbol: o.quantity for o in broker.placed} assert placed == {"SPY": 600.0, "IEF": 400.0} def test_entry_floor_uses_sizing_capital_not_full_nlv() -> None: # Regression (the IGLN gold-sleeve drop): the book is sized to a $100k envelope on a $1M paper account, so # weights come pre-scaled by envelope/nlv (=0.1). A 3% sleeve = $3k of the envelope. entry_floor is 0.5%: # on the envelope that's $500 (place), but on the raw $1M NLV it's $5k (would silently drop the sleeve). scaled = {"SPY": 0.097 * 0.1, "GLD": 0.030 * 0.1} # envelope-scaled (scale = 100k/1M) prices = {"SPY": 800.0, "GLD": 80.0} # GLD order ≈ $3,000 (between $500 and $5,000) # WITH sizing_capital (the fix): the 3% sleeve survives the envelope-based floor. b1 = FractionalFakeBroker(AccountState(nlv=1_000_000, cash=1_000_000, positions={}, is_paper=True)) ExecutionService(FakeData(), b1, _settings()).rebalance_weights( "multistrat", scaled, prices, max_gross=1.5, execute=True, sizing_capital=100_000) assert "GLD" in {o.symbol for o in b1.placed} # WITHOUT it: the same sleeve is dropped by the NLV-based floor — the bug the fix addresses. b2 = FractionalFakeBroker(AccountState(nlv=1_000_000, cash=1_000_000, positions={}, is_paper=True)) ExecutionService(FakeData(), b2, _settings()).rebalance_weights( "multistrat", scaled, prices, max_gross=1.5, execute=True) assert "GLD" not in {o.symbol for o in b2.placed} def test_rebalance_weights_blocks_when_gross_exceeds_max() -> None: broker = FractionalFakeBroker(AccountState(nlv=100_000, cash=100_000, positions={}, is_paper=True)) svc = ExecutionService(FakeData(), broker, _settings()) plan = svc.rebalance_weights("x", {"SPY": 1.2, "IEF": 0.5}, {"SPY": 100.0, "IEF": 100.0}, max_gross=1.5, execute=True) assert plan.blocked and not broker.placed # gross 1.7 > 1.5 → refuse def test_rebalance_weights_us_path_order_uses_default_contract_fields() -> None: """--venue us (no contract_specs): the placed Order carries the SMART/USD defaults and NO secId — the US path is byte-unchanged.""" broker = FractionalFakeBroker(AccountState(nlv=100_000, cash=100_000, positions={}, is_paper=True)) svc = ExecutionService(FakeData(), broker, _settings()) plan = svc.rebalance_weights("multistrat", {"SPY": 0.6}, {"SPY": 100.0}, max_gross=1.5, execute=True) assert plan.executed o = broker.placed[0] assert o.exchange == "SMART" and o.currency == "USD" assert o.sec_id_type is None and o.sec_id is None def test_rebalance_weights_ucits_path_enriches_order_with_contract_spec() -> None: """--venue ucits: contract_specs enrich the placed Order with the resolving ISIN/exchange/currency, and sizing uses the caller-supplied (REAL UCITS) price.""" broker = FractionalFakeBroker(AccountState(nlv=100_000, cash=100_000, positions={}, is_paper=True)) svc = ExecutionService(FakeData(), broker, _settings()) specs = {"CSPX": {"exchange": "SMART", "currency": "USD", "sec_id_type": "ISIN", "sec_id": "IE00B5BMR087"}} plan = svc.rebalance_weights("multistrat", {"CSPX": 0.6}, {"CSPX": 620.0}, max_gross=1.5, execute=True, contract_specs=specs) assert plan.executed o = broker.placed[0] assert o.symbol == "CSPX" assert o.sec_id_type == "ISIN" and o.sec_id == "IE00B5BMR087" assert o.currency == "USD" # sized from the REAL UCITS price 620 (60% of 100k / 620 ≈ 96.77), NOT a US price assert abs(o.quantity - (0.6 * 100_000 / 620.0)) < 1e-6