JoinQuant新手避坑指南:从零搭建你的第一个量化策略(附完整代码)
JoinQuant新手避坑指南:从零搭建你的第一个量化策略(附完整代码)
第一次打开JoinQuant的回测页面时,看着满屏的参数和代码框,我握着鼠标的手心全是汗。明明在本地跑通的策略,在这里总是莫名其妙报错;好不容易调通代码,回测结果却和预期相差十万八千里——这可能是每个量化新手都经历过的噩梦时刻。
1. 数据获取的隐藏陷阱
很多新手会直接复制社区策略里的get_price()函数,却不知道这个简单的API调用藏着三个致命坑:
# 典型错误示范 data = get_price('000001.XSHG', start_date='2020-01-01', end_date='2021-12-31')第一坑:默认复权方式
JoinQuant的get_price默认返回前复权数据。如果策略涉及分红配股,必须显式指定:
# 正确做法 data = get_price('000001.XSHG', start_date='2020-01-01', end_date='2021-12-31', fq='post') # 后复权第二坑:停牌日期黑洞
平台返回的数据会自动过滤停牌日,导致以下常见错误:
# 错误的时间对齐方式 close = data['close'] ma20 = close.rolling(20).mean() # 实际交易日数可能不足20天建议改用平台内置的移动平均函数:
# 可靠做法 from jqdatasdk import * ma20 = ta.MA(close, timeperiod=20)第三坑:分钟级数据的时区问题
当获取分钟线时,务必注意end_date包含的是北京时间15:00:
| 参数 | 时区陷阱 | 正确示例 |
|---|---|---|
end_date='2023-01-01' | 实际获取到2022-12-30 15:00 | end_date='2023-01-01 15:00:00' |
提示:使用
print(data.index[-1])确认获取数据的实际时间范围
2. 回测设置的魔鬼细节
2.1 初始资金与手续费的双重陷阱
新手最容易忽略的两个参数:
# 初始化函数中的关键设置 def initialize(context): # 必须设置初始资金(单位:元) context.cash = 1000000 # 手续费设置(买+卖各收万三) set_order_cost(OrderCost( open_tax=0, close_tax=0.001, # 印花税 open_commission=0.0003, close_commission=0.0003, min_commission=5), type='stock')常见错误对照表:
| 错误配置 | 实际影响 | 正确值 |
|---|---|---|
context.portfolio.starting_cash | 系统不识别该参数 | context.cash |
min_commission=0 | 产生不现实交易成本 | min_commission=5 |
| 忽略印花税 | 低估卖出成本 | close_tax=0.001 |
2.2 滑点模拟的三种模式
JoinQuant提供不同滑点模型,对高频策略影响显著:
# 滑点设置对比 set_slippage(FixedSlippage(0.02)) # 固定比例滑点 set_slippage(VolumeShareSlippage(volume_limit=0.1, price_impact=0.1)) # 成交量参与率模型 set_slippage(NoSlippage()) # 无滑点(默认)实测不同设置对年化收益的影响(测试周期2022年):
| 滑点类型 | 年化收益 | 最大回撤 |
|---|---|---|
| 无滑点 | 15.2% | -8.7% |
| 固定0.02 | 12.1% | -9.3% |
| VolumeShare | 9.8% | -11.5% |
3. 策略逻辑的典型误区
3.1 未来函数防不胜防
这个看似正常的均值回归策略其实暗藏未来函数:
# 危险代码:使用了未来数据 def handle_data(context, data): current_price = data['close'] future_high = max(data['high'][-5:]) # 使用未来5日最高价 if current_price < future_high * 0.9: order_target_percent(stock, 0.1)正确做法应使用历史最高价:
# 安全版本 def handle_data(context, data): hist = history_bars(stock, 20, '1d', ['high']) historical_high = max(hist['high']) current_price = data['close'] if current_price < historical_high * 0.9: order_target_percent(stock, 0.1)3.2 停牌处理的必备检查
没有处理停牌的策略会在实盘崩盘:
# 完整的安全交易函数 def safe_order(context, stock, amount): # 检查是否停牌 if get_trading_status(stock) != '交易': log.info(f"{stock} 停牌中,跳过交易") return # 检查是否ST if is_st_stock(stock): log.info(f"{stock} 是ST股票,跳过交易") return # 检查涨跌停 current_data = get_current_data()[stock] if amount > 0 and current_data.high_limit == current_data.last_price: log.info(f"{stock} 已涨停,无法买入") elif amount < 0 and current_data.low_limit == current_data.last_price: log.info(f"{stock} 已跌停,无法卖出") else: order_target(stock, amount)4. 完整策略案例:抗坑版双均线策略
# -*- coding: utf-8 -*- from jqdatasdk import * import numpy as np def initialize(context): # 1. 账户设置 context.cash = 1000000 context.security = '000300.XSHG' # 2. 回测参数 set_benchmark(context.security) set_option('use_real_price', True) # 3. 手续费设置 set_order_cost(OrderCost( open_tax=0, close_tax=0.001, open_commission=0.0003, close_commission=0.0003, min_commission=5), type='stock') # 4. 滑点设置 set_slippage(VolumeShareSlippage(volume_limit=0.1, price_impact=0.1)) # 5. 定时运行 run_daily(trade, time='14:50') def trade(context): stock = context.security # 获取历史数据(避免未来函数) hist = history_bars(stock, 60, '1d', ['close']) if len(hist) < 60: return closes = hist['close'] # 计算均线 short_ma = ta.MA(closes, timeperiod=10) long_ma = ta.MA(closes, timeperiod=30) current_price = closes[-1] # 交易信号 if short_ma[-1] > long_ma[-1] and context.portfolio.positions[stock].amount == 0: # 买入前检查 if get_trading_status(stock) == '交易' and not is_st_stock(stock): order_value(stock, context.portfolio.total_value * 0.9) elif short_ma[-1] < long_ma[-1] and context.portfolio.positions[stock].amount > 0: # 卖出前检查 current_data = get_current_data()[stock] if current_data.low_limit != current_data.last_price: order_target(stock, 0) def handle_data(context, data): # 风控模块 for stock in context.portfolio.positions: cost = context.portfolio.positions[stock].avg_cost price = context.portfolio.positions[stock].price if price / cost < 0.9: # 止损10% order_target(stock, 0) log.info(f"{stock} 触发止损")这个策略包含了以下防坑设计:
- 使用
history_bars避免未来数据 - 完整的交易前检查(停牌、ST、涨跌停)
- 动态止损机制
- 合理的滑点和手续费设置
- 定时运行避免过度交易
