拿到一堆历史行情数据后,新手最容易犯的致命错误就是"引入未来函数"——即在回测时,使用了当前时间点根本无法获取的未来数据,导致模拟盘天下无敌,实盘却一塌糊涂。
今天,我们将学习 Pandas 中最核心的两个函数:pct_change() 和 shift()。同时,我们将针对 1-2天的超短线交易风格,在代码中刻死一条 -3% 的硬性止损线。记住,在量化的世界里,止损不是靠心态,而是靠冷冰冰的布尔值(True/False)。
在金融计算中,我们最常使用的是简单收益率(Simple Return)。它的计算逻辑非常直观:用今天的收盘价减去昨天的收盘价,再除以昨天的收盘价。
用数学公式表达如下:
其中,R_t 是第 t 天的收益率,P_t 是第 t 天的收盘价,Pt-1 是前一天的收盘价。在 Pandas 中,我们不需要手动写循环来计算这个公式,一行代码即可搞定。
我们将以 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())这是量化中最常用的操作。shift(1) 代表获取"昨天的某项数据",而shift(-1) 代表获取"明天的某项数据"。在 A 股做 1-2 天的短线回测时,正确地将信号(今天产生)和收益(明天实现)对齐,是确保回测不失真的关键。
trigger_stop_loss 列生成的是 True 或 False。在随后的复杂策略中,当"买入信号 == True" 且 "止损信号 == False" 时,我们才会坚定持有。这种思维将彻底消除实盘交易中的主观犹豫。
本期思考题:
如果我们的策略改为"早盘开盘价买入",那么计算盘中最大回撤(用于判断是否触发止损)的基准价格,应该从 df['close'].shift(1) 替换成哪个字段?
第二篇的基础运算框架已经搭好。下一步,我们就要进入真正的"策略开发"环节了。你是希望先学习 基于移动平均线(MA)的趋势跟踪信号,还是想直接挑战更适合超短线的 量价突破(如放量突破平台)信号 的代码编写?