1714 lines
88 KiB
Plaintext
1714 lines
88 KiB
Plaintext
{
|
||
"cells": [
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "e51fcc58",
|
||
"metadata": {},
|
||
"source": [
|
||
"# =============================================================================\n",
|
||
"# Quantitative Trading — Event-Driven Backtest Engine\n",
|
||
"# 量化交易 — 事件驱动回测引擎\n",
|
||
"\n",
|
||
"# =============================================================================\n",
|
||
"#\n",
|
||
"# 本文件是策略开发演示 (quant_strategy_backtest_demo.py) 的续集。\n",
|
||
"# This file is the sequel to the strategy development demo.\n",
|
||
"#\n",
|
||
"# ─────────────────────────────────────────────────────────────────────────────\n",
|
||
"# Why Event-Driven? 为什么要用事件驱动?\n",
|
||
"# ─────────────────────────────────────────────────────────────────────────────\n",
|
||
"# The vectorized backtest in the previous demo computed everything at once\n",
|
||
"# using array operations. This is fast, but it has a fundamental problem:\n",
|
||
"# 上一个演示中的向量化回测一次性用数组运算计算所有结果。速度很快,但有根本缺陷:\n",
|
||
"#\n",
|
||
"# It cannot easily model:\n",
|
||
"# 难以建模:\n",
|
||
"# • Different order types 不同订单类型 (limit / stop / market orders)\n",
|
||
"# • Partial fills 部分成交\n",
|
||
"# • Intraday price path 日内价格路径 (open → high/low → close sequence)\n",
|
||
"# • Real-time risk checks 实时风控检查 (e.g. position limits, margin calls)\n",
|
||
"# • Multiple strategies 多策略组合\n",
|
||
"# • Portfolio-level rules 组合层面规则 (e.g. max sector exposure)\n",
|
||
"#\n",
|
||
"# The event-driven approach treats the backtest as a simulation of the actual\n",
|
||
"# live trading system. The same strategy & portfolio code can run in both\n",
|
||
"# backtest mode AND live trading mode — just swap the data source.\n",
|
||
"#\n",
|
||
"# 事件驱动方法将回测视为真实交易系统的模拟。\n",
|
||
"# 同一套策略和组合代码可以在回测模式和实盘模式下运行——只需替换数据源。\n",
|
||
"#\n",
|
||
"# ─────────────────────────────────────────────────────────────────────────────\n",
|
||
"# Architecture Overview 架构总览\n",
|
||
"# ─────────────────────────────────────────────────────────────────────────────\n",
|
||
"#\n",
|
||
"# ┌─────────────────────────────────┐\n",
|
||
"# │ Event Queue 事件队列 │\n",
|
||
"# │ (FIFO deque — central bus) │\n",
|
||
"# └───────────────┬─────────────────┘\n",
|
||
"# │ events flow through\n",
|
||
"# ┌────────────────────┼────────────────────┐\n",
|
||
"# ▼ ▼ ▼\n",
|
||
"# ┌────────────────┐ ┌────────────────┐ ┌────────────────┐\n",
|
||
"# │ DataHandler │ │ Strategy │ │ Portfolio │\n",
|
||
"# │ 数据处理器 │ │ 策略引擎 │ │ 组合管理器 │\n",
|
||
"# │ │ │ │ │ │\n",
|
||
"# │ Streams bars │ │ Reads market │ │ Receives order │\n",
|
||
"# │ (OHLCV) into │ │ data, computes │ │ requests from │\n",
|
||
"# │ the queue as │ │ signals, emits │ │ strategy and │\n",
|
||
"# │ MarketEvents │ │ SignalEvents │ │ emits │\n",
|
||
"# └────────────────┘ └────────────────┘ │ OrderEvents │\n",
|
||
"# └───────┬────────┘\n",
|
||
"# │\n",
|
||
"# ┌───────▼────────┐\n",
|
||
"# │ ExecutionHandler│\n",
|
||
"# │ 执行处理器 │\n",
|
||
"# │ (模拟券商) │\n",
|
||
"# │ │\n",
|
||
"# │ Fills orders, │\n",
|
||
"# │ applies slippage│\n",
|
||
"# │ & commission, │\n",
|
||
"# │ emits │\n",
|
||
"# │ FillEvents │\n",
|
||
"# └────────────────┘\n",
|
||
"#\n",
|
||
"# ─────────────────────────────────────────────────────────────────────────────\n",
|
||
"# Event flow for one trading day 单个交易日的事件流\n",
|
||
"# ─────────────────────────────────────────────────────────────────────────────\n",
|
||
"#\n",
|
||
"# [Clock ticks to new day / 时钟滴答到新的一天]\n",
|
||
"# │\n",
|
||
"# ▼\n",
|
||
"# DataHandler emits MarketEvent (新K线数据到达)\n",
|
||
"# │\n",
|
||
"# ▼\n",
|
||
"# Strategy processes MarketEvent → emits SignalEvent (信号:买/卖/平)\n",
|
||
"# │\n",
|
||
"# ▼\n",
|
||
"# Portfolio processes SignalEvent → emits OrderEvent (下单:市价/限价/止损)\n",
|
||
"# │\n",
|
||
"# ▼\n",
|
||
"# ExecutionHandler processes OrderEvent → emits FillEvent (成交确认)\n",
|
||
"# │\n",
|
||
"# ▼\n",
|
||
"# Portfolio processes FillEvent → updates positions & P&L (更新持仓和盈亏)\n",
|
||
"# │\n",
|
||
"# ▼\n",
|
||
"# [repeat for next day / 重复下一天]\n",
|
||
"#\n",
|
||
"# ─────────────────────────────────────────────────────────────────────────────\n",
|
||
"# Topics covered / 涵盖主题:\n",
|
||
"# 1. Event Classes 事件类 (Market, Signal, Order, Fill)\n",
|
||
"# 2. DataHandler 数据处理器\n",
|
||
"# 3. Strategy 策略 (MA Crossover & RSI)\n",
|
||
"# 4. Portfolio 组合管理器 (cash, positions, P&L)\n",
|
||
"# 5. ExecutionHandler 执行处理器 (slippage models, order types)\n",
|
||
"# 6. BacktestEngine 回测引擎 (main event loop)\n",
|
||
"# 7. Performance Analytics 绩效分析\n",
|
||
"# 8. Comparison: Event-Driven vs Vectorized\n",
|
||
"\n",
|
||
"# ============================================================================="
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "1120985c",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"from __future__ import annotations\n",
|
||
"\n",
|
||
"import queue # Python built-in FIFO queue\n",
|
||
"import enum # for clean event type constants\n",
|
||
"from dataclasses import dataclass, field\n",
|
||
"from abc import ABC, abstractmethod\n",
|
||
"from typing import Dict, List, Optional, Tuple\n",
|
||
"from copy import deepcopy\n",
|
||
"\n",
|
||
"import numpy as np\n",
|
||
"import pandas as pd\n",
|
||
"import matplotlib.pyplot as plt\n",
|
||
"import matplotlib.gridspec as gridspec\n",
|
||
"import warnings\n",
|
||
"\n",
|
||
"warnings.filterwarnings('ignore')\n",
|
||
"\n",
|
||
"# ── Chinese font / 中文字体 ──────────────────────────────────────────────────\n",
|
||
"plt.rcParams['font.sans-serif'] = ['WenQuanYi Zen Hei', 'Arial Unicode MS',\n",
|
||
" 'SimHei', 'DejaVu Sans']\n",
|
||
"plt.rcParams['axes.unicode_minus'] = False\n",
|
||
"\n",
|
||
"np.random.seed(42)\n",
|
||
"\n",
|
||
"print(\"=\" * 72)\n",
|
||
"print(\" 量化交易 — 事件驱动回测引擎\")\n",
|
||
"print(\" Quantitative Trading — Event-Driven Backtest Engine\")\n",
|
||
"print(\"=\" * 72)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "97b23791",
|
||
"metadata": {},
|
||
"source": [
|
||
"# =============================================================================\n",
|
||
"# SECTION 1: Event Classes 事件类\n",
|
||
"\n",
|
||
"# =============================================================================\n",
|
||
"#\n",
|
||
"# Every piece of information that flows through the system is wrapped in an\n",
|
||
"# Event object. This decouples the components — a Strategy doesn't call the\n",
|
||
"# Portfolio directly; it just puts a SignalEvent on the queue.\n",
|
||
"#\n",
|
||
"# 系统中流通的每一条信息都被包装成一个 Event 对象。\n",
|
||
"# 这使各组件解耦——策略不直接调用组合,只是把 SignalEvent 放入队列。\n",
|
||
"#\n",
|
||
"# Four event types:\n",
|
||
"# 四种事件类型:\n",
|
||
"#\n",
|
||
"# MarketEvent 市场事件 — new OHLCV bar has arrived from the data feed\n",
|
||
"# SignalEvent 信号事件 — strategy has decided to buy or sell\n",
|
||
"# OrderEvent 订单事件 — portfolio has sized the trade and placed an order\n",
|
||
"# FillEvent 成交事件 — broker has confirmed the order was executed\n",
|
||
"\n",
|
||
"# ============================================================================="
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "b99769a6",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"class EventType(enum.Enum):\n",
|
||
" \"\"\"\n",
|
||
" Enumeration of all event types in the system.\n",
|
||
" 系统中所有事件类型的枚举。\n",
|
||
" \"\"\"\n",
|
||
" MARKET = \"MARKET\" # 市场事件 — new price bar\n",
|
||
" SIGNAL = \"SIGNAL\" # 信号事件 — trade signal from strategy\n",
|
||
" ORDER = \"ORDER\" # 订单事件 — order from portfolio\n",
|
||
" FILL = \"FILL\" # 成交事件 — filled order from broker\n",
|
||
"\n",
|
||
"\n",
|
||
"class Direction(enum.Enum):\n",
|
||
" \"\"\"\n",
|
||
" Trade direction. 交易方向。\n",
|
||
" \"\"\"\n",
|
||
" LONG = \"LONG\" # 做多 / buy\n",
|
||
" SHORT = \"SHORT\" # 做空 / sell short\n",
|
||
" EXIT = \"EXIT\" # 平仓 / close position\n",
|
||
"\n",
|
||
"\n",
|
||
"class OrderType(enum.Enum):\n",
|
||
" \"\"\"\n",
|
||
" Order types. 订单类型。\n",
|
||
"\n",
|
||
" MARKET 市价单 — execute immediately at the best available price\n",
|
||
" 立即以市场最优价格成交,确保成交但价格不确定\n",
|
||
" LIMIT 限价单 — execute only at a specified price or better\n",
|
||
" 只在指定价格或更优价格成交,价格确定但不保证成交\n",
|
||
" STOP 止损单 — becomes a market order when price hits the stop level\n",
|
||
" 价格触及止损价时转变为市价单,用于控制最大亏损\n",
|
||
" \"\"\"\n",
|
||
" MARKET = \"MARKET\"\n",
|
||
" LIMIT = \"LIMIT\"\n",
|
||
" STOP = \"STOP\"\n",
|
||
"\n",
|
||
"\n",
|
||
"@dataclass\n",
|
||
"class MarketEvent:\n",
|
||
" \"\"\"\n",
|
||
" Triggered when the DataHandler has a new bar of OHLCV data ready.\n",
|
||
" 当数据处理器有新的 OHLCV K线数据就绪时触发。\n",
|
||
"\n",
|
||
" This is the \"heartbeat\" of the system — it drives all other components.\n",
|
||
" 这是系统的\"心跳\"——驱动所有其他组件。\n",
|
||
"\n",
|
||
" Fields / 字段:\n",
|
||
" symbol 股票代码 — which ticker this bar belongs to\n",
|
||
" date 日期 — the date of this bar\n",
|
||
" open 开盘价 — first trade of the session\n",
|
||
" high 最高价 — highest trade during the session\n",
|
||
" low 最低价 — lowest trade during the session\n",
|
||
" close 收盘价 — last trade of the session\n",
|
||
" volume 成交量 — total shares traded\n",
|
||
" \"\"\"\n",
|
||
" type : EventType = field(default=EventType.MARKET, init=False)\n",
|
||
" symbol : str = \"\"\n",
|
||
" date : pd.Timestamp = None\n",
|
||
" open : float = 0.0\n",
|
||
" high : float = 0.0\n",
|
||
" low : float = 0.0\n",
|
||
" close : float = 0.0\n",
|
||
" volume : float = 0.0\n",
|
||
"\n",
|
||
" def __repr__(self):\n",
|
||
" return (f\"MarketEvent({self.symbol} {self.date.date()} \"\n",
|
||
" f\"O={self.open:.2f} H={self.high:.2f} \"\n",
|
||
" f\"L={self.low:.2f} C={self.close:.2f})\")\n",
|
||
"\n",
|
||
"\n",
|
||
"@dataclass\n",
|
||
"class SignalEvent:\n",
|
||
" \"\"\"\n",
|
||
" Generated by a Strategy when it detects a trade opportunity.\n",
|
||
" 策略检测到交易机会时生成。\n",
|
||
"\n",
|
||
" A SignalEvent is a \"suggestion\" — the Portfolio decides whether and\n",
|
||
" how much to actually trade based on its own risk rules.\n",
|
||
" 信号事件是一个\"建议\"——组合管理器根据自身风控规则决定是否以及交易多少。\n",
|
||
"\n",
|
||
" Fields / 字段:\n",
|
||
" symbol 股票代码\n",
|
||
" date 信号产生日期\n",
|
||
" direction 方向: LONG(做多) / SHORT(做空) / EXIT(平仓)\n",
|
||
" strength 信号强度 [0, 1] — used for position sizing (仓位管理)\n",
|
||
" 1.0 = full conviction, 0.5 = half conviction\n",
|
||
" strategy 策略名称 — useful when running multiple strategies\n",
|
||
" \"\"\"\n",
|
||
" type : EventType = field(default=EventType.SIGNAL, init=False)\n",
|
||
" symbol : str = \"\"\n",
|
||
" date : pd.Timestamp = None\n",
|
||
" direction : Direction = Direction.LONG\n",
|
||
" strength : float = 1.0 # 信号强度 / signal strength for position sizing\n",
|
||
" strategy : str = \"\"\n",
|
||
"\n",
|
||
" def __repr__(self):\n",
|
||
" return (f\"SignalEvent({self.symbol} {self.date.date()} \"\n",
|
||
" f\"{self.direction.value} strength={self.strength:.2f})\")\n",
|
||
"\n",
|
||
"\n",
|
||
"@dataclass\n",
|
||
"class OrderEvent:\n",
|
||
" \"\"\"\n",
|
||
" Sent from Portfolio to ExecutionHandler to place a trade.\n",
|
||
" 从组合管理器发送给执行处理器以下单。\n",
|
||
"\n",
|
||
" This is a concrete order: it specifies WHAT to trade and HOW MUCH.\n",
|
||
" 这是一个具体订单:指定交易什么以及交易多少。\n",
|
||
"\n",
|
||
" Fields / 字段:\n",
|
||
" symbol 股票代码\n",
|
||
" date 下单日期\n",
|
||
" order_type 订单类型 MARKET / LIMIT / STOP\n",
|
||
" direction 方向 LONG / SHORT / EXIT\n",
|
||
" quantity 数量 number of shares (股数)\n",
|
||
" limit_price 限价 for LIMIT orders (限价单价格)\n",
|
||
" stop_price 止损价 for STOP orders (止损单价格)\n",
|
||
" \"\"\"\n",
|
||
" type : EventType = field(default=EventType.ORDER, init=False)\n",
|
||
" symbol : str = \"\"\n",
|
||
" date : pd.Timestamp = None\n",
|
||
" order_type : OrderType = OrderType.MARKET\n",
|
||
" direction : Direction = Direction.LONG\n",
|
||
" quantity : int = 0\n",
|
||
" limit_price : float = 0.0\n",
|
||
" stop_price : float = 0.0\n",
|
||
"\n",
|
||
" def __repr__(self):\n",
|
||
" return (f\"OrderEvent({self.symbol} {self.date.date()} \"\n",
|
||
" f\"{self.order_type.value} {self.direction.value} \"\n",
|
||
" f\"qty={self.quantity})\")\n",
|
||
"\n",
|
||
"\n",
|
||
"@dataclass\n",
|
||
"class FillEvent:\n",
|
||
" \"\"\"\n",
|
||
" Confirms that an order has been executed by the broker.\n",
|
||
" 确认订单已被券商执行(成交回报)。\n",
|
||
"\n",
|
||
" A Fill is the ground truth — it records what ACTUALLY happened.\n",
|
||
" 成交回报是最终事实——记录实际发生了什么。\n",
|
||
"\n",
|
||
" Fields / 字段:\n",
|
||
" symbol 股票代码\n",
|
||
" date 成交日期\n",
|
||
" direction 成交方向 LONG / SHORT / EXIT\n",
|
||
" quantity 成交数量 shares actually filled\n",
|
||
" fill_price 成交价格 actual execution price (含滑点 including slippage)\n",
|
||
" commission 佣金 brokerage commission\n",
|
||
" slippage 滑点 price impact of the trade\n",
|
||
" \"\"\"\n",
|
||
" type : EventType = field(default=EventType.FILL, init=False)\n",
|
||
" symbol : str = \"\"\n",
|
||
" date : pd.Timestamp = None\n",
|
||
" direction : Direction = Direction.LONG\n",
|
||
" quantity : int = 0\n",
|
||
" fill_price : float = 0.0\n",
|
||
" commission : float = 0.0\n",
|
||
" slippage : float = 0.0\n",
|
||
"\n",
|
||
" @property\n",
|
||
" def total_cost(self) -> float:\n",
|
||
" \"\"\"\n",
|
||
" Total cash impact of this fill.\n",
|
||
" 本次成交的总现金影响。\n",
|
||
"\n",
|
||
" For a BUY (LONG): negative (cash decreases) 现金减少\n",
|
||
" For a SELL (EXIT): positive (cash increases) 现金增加\n",
|
||
" \"\"\"\n",
|
||
" sign = -1 if self.direction in (Direction.LONG, Direction.SHORT) else +1\n",
|
||
" return sign * (self.fill_price * self.quantity + self.commission)\n",
|
||
"\n",
|
||
" def __repr__(self):\n",
|
||
" return (f\"FillEvent({self.symbol} {self.date.date()} \"\n",
|
||
" f\"{self.direction.value} qty={self.quantity} \"\n",
|
||
" f\"@{self.fill_price:.2f} comm={self.commission:.2f})\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "1cdf71ef",
|
||
"metadata": {},
|
||
"source": [
|
||
"# =============================================================================\n",
|
||
"# SECTION 2: DataHandler 数据处理器\n",
|
||
"\n",
|
||
"# =============================================================================\n",
|
||
"#\n",
|
||
"# The DataHandler is responsible for feeding price data into the system\n",
|
||
"# one bar at a time. It simulates the reality of not knowing the future.\n",
|
||
"#\n",
|
||
"# 数据处理器负责逐根K线地向系统输入价格数据。\n",
|
||
"# 它模拟了\"无法预知未来\"的现实。\n",
|
||
"#\n",
|
||
"# The key design rule:\n",
|
||
"# 核心设计规则:\n",
|
||
"#\n",
|
||
"# ❌ WRONG: strategy sees bar[t] and trades on bar[t] (lookahead bias!)\n",
|
||
"# 策略看到 bar[t] 并在 bar[t] 交易(前视偏差!)\n",
|
||
"#\n",
|
||
"# ✅ RIGHT: strategy sees bar[t], generates signal based on bar[t],\n",
|
||
"# order fills at bar[t+1] open (next day open)\n",
|
||
"# 策略看到 bar[t],生成信号,订单在 bar[t+1] 的开盘价成交\n",
|
||
"#\n",
|
||
"# This is why DataHandler, Strategy, Portfolio, and ExecutionHandler are\n",
|
||
"# separate — the separation naturally enforces the time boundary.\n",
|
||
"# 这就是为什么这四个组件分开——分离自然地强制了时间边界。\n",
|
||
"\n",
|
||
"# ============================================================================="
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "90210d19",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"class DataHandler:\n",
|
||
" \"\"\"\n",
|
||
" Streams historical OHLCV data bar by bar into the event queue.\n",
|
||
" 将历史 OHLCV 数据逐根K线地流入事件队列。\n",
|
||
"\n",
|
||
" In live trading, this would connect to a real-time market data API.\n",
|
||
" 在实盘交易中,这里会连接实时行情 API。\n",
|
||
" \"\"\"\n",
|
||
"\n",
|
||
" def __init__(self, data: pd.DataFrame, symbol: str, event_queue: queue.Queue):\n",
|
||
" \"\"\"\n",
|
||
" Parameters / 参数:\n",
|
||
" data — DataFrame with DatetimeIndex and columns:\n",
|
||
" 带 DatetimeIndex 的 DataFrame,列名为:\n",
|
||
" open, high, low, close, volume\n",
|
||
" symbol — ticker symbol / 股票代码\n",
|
||
" event_queue — the shared event queue / 共享事件队列\n",
|
||
" \"\"\"\n",
|
||
" self.data = data.copy()\n",
|
||
" self.symbol = symbol\n",
|
||
" self.event_queue = event_queue\n",
|
||
" self._dates = list(data.index) # all trading dates\n",
|
||
" self._cursor = 0 # current position in history\n",
|
||
" self._history = [] # bars seen so far (已观测的K线)\n",
|
||
"\n",
|
||
" @property\n",
|
||
" def current_date(self) -> Optional[pd.Timestamp]:\n",
|
||
" \"\"\"The date of the most recently published bar. 最新已发布K线的日期。\"\"\"\n",
|
||
" if self._cursor == 0:\n",
|
||
" return None\n",
|
||
" return self._dates[self._cursor - 1]\n",
|
||
"\n",
|
||
" @property\n",
|
||
" def n_bars_seen(self) -> int:\n",
|
||
" \"\"\"How many bars have been streamed so far. 到目前为止已流出多少根K线。\"\"\"\n",
|
||
" return self._cursor\n",
|
||
"\n",
|
||
" def has_more_data(self) -> bool:\n",
|
||
" \"\"\"Returns True if there is at least one more bar to stream.\"\"\"\n",
|
||
" return self._cursor < len(self._dates)\n",
|
||
"\n",
|
||
" def stream_next(self) -> bool:\n",
|
||
" \"\"\"\n",
|
||
" Emit the next bar as a MarketEvent onto the queue.\n",
|
||
" 将下一根K线作为 MarketEvent 发送到队列。\n",
|
||
"\n",
|
||
" Returns True if a bar was emitted, False if data is exhausted.\n",
|
||
" 如果发出了K线则返回 True,如果数据已耗尽则返回 False。\n",
|
||
" \"\"\"\n",
|
||
" if not self.has_more_data():\n",
|
||
" return False\n",
|
||
"\n",
|
||
" date = self._dates[self._cursor]\n",
|
||
" row = self.data.loc[date]\n",
|
||
"\n",
|
||
" # Build the MarketEvent for this bar / 构建本根K线的 MarketEvent\n",
|
||
" bar = MarketEvent(\n",
|
||
" symbol = self.symbol,\n",
|
||
" date = date,\n",
|
||
" open = float(row.get(\"open\", row.get(\"close\", 0))),\n",
|
||
" high = float(row.get(\"high\", row.get(\"close\", 0))),\n",
|
||
" low = float(row.get(\"low\", row.get(\"close\", 0))),\n",
|
||
" close = float(row[\"close\"]),\n",
|
||
" volume = float(row.get(\"volume\", 0)),\n",
|
||
" )\n",
|
||
"\n",
|
||
" # Add to internal history (the strategy can query this)\n",
|
||
" # 添加到内部历史记录(策略可以查询)\n",
|
||
" self._history.append(bar)\n",
|
||
" self._cursor += 1\n",
|
||
"\n",
|
||
" # Push onto the central event queue / 推送到中央事件队列\n",
|
||
" self.event_queue.put(bar)\n",
|
||
" return True\n",
|
||
"\n",
|
||
" def get_history(self, n: Optional[int] = None) -> List[MarketEvent]:\n",
|
||
" \"\"\"\n",
|
||
" Return the last n bars seen so far (default: all history).\n",
|
||
" 返回迄今为止看到的最后 n 根K线(默认:全部历史)。\n",
|
||
"\n",
|
||
" This is what the Strategy uses to compute indicators.\n",
|
||
" 这是策略用于计算技术指标的数据。\n",
|
||
" \"\"\"\n",
|
||
" if n is None:\n",
|
||
" return self._history\n",
|
||
" return self._history[-n:]\n",
|
||
"\n",
|
||
" def get_close_series(self) -> pd.Series:\n",
|
||
" \"\"\"\n",
|
||
" Return a pd.Series of all close prices seen so far.\n",
|
||
" 返回到目前为止所有已观测收盘价的 pd.Series。\n",
|
||
"\n",
|
||
" Useful for computing rolling indicators (移动平均等滚动指标).\n",
|
||
" \"\"\"\n",
|
||
" if not self._history:\n",
|
||
" return pd.Series(dtype=float)\n",
|
||
" return pd.Series(\n",
|
||
" [b.close for b in self._history],\n",
|
||
" index=[b.date for b in self._history],\n",
|
||
" )"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "0982acc9",
|
||
"metadata": {},
|
||
"source": [
|
||
"# =============================================================================\n",
|
||
"# SECTION 3: Strategy (Abstract Base + Concrete Implementations)\n",
|
||
"# 策略(抽象基类 + 具体实现)\n",
|
||
"\n",
|
||
"# =============================================================================\n",
|
||
"#\n",
|
||
"# A Strategy:\n",
|
||
"# 1. Listens for MarketEvents\n",
|
||
"# 2. Computes indicators on the historical data it has seen so far\n",
|
||
"# 3. Generates SignalEvents when its rules are triggered\n",
|
||
"#\n",
|
||
"# 策略:\n",
|
||
"# 1. 监听 MarketEvent\n",
|
||
"# 2. 根据迄今为止看到的历史数据计算指标\n",
|
||
"# 3. 当规则被触发时生成 SignalEvent\n",
|
||
"#\n",
|
||
"# The abstract base class (ABC) enforces a consistent interface so that\n",
|
||
"# we can swap strategies without changing any other component.\n",
|
||
"# 抽象基类强制统一接口,这样我们可以替换策略而不改变其他任何组件。\n",
|
||
"\n",
|
||
"# ============================================================================="
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "1520845f",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"class Strategy(ABC):\n",
|
||
" \"\"\"\n",
|
||
" Abstract base class for all strategies.\n",
|
||
" 所有策略的抽象基类。\n",
|
||
" \"\"\"\n",
|
||
"\n",
|
||
" def __init__(self, data_handler: DataHandler, event_queue: queue.Queue):\n",
|
||
" self.data = data_handler\n",
|
||
" self.queue = event_queue\n",
|
||
"\n",
|
||
" @abstractmethod\n",
|
||
" def on_market(self, event: MarketEvent) -> None:\n",
|
||
" \"\"\"\n",
|
||
" Called when a new MarketEvent arrives.\n",
|
||
" 新的 MarketEvent 到达时调用。\n",
|
||
" Subclasses implement their signal logic here.\n",
|
||
" 子类在这里实现信号逻辑。\n",
|
||
" \"\"\"\n",
|
||
" ...\n",
|
||
"\n",
|
||
" def send_signal(self, symbol: str, date: pd.Timestamp,\n",
|
||
" direction: Direction, strength: float = 1.0) -> None:\n",
|
||
" \"\"\"Helper to build and queue a SignalEvent. 构建并排队 SignalEvent 的辅助方法。\"\"\"\n",
|
||
" sig = SignalEvent(\n",
|
||
" symbol = symbol,\n",
|
||
" date = date,\n",
|
||
" direction = direction,\n",
|
||
" strength = strength,\n",
|
||
" strategy = self.__class__.__name__,\n",
|
||
" )\n",
|
||
" self.queue.put(sig)\n",
|
||
"\n",
|
||
"\n",
|
||
"class MACrossoverStrategy(Strategy):\n",
|
||
" \"\"\"\n",
|
||
" Dual Moving Average Crossover Strategy 双均线金叉/死叉策略\n",
|
||
" ─────────────────────────────────────────────────────────────\n",
|
||
" Rules / 规则:\n",
|
||
" Golden Cross (金叉): short SMA crosses ABOVE long SMA → BUY (做多)\n",
|
||
" Death Cross (死叉): short SMA crosses BELOW long SMA → SELL (平仓)\n",
|
||
"\n",
|
||
" Parameters / 参数:\n",
|
||
" short_window 短期均线窗口 (default 20 days)\n",
|
||
" long_window 长期均线窗口 (default 60 days)\n",
|
||
" \"\"\"\n",
|
||
"\n",
|
||
" def __init__(self, data_handler: DataHandler, event_queue: queue.Queue,\n",
|
||
" short_window: int = 20, long_window: int = 60):\n",
|
||
" super().__init__(data_handler, event_queue)\n",
|
||
" self.short_window = short_window\n",
|
||
" self.long_window = long_window\n",
|
||
" self._in_position = False # are we currently holding a long position?\n",
|
||
" # 当前是否持有多仓?\n",
|
||
"\n",
|
||
" def on_market(self, event: MarketEvent) -> None:\n",
|
||
" \"\"\"\n",
|
||
" Called on every new bar. 每根新K线调用一次。\n",
|
||
"\n",
|
||
" We need at least long_window bars before we can compute the long SMA.\n",
|
||
" 至少需要 long_window 根K线才能计算长期均线。\n",
|
||
" \"\"\"\n",
|
||
" closes = self.data.get_close_series()\n",
|
||
" n = len(closes)\n",
|
||
"\n",
|
||
" # Not enough data yet — wait / 数据不足,等待\n",
|
||
" if n < self.long_window:\n",
|
||
" return\n",
|
||
"\n",
|
||
" # Compute SMAs using only data available up to this bar\n",
|
||
" # 仅使用截至本K线的数据计算均线(无前视偏差)\n",
|
||
" sma_short = closes.iloc[-self.short_window:].mean()\n",
|
||
" sma_long = closes.iloc[-self.long_window:].mean()\n",
|
||
"\n",
|
||
" # Previous bar's SMAs (to detect crossover / 检测交叉)\n",
|
||
" if n < self.long_window + 1:\n",
|
||
" return # need one extra bar to detect a CHANGE in relationship\n",
|
||
"\n",
|
||
" sma_short_prev = closes.iloc[-(self.short_window + 1):-1].mean()\n",
|
||
" sma_long_prev = closes.iloc[-(self.long_window + 1):-1].mean()\n",
|
||
"\n",
|
||
" was_above = sma_short_prev > sma_long_prev # previous relationship\n",
|
||
" is_above = sma_short > sma_long # current relationship\n",
|
||
"\n",
|
||
" # ── Golden Cross 金叉 ─────────────────────────────────────────────\n",
|
||
" # Short MA just crossed ABOVE long MA → bullish signal\n",
|
||
" # 短期均线刚刚上穿长期均线 → 看涨信号\n",
|
||
" if is_above and not was_above and not self._in_position:\n",
|
||
" self.send_signal(event.symbol, event.date, Direction.LONG, strength=1.0)\n",
|
||
" self._in_position = True\n",
|
||
"\n",
|
||
" # ── Death Cross 死叉 ─────────────────────────────────────────────\n",
|
||
" # Short MA just crossed BELOW long MA → exit signal\n",
|
||
" # 短期均线刚刚下穿长期均线 → 平仓信号\n",
|
||
" elif not is_above and was_above and self._in_position:\n",
|
||
" self.send_signal(event.symbol, event.date, Direction.EXIT, strength=1.0)\n",
|
||
" self._in_position = False\n",
|
||
"\n",
|
||
"\n",
|
||
"class RSIMeanReversionStrategy(Strategy):\n",
|
||
" \"\"\"\n",
|
||
" RSI Mean Reversion Strategy RSI 均值回归策略\n",
|
||
" ─────────────────────────────────────────────\n",
|
||
" Rules / 规则:\n",
|
||
" RSI < oversold (超卖) → BUY (做多)\n",
|
||
" RSI > overbought(超买) → SELL EXIT (平多)\n",
|
||
" (Also supports going SHORT / 也支持做空)\n",
|
||
"\n",
|
||
" Parameters / 参数:\n",
|
||
" window RSI计算窗口 (default 14)\n",
|
||
" oversold 超卖阈值 (default 30)\n",
|
||
" overbought 超买阈值 (default 70)\n",
|
||
" \"\"\"\n",
|
||
"\n",
|
||
" def __init__(self, data_handler: DataHandler, event_queue: queue.Queue,\n",
|
||
" window: int = 14, oversold: float = 30, overbought: float = 70):\n",
|
||
" super().__init__(data_handler, event_queue)\n",
|
||
" self.window = window\n",
|
||
" self.oversold = oversold\n",
|
||
" self.overbought = overbought\n",
|
||
" self._position = 0 # 0=flat, 1=long, -1=short (0=空仓, 1=多仓, -1=空仓)\n",
|
||
"\n",
|
||
" def _compute_rsi(self, closes: pd.Series) -> float:\n",
|
||
" \"\"\"\n",
|
||
" Compute the current RSI value using Wilder's smoothing.\n",
|
||
" 使用 Wilder 平滑法计算当前 RSI 值。\n",
|
||
" \"\"\"\n",
|
||
" if len(closes) < self.window + 1:\n",
|
||
" return 50.0 # neutral / 中性,数据不足时返回中性值\n",
|
||
"\n",
|
||
" delta = closes.diff()\n",
|
||
" gain = delta.clip(lower=0)\n",
|
||
" loss = -delta.clip(upper=0)\n",
|
||
" avg_gain = gain.ewm(alpha=1.0 / self.window, adjust=False).mean().iloc[-1]\n",
|
||
" avg_loss = loss.ewm(alpha=1.0 / self.window, adjust=False).mean().iloc[-1]\n",
|
||
"\n",
|
||
" if avg_loss == 0:\n",
|
||
" return 100.0\n",
|
||
" rs = avg_gain / avg_loss\n",
|
||
" return 100.0 - (100.0 / (1.0 + rs))\n",
|
||
"\n",
|
||
" def on_market(self, event: MarketEvent) -> None:\n",
|
||
" closes = self.data.get_close_series()\n",
|
||
" rsi = self._compute_rsi(closes)\n",
|
||
"\n",
|
||
" # ── Entry: Oversold → Go Long 入场:超卖 → 做多 ──────────────────\n",
|
||
" if rsi < self.oversold and self._position == 0:\n",
|
||
" self.send_signal(event.symbol, event.date, Direction.LONG, strength=0.8)\n",
|
||
" self._position = 1\n",
|
||
"\n",
|
||
" # ── Entry: Overbought → Go Short 入场:超买 → 做空 ───────────────\n",
|
||
" elif rsi > self.overbought and self._position == 0:\n",
|
||
" self.send_signal(event.symbol, event.date, Direction.SHORT, strength=0.8)\n",
|
||
" self._position = -1\n",
|
||
"\n",
|
||
" # ── Exit Long when RSI recovers to neutral 多仓RSI回到中性时平仓 ──\n",
|
||
" elif self._position == 1 and rsi > 50:\n",
|
||
" self.send_signal(event.symbol, event.date, Direction.EXIT, strength=1.0)\n",
|
||
" self._position = 0\n",
|
||
"\n",
|
||
" # ── Exit Short when RSI falls back to neutral 空仓RSI回到中性时平仓\n",
|
||
" elif self._position == -1 and rsi < 50:\n",
|
||
" self.send_signal(event.symbol, event.date, Direction.EXIT, strength=1.0)\n",
|
||
" self._position = 0"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "0256875a",
|
||
"metadata": {},
|
||
"source": [
|
||
"# =============================================================================\n",
|
||
"# SECTION 4: Portfolio 组合管理器\n",
|
||
"\n",
|
||
"# =============================================================================\n",
|
||
"#\n",
|
||
"# The Portfolio is the \"brain\" that manages money. It:\n",
|
||
"# 1. Receives SignalEvents from the Strategy\n",
|
||
"# 2. Decides HOW MUCH to trade (position sizing / 仓位管理)\n",
|
||
"# 3. Creates OrderEvents for the ExecutionHandler\n",
|
||
"# 4. Receives FillEvents from the broker and updates:\n",
|
||
"# - Cash balance (现金余额)\n",
|
||
"# - Positions (持仓)\n",
|
||
"# - Realized P&L (已实现盈亏)\n",
|
||
"# - Unrealized P&L(未实现盈亏)\n",
|
||
"# 5. Records the daily equity (净值) for performance analysis\n",
|
||
"#\n",
|
||
"# 组合管理器是管理资金的\"大脑\"。它接收策略信号,决定交易多少,\n",
|
||
"# 向执行器发出订单,接收成交回报并更新现金/持仓/盈亏,记录每日净值。\n",
|
||
"#\n",
|
||
"# Position Sizing (仓位管理) methods / 常见方法:\n",
|
||
"# Fixed Fraction 固定比例 — always use X% of capital (简单,本文采用)\n",
|
||
"# Fixed Dollar 固定金额 — always trade $N worth\n",
|
||
"# Kelly Criterion 凯利公式 — optimal fraction based on edge & odds\n",
|
||
"# Volatility Scaling 波动率缩放 — scale size to target a fixed daily risk\n",
|
||
"\n",
|
||
"# ============================================================================="
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "22482bd2",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"class Portfolio:\n",
|
||
" \"\"\"\n",
|
||
" Tracks cash, positions, and P&L. Records equity curve.\n",
|
||
" 追踪现金、持仓和盈亏,记录净值曲线。\n",
|
||
" \"\"\"\n",
|
||
"\n",
|
||
" def __init__(\n",
|
||
" self,\n",
|
||
" data_handler : DataHandler,\n",
|
||
" event_queue : queue.Queue,\n",
|
||
" initial_capital: float = 1_000_000.0,\n",
|
||
" position_pct : float = 0.95, # fraction of capital to deploy per trade\n",
|
||
" # 每次交易使用资本的比例 (95%)\n",
|
||
" ):\n",
|
||
" self.data = data_handler\n",
|
||
" self.queue = event_queue\n",
|
||
" self.initial_capital = initial_capital\n",
|
||
" self.position_pct = position_pct\n",
|
||
"\n",
|
||
" # ── Account state 账户状态 ──────────────────────────────────────\n",
|
||
" self.cash = initial_capital # 现金余额 / available cash\n",
|
||
" self.positions : Dict[str, int] = {} # symbol → qty (持仓数量)\n",
|
||
" self.avg_cost : Dict[str, float] = {} # symbol → avg entry price (持仓均价)\n",
|
||
"\n",
|
||
" # ── Trade log 交易记录 ───────────────────────────────────────────\n",
|
||
" # Every completed fill gets recorded here for analysis.\n",
|
||
" # 每笔完成的成交都记录在这里以供分析。\n",
|
||
" self.trade_log: List[dict] = []\n",
|
||
"\n",
|
||
" # ── Equity curve 净值曲线 ────────────────────────────────────────\n",
|
||
" # Recorded once per day after market close.\n",
|
||
" # 每天收盘后记录一次。\n",
|
||
" self.equity_curve: List[dict] = []\n",
|
||
"\n",
|
||
" # ── Computed properties 计算属性 ─────────────────────────────────────────\n",
|
||
"\n",
|
||
" def market_value(self, current_prices: Dict[str, float]) -> float:\n",
|
||
" \"\"\"\n",
|
||
" Current market value of all open positions.\n",
|
||
" 所有开放持仓的当前市值。\n",
|
||
" 市值 = Σ(持仓数量 × 当前价格)\n",
|
||
" \"\"\"\n",
|
||
" return sum(\n",
|
||
" qty * current_prices.get(sym, 0.0)\n",
|
||
" for sym, qty in self.positions.items()\n",
|
||
" )\n",
|
||
"\n",
|
||
" def total_equity(self, current_prices: Dict[str, float]) -> float:\n",
|
||
" \"\"\"\n",
|
||
" Total portfolio value: cash + market value of positions.\n",
|
||
" 账户总价值:现金 + 持仓市值。\n",
|
||
" 总资产 = 现金余额 + 持仓市值\n",
|
||
" \"\"\"\n",
|
||
" return self.cash + self.market_value(current_prices)\n",
|
||
"\n",
|
||
" # ── Event handlers 事件处理器 ─────────────────────────────────────────────\n",
|
||
"\n",
|
||
" def on_signal(self, event: SignalEvent) -> None:\n",
|
||
" \"\"\"\n",
|
||
" Convert a SignalEvent into an OrderEvent.\n",
|
||
" 将 SignalEvent 转换为 OrderEvent(决定下多少单)。\n",
|
||
"\n",
|
||
" This is where position sizing (仓位管理) happens.\n",
|
||
" 这里进行仓位计算。\n",
|
||
" \"\"\"\n",
|
||
" symbol = event.symbol\n",
|
||
" direction = event.direction\n",
|
||
"\n",
|
||
" # ── Determine quantity 计算交易数量 ─────────────────────────────\n",
|
||
" # We use Fixed Fraction sizing: deploy position_pct of current equity.\n",
|
||
" # 使用固定比例法:每次使用当前总资产的 position_pct。\n",
|
||
" #\n",
|
||
" # quantity = floor( equity × position_pct × strength / current_price )\n",
|
||
" # 数量 = floor( 总资产 × 仓位比例 × 信号强度 / 当前价格 )\n",
|
||
"\n",
|
||
" current_price = self.data.get_close_series().iloc[-1]\n",
|
||
"\n",
|
||
" if direction == Direction.EXIT:\n",
|
||
" # Close the entire existing position / 平掉全部现有持仓\n",
|
||
" qty = abs(self.positions.get(symbol, 0))\n",
|
||
" if qty == 0:\n",
|
||
" return # nothing to close / 没有持仓可平\n",
|
||
" else:\n",
|
||
" # New entry — compute share count / 新入场——计算股数\n",
|
||
" capital_to_deploy = self.cash * self.position_pct * event.strength\n",
|
||
" qty = int(capital_to_deploy / current_price)\n",
|
||
" if qty == 0:\n",
|
||
" return # not enough cash / 资金不足\n",
|
||
"\n",
|
||
" order = OrderEvent(\n",
|
||
" symbol = symbol,\n",
|
||
" date = event.date,\n",
|
||
" order_type = OrderType.MARKET, # use market orders for simplicity\n",
|
||
" direction = direction,\n",
|
||
" quantity = qty,\n",
|
||
" )\n",
|
||
" self.queue.put(order)\n",
|
||
"\n",
|
||
" def on_fill(self, event: FillEvent) -> None:\n",
|
||
" \"\"\"\n",
|
||
" Update cash and positions when a fill is confirmed.\n",
|
||
" 成交确认后更新现金和持仓。\n",
|
||
" \"\"\"\n",
|
||
" symbol = event.symbol\n",
|
||
" qty = event.quantity\n",
|
||
" price = event.fill_price\n",
|
||
" comm = event.commission\n",
|
||
"\n",
|
||
" if event.direction in (Direction.LONG, Direction.SHORT):\n",
|
||
" # ── Opening a position 建仓 ──────────────────────────────────\n",
|
||
" sign = 1 if event.direction == Direction.LONG else -1\n",
|
||
"\n",
|
||
" # Update average cost 更新持仓均价\n",
|
||
" old_qty = self.positions.get(symbol, 0)\n",
|
||
" old_cost = self.avg_cost.get(symbol, 0.0)\n",
|
||
" new_qty = old_qty + sign * qty\n",
|
||
" if new_qty != 0:\n",
|
||
" # Weighted average cost 加权平均成本\n",
|
||
" self.avg_cost[symbol] = (\n",
|
||
" (old_cost * abs(old_qty) + price * qty) / abs(new_qty)\n",
|
||
" )\n",
|
||
" self.positions[symbol] = new_qty\n",
|
||
"\n",
|
||
" # Deduct cash (buy) / add cash (short sell)\n",
|
||
" # 扣除现金(买入)/ 增加现金(卖空)\n",
|
||
" self.cash -= sign * (price * qty) + comm\n",
|
||
"\n",
|
||
" elif event.direction == Direction.EXIT:\n",
|
||
" # ── Closing a position 平仓 ──────────────────────────────────\n",
|
||
" entry_price = self.avg_cost.get(symbol, price)\n",
|
||
" pos_sign = 1 if self.positions.get(symbol, 0) > 0 else -1\n",
|
||
"\n",
|
||
" # Realized P&L 已实现盈亏\n",
|
||
" # P&L = (exit_price - entry_price) × qty × direction\n",
|
||
" pnl = pos_sign * (price - entry_price) * qty - comm\n",
|
||
"\n",
|
||
" # Return cash from closing the position\n",
|
||
" # 平仓回收现金\n",
|
||
" self.cash += pos_sign * price * qty - comm\n",
|
||
"\n",
|
||
" # Clear position 清除持仓\n",
|
||
" self.positions[symbol] = 0\n",
|
||
" self.avg_cost[symbol] = 0.0\n",
|
||
"\n",
|
||
" # Log the trade 记录交易\n",
|
||
" self.trade_log.append({\n",
|
||
" \"date\" : event.date,\n",
|
||
" \"symbol\" : symbol,\n",
|
||
" \"direction\" : \"LONG→EXIT\" if pos_sign == 1 else \"SHORT→EXIT\",\n",
|
||
" \"entry_price\" : entry_price,\n",
|
||
" \"exit_price\" : price,\n",
|
||
" \"quantity\" : qty,\n",
|
||
" \"pnl\" : pnl,\n",
|
||
" \"commission\" : comm,\n",
|
||
" })\n",
|
||
"\n",
|
||
" def record_equity(self, date: pd.Timestamp, current_price: float) -> None:\n",
|
||
" \"\"\"\n",
|
||
" Snapshot the portfolio value at end of day.\n",
|
||
" 记录每日收盘时的组合价值快照。\n",
|
||
" \"\"\"\n",
|
||
" prices = {self.data.symbol: current_price}\n",
|
||
" self.equity_curve.append({\n",
|
||
" \"date\" : date,\n",
|
||
" \"cash\" : self.cash,\n",
|
||
" \"market_value\" : self.market_value(prices),\n",
|
||
" \"total_equity\" : self.total_equity(prices),\n",
|
||
" })\n",
|
||
"\n",
|
||
" def get_equity_series(self) -> pd.Series:\n",
|
||
" \"\"\"Return equity curve as a pd.Series. 返回净值曲线为 pd.Series。\"\"\"\n",
|
||
" if not self.equity_curve:\n",
|
||
" return pd.Series(dtype=float)\n",
|
||
" df = pd.DataFrame(self.equity_curve).set_index(\"date\")\n",
|
||
" return df[\"total_equity\"]"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "34df6025",
|
||
"metadata": {},
|
||
"source": [
|
||
"# =============================================================================\n",
|
||
"# SECTION 5: ExecutionHandler (Simulated Broker)\n",
|
||
"# 执行处理器(模拟券商)\n",
|
||
"\n",
|
||
"# =============================================================================\n",
|
||
"#\n",
|
||
"# In live trading, this connects to a real broker API (e.g. Interactive\n",
|
||
"# Brokers, Alpaca, or a Chinese broker via vnpy).\n",
|
||
"# 在实盘交易中,这里连接真实券商 API(如 Interactive Brokers、Alpaca\n",
|
||
"# 或通过 vnpy 连接国内券商)。\n",
|
||
"#\n",
|
||
"# In backtesting, we SIMULATE the broker. The key costs to model are:\n",
|
||
"# 在回测中,我们模拟券商。需要建模的主要成本:\n",
|
||
"#\n",
|
||
"# 1. Commission 佣金\n",
|
||
"# A fixed percentage of trade value charged by the broker.\n",
|
||
"# 券商按交易金额收取的固定比例费用。\n",
|
||
"# Typical in China A-shares: 万分之三 (0.03%) per side\n",
|
||
"# 中国A股典型佣金:单边万分之三\n",
|
||
"#\n",
|
||
"# 2. Slippage 滑点\n",
|
||
"# The difference between the price at signal time and actual fill price.\n",
|
||
"# 信号生成时的价格与实际成交价格之间的差异。\n",
|
||
"# Causes: bid-ask spread, market impact, order queue position.\n",
|
||
"# 原因:买卖价差、市场冲击、排队等待。\n",
|
||
"#\n",
|
||
"# Slippage models / 滑点模型:\n",
|
||
"# Fixed percentage 固定比例 — simple, conservative\n",
|
||
"# Volume-weighted 成交量加权 — larger orders → more slippage\n",
|
||
"# Fixed spread 固定点差 — realistic for liquid markets\n",
|
||
"#\n",
|
||
"# 3. Fill Assumption 成交假设\n",
|
||
"# We assume market orders fill at next bar's OPEN price.\n",
|
||
"# 我们假设市价单在下一根K线的开盘价成交。\n",
|
||
"# This is a conservative and common assumption.\n",
|
||
"# 这是保守且常用的假设。\n",
|
||
"\n",
|
||
"# ============================================================================="
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "a0af244c",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"class SimulatedBroker:\n",
|
||
" \"\"\"\n",
|
||
" Simulates a brokerage: fills market orders with realistic costs.\n",
|
||
" 模拟券商:以真实成本成交市价单。\n",
|
||
" \"\"\"\n",
|
||
"\n",
|
||
" def __init__(\n",
|
||
" self,\n",
|
||
" event_queue : queue.Queue,\n",
|
||
" commission_rate : float = 0.0003, # 0.03% per side / 单边万分之三\n",
|
||
" slippage_rate : float = 0.0001, # 0.01% slippage / 万分之一滑点\n",
|
||
" fill_on : str = \"next_open\", # when to fill / 何时成交\n",
|
||
" min_commission : float = 5.0, # minimum commission / 最低佣金\n",
|
||
" ):\n",
|
||
" \"\"\"\n",
|
||
" Parameters / 参数:\n",
|
||
" commission_rate 佣金率 (per side / 单边)\n",
|
||
" slippage_rate 滑点率 (applied as adverse price move / 不利价格移动)\n",
|
||
" fill_on 成交时机 'next_open' or 'current_close'\n",
|
||
" min_commission 最低佣金\n",
|
||
" \"\"\"\n",
|
||
" self.queue = event_queue\n",
|
||
" self.commission_rate = commission_rate\n",
|
||
" self.slippage_rate = slippage_rate\n",
|
||
" self.fill_on = fill_on\n",
|
||
" self.min_commission = min_commission\n",
|
||
"\n",
|
||
" # Store pending orders to fill on next bar's open\n",
|
||
" # 存储待处理订单,在下一根K线开盘价成交\n",
|
||
" self._pending_orders: List[OrderEvent] = []\n",
|
||
"\n",
|
||
" def on_order(self, event: OrderEvent) -> None:\n",
|
||
" \"\"\"\n",
|
||
" Receive an order from Portfolio.\n",
|
||
" 接收来自组合管理器的订单。\n",
|
||
"\n",
|
||
" For market orders, we queue them to fill at next open.\n",
|
||
" 对于市价单,我们将其排队,在下一根K线的开盘价成交。\n",
|
||
" \"\"\"\n",
|
||
" if event.order_type == OrderType.MARKET:\n",
|
||
" self._pending_orders.append(event)\n",
|
||
" # Limit and Stop orders would need price monitoring — not implemented here\n",
|
||
" # 限价单和止损单需要价格监控——此处未实现,留作扩展\n",
|
||
"\n",
|
||
" def fill_pending(self, bar: MarketEvent) -> None:\n",
|
||
" \"\"\"\n",
|
||
" Fill all pending orders at the open of the provided bar.\n",
|
||
" 以提供K线的开盘价成交所有待处理订单。\n",
|
||
"\n",
|
||
" This is called at the start of each new bar, before the Strategy\n",
|
||
" gets to process it.\n",
|
||
" 每根新K线开始时(策略处理之前)调用。\n",
|
||
" \"\"\"\n",
|
||
" for order in self._pending_orders:\n",
|
||
" fill_price = self._compute_fill_price(order, bar)\n",
|
||
" commission = self._compute_commission(order, fill_price)\n",
|
||
" slippage = fill_price - bar.open # adverse difference from open\n",
|
||
"\n",
|
||
" fill = FillEvent(\n",
|
||
" symbol = order.symbol,\n",
|
||
" date = bar.date, # filled on THIS bar's date\n",
|
||
" direction = order.direction,\n",
|
||
" quantity = order.quantity,\n",
|
||
" fill_price = fill_price,\n",
|
||
" commission = commission,\n",
|
||
" slippage = slippage,\n",
|
||
" )\n",
|
||
" self.queue.put(fill)\n",
|
||
"\n",
|
||
" self._pending_orders.clear()\n",
|
||
"\n",
|
||
" def _compute_fill_price(self, order: OrderEvent, bar: MarketEvent) -> float:\n",
|
||
" \"\"\"\n",
|
||
" Apply slippage to the open price.\n",
|
||
" 对开盘价施加滑点。\n",
|
||
"\n",
|
||
" Slippage always works AGAINST the trader:\n",
|
||
" 滑点总是对交易者不利:\n",
|
||
" BUY (LONG) : fill above open (买入时成交价高于开盘价)\n",
|
||
" SELL (EXIT) : fill below open (卖出时成交价低于开盘价)\n",
|
||
" \"\"\"\n",
|
||
" direction = order.direction\n",
|
||
" if direction in (Direction.LONG, Direction.SHORT):\n",
|
||
" # Buying: slippage pushes price UP 买入:滑点使价格上升\n",
|
||
" return bar.open * (1 + self.slippage_rate)\n",
|
||
" else:\n",
|
||
" # Selling: slippage pushes price DOWN 卖出:滑点使价格下降\n",
|
||
" return bar.open * (1 - self.slippage_rate)\n",
|
||
"\n",
|
||
" def _compute_commission(self, order: OrderEvent, fill_price: float) -> float:\n",
|
||
" \"\"\"\n",
|
||
" Compute brokerage commission.\n",
|
||
" 计算券商佣金。\n",
|
||
"\n",
|
||
" commission = max(commission_rate × trade_value, min_commission)\n",
|
||
" 佣金 = max(佣金率 × 交易金额, 最低佣金)\n",
|
||
" \"\"\"\n",
|
||
" trade_value = fill_price * order.quantity\n",
|
||
" commission = max(trade_value * self.commission_rate, self.min_commission)\n",
|
||
" return commission"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "aa2f0b07",
|
||
"metadata": {},
|
||
"source": [
|
||
"# =============================================================================\n",
|
||
"# SECTION 6: BacktestEngine 回测引擎(主事件循环)\n",
|
||
"\n",
|
||
"# =============================================================================\n",
|
||
"#\n",
|
||
"# The BacktestEngine is the conductor — it orchestrates all components by\n",
|
||
"# running the main event loop.\n",
|
||
"# 回测引擎是指挥者——通过运行主事件循环来协调所有组件。\n",
|
||
"#\n",
|
||
"# Main Loop Algorithm 主循环算法:\n",
|
||
"# ──────────────────────────────────────────────────────────────────────────\n",
|
||
"# WHILE data feed has more bars:\n",
|
||
"# 1. DataHandler.stream_next() → puts MarketEvent on queue\n",
|
||
"# 2. Broker.fill_pending(bar) → fills any leftover orders FIRST\n",
|
||
"# (orders placed yesterday fill at today's open — no lookahead!)\n",
|
||
"# (昨天的订单今天开盘价成交 — 无前视偏差!)\n",
|
||
"# 3. WHILE queue is not empty:\n",
|
||
"# event = queue.get()\n",
|
||
"# IF MarketEvent → Strategy.on_market(event)\n",
|
||
"# ELIF SignalEvent → Portfolio.on_signal(event)\n",
|
||
"# ELIF OrderEvent → Broker.on_order(event)\n",
|
||
"# ELIF FillEvent → Portfolio.on_fill(event)\n",
|
||
"# 4. Portfolio.record_equity(today's close)\n",
|
||
"# END WHILE\n",
|
||
"# ──────────────────────────────────────────────────────────────────────────\n",
|
||
"#\n",
|
||
"# Why is step 2 (fill_pending) before step 3 (process queue)?\n",
|
||
"# 为什么步骤2(成交待处理)在步骤3(处理队列)之前?\n",
|
||
"#\n",
|
||
"# Orders generated on day T are pending. On day T+1 the broker fills them\n",
|
||
"# at the open BEFORE the strategy sees day T+1's data.\n",
|
||
"# T日生成的订单是待处理状态。T+1日的开盘价成交(在策略看到T+1数据之前)。\n",
|
||
"# This correctly models the one-day execution delay of real trading.\n",
|
||
"# 这正确模拟了真实交易的一天执行延迟。\n",
|
||
"\n",
|
||
"# ============================================================================="
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "735f99e9",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"class BacktestEngine:\n",
|
||
" \"\"\"\n",
|
||
" Main event-driven backtest engine. 主事件驱动回测引擎。\n",
|
||
" Wires together DataHandler, Strategy, Portfolio, and Broker.\n",
|
||
" 将 DataHandler、Strategy、Portfolio 和 Broker 组合在一起。\n",
|
||
" \"\"\"\n",
|
||
"\n",
|
||
" def __init__(\n",
|
||
" self,\n",
|
||
" data_handler : DataHandler,\n",
|
||
" strategy : Strategy,\n",
|
||
" portfolio : Portfolio,\n",
|
||
" broker : SimulatedBroker,\n",
|
||
" name : str = \"Event-Driven Backtest\",\n",
|
||
" ):\n",
|
||
" self.data = data_handler\n",
|
||
" self.strategy = strategy\n",
|
||
" self.portfolio = portfolio\n",
|
||
" self.broker = broker\n",
|
||
" self.name = name\n",
|
||
"\n",
|
||
" # All components share this single event queue / 所有组件共享这一事件队列\n",
|
||
" assert (data_handler.event_queue is strategy.queue is\n",
|
||
" portfolio.queue is broker.queue), \\\n",
|
||
" \"All components must share the same event queue!\"\n",
|
||
"\n",
|
||
" self._queue = data_handler.event_queue\n",
|
||
"\n",
|
||
" def run(self, verbose: bool = False) -> None:\n",
|
||
" \"\"\"\n",
|
||
" Execute the backtest from start to finish.\n",
|
||
" 从头到尾执行回测。\n",
|
||
"\n",
|
||
" Parameters / 参数:\n",
|
||
" verbose — if True, print every event (useful for debugging)\n",
|
||
" 如果为 True,打印每个事件(用于调试)\n",
|
||
" \"\"\"\n",
|
||
" bar_count = 0\n",
|
||
" event_count= 0\n",
|
||
"\n",
|
||
" print(f\"\\n{'─' * 60}\")\n",
|
||
" print(f\" 回测开始: {self.name}\")\n",
|
||
" print(f\" Backtest Start: {self.data.data.index[0].date()} → \"\n",
|
||
" f\"{self.data.data.index[-1].date()}\")\n",
|
||
" print(f\"{'─' * 60}\")\n",
|
||
"\n",
|
||
" # ── Main loop 主循环 ─────────────────────────────────────────────\n",
|
||
" while self.data.has_more_data():\n",
|
||
"\n",
|
||
" # ── Step 1: Stream the next bar 流出下一根K线 ─────────────────\n",
|
||
" # This puts a MarketEvent on the queue.\n",
|
||
" # 这会将一个 MarketEvent 放入队列。\n",
|
||
" self.data.stream_next()\n",
|
||
" bar_count += 1\n",
|
||
"\n",
|
||
" # ── Step 2: Fill yesterday's pending orders at today's open ───\n",
|
||
" # ── 步骤2:以今天的开盘价成交昨天的待处理订单 ─────────────────\n",
|
||
" current_bar = self.data._history[-1]\n",
|
||
" self.broker.fill_pending(current_bar)\n",
|
||
"\n",
|
||
" # ── Step 3: Process all events in the queue 处理队列中的所有事件\n",
|
||
" while not self._queue.empty():\n",
|
||
" event = self._queue.get()\n",
|
||
" event_count += 1\n",
|
||
"\n",
|
||
" if verbose:\n",
|
||
" print(f\" [{event.type.value}] {event}\")\n",
|
||
"\n",
|
||
" if event.type == EventType.MARKET:\n",
|
||
" # Strategy sees the new price bar and may emit a signal\n",
|
||
" # 策略看到新K线,可能发出信号\n",
|
||
" self.strategy.on_market(event)\n",
|
||
"\n",
|
||
" elif event.type == EventType.SIGNAL:\n",
|
||
" # Portfolio receives signal and may emit an order\n",
|
||
" # 组合管理器接收信号,可能发出订单\n",
|
||
" self.portfolio.on_signal(event)\n",
|
||
"\n",
|
||
" elif event.type == EventType.ORDER:\n",
|
||
" # Broker receives the order (will fill on next bar's open)\n",
|
||
" # 券商接收订单(将在下一根K线的开盘价成交)\n",
|
||
" self.broker.on_order(event)\n",
|
||
"\n",
|
||
" elif event.type == EventType.FILL:\n",
|
||
" # Portfolio updates its cash & position records\n",
|
||
" # 组合管理器更新现金和持仓记录\n",
|
||
" self.portfolio.on_fill(event)\n",
|
||
"\n",
|
||
" # ── Step 4: Record end-of-day equity 记录当日收盘净值 ───────\n",
|
||
" self.portfolio.record_equity(current_bar.date, current_bar.close)\n",
|
||
"\n",
|
||
" n_trades = len(self.portfolio.trade_log)\n",
|
||
" print(f\" 回测完成! Backtest Complete!\")\n",
|
||
" print(f\" 总K线数: {bar_count} 总事件数: {event_count} 总交易数: {n_trades}\")\n",
|
||
" print(f\"{'─' * 60}\")\n",
|
||
"\n",
|
||
" # ── Performance metrics 绩效指标 ─────────────────────────────────────────\n",
|
||
"\n",
|
||
" def metrics(self) -> dict:\n",
|
||
" \"\"\"\n",
|
||
" Compute performance metrics from the equity curve.\n",
|
||
" 从净值曲线计算绩效指标。\n",
|
||
" \"\"\"\n",
|
||
" eq = self.portfolio.get_equity_series()\n",
|
||
" ret = eq.pct_change().fillna(0)\n",
|
||
" n = len(ret)\n",
|
||
" years = n / 252.0\n",
|
||
"\n",
|
||
" total_return = (eq.iloc[-1] / self.portfolio.initial_capital) - 1\n",
|
||
" cagr = (1 + total_return) ** (1 / years) - 1 if years > 0 else 0\n",
|
||
" ann_vol = ret.std() * np.sqrt(252)\n",
|
||
" sharpe = (ret.mean() / ret.std()) * np.sqrt(252) if ret.std() > 0 else 0\n",
|
||
"\n",
|
||
" downside_std = ret[ret < 0].std() * np.sqrt(252)\n",
|
||
" sortino = cagr / downside_std if downside_std > 0 else 0\n",
|
||
"\n",
|
||
" rolling_max = eq.cummax()\n",
|
||
" drawdown = (eq - rolling_max) / rolling_max\n",
|
||
" max_dd = drawdown.min()\n",
|
||
" calmar = cagr / abs(max_dd) if max_dd != 0 else 0\n",
|
||
" win_rate = (ret > 0).mean()\n",
|
||
"\n",
|
||
" # Trade-level statistics 交易级别统计\n",
|
||
" trades = self.portfolio.trade_log\n",
|
||
" n_trades = len(trades)\n",
|
||
" if n_trades > 0:\n",
|
||
" pnls = [t[\"pnl\"] for t in trades]\n",
|
||
" win_trades = [p for p in pnls if p > 0]\n",
|
||
" loss_trades= [p for p in pnls if p < 0]\n",
|
||
" trade_win_rate = len(win_trades) / n_trades\n",
|
||
" avg_win = np.mean(win_trades) if win_trades else 0\n",
|
||
" avg_loss = np.mean(loss_trades) if loss_trades else 0\n",
|
||
" profit_factor = (sum(win_trades) / abs(sum(loss_trades))\n",
|
||
" if loss_trades else np.inf)\n",
|
||
" else:\n",
|
||
" trade_win_rate = profit_factor = avg_win = avg_loss = 0\n",
|
||
"\n",
|
||
" return {\n",
|
||
" \"总收益率 Total Return\" : f\"{total_return:.2%}\",\n",
|
||
" \"年化收益率 CAGR\" : f\"{cagr:.2%}\",\n",
|
||
" \"年化波动率 Ann. Volatility\" : f\"{ann_vol:.2%}\",\n",
|
||
" \"夏普比率 Sharpe Ratio\" : f\"{sharpe:.3f}\",\n",
|
||
" \"索提诺比率 Sortino Ratio\" : f\"{sortino:.3f}\",\n",
|
||
" \"最大回撤 Max Drawdown\" : f\"{max_dd:.2%}\",\n",
|
||
" \"卡玛比率 Calmar Ratio\" : f\"{calmar:.3f}\",\n",
|
||
" \"日胜率 Daily Win Rate\" : f\"{win_rate:.2%}\",\n",
|
||
" \"交易次数 # Trades (round trip)\": str(n_trades),\n",
|
||
" \"交易胜率 Trade Win Rate\" : f\"{trade_win_rate:.2%}\",\n",
|
||
" \"平均盈利 Avg Win (per trade)\" : f\"¥{avg_win:,.0f}\",\n",
|
||
" \"平均亏损 Avg Loss (per trade)\": f\"¥{avg_loss:,.0f}\",\n",
|
||
" \"盈亏比 Profit Factor\" : f\"{profit_factor:.3f}\",\n",
|
||
" }\n",
|
||
"\n",
|
||
" def print_metrics(self):\n",
|
||
" \"\"\"Pretty-print the performance report. 格式化打印绩效报告。\"\"\"\n",
|
||
" print(f\"\\n{'=' * 60}\")\n",
|
||
" print(f\" 策略绩效报告 / Performance Report\")\n",
|
||
" print(f\" {self.name}\")\n",
|
||
" print(f\"{'=' * 60}\")\n",
|
||
" for k, v in self.metrics().items():\n",
|
||
" print(f\" {k:<40} {v}\")\n",
|
||
"\n",
|
||
" print(f\"\\n ── 最近 5 笔交易 Last 5 Trades ────────────────────────\")\n",
|
||
" trades = self.portfolio.trade_log[-5:]\n",
|
||
" if trades:\n",
|
||
" for t in trades:\n",
|
||
" pnl_sign = \"+\" if t[\"pnl\"] >= 0 else \"\"\n",
|
||
" print(f\" {t['date'].date()} {t['direction']:<12} \"\n",
|
||
" f\"进价 {t['entry_price']:>8.2f} 出价 {t['exit_price']:>8.2f} \"\n",
|
||
" f\"股数 {t['quantity']:>5} \"\n",
|
||
" f\"盈亏 {pnl_sign}{t['pnl']:>10,.0f}¥\")\n",
|
||
" else:\n",
|
||
" print(\" (无完整交易 / No completed round-trip trades)\")\n",
|
||
" print(f\"{'=' * 60}\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "1bf6c24a",
|
||
"metadata": {},
|
||
"source": [
|
||
"# =============================================================================\n",
|
||
"# SECTION 7: Assemble & Run 组装并运行\n",
|
||
"\n",
|
||
"# =============================================================================\n",
|
||
"\n",
|
||
"# ── Generate synthetic price data (same seed as previous demo for consistency)\n",
|
||
"# ── 生成合成价格数据(与前一个演示相同的随机种子以保持一致性)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "fcb04ce6",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def generate_price_series(n_days=1500, mu=0.10, sigma=0.25, s0=100.0, seed=42):\n",
|
||
" \"\"\"Geometric Brownian Motion price series. 几何布朗运动价格序列。\"\"\"\n",
|
||
" np.random.seed(seed)\n",
|
||
" dt = 1.0 / 252\n",
|
||
" eps = np.random.randn(n_days)\n",
|
||
" log_ret = (mu - 0.5 * sigma**2) * dt + sigma * np.sqrt(dt) * eps\n",
|
||
" prices = s0 * np.exp(np.cumsum(log_ret))\n",
|
||
" dates = pd.bdate_range(start=\"2019-01-02\", periods=n_days)\n",
|
||
"\n",
|
||
" # Build a realistic OHLCV DataFrame 构建真实的 OHLCV DataFrame\n",
|
||
" daily_range = prices * np.abs(np.random.randn(n_days)) * 0.015\n",
|
||
" opens = prices * (1 + np.random.randn(n_days) * 0.003)\n",
|
||
" highs = np.maximum(prices, opens) + np.abs(np.random.randn(n_days)) * daily_range * 0.5\n",
|
||
" lows = np.minimum(prices, opens) - np.abs(np.random.randn(n_days)) * daily_range * 0.5\n",
|
||
" vols = np.random.lognormal(mean=14.5, sigma=0.5, size=n_days).astype(int)\n",
|
||
"\n",
|
||
" return pd.DataFrame({\n",
|
||
" \"open\": opens, \"high\": highs, \"low\": lows,\n",
|
||
" \"close\": prices, \"volume\": vols,\n",
|
||
" }, index=dates)\n",
|
||
"\n",
|
||
"\n",
|
||
"ohlcv = generate_price_series()\n",
|
||
"SYMBOL = \"SIM_STOCK\"\n",
|
||
"\n",
|
||
"print(f\"\\n[数据] 生成模拟 OHLCV 数据:\")\n",
|
||
"print(f\" 交易日数: {len(ohlcv)}\")\n",
|
||
"print(f\" 日期范围: {ohlcv.index[0].date()} → {ohlcv.index[-1].date()}\")\n",
|
||
"print(f\" 价格区间: {ohlcv['close'].min():.2f} ~ {ohlcv['close'].max():.2f}\")\n",
|
||
"\n",
|
||
"\n",
|
||
"def build_engine(strategy_class, strategy_kwargs: dict,\n",
|
||
" name: str, commission=0.0003, slippage=0.0001\n",
|
||
" ) -> BacktestEngine:\n",
|
||
" \"\"\"\n",
|
||
" Factory function: creates a fresh, independent backtest engine.\n",
|
||
" 工厂函数:创建一个全新的、独立的回测引擎。\n",
|
||
" 每次回测共享同样的价格数据,但所有组件(事件队列、组合、策略)\n",
|
||
" 都是独立实例,相互不干扰。\n",
|
||
" \"\"\"\n",
|
||
" q = queue.Queue() # fresh queue / 全新事件队列\n",
|
||
" data = DataHandler(ohlcv, SYMBOL, q) # streams OHLCV bar by bar\n",
|
||
" strategy = strategy_class(data, q, **strategy_kwargs)\n",
|
||
" portfolio = Portfolio(data, q, initial_capital=1_000_000.0)\n",
|
||
" broker = SimulatedBroker(q, commission_rate=commission,\n",
|
||
" slippage_rate=slippage)\n",
|
||
" return BacktestEngine(data, strategy, portfolio, broker, name=name)\n",
|
||
"\n",
|
||
"\n",
|
||
"# ── Run MA Crossover 运行双均线策略 ──────────────────────────────────────────\n",
|
||
"engine_ma = build_engine(\n",
|
||
" MACrossoverStrategy,\n",
|
||
" {\"short_window\": 20, \"long_window\": 60},\n",
|
||
" name=\"事件驱动 — 双均线策略 (MA Crossover 20/60)\",\n",
|
||
")\n",
|
||
"engine_ma.run(verbose=False)\n",
|
||
"engine_ma.print_metrics()\n",
|
||
"\n",
|
||
"# ── Run RSI Mean Reversion 运行RSI均值回归策略 ──────────────────────────────\n",
|
||
"engine_rsi = build_engine(\n",
|
||
" RSIMeanReversionStrategy,\n",
|
||
" {\"window\": 14, \"oversold\": 30, \"overbought\": 70},\n",
|
||
" name=\"事件驱动 — RSI均值回归 (RSI Mean Reversion 14/30/70)\",\n",
|
||
")\n",
|
||
"engine_rsi.run(verbose=False)\n",
|
||
"engine_rsi.print_metrics()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "d1e2b409",
|
||
"metadata": {},
|
||
"source": [
|
||
"# =============================================================================\n",
|
||
"# SECTION 8: Visualization 可视化\n",
|
||
"\n",
|
||
"# ============================================================================="
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "460f465c",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"fig = plt.figure(figsize=(16, 20))\n",
|
||
"gs = gridspec.GridSpec(5, 2, figure=fig, hspace=0.5, wspace=0.32)\n",
|
||
"\n",
|
||
"price_series = ohlcv[\"close\"]\n",
|
||
"\n",
|
||
"# ── Plot 1 (top, full width): Price + MA signals\n",
|
||
"# ── 图1(顶部,全宽): 价格 + 均线信号\n",
|
||
"ax1 = fig.add_subplot(gs[0, :])\n",
|
||
"ax1.plot(price_series, color=\"#1f77b4\", linewidth=1, alpha=0.9, label=\"价格 Close\")\n",
|
||
"sma20 = price_series.rolling(20).mean()\n",
|
||
"sma60 = price_series.rolling(60).mean()\n",
|
||
"ax1.plot(sma20, color=\"orange\", linewidth=1.3, label=\"SMA20 (快线)\")\n",
|
||
"ax1.plot(sma60, color=\"crimson\", linewidth=1.3, label=\"SMA60 (慢线)\")\n",
|
||
"\n",
|
||
"# Mark trade entries / exits from the event-driven engine\n",
|
||
"# 标记事件驱动引擎的交易进出场点\n",
|
||
"for trade in engine_ma.portfolio.trade_log:\n",
|
||
" color = \"green\" if \"LONG\" in trade[\"direction\"] else \"red\"\n",
|
||
" ax1.axvline(x=trade[\"date\"], color=color, alpha=0.25, linewidth=0.8)\n",
|
||
"\n",
|
||
"ax1.set_title(\"双均线策略 — 价格与信号 (MA Crossover: Price + Signals)\",\n",
|
||
" fontsize=12, fontweight=\"bold\")\n",
|
||
"ax1.legend(loc=\"upper left\", fontsize=9)\n",
|
||
"ax1.set_ylabel(\"价格 Price\")\n",
|
||
"ax1.grid(alpha=0.3)\n",
|
||
"\n",
|
||
"# ── Plot 2 (full width): Event flow detail for one trade\n",
|
||
"# ── 图2(全宽): 一笔交易的事件流细节(放大展示)\n",
|
||
"ax2 = fig.add_subplot(gs[1, :])\n",
|
||
"# Show a 120-day window around the first trade to illustrate the event flow\n",
|
||
"# 展示第一笔交易前后120天的窗口,说明事件流\n",
|
||
"if engine_ma.portfolio.trade_log:\n",
|
||
" trade0 = engine_ma.portfolio.trade_log[0]\n",
|
||
" focus_date = trade0[\"date\"]\n",
|
||
" window = pd.Timedelta(days=90)\n",
|
||
" mask = (price_series.index >= focus_date - window) & \\\n",
|
||
" (price_series.index <= focus_date + window)\n",
|
||
" ax2.plot(price_series[mask], color=\"#1f77b4\", linewidth=1.5)\n",
|
||
" ax2.plot(sma20[mask], color=\"orange\", linewidth=1.2, linestyle=\"--\", label=\"SMA20\")\n",
|
||
" ax2.plot(sma60[mask], color=\"crimson\", linewidth=1.2, linestyle=\"--\", label=\"SMA60\")\n",
|
||
" # Mark the exit point\n",
|
||
" ax2.axvline(x=focus_date, color=\"red\", linewidth=1.5,\n",
|
||
" label=f\"平仓 Exit @ {focus_date.date()}\")\n",
|
||
" ax2.set_title(\n",
|
||
" f\"放大: 第1笔交易前后 90 天 \"\n",
|
||
" f\"(Zoom: 90 days around Trade #1 Exit)\\n\"\n",
|
||
" f\"进价 Entry ¥{trade0['entry_price']:.2f} \"\n",
|
||
" f\"出价 Exit ¥{trade0['exit_price']:.2f} \"\n",
|
||
" f\"盈亏 P&L ¥{trade0['pnl']:,.0f}\",\n",
|
||
" fontsize=10, fontweight=\"bold\",\n",
|
||
" )\n",
|
||
" ax2.legend(fontsize=9)\n",
|
||
" ax2.grid(alpha=0.3)\n",
|
||
"\n",
|
||
"# ── Plot 3 (left): Equity curves 净值曲线\n",
|
||
"ax3 = fig.add_subplot(gs[2, 0])\n",
|
||
"eq_ma = engine_ma.portfolio.get_equity_series()\n",
|
||
"eq_rsi = engine_rsi.portfolio.get_equity_series()\n",
|
||
"eq_bh = 1_000_000 * (1 + price_series.pct_change().fillna(0)).cumprod()\n",
|
||
"\n",
|
||
"ax3.plot(eq_ma, color=\"steelblue\", linewidth=1.5, label=\"MA策略\")\n",
|
||
"ax3.plot(eq_rsi, color=\"purple\", linewidth=1.5, label=\"RSI策略\")\n",
|
||
"ax3.plot(eq_bh, color=\"gray\", linewidth=1.2, linestyle=\"--\", label=\"Buy & Hold\")\n",
|
||
"ax3.set_title(\"净值曲线对比 (Equity Curves)\", fontsize=11, fontweight=\"bold\")\n",
|
||
"ax3.set_ylabel(\"账户价值 Portfolio Value\")\n",
|
||
"ax3.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f\"¥{x/1e4:.0f}万\"))\n",
|
||
"ax3.legend(fontsize=9)\n",
|
||
"ax3.grid(alpha=0.3)\n",
|
||
"\n",
|
||
"# ── Plot 4 (right): Drawdown 回撤曲线\n",
|
||
"ax4 = fig.add_subplot(gs[2, 1])\n",
|
||
"def compute_dd(eq):\n",
|
||
" return (eq - eq.cummax()) / eq.cummax()\n",
|
||
"\n",
|
||
"dd_ma = compute_dd(eq_ma)\n",
|
||
"dd_rsi = compute_dd(eq_rsi)\n",
|
||
"dd_bh = compute_dd(eq_bh)\n",
|
||
"\n",
|
||
"ax4.fill_between(dd_ma.index, dd_ma, 0, alpha=0.5, color=\"steelblue\", label=\"MA策略\")\n",
|
||
"ax4.fill_between(dd_rsi.index, dd_rsi, 0, alpha=0.4, color=\"purple\", label=\"RSI策略\")\n",
|
||
"ax4.fill_between(dd_bh.index, dd_bh, 0, alpha=0.3, color=\"gray\", label=\"Buy & Hold\")\n",
|
||
"ax4.set_title(\"回撤曲线 (Drawdowns)\", fontsize=11, fontweight=\"bold\")\n",
|
||
"ax4.set_ylabel(\"回撤 Drawdown\")\n",
|
||
"ax4.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f\"{x:.0%}\"))\n",
|
||
"ax4.legend(fontsize=9)\n",
|
||
"ax4.grid(alpha=0.3)\n",
|
||
"\n",
|
||
"# ── Plot 5 (left): Trade P&L distribution 交易盈亏分布\n",
|
||
"ax5 = fig.add_subplot(gs[3, 0])\n",
|
||
"pnls = [t[\"pnl\"] for t in engine_ma.portfolio.trade_log]\n",
|
||
"if pnls:\n",
|
||
" colors_bar = [\"green\" if p > 0 else \"red\" for p in pnls]\n",
|
||
" ax5.bar(range(len(pnls)), pnls, color=colors_bar, alpha=0.75, edgecolor=\"white\")\n",
|
||
" ax5.axhline(0, color=\"black\", linewidth=0.8)\n",
|
||
" ax5.set_title(f\"MA策略 — 每笔交易盈亏 (Per-Trade P&L)\\n\"\n",
|
||
" f\"共{len(pnls)}笔, 胜率 \"\n",
|
||
" f\"{sum(1 for p in pnls if p > 0)/len(pnls):.0%}\",\n",
|
||
" fontsize=10, fontweight=\"bold\")\n",
|
||
" ax5.set_xlabel(\"交易编号 Trade #\")\n",
|
||
" ax5.set_ylabel(\"盈亏 P&L (¥)\")\n",
|
||
" ax5.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f\"¥{x/1e4:.1f}万\"))\n",
|
||
"ax5.grid(alpha=0.3, axis=\"y\")\n",
|
||
"\n",
|
||
"# ── Plot 6 (right): Cash vs Market Value over time\n",
|
||
"# ── 图6(右): 现金 vs 持仓市值随时间变化\n",
|
||
"ax6 = fig.add_subplot(gs[3, 1])\n",
|
||
"eq_df = pd.DataFrame(engine_ma.portfolio.equity_curve).set_index(\"date\")\n",
|
||
"ax6.stackplot(eq_df.index,\n",
|
||
" eq_df[\"cash\"], eq_df[\"market_value\"],\n",
|
||
" labels=[\"现金 Cash\", \"持仓市值 Market Value\"],\n",
|
||
" colors=[\"#2ca02c\", \"#ff7f0e\"], alpha=0.7)\n",
|
||
"ax6.set_title(\"现金 vs 持仓市值 (Cash vs Position Value)\", fontsize=11, fontweight=\"bold\")\n",
|
||
"ax6.set_ylabel(\"金额 Amount (¥)\")\n",
|
||
"ax6.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f\"¥{x/1e4:.0f}万\"))\n",
|
||
"ax6.legend(loc=\"upper left\", fontsize=9)\n",
|
||
"ax6.grid(alpha=0.3)\n",
|
||
"\n",
|
||
"# ── Plot 7 (full width): Rolling metrics — Sharpe & Volatility\n",
|
||
"# ── 图7(全宽): 滚动指标 — 夏普比率 & 波动率\n",
|
||
"ax7a = fig.add_subplot(gs[4, 0])\n",
|
||
"ax7b = fig.add_subplot(gs[4, 1])\n",
|
||
"\n",
|
||
"ret_ma = eq_ma.pct_change().fillna(0)\n",
|
||
"roll_sharpe = ret_ma.rolling(60).mean() / ret_ma.rolling(60).std() * np.sqrt(252)\n",
|
||
"roll_vol = ret_ma.rolling(60).std() * np.sqrt(252)\n",
|
||
"\n",
|
||
"ax7a.plot(roll_sharpe, color=\"steelblue\", linewidth=1)\n",
|
||
"ax7a.axhline(0, color=\"black\", linewidth=0.8, linestyle=\"--\")\n",
|
||
"ax7a.axhline(1, color=\"green\", linewidth=0.8, linestyle=\":\", label=\"Sharpe=1 (目标线)\")\n",
|
||
"ax7a.fill_between(roll_sharpe.index, roll_sharpe, 0,\n",
|
||
" where=(roll_sharpe > 0), alpha=0.2, color=\"green\")\n",
|
||
"ax7a.fill_between(roll_sharpe.index, roll_sharpe, 0,\n",
|
||
" where=(roll_sharpe < 0), alpha=0.2, color=\"red\")\n",
|
||
"ax7a.set_title(\"滚动60日夏普比率 (60-day Rolling Sharpe)\", fontsize=11, fontweight=\"bold\")\n",
|
||
"ax7a.set_ylabel(\"夏普比率 Sharpe\")\n",
|
||
"ax7a.legend(fontsize=8)\n",
|
||
"ax7a.grid(alpha=0.3)\n",
|
||
"\n",
|
||
"ax7b.plot(roll_vol, color=\"darkorange\", linewidth=1)\n",
|
||
"ax7b.fill_between(roll_vol.index, roll_vol, alpha=0.2, color=\"orange\")\n",
|
||
"ax7b.set_title(\"滚动60日年化波动率 (60-day Rolling Volatility)\", fontsize=11, fontweight=\"bold\")\n",
|
||
"ax7b.set_ylabel(\"年化波动率 Ann. Volatility\")\n",
|
||
"ax7b.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f\"{x:.0%}\"))\n",
|
||
"ax7b.grid(alpha=0.3)\n",
|
||
"\n",
|
||
"plt.suptitle(\n",
|
||
" \"量化交易 — 事件驱动回测引擎 Quantitative Trading: Event-Driven Backtest\",\n",
|
||
" fontsize=15, fontweight=\"bold\", y=1.003,\n",
|
||
")\n",
|
||
"plt.savefig(\"event_driven_backtest.png\", dpi=120, bbox_inches=\"tight\")\n",
|
||
"plt.show()\n",
|
||
"print(\"\\n[图表] 已保存至 event_driven_backtest.png\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "ed738eb1",
|
||
"metadata": {},
|
||
"source": [
|
||
"# =============================================================================\n",
|
||
"# SECTION 9: Compare Event-Driven vs Vectorized\n",
|
||
"# 对比事件驱动 vs 向量化回测\n",
|
||
"\n",
|
||
"# =============================================================================\n",
|
||
"#\n",
|
||
"# Run a quick vectorized version of the SAME MA strategy on the SAME data\n",
|
||
"# to show how results can differ due to:\n",
|
||
"# 对相同数据运行相同MA策略的快速向量化版本,展示结果差异的原因:\n",
|
||
"#\n",
|
||
"# 1. Fill timing 成交时机 — event-driven uses next-bar open;\n",
|
||
"# vectorized typically uses same-bar close\n",
|
||
"# 2. Cost modeling 成本建模 — event-driven has min commission floor;\n",
|
||
"# vectorized applies flat percentage only\n",
|
||
"# 3. Integer shares 整数股 — event-driven floors to whole shares;\n",
|
||
"# vectorized trades fractional shares\n",
|
||
"\n",
|
||
"# ============================================================================="
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "7ab80536",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"print(f\"\\n{'=' * 60}\")\n",
|
||
"print(\" 对比: 事件驱动 vs 向量化回测\")\n",
|
||
"print(\" Comparison: Event-Driven vs Vectorized Backtest\")\n",
|
||
"print(f\"{'=' * 60}\")\n",
|
||
"print(\" 相同策略: 双均线 MA Crossover (20/60)\")\n",
|
||
"print(\" 相同数据: 同一模拟价格序列\")\n",
|
||
"print()\n",
|
||
"\n",
|
||
"# Vectorized version (快速向量化版本)\n",
|
||
"closes = ohlcv[\"close\"]\n",
|
||
"sma20v = closes.rolling(20).mean()\n",
|
||
"sma60v = closes.rolling(60).mean()\n",
|
||
"sig_vec = (sma20v > sma60v).astype(float).shift(1).fillna(0)\n",
|
||
"ret_vec = closes.pct_change().fillna(0)\n",
|
||
"strat_ret_gross = sig_vec * ret_vec\n",
|
||
"# Simple flat cost: 0.03% commission + 0.01% slippage = 0.04% per side\n",
|
||
"position_change = sig_vec.diff().abs().fillna(0)\n",
|
||
"cost_vec = position_change * (0.0003 + 0.0001)\n",
|
||
"strat_ret_net = strat_ret_gross - cost_vec\n",
|
||
"eq_vec = 1_000_000 * (1 + strat_ret_net).cumprod()\n",
|
||
"\n",
|
||
"# Compare\n",
|
||
"m_ed = engine_ma.metrics()\n",
|
||
"total_ed = float(m_ed[\"总收益率 Total Return\"].strip(\"%\")) / 100\n",
|
||
"sharpe_ed = float(m_ed[\"夏普比率 Sharpe Ratio\"])\n",
|
||
"maxdd_ed = float(m_ed[\"最大回撤 Max Drawdown\"].strip(\"%\")) / 100\n",
|
||
"\n",
|
||
"total_vec = (eq_vec.iloc[-1] / 1_000_000) - 1\n",
|
||
"ret_v_s = strat_ret_net\n",
|
||
"sharpe_vec = (ret_v_s.mean() / ret_v_s.std()) * np.sqrt(252)\n",
|
||
"rolling_mx = eq_vec.cummax()\n",
|
||
"maxdd_vec = ((eq_vec - rolling_mx) / rolling_mx).min()\n",
|
||
"\n",
|
||
"print(f\" {'指标 Metric':<30} {'事件驱动 Event-Driven':>22} {'向量化 Vectorized':>20}\")\n",
|
||
"print(f\" {'─'*72}\")\n",
|
||
"print(f\" {'总收益率 Total Return':<30} {total_ed:>21.2%} {total_vec:>19.2%}\")\n",
|
||
"print(f\" {'夏普比率 Sharpe Ratio':<30} {sharpe_ed:>21.3f} {sharpe_vec:>19.3f}\")\n",
|
||
"print(f\" {'最大回撤 Max Drawdown':<30} {maxdd_ed:>21.2%} {maxdd_vec:>19.2%}\")\n",
|
||
"print()\n",
|
||
"print(\" 差异原因 / Why they differ:\")\n",
|
||
"print(\" ① 事件驱动以「下一日开盘价」成交 (Event-driven fills at next open)\")\n",
|
||
"print(\" 向量化以「当日收盘价」交易 (Vectorized trades at same-day close)\")\n",
|
||
"print(\" ② 事件驱动有最低佣金下限 ¥5 (Event-driven has min commission floor)\")\n",
|
||
"print(\" ③ 事件驱动按整数股买卖 (Event-driven uses integer share quantities)\")\n",
|
||
"print(\" ④ 资金是随时间变化的 (Capital base grows/shrinks with P&L)\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "c08fbbf2",
|
||
"metadata": {},
|
||
"source": [
|
||
"# =============================================================================\n",
|
||
"# SECTION 10: What's Next 下一步\n",
|
||
"\n",
|
||
"# ============================================================================="
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "a0523673",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"print(f\"\"\"\n",
|
||
"{'=' * 72}\n",
|
||
" 总结 Summary\n",
|
||
"{'=' * 72}\n",
|
||
"\n",
|
||
" 事件驱动回测引擎的核心组件 / Core Components of Event-Driven Engine:\n",
|
||
"\n",
|
||
" ┌─────────────────────┬────────────────────────────────────────────┐\n",
|
||
" │ 组件 Component │ 职责 Responsibility │\n",
|
||
" ├─────────────────────┼────────────────────────────────────────────┤\n",
|
||
" │ Event (事件) │ 解耦组件间通信的消息对象 │\n",
|
||
" │ │ Message objects that decouple components │\n",
|
||
" ├─────────────────────┼────────────────────────────────────────────┤\n",
|
||
" │ DataHandler │ 逐根K线流出历史数据(模拟无未来信息) │\n",
|
||
" │ 数据处理器 │ Streams bars one-by-one (no future info) │\n",
|
||
" ├─────────────────────┼────────────────────────────────────────────┤\n",
|
||
" │ Strategy (策略) │ 观察市场 → 生成交易信号 │\n",
|
||
" │ │ Observes market → generates trade signals │\n",
|
||
" ├─────────────────────┼────────────────────────────────────────────┤\n",
|
||
" │ Portfolio (组合) │ 仓位管理 + 现金/持仓/盈亏追踪 │\n",
|
||
" │ │ Position sizing + cash/position/P&L track │\n",
|
||
" ├─────────────────────┼────────────────────────────────────────────┤\n",
|
||
" │ SimulatedBroker │ 模拟券商:滑点+佣金+成交时机 │\n",
|
||
" │ 模拟券商 │ Simulates broker: slippage+commission+fill │\n",
|
||
" ├─────────────────────┼────────────────────────────────────────────┤\n",
|
||
" │ BacktestEngine │ 主事件循环,驱动所有组件 │\n",
|
||
" │ 回测引擎 │ Main event loop, orchestrates all parts │\n",
|
||
" └─────────────────────┴────────────────────────────────────────────┘\n",
|
||
"\n",
|
||
" 下一步学习方向 Next Steps:\n",
|
||
" ──────────────────────────────────────────────────────────────────\n",
|
||
" • 限价单/止损单 Limit & Stop orders in ExecutionHandler\n",
|
||
" • 多标的组合 Multi-asset portfolio (stocks + bonds, sector rotation)\n",
|
||
" • 因子选股 Alpha factor models (Fama-French, Momentum, Quality)\n",
|
||
" • 机器学习信号 ML-based signals (XGBoost, LSTM)\n",
|
||
" • 风险管理 Risk management (VaR, CVaR, Kelly position sizing)\n",
|
||
" • 实盘对接 Live trading connection (vnpy, Alpaca API)\n",
|
||
"{'=' * 72}\n",
|
||
"\"\"\")"
|
||
]
|
||
}
|
||
],
|
||
"metadata": {
|
||
"kernelspec": {
|
||
"display_name": "Python 3 (trading)",
|
||
"language": "python",
|
||
"name": "python3"
|
||
},
|
||
"language_info": {
|
||
"name": "python",
|
||
"version": "3.11.0"
|
||
}
|
||
},
|
||
"nbformat": 4,
|
||
"nbformat_minor": 5
|
||
}
|