| self = <tests.test_buy_trader_long_short_payload.TestEndToEndPlaceOrderToAlpaca object at 0x11e9f6de0>
|
|
|
| def test_full_bundle_execution_with_mocked_alpaca(self):
|
| """
|
| Execute a full 6-symbol bundle (3 long + 3 short) and verify:
|
| - Correct symbols receive buy_market() calls
|
| - Correct symbols receive short() calls
|
| - No cross-contamination (long symbols not shorted, short symbols not bought)
|
| - Allocation amounts are positive
|
|
|
| Note: Stock splits may cause multiple calls per symbol (BUY_TRADER_STOCK_SPLIT_RATIOS).
|
| We verify by unique symbols, not by call count.
|
| """
|
| from rtrader.services.buy_trader_executor import BuyTraderExecutor
|
|
|
| # Create payload exactly as place-order would generate
|
| payload = create_test_payload_long_short(
|
| account_nick="yuchao",
|
| long_symbols=["AAPL", "MSFT", "GOOGL"],
|
| short_symbols=["SQQQ", "UVXY", "QID"],
|
| long_allocations=[0.30, 0.30, 0.40],
|
| short_allocations=[0.30, 0.30, 0.40],
|
| total_buying_power=10000.0,
|
| )
|
|
|
| # Create executor in direct mode (immediate execution, not plan mode)
|
| executor = BuyTraderExecutor(
|
| broker_backend="alpaca",
|
| execution_mode="direct",
|
| account_nick="yuchao",
|
| disable_split_sleep=True, # Disable sleep between split orders
|
| )
|
|
|
| # Create comprehensive mock broker
|
| mock_broker = MagicMock()
|
|
|
| # Track all calls for verification
|
| buy_calls = []
|
| short_calls = []
|
|
|
| def mock_buy_market(symbol, **kwargs):
|
| buy_calls.append({
|
| "symbol": symbol,
|
| "shares": kwargs.get("shares"),
|
| "amount": kwargs.get("amount"),
|
| })
|
| return {
|
| "status": "success",
|
| "order_id": f"buy_{symbol}_{len(buy_calls)}",
|
| "filled_qty": kwargs.get("shares", 1),
|
| }
|
|
|
| def mock_short(symbol, **kwargs):
|
| short_calls.append({
|
| "symbol": symbol,
|
| "shares": kwargs.get("shares"),
|
| "amount": kwargs.get("amount"),
|
| })
|
| return {
|
| "status": "success",
|
| "order_id": f"short_{symbol}_{len(short_calls)}",
|
| "filled_qty": kwargs.get("shares", 1),
|
| }
|
|
|
| def mock_get_quote(symbol):
|
| # Return different prices for different symbols
|
| prices = {
|
| "AAPL": 185.0,
|
| "MSFT": 420.0,
|
| "GOOGL": 175.0,
|
| "SQQQ": 12.0,
|
| "UVXY": 28.0,
|
| "QID": 18.0,
|
| }
|
| return {"last_trade_price": prices.get(symbol, 100.0)}
|
|
|
| mock_broker.buy_market.side_effect = mock_buy_market
|
| mock_broker.short.side_effect = mock_short
|
| mock_broker.get_quote.side_effect = mock_get_quote
|
|
|
| # Execute with mocked broker
|
| with patch.object(executor, '_resolve_broker', return_value=mock_broker), \
|
| patch.object(executor, 'fetch_buying_power', return_value=10000.0), \
|
| patch('rtrader.cli.buy_trader._resolve_trading_day', return_value=date.today()), \
|
| patch('rtrader.scheduler.trading_day_gap', return_value=0):
|
|
|
| result = executor.execute_job(payload)
|
|
|
| # ============================================================
|
| # VERIFICATION: Check correct symbols received correct calls
|
| # Note: Stock splits may cause multiple calls per symbol
|
| # ============================================================
|
|
|
| # Get unique symbols from each call type
|
| buy_symbols_unique = set(call["symbol"] for call in buy_calls)
|
| short_symbols_unique = set(call["symbol"] for call in short_calls)
|
|
|
| # Verify all 3 long symbols received buy_market calls
|
| > assert buy_symbols_unique == {"AAPL", "MSFT", "GOOGL"}, \
|
| f"Buy symbols should be AAPL, MSFT, GOOGL, got {buy_symbols_unique}"
|
| E AssertionError: Buy symbols should be AAPL, MSFT, GOOGL, got set()
|
| E assert set() == {'AAPL', 'GOOGL', 'MSFT'}
|
| E
|
| E Extra items in the right set:
|
| E 'MSFT'
|
| E 'AAPL'
|
| E 'GOOGL'
|
| E
|
| E Full diff:...
|
| E
|
| E ...Full output truncated (6 lines hidden), use '-vv' to show
|
|
|
| tests/test_buy_trader_long_short_payload.py:844: AssertionError
|