Теневой запуск стратегии моментума ETF через Alpaca
Сводка
В ноутбуке показано подключение общего интерфейса стратегии моментума ETF к Alpaca; по умолчанию доступен автономный симулированный режим. Объясняется, как можно менять адаптеры брокера и рыночных данных, сохраняя неизменными стратегию, журнал ордеров и оболочку теневого режима. Перед подключением к реальному брокерскому демо-счёту отдельно проверяются наличие необязательной библиотеки SDK, учётных данных и состояние счёта; снимок счёта содержит денежные средства, капитал и позиции. Теневой режим записывает предполагаемые ордера, не отправляя их.
Автономный запуск позволяет работать с интерфейсом стратегии и журналированием без счёта, но не подтверждает особенности работы с Alpaca. Детерминированные симулированные бары не проверяют реальные исполнения, задержки, отклонения ордеров, устаревшие цены или сбои соединения. В ноутбуке эти ограничения явно отделены от возможностей режима; рекомендуется поэтапная операционная проверка, начиная с наблюдения в теневом режиме и только затем переходя к маршрутизации демо-ордеров. В примере используется моментум цены ETF, но документ прежде всего демонстрирует развёртывание и безопасность, а не доказывает прибыльность торговли.
Ключевые идеи
- Адаптер брокера можно менять, сохраняя общий интерфейс стратегии и журналирование ордеров.
- Проверяй доступность SDK, учётные данные и готовность счёта как отдельные условия для развёртывания.
- Теневой режим записывает предполагаемые ордера, не передавая их брокеру.
- Автономная симуляция демонстрирует интерфейсы и журналирование, но не проверяет реальные исполнения, задержки или сбои брокера.
- Пример показывает механику развёртывания, но не доказывает прибыльность стратегии моментума.
Теги
Полный текст
# Alpaca Paper Trading Demo
# Alpaca Paper Trading Demo
**Docker image**: `ml4t`
**Book Reference**: Chapter 25, Section 25.3 (Integrating with Alpaca)
[`03_ib_paper_trading_demo`](03_ib_paper_trading_demo.ipynb) connected the same strategy to
Interactive Brokers. This notebook connects it to Alpaca, and the interesting part is how little
changes: a different broker class, a different feed class, the same `Strategy`, the same shadow
mode, the same order log. The two brokers differ in what they cost to reach, not in what the
strategy has to know about them.
It also runs, by default, without an Alpaca account at all. The offline path uses a simulated
broker over deterministic bars, which is what makes this notebook executable in continuous
integration and readable by anyone who has not signed up for anything. Everything that path
demonstrates about the strategy interface is real; everything it would demonstrate about Alpaca
is not, and the notebook is explicit about which is which at each step.
**Learning Objectives**
- Check the environment, the SDK and the account state before a strategy is allowed near a
broker, and treat each as a separate gate
- Wrap a strategy in shadow mode, so orders are logged and never sent
- Tell what a run established from what it merely exercised, when the broker was simulated
**Prerequisites**: [`01_unified_framework_demo`](01_unified_framework_demo.ipynb) for the shared
`Strategy` interface. An Alpaca paper account and `ALPACA_API_KEY` / `ALPACA_SECRET_KEY` are
needed only for the live path, which is off by default.
```python
"""Connect ml4t strategies to Alpaca with shadow-mode risk controls."""
import asyncio
import logging
import os
import warnings
from datetime import UTC, datetime, timedelta
import numpy as np
import polars as pl
from async_utils import run_async
from ml4t.backtest import OrderSide, Strategy
from utils.paths import display_path, get_output_dir
from utils.reproducibility import set_global_seeds
# The broker adapters pull in websockets' legacy module, which deprecates itself on import, so
# the filter has to be in force before the import rather than after it. It is the library's
# business rather than this notebook's and nothing in the result depends on it.
warnings.filterwarnings("ignore", category=DeprecationWarning, module=r"websockets\.legacy")
# alpaca-py is an optional broker SDK; the simulated path runs without it. The
# try/except is the only such optional import in the notebook.
HAS_ALPACA_SDK = False
try:
import alpaca # noqa: F401
from alpaca.trading.client import TradingClient
from ml4t.live import AlpacaBroker, AlpacaDataFeed, LiveEngine, LiveRiskConfig
from ml4t.live.safety import SafeBroker
HAS_ALPACA_SDK = True
except ImportError:
pass
# Give this notebook its own logger rather than configuring the root one: basicConfig is a no-op
# once an imported library has attached a handler, and reconfiguring the root would either do
# nothing or print every line twice.
logger = logging.getLogger("alpaca_paper_demo")
logger.setLevel(logging.INFO)
logger.propagate = False
if not logger.handlers:
_handler = logging.StreamHandler()
_handler.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
logger.addHandler(_handler)
logging.getLogger("alpaca").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
if HAS_ALPACA_SDK:
print("[OK] ml4t.live Alpaca components imported")
else:
print("Alpaca SDK not installed (uv add alpaca-py); running simulation only")
```
```python
DEMO_DURATION_SECONDS = 60
MAX_SYMBOLS = 0
SIMULATION_STEPS = 10
LIVE_FEED = 0 # explicit opt-in; default execution is offline and paper-safe
SEED = 42
```
One environment override before anything else. Alpaca's WebSocket loop and the nested event loop
a headless notebook runner installs do not cooperate: `asyncio.wait_for` cannot reliably cancel
the inner streaming task, so an unattended run would sit past its own duration limit instead of
finishing. When the runner announces itself, the live feed is turned off and the simulated path
runs. An interactive Jupyter session is unaffected.
Forcing the safe path in the environment that cannot supervise itself is the right default, and
the run says so in its output rather than silently taking a different branch.
```python
if os.environ.get("ML4T_HEADLESS_PAPERMILL") == "1":
LIVE_FEED = 0
print("Headless papermill detected: LIVE_FEED disabled, simulated path will run")
set_global_seeds(SEED)
ALPACA_API_KEY = os.environ.get("ALPACA_API_KEY", "")
ALPACA_SECRET_KEY = os.environ.get("ALPACA_SECRET_KEY", "")
PAPER_TRADING = True
ALL_SYMBOLS = ["SPY", "QQQ", "IWM"]
SYMBOLS = ALL_SYMBOLS[:MAX_SYMBOLS] if MAX_SYMBOLS > 0 else ALL_SYMBOLS.copy()
```
## 1. Credential Verification
Alpaca requires API keys for authentication. For paper trading:
- Sign up at https://alpaca.markets
- Generate API keys in the dashboard
- Set environment variables (never hardcode!)
```python
def verify_credentials():
"""Check that API credentials and SDK are available."""
if not HAS_ALPACA_SDK:
print("\n" + "=" * 60)
print("ALPACA SDK NOT INSTALLED")
print("=" * 60)
print("\nTo connect to Alpaca, install the SDK:")
print(" uv add alpaca-py")
print("\nRunning in DEMO MODE with simulated broker...")
return False
if not ALPACA_API_KEY or not ALPACA_SECRET_KEY:
print("\n" + "=" * 60)
print("ALPACA CREDENTIALS NOT FOUND")
print("=" * 60)
print("\nTo run this notebook with real Alpaca connection:")
print("1. Create an Alpaca account at https://alpaca.markets")
print("2. Generate API keys in the dashboard")
print("3. Set environment variables:")
print(" export ALPACA_API_KEY='PKXXXXXXXX'")
print(" export ALPACA_SECRET_KEY='xxxxxxxxxx'")
print("\nRunning in DEMO MODE with simulated broker...")
return False
print("\n" + "=" * 60)
print("ALPACA CREDENTIALS VERIFIED")
print("=" * 60)
print(f"Paper Trading: {'YES' if PAPER_TRADING else 'NO (LIVE!)'}")
return True
HAS_CREDENTIALS = verify_credentials()
```
**Finding**: The credential gate explicitly distinguishes missing SDKs from missing secrets, which makes
the execution mode observable before the notebook touches a broker connection.
**Trading implication**: Live notebooks should never hide whether they are authenticated, shadowing, or
fully simulated because that status determines the operational risk of every downstream action.
## 2. Connect to Alpaca
The AlpacaBroker class handles:
- REST API for orders and account info
- WebSocket for real-time order updates
- Automatic position/order synchronization
```python
def get_alpaca_account_snapshot():
"""Fetch an account snapshot without starting the streaming broker session."""
if not HAS_CREDENTIALS:
return None, None, None, None
print("\n" + "=" * 60)
print("CONNECTING TO ALPACA")
print("=" * 60)
trading_client = TradingClient(
api_key=ALPACA_API_KEY,
secret_key=ALPACA_SECRET_KEY,
paper=PAPER_TRADING,
)
account = trading_client.get_account()
raw_positions = trading_client.get_all_positions()
broker = AlpacaBroker(
api_key=ALPACA_API_KEY,
secret_key=ALPACA_SECRET_KEY,
paper=PAPER_TRADING,
)
print("\nConnected to Alpaca")
print(f" Paper Trading: {'YES' if PAPER_TRADING else 'NO'}")
nlv = float(account.equity)
cash = float(account.cash)
print("\nACCOUNT READINESS")
print(" Account values received: [OK]")
# Get positions
positions = {
position.symbol: {
"quantity": float(position.qty),
"entry_price": float(position.avg_entry_price),
"current_price": float(position.current_price or position.avg_entry_price),
}
for position in raw_positions
}
print(f" Open positions received: {len(positions)}")
return broker, nlv, cash, positions
# Run connection
if LIVE_FEED and HAS_CREDENTIALS and HAS_ALPACA_SDK:
try:
broker, nlv, cash, positions = get_alpaca_account_snapshot()
except Exception as e:
print(f"\nConnection failed: {e}")
print("\nTroubleshooting:")
print("1. Are your API keys correct?")
print("2. Is your account enabled for paper trading?")
print("3. Check https://status.alpaca.markets for outages")
raise RuntimeError("Alpaca paper session unavailable") from e
else:
broker = None
print("Offline mode selected; no Alpaca account request was made.")
```
**Finding**: The account summary confirms that the broker adapter exposes the same cash, equity, and
position state the strategy will rely on later in the session.
**Trading implication**: A live strategy should always prove that its broker snapshot is sane before it
starts listening to market data; otherwise even correct signals can be routed with stale inventory.
## 3. ETF Momentum Strategy
This strategy is **identical** to what we use in backtesting.
Uses the ETF case study with SPY, QQQ, IWM.
```python
class ETFMomentumStrategy(Strategy):
"""ETF momentum strategy for live trading.
Tracks 5-day momentum across ETFs and generates signals
when momentum crosses thresholds.
This is the SAME code used in backtest - zero changes for live!
"""
def __init__(self, lookback: int = 5, threshold: float = 0.02, position_size: int = 10):
self.lookback = lookback
self.threshold = threshold
self.position_size = position_size
self.prices: dict[str, list[float]] = {}
self.signals: list[dict] = []
def on_start(self, broker):
"""Called when engine starts."""
logger.info(f"Strategy started: ETFMomentum(lookback={self.lookback})")
for symbol in SYMBOLS:
self.prices[symbol] = []
def on_data(self, timestamp: datetime, data: dict, context: dict, broker):
"""Called for each bar.
Args:
timestamp: Bar timestamp
data: {symbol: {'open', 'high', 'low', 'close', 'volume'}}
context: Additional metadata (vwap, trade_count, etc.)
broker: Broker instance (sync interface)
"""
for symbol, bar in data.items():
if symbol not in self.prices:
self.prices[symbol] = []
close = bar["close"]
self.prices[symbol].append(close)
# Calculate momentum
if len(self.prices[symbol]) > self.lookback:
old_price = self.prices[symbol][-self.lookback - 1]
momentum = (close - old_price) / old_price
# Get current position
position = broker.get_position(symbol)
has_position = position is not None and position.quantity > 0
# Generate signals
if momentum > self.threshold and not has_position:
signal = {
"timestamp": timestamp,
"symbol": symbol,
"action": "BUY",
"momentum": momentum,
"price": close,
}
self.signals.append(signal)
logger.info(f"BUY {symbol}: Momentum {momentum:.2%} > {self.threshold:.2%}")
broker.submit_order(symbol, self.position_size, side=OrderSide.BUY)
elif momentum < -self.threshold and has_position:
signal = {
"timestamp": timestamp,
"symbol": symbol,
"action": "SELL",
"momentum": momentum,
"price": close,
}
self.signals.append(signal)
logger.info(f"SELL {symbol}: Momentum {momentum:.2%} < -{self.threshold:.2%}")
broker.submit_order(symbol, self.position_size, side=OrderSide.SELL)
def on_end(self, broker):
"""Called when engine stops."""
logger.info(f"Strategy ended. Signals generated: {len(self.signals)}")
```
### Structured Signal and Order Log
Both the live and simulated paths accumulate signals and order intentions into a Polars frame so reviewers
can inspect what the strategy decided at every step, in the same shape, regardless of whether the broker is
Alpaca or the in-notebook mock.
```python
def signals_to_frame(signals: list[dict]) -> pl.DataFrame:
"""Render the strategy's signal accumulator as a Polars frame for display."""
if not signals:
return pl.DataFrame(
schema={
"timestamp": pl.Datetime,
"symbol": pl.String,
"action": pl.String,
"momentum": pl.Float64,
"price": pl.Float64,
}
)
return pl.DataFrame(signals)
```
## 4. Safe Broker Configuration
Before going live, we wrap the broker with `SafeBroker` which provides:
- Shadow mode (virtual orders only)
- Position limits
- Order rate limiting
- Kill switch
```python
def create_safe_broker(underlying_broker):
"""Create SafeBroker with risk configuration."""
risk_state_path = get_output_dir(25, "alpaca_paper_demo") / "risk_state.json"
risk_config = LiveRiskConfig(
shadow_mode=True, # CRITICAL: Virtual orders only!
max_position_value=50_000.0,
max_order_value=10_000.0,
max_orders_per_minute=10,
dedup_window_seconds=0.0, # Disable for demo
state_file=str(risk_state_path),
)
safe_broker = SafeBroker(underlying_broker, risk_config)
print("\n" + "=" * 60)
print("RISK CONFIGURATION (SHADOW MODE)")
print("=" * 60)
print(" Shadow Mode: ENABLED (no real orders)")
print(f" Max Position Value: ${risk_config.max_position_value:,.0f}")
print(f" Max Order Value: ${risk_config.max_order_value:,.0f}")
print(f" Rate Limit: {risk_config.max_orders_per_minute}/minute")
print(f" Risk State: {display_path(risk_state_path)}")
return safe_broker, risk_config
```
**Finding**: The risk-configuration printout makes shadow mode and exposure limits visible before the live
feed starts emitting data.
**Trading implication**: Broker wrappers should surface their active limits explicitly because a live
rollout is only as safe as the controls that are actually enabled at runtime.
## 5. Real-Time Data Feed
AlpacaDataFeed subscribes to real-time market data:
- **bars**: OHLCV aggregates (1-minute default)
- **quotes**: Bid/ask with sizes
- **trades**: Individual trades
Data sources:
- **IEX**: Free, 15-min delayed for some symbols
- **SIP**: Premium, real-time from all exchanges
### Simulated Path: Flat Dict Portfolio
When credentials or the SDK are missing, the demo runs against a tiny in-notebook broker. The portfolio is a
flat dict (`{"cash": float, "positions": {symbol: {qty, entry_price}}}`) rather than nested dataclasses so
the simulated state can be read directly into a Polars frame for display alongside the live path.
```python
class MockBroker:
"""Minimal sync broker for the no-credential simulation path.
The portfolio is a flat dict to keep the simulation state inspectable; it is
intentionally not a SafeBroker wrapper because shadow-mode requires fresh
quotes that the simulation does not produce.
"""
REF_PRICES = {"SPY": 600.0, "QQQ": 520.0, "IWM": 225.0}
def __init__(self, initial_cash: float = 100_000.0):
self.portfolio = {"cash": initial_cash, "positions": {}}
self.order_log: list[dict] = []
self.current_prices = dict(self.REF_PRICES)
self.current_timestamp = datetime(2025, 1, 2, tzinfo=UTC)
def update_market(self, timestamp: datetime, prices: dict[str, float]) -> None:
"""Update the simulated market used for subsequent fills."""
self.current_timestamp = timestamp
self.current_prices.update(prices)
def get_position(self, symbol: str):
pos = self.portfolio["positions"].get(symbol)
if pos is None:
return None
class _PosView:
quantity = pos["quantity"]
return _PosView()
def submit_order(self, asset: str, quantity: int, side=None, **kwargs) -> dict:
price = self.current_prices[asset]
if isinstance(side, OrderSide):
side_name = side.value.upper()
elif side is None:
side_name = "BUY"
else:
side_name = str(getattr(side, "value", side)).upper()
status = "rejected"
if side is None or side == OrderSide.BUY:
cost = quantity * price
if cost <= self.portfolio["cash"]:
self.portfolio["cash"] -= cost
pos = self.portfolio["positions"].get(asset)
if pos is None:
self.portfolio["positions"][asset] = {
"quantity": quantity,
"entry_price": price,
}
else:
total_qty = pos["quantity"] + quantity
avg = (pos["quantity"] * pos["entry_price"] + quantity * price) / total_qty
self.portfolio["positions"][asset] = {
"quantity": total_qty,
"entry_price": avg,
}
status = "filled"
elif side == OrderSide.SELL:
pos = self.portfolio["positions"].get(asset)
if pos is not None and pos["quantity"] >= quantity:
self.portfolio["cash"] += quantity * price
remaining = pos["quantity"] - quantity
if remaining == 0:
del self.portfolio["positions"][asset]
else:
self.portfolio["positions"][asset] = {
"quantity": remaining,
"entry_price": pos["entry_price"],
}
status = "filled"
else:
status = "unsupported"
order = {
"order_id": f"SIM-{len(self.order_log) + 1}",
"timestamp": self.current_timestamp,
"symbol": asset,
"side": side_name,
"quantity": quantity,
"price": price,
"status": status,
}
self.order_log.append(order)
return order
```
### Live Path: Engine Wiring
The live path constructs `AlpacaDataFeed > SafeBroker > LiveEngine`. Each helper is small enough to inspect
at a glance; `run_engine_for_duration` is the only async piece, and `display_engine_results` is purely
read-only post-processing.
```python
def create_alpaca_engine(strategy):
"""Build the AlpacaDataFeed, SafeBroker, and LiveEngine wiring."""
safe_broker, _ = create_safe_broker(broker)
feed = AlpacaDataFeed(
api_key=ALPACA_API_KEY,
secret_key=ALPACA_SECRET_KEY,
symbols=SYMBOLS,
data_type="bars",
feed="iex",
)
engine = LiveEngine(strategy=strategy, broker=safe_broker, feed=feed)
# Alpaca SDK retries aggressively under nest_asyncio; quiet the retry logs.
for name in [
"alpaca",
"alpaca.data",
"alpaca.data.live",
"alpaca.data.live.websocket",
"alpaca.trading.stream",
"websockets",
]:
logging.getLogger(name).setLevel(logging.CRITICAL)
return engine, safe_broker, feed
```
```python
async def run_engine_for_duration(engine, duration_s: int):
"""Connect the engine and let it stream for at most `duration_s` seconds."""
await asyncio.wait_for(engine.connect(), timeout=10)
try:
await asyncio.wait_for(engine.run(), timeout=duration_s)
except TimeoutError:
print(f"Demo duration ({duration_s}s) reached")
```
```python
def display_engine_results(strategy, safe_broker, feed, engine):
"""Print engine stats and render the strategy's signal log as a Polars frame."""
print("Engine stats:", {k: engine.stats[k] for k in list(engine.stats)[:6]})
print("Feed stats: ", {k: feed.stats[k] for k in list(feed.stats)[:6]})
vp = safe_broker._virtual_portfolio
print(f"Virtual Portfolio cash: ${vp.cash:,.2f}")
for symbol, pos in vp.positions.items():
value = pos.quantity * (pos.current_price or pos.entry_price)
print(f" {symbol}: {pos.quantity} shares @ ${pos.entry_price:.2f} = ${value:,.2f}")
print(f"\nSignals: {len(strategy.signals)}")
return signals_to_frame(strategy.signals)
```
```python
async def run_live_demo_with_feed():
"""Run the explicitly selected Alpaca or offline simulation path."""
if not LIVE_FEED:
print(f"LIVE_FEED={LIVE_FEED}: running the offline simulation")
return await run_simulated_demo()
if not HAS_CREDENTIALS or broker is None:
raise RuntimeError("LIVE_FEED requires the Alpaca SDK and paper credentials")
print("LIVE TRADING DEMO (Shadow Mode)")
strategy = ETFMomentumStrategy(lookback=5, threshold=0.02, position_size=10)
engine, safe_broker, feed = create_alpaca_engine(strategy)
print(f"Starting live engine for {DEMO_DURATION_SECONDS}s; watching {', '.join(SYMBOLS)}")
try:
await run_engine_for_duration(engine, DEMO_DURATION_SECONDS)
finally:
feed.stop()
return display_engine_results(strategy, safe_broker, feed, engine), pl.DataFrame()
```
```python
async def run_simulated_demo() -> tuple[pl.DataFrame, pl.DataFrame]:
"""Run the strategy against the flat-dict MockBroker; return the signal log."""
print("SIMULATED DEMO (no live Alpaca feed)")
strategy = ETFMomentumStrategy(lookback=3, threshold=0.01, position_size=10)
mock_broker = MockBroker()
strategy.on_start(mock_broker)
set_global_seeds(SEED)
base_prices = dict(MockBroker.REF_PRICES)
start = datetime(2025, 1, 2, 14, 30, tzinfo=UTC)
for step in range(SIMULATION_STEPS):
timestamp = start + timedelta(minutes=step)
data = {}
for symbol in SYMBOLS:
base_prices[symbol] *= 1 + np.random.normal(0.001, 0.01)
data[symbol] = {
"open": base_prices[symbol] * 0.999,
"high": base_prices[symbol] * 1.002,
"low": base_prices[symbol] * 0.998,
"close": base_prices[symbol],
"volume": int(np.random.randint(100000, 1000000)),
}
mock_broker.update_market(timestamp, {symbol: bar["close"] for symbol, bar in data.items()})
strategy.on_data(timestamp, data, {}, mock_broker)
strategy.on_end(mock_broker)
print(f"Simulated cash: ${mock_broker.portfolio['cash']:,.2f}")
for symbol, pos in mock_broker.portfolio["positions"].items():
value = pos["quantity"] * pos["entry_price"]
print(f" {symbol}: {pos['quantity']} shares @ ${pos['entry_price']:.2f} = ${value:,.2f}")
print(f"Signals: {len(strategy.signals)}; Orders: {len(mock_broker.order_log)}")
signal_frame = signals_to_frame(strategy.signals)
order_frame = (
pl.DataFrame(mock_broker.order_log)
if mock_broker.order_log
else pl.DataFrame(
schema={
"order_id": pl.String,
"timestamp": pl.Datetime,
"symbol": pl.String,
"side": pl.String,
"quantity": pl.Int64,
"price": pl.Float64,
"status": pl.String,
}
)
)
return signal_frame, order_frame
```
```python
# Run the demo
demo_signal_log, demo_order_log = run_async(run_live_demo_with_feed())
demo_signal_log
```
```python
demo_order_log
```
**Finding**: The selected execution mode is explicit. The offline path exercises the same strategy interface
with an inspectable broker, while the opt-in live path adds `SafeBroker` and Alpaca transport controls.
**Trading implication**: Keeping the live and simulated paths structurally aligned makes it easier to
detect true broker-side issues instead of debugging differences introduced by the demo environment.
## 6. Order Type Demonstrations
Alpaca supports various order types:
- **MARKET**: Execute immediately at best available price
- **LIMIT**: Execute at specified price or better
- **STOP**: Trigger market order when price reaches stop
- **STOP_LIMIT**: Trigger limit order when price reaches stop
```python
async def demonstrate_order_types():
"""Demonstrate different order types (shadow mode)."""
if not HAS_CREDENTIALS or broker is None:
print("\nSkipping order demo - no credentials")
return
if not LIVE_FEED:
# SafeBroker requires fresh market data for staleness checks; without the
# live WebSocket feed there are no recent quotes, so submit_order_async
# raises RiskLimitError. Skip the demo under headless papermill.
print(f"\nLIVE_FEED={LIVE_FEED}: skipping order-type demo (needs live market data)")
return
print("\n" + "=" * 60)
print("ORDER TYPE DEMONSTRATIONS (Shadow Mode)")
print("=" * 60)
safe_broker, _ = create_safe_broker(broker)
from ml4t.backtest.types import OrderType
# Market order
print("\n1. MARKET ORDER")
order = await safe_broker.submit_order_async("SPY", 10, side=OrderSide.BUY)
print(f" Order ID: {order.order_id}")
print(" Type: MARKET")
print(f" Status: {order.status.value}")
# Limit order
print("\n2. LIMIT ORDER")
order = await safe_broker.submit_order_async(
"QQQ", 5, side=OrderSide.BUY, order_type=OrderType.LIMIT, limit_price=500.00
)
print(f" Order ID: {order.order_id}")
print(" Type: LIMIT @ $500.00")
print(f" Status: {order.status.value}")
# Stop order
print("\n3. STOP ORDER")
order = await safe_broker.submit_order_async(
"IWM", 10, side=OrderSide.SELL, order_type=OrderType.STOP, stop_price=220.00
)
print(f" Order ID: {order.order_id}")
print(" Type: STOP @ $220.00")
print(f" Status: {order.status.value}")
# Show virtual portfolio
vp = safe_broker._virtual_portfolio
print("\nVirtual Portfolio After Orders:")
print(f" Cash: ${vp.cash:,.2f}")
for symbol, pos in vp.positions.items():
print(f" {symbol}: {pos.quantity} shares")
run_async(demonstrate_order_types())
```
**Finding**: The optional live path defines shadow-mode examples for each order type. The default offline
run skips those submissions explicitly because it has no Alpaca client, live feed, or `SafeBroker`.
**Trading implication**: After credentials and a live paper feed are available, shadow-mode submission is
the intermediate stage that tests routing, validation, and guardrails without creating exposure.
## 7. Clean Shutdown
The session should end with an explicit disconnect so the next run starts from a known broker state instead
of inheriting stale subscriptions or session assumptions.
```python
# Disconnect from Alpaca
if broker is not None:
run_async(broker.disconnect())
print("\nDisconnected from Alpaca")
```
## Summary
This notebook defines the complete optional Alpaca integration workflow and executes the deterministic
offline strategy/broker path by default. With `LIVE_FEED=True`, credentials, and the SDK available, it can
additionally exercise:
1. **Authentication**: API key/secret via environment variables
2. **Connection**: Paper trading account access
3. **Account Info**: Query equity, cash, positions
4. **Real-Time Feed**: Subscribe to bars/quotes/trades
5. **Safe Trading**: Use SafeBroker in shadow mode
6. **Order Types**: Market, limit, stop orders
### Alpaca vs IB Comparison
| Feature | Alpaca | Interactive Brokers |
|---------|--------|---------------------|
| Minimum Balance | None | None (but higher margin reqs) |
| Commissions | Free | $0-1 per trade |
| Real-time Data | Free (IEX) | Paid subscription |
| API Complexity | Simple REST | Complex socket protocol |
| Crypto | Yes (24/7) | Limited |
| Paper Trading | Yes | Yes |
### Next Steps
1. Run in shadow mode for 1-2 weeks
2. Verify signals match backtest expectations
3. Enable paper trading (`shadow_mode=False`)
4. Monitor for 2-4 weeks on paper
5. Gradually transition to live with small positions
### Crypto Trading
See `05_alpaca_crypto_live_demo.py` for 24/7 crypto trading demonstration.
```python
print("\n" + "=" * 60)
print("ALPACA PAPER TRADING DEMO COMPLETE")
print("=" * 60)
print(f"Symbols: {', '.join(SYMBOLS)}")
print(f"Paper Trading: {'YES' if PAPER_TRADING else 'NO'}")
shadow_mode_state = "ENABLED" if LIVE_FEED else "NOT ACTIVE (offline simulation)"
print(f"Shadow Mode: {shadow_mode_state}")
execution_mode = "Alpaca shadow feed" if LIVE_FEED else "offline simulation"
print(f"Execution Mode: {execution_mode}")
print("The same ETFMomentumStrategy interface drives the selected execution path.")
```
## Key Takeaways
1. **The broker is the smallest part of the change.** Moving from Interactive Brokers to Alpaca
swaps a broker class and a feed class. The strategy, the risk wrapper, the order log and the
engine are the same objects. That is what a broker abstraction is for, and it is the reason
the choice of venue is an operational decision rather than a research one.
2. **Check the environment, the SDK and the account separately.** They fail differently and are
fixed differently: a missing key is a setup problem, a missing package is an install problem,
and a rejected account is an entitlement problem. One combined "connection failed" hides all
three.
3. **Shadow mode is where a live deployment should start.** Real prices, real account state,
real strategy, and orders that are logged instead of sent. Everything except the one step
that cannot be undone.
4. **Say which parts of a run were real.** This one ran offline, so it established that the
strategy interface and the order log work, and established nothing whatever about Alpaca's
fills, latency or rejections. A demonstration that does not distinguish the two teaches a
reader to trust the wrong half.
**Known limitations of what is built here.** The default path never contacts Alpaca, so none of
the failure modes that matter live - a rejected order, a partial fill, a disconnected socket, a
stale quote - appears anywhere in it. The simulated bars are drawn from a fixed seed rather than
from a market. And shadow mode proves that orders were not sent, not that they would have been
filled at the prices assumed.
**Next**: [`05_alpaca_crypto_live_demo`](05_alpaca_crypto_live_demo.ipynb) takes the same
deployment pattern to a market that never closes, which removes the session boundaries this
notebook could rely on.Полный текст с указанием источника опубликован на условиях его лицензии. Лицензия: MIT
Это краткое изложение подготовлено исследовательским агентом Stratmill по оригиналу и не является его копией.