问题描述
解决方案
这是一个关于量化策略设计与实现的问题。
PSY (心理线) 指标策略概述
PSY (Psychological Line) 是一种基于投资者心理预期的市场情绪指标。它通过计算一段时间内股价上涨天数占总天数的比例,来判断市场的超买或超卖状态。
计算公式:
$$PSY(N) = \frac{N日内上涨天数}{N} \times 100$$
通常 $N$ 取值为 12。
策略逻辑:
- 超卖(买入信号): 当 PSY 值低于某个下限(如 25),表示市场情绪过度悲观,可能出现反弹,执行买入。
- 超买(卖出信号): 当 PSY 值高于某个上限(如 75),表示市场情绪过度乐观,可能出现回调,执行卖出。
QMT Python 策略代码实现
以下代码展示了如何在 QMT 平台实现该策略。代码包含了数据获取、指标计算、交易信号判断及下单操作。
# -*- coding: gbk -*-
import pandas as pd
import numpy as np
def init(ContextInfo):
"""
初始化函数,设定策略参数和股票池
"""
# 策略参数设置
ContextInfo.N = 12 # PSY计算周期
ContextInfo.buy_threshold = 25 # 买入阈值
ContextInfo.sell_threshold = 75 # 卖出阈值
ContextInfo.stock_code = '600000.SH' # 示例标的:浦发银行
ContextInfo.account_id = '6000000000' # 请替换为您的真实资金账号
# 设置股票池
ContextInfo.set_universe([ContextInfo.stock_code])
# 设置交易账号
ContextInfo.set_account(ContextInfo.account_id)
print(f"策略初始化完成,标的:{ContextInfo.stock_code},PSY周期:{ContextInfo.N}")
def handlebar(ContextInfo):
"""
K线周期运行函数,每根K线执行一次
"""
# 获取当前K线索引
index = ContextInfo.barpos
# 如果历史数据不足以计算PSY(需要N+1根K线来计算N次涨跌),则直接返回
if index < ContextInfo.N:
return
# 获取当前K线的时间戳,用于截取数据
timetag = ContextInfo.get_bar_timetag(index)
# 将时间戳转换为字符串格式 'YYYYMMDDHHMMSS'
end_time_str = timetag_to_datetime(timetag, '%Y%m%d%H%M%S')
# 获取历史行情数据
# 我们需要 N+1 个数据点来计算 N 个交易日的涨跌情况
# count = N + 1
data_df = ContextInfo.get_market_data_ex(
fields=['close'],
stock_code=[ContextInfo.stock_code],
period='1d',
end_time=end_time_str,
count=ContextInfo.N + 1,
dividend_type='front' # 前复权
)
# 检查数据是否获取成功
if ContextInfo.stock_code not in data_df or data_df[ContextInfo.stock_code].empty:
return
# 提取收盘价序列
close_series = data_df[ContextInfo.stock_code]['close']
# 再次确认数据长度是否足够
if len(close_series) < ContextInfo.N + 1:
return
# --- PSY 指标计算逻辑 ---
# 计算每日涨跌:今日收盘 - 昨日收盘
diff = close_series.diff()
# 取最近 N 天的涨跌数据(去掉第一个NaN值)
# diff序列的最后 N 个值即为我们要统计的区间
recent_diff = diff.iloc[-ContextInfo.N:]
# 统计上涨天数 (diff > 0)
up_days = recent_diff[recent_diff > 0].count()
# 计算 PSY 值
psy_value = (up_days / ContextInfo.N) * 100
# 打印调试信息(可选)
# print(f"时间: {end_time_str}, PSY: {psy_value:.2f}, 收盘价: {close_series.iloc[-1]}")
# --- 交易逻辑 ---
# 获取当前持仓
positions = get_trade_detail_data(ContextInfo.account_id, 'stock', 'position')
current_vol = 0
for pos in positions:
if pos.m_strInstrumentID + '.' + pos.m_strExchangeID == ContextInfo.stock_code:
current_vol = pos.m_nVolume
break
# 1. 买入信号:PSY < 超卖阈值 且 当前无持仓
if psy_value < ContextInfo.buy_threshold and current_vol == 0:
print(f"触发买入信号: PSY={psy_value:.2f} < {ContextInfo.buy_threshold}")
# 全仓买入示例(实际请根据资金管理调整)
# 这里简单演示买入 1000 股
passorder(23, 1101, ContextInfo.account_id, ContextInfo.stock_code, 5, -1, 1000, ContextInfo)
# 2. 卖出信号:PSY > 超买阈值 且 有持仓
elif psy_value > ContextInfo.sell_threshold and current_vol > 0:
print(f"触发卖出信号: PSY={psy_value:.2f} > {ContextInfo.sell_threshold}")
# 卖出所有持仓
passorder(24, 1101, ContextInfo.account_id, ContextInfo.stock_code, 5, -1, current_vol, ContextInfo)
代码关键点解析
-
数据获取 (
get_market_data_ex):- 我们使用
count=ContextInfo.N + 1。这是因为计算 $N$ 日的涨跌需要 $N+1$ 个收盘价(第1天的涨跌需要第0天的数据作为参照)。 end_time参数至关重要,它确保我们在回测时获取的是“当时”的数据,而不是未来的数据,避免未来函数。
- 我们使用
-
指标计算:
- 利用 Pandas 的
.diff()方法快速计算价格差。 - 利用布尔索引
recent_diff > 0和.count()快速统计上涨天数。
- 利用 Pandas 的
-
交易执行 (
passorder):23代表买入,24代表卖出。1101代表单股、单账号、普通、股/手方式下单。5代表使用最新价(市价)下单,保证成交概率(回测中通常以收盘价成交)。
-
持仓检查:
- 使用
get_trade_detail_data获取当前持仓,避免重复买入或卖空(A股不支持直接卖空)。
- 使用
如何运行
- 打开 QMT 客户端,进入【模型研究】或【策略编辑器】。
- 新建一个 Python 策略,将上述代码粘贴进去。
- 修改
ContextInfo.account_id为你的模拟或实盘账号。 - 在右侧设置回测参数(如资金、费率等),选择回测周期(建议日线)。
- 点击【运行】或【回测】。
Q&A
Q: 为什么 PSY 指标有时候会钝化(长期停留在高位或低位)?
A: PSY 只是统计上涨概率,不考虑上涨幅度。如果股价连续微涨,PSY 会很高,但股价可能并没有暴涨。通常建议结合 VR(容量比率)或 ARBR 指标一起使用。
Q: 如何修改为多只股票轮动?
A: 可以在 init 中将 stock_code 设为一个列表,然后在 handlebar 中使用 for 循环遍历这个列表,分别获取数据和计算指标。
Q: get_market_data_ex 返回的数据结构是什么?
A: 当请求单只股票时,它返回一个字典,Key 是股票代码,Value 是一个 Pandas DataFrame。DataFrame 的 Index 是时间字符串,Columns 是你请求的字段(如 'close')。