通过现货经纪商部署加密货币Z分数策略
笔记本 《交易机器学习》
总结
本演示介绍一个连接至 Alpaca USD现货市场、持续运行的加密货币交易循环。它将永续期货案例研究的资产范围映射到该交易场所支持的现货交易对,并明确说明只有其中一部分可在该处交易。示例信号是收盘到收盘动量的滚动Z分数,用作永续期货与现货价差的代理信号。笔记说明,生产版本会改为根据永续期货和现货价格计算价差,并将资金费率窗口的时间戳统一转换为UTC,以便进行运行日志记录。
本笔记区分模拟经纪商仿真与使用凭证连接经纪商的方式,并说明现货持仓不会获得永续掉期资金费。笔记还指出持续运行所需的监控、重连和周末风险管理。这项演示用于说明运行流程,并非完整的资金费策略:它没有实现溢价指数数据流或永续期货资金费现金流;交易场所的覆盖范围可能变化;仿真执行也不是经纪商影子模式。
核心观点
- 交易场所的上市范围决定策略资产中的哪些可以经由经纪商下单。
- 演示信号是动量Z分数代理,并非永续期货与现货价差的计算。
- 生产环境中的溢价信号需要从合适的交易场所数据流获取永续期货和现货价格。
- 比较资金费率窗口的小时数之前,应先将时间戳转换为UTC。
- Alpaca现货头寸不会赚取或支付永续期货资金费,模拟经纪商也不等于影子执行。
标签
全文
# Alpaca Crypto Live Trading Demo
# Alpaca Crypto Live Trading Demo
**Docker image**: `ml4t`
**Book Reference**: Chapter 25, Section 25.3 (Integrating with Alpaca)
**Purpose**: Demonstrate the operational shape of an always-on crypto strategy connected to Alpaca: how the
19-perp case study universe maps onto Alpaca's USD spot venue, how a momentum-based z-score signal is
routed through a broker connection, and where funding-window timing fits in the execution loop.
**Data contract - what is and is not implemented**:
- The deployed signal is a **momentum z-score proxy**, not the Chapter 6 perp-spot premium index. The
production path for the Ch6 premium index would read perp+spot prices from a venue feed and compute
`(perp - spot) / spot`, then take a rolling z-score; this notebook stops short of that wiring and uses
the close-to-close momentum z-score as a shape-equivalent stand-in.
- The simulated path (no credentials) uses a minimal mock broker; it is **not** a SafeBroker shadow-mode
test. The credential-present live path is the only SafeBroker-mediated execution path in this notebook.
- The 19-perp universe is the case-study target; Alpaca supports a strict subset as USD spot pairs. The
universe-mapping table below makes the gap explicit.
**Learning Objectives**
- Contrast the operational demands of 24/7 crypto deployment with regular-hours equity deployment.
- Inspect which case-study perps are tradeable on Alpaca and which are dropped at the venue boundary.
- Connect funding-window-aware logging to a broker-facing crypto execution loop.
**Prerequisites**:
- Alpaca account with crypto trading enabled (live path)
```python
"""Demonstrate a paper-safe crypto deployment loop for an always-on market."""
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.
warnings.filterwarnings("ignore", category=DeprecationWarning, module=r"websockets\.legacy")
HAS_ALPACA_SDK = False
try:
import alpaca # noqa: F401
from ml4t.live import AlpacaBroker, AlpacaDataFeed, LiveEngine, LiveRiskConfig
from ml4t.live.safety import SafeBroker
HAS_ALPACA_SDK = True
except ImportError:
pass
# basicConfig is a no-op once an imported library has attached a root handler, so this notebook
# takes its own logger rather than depending on which import happened to run first.
logger = logging.getLogger("alpaca_crypto_demo")
logger.setLevel(logging.INFO)
logger.propagate = False
if not logger.handlers:
_handler = logging.StreamHandler()
_handler.setFormatter(logging.Formatter("%(asctime)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 = 20
LIVE_FEED = 0 # explicit opt-in; default execution is offline and paper-safe
SEED = 42
```
One environment override before anything else, for the same reason as in
[`04_alpaca_paper_trading_demo`](04_alpaca_paper_trading_demo.ipynb): Alpaca's WebSocket loop and
the nested event loop a headless runner installs do not cooperate, so an unattended run would
hang past its own duration limit rather than finishing. When the runner announces itself, the
live feed is turned off and the simulated path runs instead. Interactive Jupyter is unaffected.
```python
if os.environ.get("ML4T_HEADLESS_PAPERMILL") == "1":
LIVE_FEED = 0
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
```
## 1. Universe Mapping: Case Study to Alpaca
The crypto_perps_funding case study defines a 19-perp Binance universe (USDT suffix). Alpaca runs a USD
spot venue and only lists a subset of these. The table below makes the gap explicit so readers can see
exactly which strategy holdings would route through Alpaca and which would need a different venue.
```python
# Source: case_studies/crypto_perps_funding/config/setup.yaml :: universe.symbols (n_assets = 19)
CASE_STUDY_PERP_UNIVERSE = [
"AAVEUSDT",
"ADAUSDT",
"APTUSDT",
"ATOMUSDT",
"AVAXUSDT",
"BNBUSDT",
"BTCUSDT",
"COMPUSDT",
"DOGEUSDT",
"DOTUSDT",
"ETHUSDT",
"INJUSDT",
"LINKUSDT",
"MKRUSDT",
"NEARUSDT",
"SOLUSDT",
"SUIUSDT",
"UNIUSDT",
"XRPUSDT",
]
# Alpaca USD spot symbols that map to the case-study perps, checked on 2026-07-22
# against <https://alpaca.markets/support/what-cryptocurrencies-does-alpaca-currently-support>.
# Alpaca's Assets API remains authoritative because venue coverage can change.
PERP_TO_ALPACA_USD = {
"AAVEUSDT": "AAVE/USD",
"ADAUSDT": "ADA/USD",
"AVAXUSDT": "AVAX/USD",
"BTCUSDT": "BTC/USD",
"DOGEUSDT": "DOGE/USD",
"DOTUSDT": "DOT/USD",
"ETHUSDT": "ETH/USD",
"LINKUSDT": "LINK/USD",
"MKRUSDT": "MKR/USD",
"SOLUSDT": "SOL/USD",
"UNIUSDT": "UNI/USD",
"XRPUSDT": "XRP/USD",
}
UNIVERSE_MAPPING = pl.DataFrame(
[
{
"perp_symbol": perp,
"alpaca_usd_pair": PERP_TO_ALPACA_USD.get(perp),
"tradeable_on_alpaca": perp in PERP_TO_ALPACA_USD,
}
for perp in CASE_STUDY_PERP_UNIVERSE
]
)
UNIVERSE_MAPPING
```
```python
COVERAGE_FRACTION = UNIVERSE_MAPPING["tradeable_on_alpaca"].mean()
print(
f"\nCoverage: {len(PERP_TO_ALPACA_USD)}/{len(CASE_STUDY_PERP_UNIVERSE)} perps "
f"({COVERAGE_FRACTION:.0%}) tradeable on Alpaca USD spot."
)
# The runnable subset for the rest of the notebook (in case-study order)
ALL_CRYPTO_SYMBOLS = [
PERP_TO_ALPACA_USD[p] for p in CASE_STUDY_PERP_UNIVERSE if p in PERP_TO_ALPACA_USD
]
CRYPTO_SYMBOLS = ALL_CRYPTO_SYMBOLS[:MAX_SYMBOLS] if MAX_SYMBOLS > 0 else ALL_CRYPTO_SYMBOLS.copy()
```
## 2. Crypto Market Characteristics
Crypto markets differ from equities:
| Aspect | Equities | Crypto |
|--------|----------|--------|
| Trading Hours | 9:30-16:00 ET | 24/7/365 |
| Settlement | T+2 | Instant |
| Minimum Trade | 1 share | Fractional |
| Volatility | lower | several times higher |
| Funding Rates | N/A | 8-hour intervals |
```python
print(f"24/7 market access; symbols routed via Alpaca USD spot: {', '.join(CRYPTO_SYMBOLS)}")
```
## 3. Credential Check
The credential check determines which path the notebook runs: live (Alpaca USD spot) or simulated (mock
broker). It is not a shadow-mode test in the simulated case - that distinction matters because shadow mode
implies a real broker connection with virtualised order routing, which the simulation path does not have.
```python
HAS_CREDENTIALS = bool(ALPACA_API_KEY and ALPACA_SECRET_KEY) and HAS_ALPACA_SDK
if HAS_CREDENTIALS:
print("Alpaca paper credentials are available; live transport still requires LIVE_FEED=1.")
else:
print("No Alpaca credentials; running mock-broker simulation (not SafeBroker shadow mode)")
```
**Finding**: The credential block exposes whether the notebook is connected, simulated, or only partially
configured before any strategy state is created.
**Trading implication**: Crypto notebooks should make execution mode obvious because overnight and weekend
operation amplify the cost of confusing a shadow session with a routed broker session.
## 4. Momentum Z-Score Strategy (Premium-Proxy)
The deployed signal is a **momentum z-score proxy**, not the Chapter 6 premium index. The strategy
computes the per-bar return, then converts it into a z-score using rolling mean and stdev. This shares the
*shape* of the premium signal (a centered, dimensionless mean-reversion driver) without requiring a perp
feed, so it can run on the Alpaca USD spot venue end-to-end.
**Production wiring (not implemented here)**: read perp and spot closes from a venue feed, compute
`(perp - spot) / spot`, then take the rolling z-score. The strategy structure below would consume the
resulting series in place of the momentum proxy.
### Funding windows
A perpetual future has no expiry, so nothing forces its price toward the spot price except a
periodic cash payment between longs and shorts: the **funding rate**, settled at fixed hours.
Positions held across one of those hours pay or receive it, which makes the funding clock part
of the execution decision rather than a detail of the accounting.
The hours are stated in UTC and the comparison has to normalise before checking. A naive
timestamp is treated as UTC and a tz-aware one is converted, because a clock in any other zone
would silently miss every window while appearing to check for them.
```python
FUNDING_HOURS_UTC = [0, 8, 16]
def _is_funding_hour(timestamp: datetime) -> bool:
"""Return True if `timestamp` falls on a Binance funding hour, in UTC.
Naive timestamps are interpreted as UTC; tz-aware timestamps are converted
before comparison.
"""
if timestamp.tzinfo is None:
ts_utc = timestamp.replace(tzinfo=UTC)
else:
ts_utc = timestamp.astimezone(UTC)
return ts_utc.hour in FUNDING_HOURS_UTC
def _compute_momentum_zscore(prices: list[float], lookback: int) -> float:
"""Premium-proxy: rolling z-score of close-to-close returns."""
if len(prices) < lookback + 2:
return 0.0
baseline_prices = prices[-lookback - 2 : -1]
baseline_returns = [
(baseline_prices[i] - baseline_prices[i - 1]) / baseline_prices[i - 1]
for i in range(1, len(baseline_prices))
]
mean_ret = float(np.mean(baseline_returns))
std_ret = float(np.std(baseline_returns))
if std_ret < 1e-8:
return 0.0
current_ret = (prices[-1] - prices[-2]) / prices[-2] if len(prices) >= 2 else 0.0
return (current_ret - mean_ret) / std_ret
```
```python
# compliance: skip cell_size - cohesive Strategy implementation binds signals to broker state
class CryptoPremiumStrategy(Strategy):
"""Mean-reversion on a momentum z-score proxy, routed through a crypto broker."""
def __init__(
self,
lookback: int = 10,
entry_threshold: float = 1.5,
exit_threshold: float = 0.25,
position_size: float = 0.1,
):
self.lookback = lookback
self.entry_threshold = entry_threshold
self.exit_threshold = exit_threshold
self.position_size = position_size
self.prices: dict[str, list[float]] = {}
self.signals: list[dict] = []
self.funding_events: list[dict] = []
def on_start(self, broker):
logger.info(
f"Strategy started: lookback={self.lookback}, entry={self.entry_threshold:.2f}z"
)
for symbol in CRYPTO_SYMBOLS:
self.prices[symbol] = []
def _route_signal(self, broker, timestamp, symbol, close, zscore, current_qty):
"""Translate a z-score into a long-only spot entry or exit."""
if zscore < -self.entry_threshold and current_qty <= 0:
action, side = "BUY", OrderSide.BUY
qty = self.position_size
reason = "z-score below -entry_threshold (mean reversion long)"
elif zscore >= -self.exit_threshold and current_qty > 0:
action = "CLOSE"
side = OrderSide.SELL
qty = current_qty
reason = "negative z-score reverted toward zero"
else:
return
self.signals.append(
{
"timestamp": timestamp,
"symbol": symbol,
"action": action,
"zscore": zscore,
"price": close,
"reason": reason,
}
)
logger.info(f"{action} {symbol}: z-score={zscore:.2f}")
order = broker.submit_order(symbol, qty, side=side)
if isinstance(order, dict):
self.signals[-1]["order_status"] = order["status"]
def on_data(self, timestamp: datetime, data: dict, context: dict, broker):
for symbol, bar in data.items():
if symbol not in self.prices:
self.prices[symbol] = []
close = bar["close"]
self.prices[symbol].append(close)
zscore = _compute_momentum_zscore(self.prices[symbol], self.lookback)
position = broker.get_position(symbol)
current_qty = position.quantity if position else 0.0
if _is_funding_hour(timestamp):
self.funding_events.append(
{
"timestamp": timestamp,
"symbol": symbol,
"position": current_qty,
"zscore": zscore,
}
)
self._route_signal(broker, timestamp, symbol, close, zscore, current_qty)
def on_end(self, broker):
logger.info(
f"Strategy ended. Signals: {len(self.signals)}; funding events: {len(self.funding_events)}"
)
```
## 5. Simulated Path: Flat-Dict Mock Broker
When credentials are missing, the demo runs against a tiny mock broker. The portfolio is a flat dict so
the simulation state is inspectable without nested dataclasses. This is **not** a SafeBroker shadow-mode
test because shadow mode requires a real underlying broker connection.
```python
DEFAULT_REF_PRICES = {
"AAVE/USD": 280.0,
"ADA/USD": 0.65,
"AVAX/USD": 35.0,
"BTC/USD": 43000.0,
"DOGE/USD": 0.15,
"DOT/USD": 7.0,
"ETH/USD": 2200.0,
"LINK/USD": 14.0,
"MKR/USD": 1700.0,
"SOL/USD": 100.0,
"UNI/USD": 6.5,
"XRP/USD": 0.55,
}
class MockCryptoBroker:
"""Minimal sync broker for the no-credential simulation path.
Long-only: a SELL without sufficient existing inventory is rejected.
The strategy matches this spot-venue constraint and never opens shorts.
"""
def __init__(self, initial_cash: float = 10_000.0):
self.portfolio = {"cash": initial_cash, "positions": {}}
self.order_log: list[dict] = []
self.current_prices = dict(DEFAULT_REF_PRICES)
self.current_timestamp = datetime(2026, 1, 1, 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: float, side=None, **kwargs) -> dict:
price = self.current_prices[asset]
side_name = side.value if hasattr(side, "value") else str(side or "BUY")
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:
self.portfolio["positions"][asset] = {
"quantity": remaining,
"entry_price": pos["entry_price"],
}
else:
del self.portfolio["positions"][asset]
status = "filled"
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
```
```python
async def run_simulated_crypto_demo() -> dict:
"""Drive the strategy against the mock broker; return a small results dict."""
print("MOCK-BROKER SIMULATION (no Alpaca connection)")
strategy = CryptoPremiumStrategy(
lookback=10,
entry_threshold=1.5,
exit_threshold=0.25,
position_size=0.01,
)
mock = MockCryptoBroker()
strategy.on_start(mock)
set_global_seeds(SEED)
base_prices = {sym: DEFAULT_REF_PRICES[sym] for sym in CRYPTO_SYMBOLS}
# Start at a known funding hour in UTC so the funding-event capture exercises the new tz-aware check.
start = datetime(2026, 1, 1, 0, 0, tzinfo=UTC)
for i in range(SIMULATION_STEPS):
timestamp = start + timedelta(hours=i)
data = {}
for symbol in CRYPTO_SYMBOLS:
base_prices[symbol] *= 1 + np.random.normal(0.001, 0.03)
data[symbol] = {
"open": base_prices[symbol] * 0.998,
"high": base_prices[symbol] * 1.01,
"low": base_prices[symbol] * 0.99,
"close": base_prices[symbol],
"volume": int(np.random.randint(100, 10000)),
}
mock.update_market(timestamp, {symbol: bar["close"] for symbol, bar in data.items()})
strategy.on_data(timestamp, data, {}, mock)
strategy.on_end(mock)
signals_df = pl.DataFrame(strategy.signals) if strategy.signals else pl.DataFrame()
funding_df = (
pl.DataFrame(strategy.funding_events) if strategy.funding_events else pl.DataFrame()
)
orders_df = pl.DataFrame(mock.order_log) if mock.order_log else pl.DataFrame()
if len(orders_df):
assert orders_df.filter(pl.col("status") == "rejected").is_empty()
return {
"signals": signals_df,
"funding_events": funding_df,
"orders": orders_df,
"broker": mock,
}
```
## 6. Live Path: Alpaca Engine Wiring
When credentials are present and `LIVE_FEED=1`, the demo wires `AlpacaDataFeed > SafeBroker > LiveEngine`
and runs for at most `DEMO_DURATION_SECONDS`. The SafeBroker layer is the only place this notebook talks
about shadow mode. The simulated path uses the mock broker above and is not a shadow-mode test.
```python
def create_alpaca_crypto_engine(strategy):
"""Wire AlpacaDataFeed, SafeBroker, and LiveEngine for the live crypto demo."""
broker = AlpacaBroker(api_key=ALPACA_API_KEY, secret_key=ALPACA_SECRET_KEY, paper=PAPER_TRADING)
risk_state_path = get_output_dir(25, "alpaca_crypto_demo") / "risk_state.json"
risk_config = LiveRiskConfig(
shadow_mode=True,
max_position_value=5_000.0,
max_order_value=1_000.0,
max_orders_per_minute=20,
state_file=str(risk_state_path),
)
safe_broker = SafeBroker(broker, risk_config)
feed = AlpacaDataFeed(
api_key=ALPACA_API_KEY,
secret_key=ALPACA_SECRET_KEY,
symbols=CRYPTO_SYMBOLS,
data_type="bars",
)
engine = LiveEngine(strategy=strategy, broker=safe_broker, feed=feed)
for name in [
"alpaca",
"alpaca.data",
"alpaca.data.live",
"alpaca.data.live.websocket",
"alpaca.trading.stream",
"websockets",
]:
logging.getLogger(name).setLevel(logging.CRITICAL)
print(f"Risk State: {display_path(risk_state_path)}")
return engine, safe_broker, feed, broker
```
```python
async def run_live_alpaca_demo() -> dict:
"""Drive the explicitly selected live Alpaca paper path."""
strategy = CryptoPremiumStrategy(
lookback=10,
entry_threshold=1.5,
exit_threshold=0.25,
position_size=0.01,
)
engine, safe_broker, feed, raw_broker = create_alpaca_crypto_engine(strategy)
print(
f"Starting Alpaca crypto engine for {DEMO_DURATION_SECONDS}s; symbols {', '.join(CRYPTO_SYMBOLS)}"
)
try:
await asyncio.wait_for(engine.connect(), timeout=10)
await asyncio.wait_for(engine.run(), timeout=DEMO_DURATION_SECONDS)
except TimeoutError:
print("Demo duration reached")
finally:
feed.stop()
await raw_broker.disconnect()
signals_df = pl.DataFrame(strategy.signals) if strategy.signals else pl.DataFrame()
funding_df = (
pl.DataFrame(strategy.funding_events) if strategy.funding_events else pl.DataFrame()
)
return {
"signals": signals_df,
"funding_events": funding_df,
"orders": pl.DataFrame(),
"broker": safe_broker,
}
```
```python
# Dispatch: explicit LIVE_FEED opt-in gates the live path.
async def crypto_demo_dispatch():
if not LIVE_FEED:
print(f"LIVE_FEED={LIVE_FEED}: running the offline mock-broker simulation")
return await run_simulated_crypto_demo()
if not HAS_CREDENTIALS:
raise RuntimeError("LIVE_FEED requires the Alpaca SDK and paper credentials")
return await run_live_alpaca_demo()
crypto_results = run_async(crypto_demo_dispatch())
print("\nRESULTS")
print(
f"Signals: {len(crypto_results['signals'])}; "
f"Funding events: {len(crypto_results['funding_events'])}; "
f"Orders: {len(crypto_results['orders'])}"
)
crypto_results["signals"]
```
```python
crypto_results["funding_events"]
```
```python
crypto_results["orders"]
```
**Finding**: The results section ties signals, funding events, and portfolio state together in one replay.
That gives the reader a broker-facing view of how the strategy behaves outside a pure backtest.
**Trading implication**: The timestamps are operational markers only. Alpaca spot positions do not receive
perpetual-swap funding; a production funding strategy needs perp-venue cash-flow records.
## 7. 24/7 Trading Considerations
When deploying crypto strategies:
1. **Infrastructure**:
- Use cloud deployment (always-on)
- Implement heartbeat monitoring
- Handle reconnection gracefully
2. **Risk Management**:
- Higher volatility = tighter stop losses
- Consider liquidation risk on leveraged positions
- Monitor exchange maintenance windows
3. **Funding Rates**:
- Binance: 00:00, 08:00, 16:00 UTC
- Track funding to optimize entry/exit timing
- Funding can be significant over time
```python
print("\n" + "=" * 60)
print("24/7 TRADING CONSIDERATIONS")
print("=" * 60)
print("\n1. Always-On Infrastructure:")
print(" - Cloud VM or container deployment")
print(" - Automatic restart on failure")
print(" - Health check endpoints")
print("\n2. Funding Rate Timing:")
print(" - Binance: 00:00, 08:00, 16:00 UTC")
print(" - Treat these as clock markers on Alpaca spot, not funding cash flows")
print(" - Reconcile actual funding PnL only on the perpetual venue")
print("\n3. Weekend Considerations:")
print(" - Liquidity may be lower")
print(" - Volatility can spike")
print(" - No 'market close' for stops")
```
**Finding**: The operational checklist shows that 24/7 trading is an infrastructure problem as much as a
signal problem.
**Trading implication**: A strategy that looks stable in backtests can still fail operationally if it
assumes maintenance windows or supervision patterns borrowed from regular-hours equity trading.
## Key Takeaways
- **Universe mapping is a hard venue constraint, not a soft target.** The current Alpaca list adds ADA to
the mapped subset. The output above computes current coverage; unsupported perps need another adapter.
- **The deployed signal is a momentum z-score proxy, not the Ch6 premium index.** Production wiring would
read perp + spot closes from a venue feed and compute `(perp - spot) / spot`; the structure of the
strategy is the same, but the input series is the missing piece on the Alpaca USD spot venue.
- **Funding-window detection must normalise to UTC.** The check treats a naive timestamp as UTC and
converts a tz-aware one before comparing against the funding hours. A clock in any other zone
would skip every funding event while appearing to look for them, which is the failure mode that
produces no error and no log line.
- **The simulated path is not shadow mode.** Without credentials the demo uses a flat-dict `MockCryptoBroker`
for inspection only; shadow mode requires a real broker connection wrapped in `SafeBroker`, which the
credential-present live path provides.
**Next**: see `09_crypto_funding_deployment_loop.py` for the production-style perp deployment loop with
OKX as the venue.在遵守原作品许可的前提下,附作者信息全文展示。 许可协议: MIT
此摘要由 Stratmill 研究智能体根据原文撰写,并非原文副本。