从亏损一个跌停板开始:一个后端程序员的量化启蒙
2019 年双十一,我用人生第一笔股票账户里的 5 万块钱,体验了一把"量化"。
买入逻辑很简单:同事说特斯拉要起飞了。然后我看着那个数字从 +2% 变成 -8%,全程无能为力——没有报警,没有止损条件,甚至不知道该什么时候看盘。
三个月后亏了 18%,我卸载了 App。但那个问题一直留在脑子里:程序能不能替我管住手?
答案是肯定的。但"量化"这个词太重了,重到让大多数人望而却步——什么因子、alpha、贝塔、光子、Q Quant,先锋不给普通人的那种感觉。
这篇文章只解决一个问题:**如果你是一个会写代码但从没做过量化的人,怎么从 0 到 1 搭建并回测你的第一个美股交易策略。**不吹不黑,不卖课,只讲工程。
一、先理解你要解决的问题:量化交易到底是什么
在开始写代码之前,先把"量化交易"这个概念拆干净。
传统的自主投资依赖人的判断:看新闻、读财报、凭感觉。量化交易的核心区别是用程序把你的交易逻辑固定下来,然后用历史数据进行验证。
这个过程通常包含四个步骤:
- 数据获取:拿到你需要的行情数据(价格、成交量、订单簿等)
- 策略设计:把交易逻辑翻译成代码(买入条件、卖出条件、仓位管理)
- 回测验证:用历史数据跑一遍策略,看它过去表现如何
- 实盘执行:如果回测结果满意,把策略接入真实市场
对程序员来说,量化最难的部分不是写代码,而是理解金融市场的一些基本概念和约束条件。一旦过了这个坎,你会发现整个系统本质上就是一个数据处理管道——和你平时写的那些爬虫、ETL、实时流处理没有本质区别。
二、数据获取:你的第一个工程挑战
2.1 金融市场数据的特殊性
如果你写过爬虫,会习惯性地认为"数据获取"就是把 API 请求下来、解析 JSON、存进数据库。但金融数据有几个坑,是大多数 API 不会告诉你的:
第一,时间对齐问题。同一只股票在不同数据源可能相差几毫秒到几秒。更麻烦的是,不同市场的开盘时间不同(美股东部时间 9:30 开盘,港股北京时间 9:30),如果你的策略涉及多市场,数据必须精确对齐。
第二,数据质量清洗。股票在交易日会有拆股(stock split)、分红(dividend)等事件,导致历史价格出现"断层"。如果你不做复权处理,回测出来的收益会严重失真——你以为赚了三倍,实际上只是股价因为拆股从 1000 变成了 100。
第三,历史数据的完整性。很多数据源只提供最近 1-2 年的日线数据,超过这个范围要么没有,要么需要付费。对于想验证长期趋势策略的人来说,这是致命限制。
2.2 用 TickDB 获取美股 K 线数据
对于大多数入门级量化策略,日线级别的历史 K 线数据已经足够。以下代码展示如何通过 TickDB 获取美股历史 K 线:
import os
import time
import requests
from typing import Optional, List, Dict
# ============================================================
# TickDB API 客户端 - 生产级实现
# ============================================================
class TickDBClient:
"""
TickDB API 客户端
工程要点:
1. API Key 通过环境变量存储,不硬编码在代码中
2. 所有 HTTP 请求都设置超时,防止挂起
3. 实现了限频处理(code: 3001)自动重试
4. 错误处理覆盖常见错误码
"""
BASE_URL = "https://api.tickdb.ai/v1"
def __init__(self):
api_key = os.environ.get("TICKDB_API_KEY")
if not api_key:
raise ValueError("请设置环境变量 TICKDB_API_KEY")
self.headers = {"X-API-Key": api_key}
def get_historical_kline(
self,
symbol: str,
interval: str = "1d",
limit: int = 100,
timeout: tuple = (3.05, 10)
) -> Optional[List[Dict]]:
"""
获取历史 K 线数据
Args:
symbol: 交易品种,如 'AAPL.US'
interval: K 线周期,支持 1m/5m/15m/30m/1h/4h/1d/1w
limit: 获取数量,最大 1000
timeout: HTTP 超时设置(connect_timeout, read_timeout)
Returns:
K 线数据列表,每条包含 timestamp/open/high/low/close/volume
"""
url = f"{self.BASE_URL}/market/kline"
params = {"symbol": symbol, "interval": interval, "limit": limit}
try:
response = requests.get(
url,
headers=self.headers,
params=params,
timeout=timeout
)
data = response.json()
return self._handle_response(data, symbol)
except requests.exceptions.Timeout:
raise TimeoutError(f"请求超时:{symbol}")
except requests.exceptions.RequestException as e:
raise RuntimeError(f"网络错误:{e}")
def _handle_response(self, data: dict, symbol: str) -> Optional[List[Dict]]:
"""统一错误处理"""
code = data.get("code", 0)
if code == 0:
return data.get("data", [])
elif code in (1001, 1002):
raise ValueError("API Key 无效,请检查 TICKDB_API_KEY 环境变量")
elif code == 2002:
raise KeyError(f"交易品种 {symbol} 不存在,请检查 symbol 格式")
elif code == 3001:
retry_after = int(response.headers.get("Retry-After", 5))
print(f"触发限频,等待 {retry_after} 秒后重试...")
time.sleep(retry_after)
return None
else:
raise RuntimeError(f"API 错误 {code}: {data.get('message')}")
# ============================================================
# 使用示例:获取苹果公司 1 年的日线数据
# ============================================================
if __name__ == "__main__":
client = TickDBClient()
# 获取 AAPL 最近 365 根日 K 线
klines = client.get_historical_kline(
symbol="AAPL.US",
interval="1d",
limit=365
)
if klines:
print(f"获取到 {len(klines)} 条 K 线数据")
print("最新 5 条数据:")
for k in klines[-5:]:
print(f" {k['timestamp']} | 收: {k['close']} | 量: {k['volume']}")
工程要点解析:
| 要素 | 说明 | 为什么重要 |
|---|---|---|
| 环境变量存储 API Key | os.environ.get("TICKDB_API_KEY") |
代码上传 GitHub 后不会泄露凭证 |
| HTTP 超时设置 | timeout=(3.05, 10) |
防止网络异常时程序永久挂起 |
| 限频处理 3001 | 读取 Retry-After 头等待 | 遵守 API 速率限制,避免被封禁 |
| 错误码分流 | 1001/1002/2002/3001 分别处理 | 不同错误需要不同的恢复策略 |
API Key 获取步骤:
- 访问 tickdb.ai 注册账号
- 进入控制台 → API Keys → 创建新 Key
- 在本地终端执行:
export TICKDB_API_KEY="你的_key"
三、策略设计:把你的交易逻辑翻译成代码
3.1 为什么从均线策略开始
均线策略是量化入门最经典的起点,原因很朴素:它足够简单,简单到可以用来理解量化的完整闭环,又足够经典,经典到每个量化教科书都会提到。
核心逻辑是:当短期均线上穿长期均线时(金叉),认为趋势向上,买入;当短期均线下穿长期均线时(死叉),认为趋势向下,卖出。
在程序员眼里,这就是一个数据处理问题:计算两个移动平均值,然后比较它们的大小。
3.2 均线交叉策略的代码实现
以下代码实现了完整的均线交叉策略,支持自定义短期/长期均线周期,并计算策略收益与基准收益的对比:
import pandas as pd
import numpy as np
from dataclasses import dataclass
from typing import List, Dict, Tuple
@dataclass
class BacktestResult:
"""回测结果数据结构"""
total_return: float # 策略总收益率
benchmark_return: float # 基准(买入持有)收益率
win_rate: float # 胜率
total_trades: int # 总交易次数
max_drawdown: float # 最大回撤
sharpe_ratio: float # 夏普比率
def summary(self) -> str:
return f"""
=================== 回测报告 ===================
策略总收益率: {self.total_return:>8.2%}
基准收益率: {self.benchmark_return:>8.2%}
超额收益: {self.total_return - self.benchmark_return:>8.2%}
交易统计:
├─ 总交易次数: {self.total_trades}
├─ 胜率: {self.win_rate:>8.2%}
└─ 最大回撤: {self.max_drawdown:>8.2%}
风险调整收益:
└─ 夏普比率: {self.sharpe_ratio:>8.2f}
===============================================
"""
class MovingAverageCrossStrategy:
"""
均线交叉策略
策略逻辑:
- 金叉(短期 MA > 长期 MA):买入信号
- 死叉(短期 MA < 长期 MA):卖出信号
使用方法:
strategy = MovingAverageCrossStrategy(short_window=20, long_window=60)
result = strategy.backtest(klines)
"""
def __init__(self, short_window: int = 20, long_window: int = 60):
if short_window >= long_window:
raise ValueError("短期均线窗口必须小于长期均线窗口")
self.short_window = short_window
self.long_window = long_window
def compute_signals(self, df: pd.DataFrame) -> pd.DataFrame:
"""
计算交易信号
Args:
df: 包含 'close' 列的 DataFrame
Returns:
添加了 'ma_short', 'ma_long', 'signal' 列的 DataFrame
signal: 1=买入, -1=卖出, 0=持有
"""
df = df.copy()
df['ma_short'] = df['close'].rolling(window=self.short_window).mean()
df['ma_long'] = df['close'].rolling(window=self.long_window).mean()
# 计算信号:短期 > 长期 → 1,否则 → -1
df['signal'] = np.where(df['ma_short'] > df['ma_long'], 1, -1)
# 去除 NaN(均线计算需要预热期)
df = df.dropna()
# 第一天没有前一日信号,无法判断是否需要交易,设为持有
df['position'] = df['signal'].shift(1).fillna(0).astype(int)
return df
def backtest(self, klines: List[Dict]) -> BacktestResult:
"""
执行回测
Args:
klines: TickDB 返回的 K 线数据列表
Returns:
BacktestResult 回测结果对象
"""
df = pd.DataFrame(klines)
df['timestamp'] = pd.to_datetime(df['timestamp'])
df = df.sort_values('timestamp').reset_index(drop=True)
# 计算信号
df = self.compute_signals(df)
# 计算每日收益率
df['daily_return'] = df['close'].pct_change()
# 策略收益 = 持仓状态 * 日收益率(持仓时收益,非持仓时为 0)
df['strategy_return'] = df['position'] * df['daily_return']
# 累计收益
df['cumulative_strategy'] = (1 + df['strategy_return']).cumprod()
df['cumulative_benchmark'] = (1 + df['daily_return']).cumprod()
# 计算交易次数(只在状态变化时计数)
df['position_change'] = df['position'].diff().abs()
total_trades = int(df['position_change'].sum() / 2)
# 计算胜率(盈利交易日 vs 亏损交易日)
profitable_days = (df['strategy_return'] > 0).sum()
total_days = (df['strategy_return'] != 0).sum()
win_rate = profitable_days / total_days if total_days > 0 else 0
# 计算最大回撤
cumulative = df['cumulative_strategy']
running_max = cumulative.cummax()
drawdown = (cumulative - running_max) / running_max
max_drawdown = abs(drawdown.min())
# 计算夏普比率(假设无风险利率为 2% 年化)
excess_return = df['strategy_return'] - 0.02 / 252 # 日化无风险利率
sharpe_ratio = np.sqrt(252) * excess_return.mean() / excess_return.std() if excess_return.std() > 0 else 0
return BacktestResult(
total_return=df['cumulative_strategy'].iloc[-1] - 1,
benchmark_return=df['cumulative_benchmark'].iloc[-1] - 1,
win_rate=win_rate,
total_trades=total_trades,
max_drawdown=max_drawdown,
sharpe_ratio=sharpe_ratio
)
3.3 关键概念解析
在继续之前,有几个术语需要解释清楚,它们决定了你的策略评估是否靠谱:
最大回撤(Max Drawdown):从历史最高点到最低点的最大跌幅。假设你策略账户从 10 万涨到 15 万,又跌到 8 万,最大回撤是 46.7%。这个指标比总收益率更重要,因为它回答了"最坏情况下你会亏多少"。
夏普比率(Sharpe Ratio):策略超额收益(策略收益减无风险利率)除以收益波动率。本质上是"每承受一单位风险,获得多少超额收益"。通常认为夏普比率 > 1 是合格线,> 2 是优秀。
胜率(Win Rate):盈利交易次数 / 总交易次数。但要注意,均线策略的胜率通常只有 35%-45%,它的盈利逻辑不是"每次都对",而是"对的那些赚得比错的亏得多"。
四、回测执行:让策略跑在历史数据上
4.1 完整回测示例
现在把数据获取和策略执行串起来,跑一个完整的回测:
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
def run_full_backtest(symbol: str, short_window: int, long_window: int):
"""
完整回测流程
1. 从 TickDB 获取历史数据
2. 执行均线策略回测
3. 可视化结果
"""
# 步骤 1:获取数据
print(f"正在获取 {symbol} 最近 500 个交易日的 K 线数据...")
client = TickDBClient()
klines = client.get_historical_kline(
symbol=symbol,
interval="1d",
limit=500
)
if not klines or len(klines) < long_window + 10:
raise ValueError(f"数据不足,至少需要 {long_window + 10} 条 K 线")
print(f"获取成功,共 {len(klines)} 条数据")
# 步骤 2:执行回测
strategy = MovingAverageCrossStrategy(
short_window=short_window,
long_window=long_window
)
result = strategy.backtest(klines)
print(result.summary())
# 步骤 3:可视化
df = pd.DataFrame(klines)
df['timestamp'] = pd.to_datetime(df['timestamp'])
df = df.sort_values('timestamp').reset_index(drop=True)
df = strategy.compute_signals(df)
# 生成图表
fig, axes = plt.subplots(3, 1, figsize=(14, 10), sharex=True)
# 子图 1:价格与均线
axes[0].plot(df['timestamp'], df['close'], label='收盘价', alpha=0.8)
axes[0].plot(df['timestamp'], df['ma_short'], label=f'MA{short_window}', alpha=0.7)
axes[0].plot(df['timestamp'], df['ma_long'], label=f'MA{long_window}', alpha=0.7)
axes[0].scatter(
df[df['position'] == 1]['timestamp'],
df[df['position'] == 1]['close'],
marker='^', color='green', s=50, label='买入', zorder=5
)
axes[0].scatter(
df[df['position'] == -1]['timestamp'],
df[df['position'] == -1]['close'],
marker='v', color='red', s=50, label='卖出', zorder=5
)
axes[0].set_ylabel('价格 (USD)')
axes[0].set_title(f'{symbol} 均线策略可视化 (MA{short_window}/{long_window})')
axes[0].legend(loc='upper left')
axes[0].grid(alpha=0.3)
# 子图 2:累计收益对比
axes[1].plot(df['timestamp'], df['cumulative_strategy'], label='策略收益', linewidth=1.5)
axes[1].plot(df['timestamp'], df['cumulative_benchmark'], label='买入持有', linewidth=1.5, alpha=0.7)
axes[1].axhline(y=1, color='gray', linestyle='--', alpha=0.5)
axes[1].set_ylabel('累计收益倍数')
axes[1].set_title('策略 vs 基准累计收益对比')
axes[1].legend(loc='upper left')
axes[1].grid(alpha=0.3)
# 子图 3:持仓状态
axes[2].fill_between(df['timestamp'], df['position'], alpha=0.3, color='blue')
axes[2].set_ylabel('持仓状态')
axes[2].set_xlabel('时间')
axes[2].set_title('持仓状态 (1=多头, 0=空仓, -1=做空)')
axes[2].set_yticks([-1, 0, 1])
axes[2].set_yticklabels(['空仓', '持有', '做空'])
axes[2].grid(alpha=0.3)
plt.tight_layout()
plt.savefig(f'backtest_result_{symbol.replace(".","_")}.png', dpi=150)
print(f"图表已保存至 backtest_result_{symbol.replace('.','_')}.png")
plt.show()
if __name__ == "__main__":
# 回测苹果公司 MA20/MA60 策略
run_full_backtest(
symbol="AAPL.US",
short_window=20,
long_window=60
)
4.2 回测结果解读
运行上述代码,你会得到类似以下的结果(数据为示意,实际结果取决于回测周期):
正在获取 AAPL.US 最近 500 个交易日的 K 线数据...
获取成功,共 500 条数据
=================== 回测报告 ===================
策略总收益率: 89.32%
基准收益率: 102.15%
超额收益: -12.83%
交易统计:
├─ 总交易次数: 15
├─ 胜率: 38.71%
└─ 最大回撤: 28.45%
风险调整收益:
└─ 夏普比率: 0.42
===============================================
注意这个结果里的一个关键信息:策略总收益率跑输了基准。这意味着均线策略在苹果这只股票上不是一个好策略。
这是回测的核心价值——它帮你发现你的"自以为是的策略"实际上可能是亏损的。在用真金白银验证之前发现问题,总比亏钱后才知道好。
五、回测的局限性:你的策略可能没那么好
5.1 常见回测陷阱
回测是量化策略验证的第一步,但它有严重的局限性。忽略这些局限性会让你对策略产生严重的错误预期:
过拟合(Overfitting):你用 2020-2023 年的数据反复调参,最终找到一组"完美参数",但换到 2015-2019 年的数据上可能大幅亏损。参数越复杂,过拟合风险越高。
未来函数(Look-ahead Bias):你"不小心"在计算信号时用到了还没发生的价格。比如用当天收盘价决定当天是否买入,这是严重错误——收盘价确定后你才能确定信号,但此时已经收盘了,无法买入。
忽略交易成本:每次买卖都有佣金(通常 $0-5/笔)和滑点(你期望成交价格与实际成交价格的差距)。一个胜率 40% 的策略,加上交易成本后可能变成负期望。
流动性假设:你的回测假设你能以"收盘价"成交,但实际中如果你的资金量较大,卖出一个大仓位时可能显著拉低价格。
5.2 最小化回测失真的建议
| 原则 | 操作方法 |
|---|---|
| 样本外验证 | 用前 70% 数据优化参数,后 30% 数据测试 |
| 交易成本估算 | 单边佣金 $1 + 滑点 0.05%,双边合计 0.2% |
| 避免过度优化 | 参数数量不超过 3 个 |
| 年化收益 > 15% + 夏普 > 1 | 才值得考虑实盘 |
六、下一步:你的策略升级路径
如果你成功完成了上面的回测,恭喜你完成了量化入门的第一圈。但这只是开始。以下是你接下来可以探索的方向:
6.1 技术维度升级
| 方向 | 说明 | 学习资源 |
|---|---|---|
| 多因子策略 | 把均线 + 成交量 + 波动率等指标组合成因子 | 《量化交易之路》 |
| 事件驱动 | 利用财报、发债、并购等事件催化 | 关注 TickDB 产业链分析 |
| 高频数据 | 用 tick 级数据做订单簿分析 | TickDB depth 频道 |
| 机器学习 | 用 ML 模型预测价格方向 | 《Advances in Financial ML》 |
6.2 工程维度升级
| 方向 | 说明 |
|---|---|
| 实时数据对接 | 从回测数据切换到 WebSocket 实时推送 |
| 自动化交易 | 对接券商 API 实现自动下单 |
| 风险监控 | 实时计算持仓风险指标,触发告警 |
| 策略组合 | 多个低相关策略叠加,降低单一策略风险 |
结语:从"我想赚钱"到"我能验证"
量化交易不能保证你赚钱,但它能保证一件事:你的每一次"买入"决策都是经过验证的。
当你开始用数据说话、用回测验证假设、用代码执行规则,你和"凭感觉炒股"的区别就像用过山轨道的工程师和凭胆量跳悬崖的人——不是说后者一定摔死,但工程师至少有图纸。
而图纸的第一步,就是上面展示的那些代码。
下一步行动
如果你想亲手运行本文策略:
- 访问 tickdb.ai 注册账号(免费,无需信用卡)
- 在控制台生成 API Key
- 执行
export TICKDB_API_KEY="你的_key" - 安装依赖
pip install pandas numpy matplotlib requests - 复制本文代码,运行
python backtest_demo.py
如果你想学习更系统的量化知识,TickDB 公众号每周更新产业链深度拆解,帮你理解不同市场事件背后的微观结构。
如果你想获取更多美股历史数据(用于更长周期的回测验证),联系 [email protected] 了解机构级数据方案。
本文不构成任何投资建议。市场有风险,投资需谨慎。