模拟交易系统架构设计:从信号扫描到自动执行
在量化交易开发中,模拟交易(Paper Trading)系统是连接策略研究与实盘的关键桥梁。一个设计良好的模拟交易系统,不仅要能验证策略逻辑,还要能暴露实盘中可能遇到的延迟、滑点、数据异常等问题。本文分享一个完整的模拟交易系统架构,涵盖信号扫描、市场分析、交易执行和通知推送四个核心模块。
系统架构总览
整个系统采用模块化设计,各模块通过消息队列或直接调用解耦,便于独立测试和替换。数据流向为:行情数据源 → 信号扫描模块 → 市场分析模块 → 模拟交易引擎 → 推送通知模块。
sql
+----------------+ +------------------+ +------------------+
| 行情数据源 | --> | 信号扫描模块 | --> | 市场分析模块 |
| (WebSocket/API) | | (Strategy Engine)| | (Risk Checker) |
+----------------+ +------------------+ +--------+---------+
|
v
+----------------+ +------------------+ +------------------+
| 推送通知模块 | <-- | 模拟交易引擎 | <-- | 订单生成器 |
| (Telegram/邮件) | | (Paper Engine) | | (Order Creator) |
+----------------+ +------------------+ +------------------+
模块职责划分
1. 信号扫描模块
该模块负责监听行情数据,运行策略逻辑,生成交易信号。核心设计要点是策略与数据解耦------策略只接收标准化的K线数据,不关心数据来源。
python
# signal_scanner.py
from abc import ABC, abstractmethod
import pandas as pd
import numpy as np
from typing import Dict, List, Optional
from dataclasses import dataclass, field
from datetime import datetime
@dataclass
class Signal:
symbol: str
direction: str # 'long' or 'short'
strength: float # 0.0 to 1.0
timestamp: datetime
price: float
metadata: Dict = field(default_factory=dict)
class BaseStrategy(ABC):
"""策略基类,所有策略必须实现generate_signal方法"""
@abstractmethod
def generate_signal(self, data: pd.DataFrame) -> Optional[Signal]:
"""输入标准化K线数据,输出交易信号"""
pass
class MovingAverageCrossStrategy(BaseStrategy):
"""双均线交叉策略示例"""
def __init__(self, fast_period: int = 5, slow_period: int = 20):
self.fast_period = fast_period
self.slow_period = slow_period
def generate_signal(self, data: pd.DataFrame) -> Optional[Signal]:
if len(data) < self.slow_period + 1:
return None
fast_ma = data['close'].rolling(self.fast_period).mean()
slow_ma = data['close'].rolling(self.slow_period).mean()
# 金叉
if fast_ma.iloc[-2] <= slow_ma.iloc[-2] and fast_ma.iloc[-1] > slow_ma.iloc[-1]:
strength = min(abs(fast_ma.iloc[-1] - slow_ma.iloc[-1]) / slow_ma.iloc[-1] * 100, 1.0)
return Signal(
symbol=data['symbol'].iloc[-1],
direction='long',
strength=strength,
timestamp=datetime.now(),
price=data['close'].iloc[-1]
)
# 死叉
elif fast_ma.iloc[-2] >= slow_ma.iloc[-2] and fast_ma.iloc[-1] < slow_ma.iloc[-1]:
strength = min(abs(fast_ma.iloc[-1] - slow_ma.iloc[-1]) / slow_ma.iloc[-1] * 100, 1.0)
return Signal(
symbol=data['symbol'].iloc[-1],
direction='short',
strength=strength,
timestamp=datetime.now(),
price=data['close'].iloc[-1]
)
return None
class SignalScanner:
"""信号扫描器,管理多个策略和数据源"""
def __init__(self, strategies: List[BaseStrategy]):
self.strategies = strategies
self.data_buffer = {} # symbol -> DataFrame
def on_bar_update(self, symbol: str, bar: Dict):
"""接收新K线数据,更新缓冲区并运行策略"""
if symbol not in self.data_buffer:
self.data_buffer[symbol] = pd.DataFrame(columns=['open', 'high', 'low', 'close', 'volume'])
# 添加新bar,保留最近500根
new_row = pd.DataFrame([bar])
self.data_buffer[symbol] = pd.concat([self.data_buffer[symbol], new_row], ignore_index=True)
self.data_buffer[symbol] = self.data_buffer[symbol].tail(500)
# 运行所有策略
signals = []
for strategy in self.strategies:
signal = strategy.generate_signal(self.data_buffer[symbol])
if signal:
signals.append(signal)
return signals
2. 市场分析模块
该模块在信号生成后进行二次确认,主要做风险过滤和仓位计算。我将其设计为可插拔的过滤器链,每个过滤器独立负责一项检查。
python
# market_analyzer.py
from typing import List, Callable
from dataclasses import dataclass
import numpy as np
@dataclass
class AnalysisResult:
approved: bool
position_size: float
risk_score: float
reason: str = ""
class MarketAnalyzer:
"""市场分析器,执行一系列过滤器"""
def __init__(self, filters: List[Callable] = None):
self.filters = filters or []
def add_filter(self, filter_func: Callable):
self.filters.append(filter_func)
def analyze(self, signal, market_data: dict) -> AnalysisResult:
"""运行所有过滤器,决定是否接受信号"""
for filter_func in self.filters:
result = filter_func(signal, market_data)
if not result.approved:
return result
return AnalysisResult(approved=True, position_size=1.0, risk_score=0.0)
# 过滤器示例
def volatility_filter(signal, market_data, max_volatility=0.05):
"""波动率过滤:拒绝高波动行情"""
symbol = signal.symbol
recent_bars = market_data.get(symbol, [])
if len(recent_bars) < 20:
return AnalysisResult(approved=False, position_size=0, risk_score=1.0, reason="数据不足")
returns = np.diff([bar['close'] for bar in recent_bars[-20:]])
volatility = np.std(returns)
if volatility > max_volatility:
return AnalysisResult(approved=False, position_size=0, risk_score=volatility,
reason=f"波动率过高: {volatility:.4f}")
return AnalysisResult(approved=True, position_size=1.0, risk_score=volatility)
def volume_filter(signal, market_data, min_volume=1000):
"""成交量过滤:拒绝流动性不足的品种"""
symbol = signal.symbol
recent_bars = market_data.get(symbol, [])
if not recent_bars:
return AnalysisResult(approved=False, position_size=0, risk_score=1.0, reason="无数据")
avg_volume = np.mean([bar['volume'] for bar in recent_bars[-10:]])
if avg_volume < min_volume:
return AnalysisResult(approved=False, position_size=0, risk_score=1.0,
reason=f"成交量不足: {avg_volume:.0f}")
return AnalysisResult(approved=True, position_size=1.0, risk_score=0.0)
3. 模拟交易引擎
这是系统的核心。模拟交易引擎需要处理订单状态机、持仓管理和资金核算。关键设计是使用撮合队列模拟真实撮合延迟,而不是立即成交。
python
# paper_trading_engine.py
from enum import Enum
from typing import Dict, List
import asyncio
from datetime import datetime
from dataclasses import dataclass, field
class OrderStatus(Enum):
PENDING = "pending"
FILLED = "filled"
REJECTED = "rejected"
CANCELLED = "cancelled"
class OrderType(Enum):
MARKET = "market"
LIMIT = "limit"
@dataclass
class Order:
order_id: str
symbol: str
side: str # 'buy' or 'sell'
quantity: float
order_type: OrderType
price: float = 0.0
status: OrderStatus = OrderStatus.PENDING
created_at: datetime = field(default_factory=datetime.now)
filled_at: datetime = None
@dataclass
class Position:
symbol: str
quantity: float = 0.0
avg_price: float = 0.0
def update(self, side: str, price: float, quantity: float):
"""更新持仓"""
if side == 'buy':
total_cost = self.avg_price * self.quantity + price * quantity
self.quantity += quantity
self.avg_price = total_cost / self.quantity if self.quantity > 0 else 0
else:
self.quantity -= quantity
if self.quantity <= 0:
self.quantity = 0
self.avg_price = 0
class PaperTradingEngine:
"""模拟交易引擎"""
def __init__(self, initial_capital: float = 100000.0, fill_delay_ms: int = 100):
self.initial_capital = initial_capital
self.cash = initial_capital
self.positions: Dict[str, Position] = {}
self.orders: List[Order] = []
self.fill_delay_ms = fill_delay_ms
async def place_order(self, symbol: str, side: str, quantity: float,
order_type: OrderType = OrderType.MARKET, price: float = 0.0) -> Order:
"""下单并模拟撮合延迟"""
order = Order(
order_id=f"ORD{len(self.orders)+1:06d}",
symbol=symbol,
side=side,
quantity=quantity,
order_type=order_type,
price=price
)
self.orders.append(order)
# 模拟撮合延迟
await asyncio.sleep(self.fill_delay_ms / 1000)
# 市价单直接成交,限价单检查价格
if order.order_type == OrderType.MARKET:
order.status = OrderStatus.FILLED
order.filled_at = datetime.now()
self._execute_fill(order, order.price if order.price > 0 else self._get_market_price(symbol))
else:
# 限价单需要外部触发价格检查
order.status = OrderStatus.PENDING
return order
def _execute_fill(self, order: Order, fill_price: float):
"""执行成交,更新账户"""
# 检查资金
cost = fill_price * order.quantity
if order.side == 'buy' and cost > self.cash:
order.status = OrderStatus.REJECTED
return
# 更新持仓
if order.symbol not in self.positions:
self.positions[order.symbol] = Position(symbol=order.symbol)
self.positions[order.symbol].update(order.side, fill_price, order.quantity)
# 更新现金
if order.side == 'buy':
self.cash -= cost
else:
self.cash += cost
order.status = OrderStatus.FILLED
order.filled_at = datetime.now()
def _get_market_price(self, symbol: str) -> float:
"""获取市场价,实际系统中从行情模块获取"""
# 简化实现
return 100.0
def get_portfolio_value(self, market_prices: Dict[str, float]) -> float:
"""计算总资产"""
total = self.cash
for symbol, pos in self.positions.items():
if symbol in market_prices:
total += pos.quantity * market_prices[symbol]
return total
# 使用示例
async def main():
engine = PaperTradingEngine(initial_capital=50000)
# 模拟下单
order = await engine.place_order("BTCUSDT", "buy", 0.1, OrderType.MARKET, price=40000)
print(f"Order {order.order_id}: {order.status}")
order2 = await engine.place_order("BTCUSDT", "sell", 0.05, OrderType.MARKET, price=41000)
print(f"Order {order2.order_id}: {order2.status}")
# 计算总资产
market_prices = {"BTCUSDT": 41000}
total = engine.get_portfolio_value(market_prices)
print(f"Total portfolio value: {total:.2f}")
if __name__ == "__main__":
asyncio.run(main())
4. 推送通知模块
信号触发和订单成交后需要及时通知。这里设计一个支持多通道的通知管理器,通过异步队列解耦通知发送与业务逻辑。
python
# notification.py
import asyncio
from typing import Dict, List, Callable
from dataclasses import dataclass
from datetime import datetime
@dataclass
class Notification:
title: str
message: str
level: str # 'info', 'warning', 'critical'
timestamp: datetime = datetime.now()
class NotificationManager:
"""通知管理器,支持多通道异步发送"""
def __init__(self):
self.channels: List[Callable] = []
self.queue = asyncio.Queue()
self._worker_task = None
def add_channel(self, channel_func: Callable):
"""添加通知通道,如Telegram、邮件、Webhook"""
self.channels.append(channel_func)
async def start(self):
"""启动后台发送任务"""
self._worker_task = asyncio.create_task(self._worker())
async def stop(self):
"""停止后台任务"""
if self._worker_task:
self._worker_task.cancel()
async def send(self, title: str, message: str, level: str = "info"):
"""异步发送通知"""
notification = Notification(title=title, message=message, level=level)
await self.queue.put(notification)
async def _worker(self):
"""后台消费队列并发送通知"""
while True:
try:
notification = await self.queue.get()
for channel in self.channels:
try:
await channel(notification)
except Exception as e:
print(f"Channel error: {e}")
except asyncio.CancelledError:
break
# 通道实现示例:Telegram
async def telegram_channel(notification: Notification, bot_token: str, chat_id: str):
"""发送Telegram通知"""
import httpx
text = f"*{notification.title}*\n{notification.message}\n{notification.timestamp}"
url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
async with httpx.AsyncClient() as client:
await client.post(url, json={
"chat_id": chat_id,
"text": text,
"parse_mode": "Markdown"
})
# 通道实现示例:Webhook
async def webhook_channel(notification: Notification, webhook_url: str):
"""发送Webhook通知"""
import httpx
payload = {
"title": notification.title,
"message": notification.message,
"level": notification.level,
"timestamp": notification.timestamp.isoformat()
}
async with httpx.AsyncClient() as client:
await client.post(webhook_url, json=payload)
# 使用示例
async def notification_demo():
manager = NotificationManager()
# 配置通道
manager.add_channel(lambda n: telegram_channel(n, "YOUR_BOT_TOKEN", "YOUR_CHAT_ID"))
manager.add_channel(lambda n: webhook_channel(n, "http://localhost:3000/webhook"))
await manager.start()
# 发送通知
await manager.send(
title="交易信号触发",
message="BTCUSDT 出现金叉信号,建议做多",
level="warning"
)
# 等待发送完成
await asyncio.sleep(2)
await manager.stop()
if __name__ == "__main__":
asyncio.run(notification_demo())
数据流向与关键技术选型
数据流时序
- 行情采集:使用WebSocket接收实时K线,比REST轮询延迟更低
- 信号生成:每次新K线到达时,触发策略计算
- 风险评估:信号产生后同步执行过滤器链
- 订单执行:通过异步队列提交订单,模拟撮合延迟
- 通知推送:订单状态变化或信号产生时异步推送
技术选型建议
| 组件 | 推荐方案 | 理由 |
|---|---|---|
| 异步框架 | asyncio + aiohttp | 原生异步,适合IO密集型任务 |
| 数据存储 | SQLite(开发)/ TimescaleDB(生产) | 时序数据用专业数据库 |
| 消息队列 | Redis Streams | 轻量级,支持消费者组 |
| 任务调度 | APScheduler | 支持cron表达式,适合定时任务 |
| 配置管理 | pydantic + .env | 类型安全,环境隔离 |
关键设计要点
- 异步边界:所有IO操作(网络请求、数据库)必须异步化,避免阻塞事件循环
- 错误隔离:每个模块独立try-except,防止单点故障导致整个系统崩溃
- 状态持久化:定期将订单和持仓状态写入数据库,支持系统重启恢复
- 回放能力:记录所有市场数据和交易决策,便于