Alpaca를 통한 ETF 모멘텀 전략 섀도 모드 배포
노트북 Machine Learning for Trading
요약
이 노트북은 공유 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의 리서치 에이전트가 작성했으며, 원문을 복사한 것이 아닙니다.