【量化专栏 02】让数据开口说话:收益率计算与绝对止损的代码实现

核心导读

拿到一堆历史行情数据后,新手最容易犯的致命错误就是"引入未来函数"——即在回测时,使用了当前时间点根本无法获取的未来数据,导致模拟盘天下无敌,实盘却一塌糊涂。

今天,我们将学习 Pandas 中最核心的两个函数:pct_change() 和 shift()。同时,我们将针对 1-2天的超短线交易风格,在代码中刻死一条 -3% 的硬性止损线。记住,在量化的世界里,止损不是靠心态,而是靠冷冰冰的布尔值(True/False)。

1. 核心数学概念:收益率的量化表达

在金融计算中,我们最常使用的是简单收益率(Simple Return)。它的计算逻辑非常直观:用今天的收盘价减去昨天的收盘价,再除以昨天的收盘价。

用数学公式表达如下:

Rt = (Pt - Pt-1) / Pt-1

其中,R_t 是第 t 天的收益率,P_t 是第 t 天的收盘价,Pt-1 是前一天的收盘价。在 Pandas 中,我们不需要手动写循环来计算这个公式,一行代码即可搞定

2. 实战演练:计算收益率与规避未来函数

我们将以 TCL科技(000100)的日线数据为例,进行代码实操。

在编写策略时,我们要继续坚持"尽早报错 (Fail Fast)"的原则。如果数据源存在缺失(例如某天停牌导致没有价格数据),不要用宽泛的错误捕获去掩盖,而是让 Pandas 暴露这些空值(NaN),并用专门的逻辑去处理它们。

import akshare as ak
import pandas as pd

def process_trading_signals(symbol: str, start_date: str, end_date: str) -> pd.DataFrame:
    """
    获取数据并计算收益率与止损信号
    """
    print(f"正在拉取并处理 {symbol} 的行情数据...")
    
    df = ak.stock_zh_a_hist(symbol=symbol, period="daily", start_date=start_date, end_date=end_date, adjust="qfq")
    
    # 基础数据校验
    if df is None or df.empty:
        raise ValueError(f"数据获取失败!请检查网络或股票代码:{symbol}")
    
    df = df.rename(columns={'日期': 'date', '开盘': 'open', '收盘': 'close', '最高': 'high', '最低': 'low'})
    df['date'] = pd.to_datetime(df['date'])
    df.set_index('date', inplace=True)
    
    # ---------------- 核心计算逻辑 ----------------
    
    # 1. 计算当日收益率 (pct_change 默认计算与前一行的百分比变化)
    df['daily_return'] = df['close'].pct_change()
    
    # 2. 规避未来函数:A股是T+1,如果你今天尾盘买入,你的真实收益其实是明天的涨跌幅。
    # 我们使用 shift(-1) 将明天的收益率向上移动一行,作为"今天买入、明天卖出"的预期收益。
    df['next_day_return'] = df['daily_return'].shift(-1)
    
    # 3. 核心风控:构建 -3% 绝对止损逻辑
    # 假设我们昨日收盘买入,今天的盘中最低价如果跌破昨日收盘价的 3%,即触发止损。
    # 公式:(今日最低价 - 昨日收盘价) / 昨日收盘价 <= -0.03
    df['intraday_drawdown'] = (df['low'] - df['close'].shift(1)) / df['close'].shift(1)
    
    # 生成止损触发信号 (True 代表触发,False 代表安全)
    df['trigger_stop_loss'] = df['intraday_drawdown'] <= -0.03
    
    # 清理计算产生的空值 (第一天没有前一天数据,最后一天没有后一天数据)
    df = df.dropna()
    
    return df[['open', 'close', 'low', 'daily_return', 'next_day_return', 'intraday_drawdown', 'trigger_stop_loss']]

# === 测试环节 ===
if __name__ == "__main__":
    # 以 TCL科技(000100) 近期数据为例
    df_tcl = process_trading_signals("000100", "20231001", "20231031")
    
    print("\n--- 收益率与止损信号表 ---")
    
    # 筛选出触发了 -3% 止损线的交易日进行观察
    stop_loss_days = df_tcl[df_tcl['trigger_stop_loss'] == True]
    
    print(f"\n在回测区间内,共有 {len(stop_loss_days)} 天盘中触及 -3% 止损线。")
    print(stop_loss_days[['close', 'low', 'intraday_drawdown', 'trigger_stop_loss']].head())

3. 代码深层解析

shift(1) 与 shift(-1) 的艺术

这是量化中最常用的操作。shift(1) 代表获取"昨天的某项数据",而shift(-1) 代表获取"明天的某项数据"。在 A 股做 1-2 天的短线回测时,正确地将信号(今天产生)和收益(明天实现)对齐,是确保回测不失真的关键。

布尔值思维

trigger_stop_loss 列生成的是 TrueFalse。在随后的复杂策略中,当"买入信号 == True" 且 "止损信号 == False" 时,我们才会坚定持有。这种思维将彻底消除实盘交易中的主观犹豫

本期思考题:

如果我们的策略改为"早盘开盘价买入",那么计算盘中最大回撤(用于判断是否触发止损)的基准价格,应该从 df['close'].shift(1) 替换成哪个字段?

第二篇的基础运算框架已经搭好。下一步,我们就要进入真正的"策略开发"环节了。你是希望先学习 基于移动平均线(MA)的趋势跟踪信号,还是想直接挑战更适合超短线的 量价突破(如放量突破平台)信号 的代码编写?