从亏损一个跌停板开始:一个后端程序员的量化启蒙

2019 年双十一,我用人生第一笔股票账户里的 5 万块钱,体验了一把"量化"。

买入逻辑很简单:同事说特斯拉要起飞了。然后我看着那个数字从 +2% 变成 -8%,全程无能为力——没有报警,没有止损条件,甚至不知道该什么时候看盘。

三个月后亏了 18%,我卸载了 App。但那个问题一直留在脑子里:程序能不能替我管住手?

答案是肯定的。但"量化"这个词太重了,重到让大多数人望而却步——什么因子、alpha、贝塔、光子、Q Quant,先锋不给普通人的那种感觉。

这篇文章只解决一个问题:**如果你是一个会写代码但从没做过量化的人,怎么从 0 到 1 搭建并回测你的第一个美股交易策略。**不吹不黑,不卖课,只讲工程。


一、先理解你要解决的问题:量化交易到底是什么

在开始写代码之前,先把"量化交易"这个概念拆干净。

传统的自主投资依赖人的判断:看新闻、读财报、凭感觉。量化交易的核心区别是用程序把你的交易逻辑固定下来,然后用历史数据进行验证。

这个过程通常包含四个步骤:

  1. 数据获取:拿到你需要的行情数据(价格、成交量、订单簿等)
  2. 策略设计:把交易逻辑翻译成代码(买入条件、卖出条件、仓位管理)
  3. 回测验证:用历史数据跑一遍策略,看它过去表现如何
  4. 实盘执行:如果回测结果满意,把策略接入真实市场

对程序员来说,量化最难的部分不是写代码,而是理解金融市场的一些基本概念和约束条件。一旦过了这个坎,你会发现整个系统本质上就是一个数据处理管道——和你平时写的那些爬虫、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 获取步骤

  1. 访问 tickdb.ai 注册账号
  2. 进入控制台 → API Keys → 创建新 Key
  3. 在本地终端执行: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 实现自动下单
风险监控 实时计算持仓风险指标,触发告警
策略组合 多个低相关策略叠加,降低单一策略风险

结语:从"我想赚钱"到"我能验证"

量化交易不能保证你赚钱,但它能保证一件事:你的每一次"买入"决策都是经过验证的。

当你开始用数据说话、用回测验证假设、用代码执行规则,你和"凭感觉炒股"的区别就像用过山轨道的工程师和凭胆量跳悬崖的人——不是说后者一定摔死,但工程师至少有图纸。

而图纸的第一步,就是上面展示的那些代码。


下一步行动

如果你想亲手运行本文策略

  1. 访问 tickdb.ai 注册账号(免费,无需信用卡)
  2. 在控制台生成 API Key
  3. 执行 export TICKDB_API_KEY="你的_key"
  4. 安装依赖 pip install pandas numpy matplotlib requests
  5. 复制本文代码,运行 python backtest_demo.py

如果你想学习更系统的量化知识,TickDB 公众号每周更新产业链深度拆解,帮你理解不同市场事件背后的微观结构。

如果你想获取更多美股历史数据(用于更长周期的回测验证),联系 [email protected] 了解机构级数据方案。


本文不构成任何投资建议。市场有风险,投资需谨慎。