问题描述
解决方案
在 QMT 中过滤“小盘股”和“成交不活跃股”是量化策略中非常基础且重要的风控步骤。这通常涉及到两个核心指标的计算:
- 流通市值(Circulating Market Cap):用于衡量盘子大小。
- 公式:
流通市值 = 最新收盘价 × 流通股本
- 公式:
- 成交额(Turnover Amount):用于衡量活跃度。
- 通常使用过去 N 天(如 20 天)的日均成交额来平滑单日的异常波动。
核心思路
- 获取数据:使用
get_market_data_ex批量获取股票池的收盘价和成交额历史数据。 - 获取股本:使用
get_last_volume获取流通股本(注意:回测严谨模式下建议用财务数据接口get_financial_data获取历史股本,实盘或简单示例可用get_last_volume)。 - 计算与过滤:遍历股票,计算市值和均额,剔除不满足阈值的标的。
代码实现
以下是一个完整的策略示例代码。它定义了一个过滤函数,并在 handlebar 中调用,筛选出符合条件的股票。
# -*- coding: gbk -*-
import pandas as pd
import numpy as np
def init(ContextInfo):
# 1. 设置过滤阈值
# 最小流通市值:例如 50亿 (单位:元)
ContextInfo.min_mkt_cap = 50 * 10000 * 10000
# 最小日均成交额:例如 3000万 (单位:元)
ContextInfo.min_avg_amount = 3000 * 10000
# 2. 设定基础股票池(这里以沪深300成分股为例,实际可设为全A)
# 注意:实际运行时建议在界面设置股票池,或者用 get_stock_list_in_sector('沪深A股')
ContextInfo.stock_pool = ContextInfo.get_stock_list_in_sector('沪深300')
print("策略初始化完成,阈值设置:市值>{}亿, 日均成交>{}万".format(
ContextInfo.min_mkt_cap/1e8, ContextInfo.min_avg_amount/1e4))
def handlebar(ContextInfo):
# 为了演示,我们只在最后一根K线(如果是实盘就是最新时刻)运行过滤逻辑
if not ContextInfo.is_last_bar():
return
# 获取当前时间
current_date = ContextInfo.get_bar_timetag(ContextInfo.barpos)
# 执行过滤函数
selected_stocks = filter_stocks(ContextInfo, ContextInfo.stock_pool)
print("=" * 30)
print(f"原始股票数量: {len(ContextInfo.stock_pool)}")
print(f"过滤后股票数量: {len(selected_stocks)}")
print(f"部分入选股票: {selected_stocks[:5]}")
print("=" * 30)
# 在这里可以继续执行后续的择时或下单逻辑
# ...
def filter_stocks(ContextInfo, stock_list):
"""
股票过滤核心函数
:param stock_list: 待筛选的股票列表
:return: 筛选后的股票列表
"""
filtered_list = []
# 1. 批量获取行情数据
# 获取过去20天的数据,用于计算日均成交额
# fields: close(收盘价), amount(成交额)
data_map = ContextInfo.get_market_data_ex(
['close', 'amount'],
stock_list,
period='1d',
count=20,
subscribe=False
)
for stock in stock_list:
if stock not in data_map:
continue
df = data_map[stock]
if df is None or df.empty:
continue
# 2. 计算活跃度指标 (20日均成交额)
# 如果数据不足20天(如新股),取现有数据的均值
avg_amount = df['amount'].mean()
# 如果成交额不达标,直接跳过
if avg_amount < ContextInfo.min_avg_amount:
continue
# 3. 计算市值指标
# 获取最新收盘价
last_close = df['close'].iloc[-1]
# 获取流通股本 (单位:股)
# get_last_volume 获取的是最新流通股本。
# 注意:如果是做非常严谨的历史回测,建议使用 get_financial_data 获取历史时刻的股本,
# 避免因股本变动(如送转股)导致的未来函数。实盘使用 get_last_volume 即可。
circulating_shares = ContextInfo.get_last_volume(stock)
# 计算流通市值
mkt_cap = last_close * circulating_shares
# 如果市值不达标,跳过
if mkt_cap < ContextInfo.min_mkt_cap:
continue
# 4. 通过筛选
filtered_list.append(stock)
return filtered_list
代码关键点解析
-
ContextInfo.get_market_data_ex:- 这是获取行情数据的核心接口。我们请求了
['close', 'amount']两个字段。 count=20表示获取过去 20 根 K 线的数据,这样我们可以计算df['amount'].mean()来得到 20 日均成交额,这比只看当天的成交额更能反映股票的真实流动性。
- 这是获取行情数据的核心接口。我们请求了
-
ContextInfo.get_last_volume(stock):- 该函数返回指定股票的最新流通股本(单位:股)。
- 市值计算公式:
流通市值 = 收盘价 * 流通股本。
-
逻辑判断:
if avg_amount < ContextInfo.min_avg_amount: 剔除成交低迷的股票。if mkt_cap < ContextInfo.min_mkt_cap: 剔除小盘股。
进阶提示:关于回测中的“未来函数”
在上述代码中,get_last_volume 获取的是当前最新的股本。如果你在 2020 年的数据上回测,但使用的是 2023 年的股本数据,可能会导致计算出的市值不准确(因为期间可能有增发、送转)。
严谨的回测写法应使用财务数据接口获取历史股本:
def get_historical_mkt_cap(ContextInfo, stock, current_price, timetag):
# 获取股本结构中的 'circulating_capital' (已上市流通A股)
# 注意:财务数据返回单位通常是 '股' 或 '万股',需根据实际数据确认,QMT通常返回基本单位
field_list = ['CAPITALSTRUCTURE.circulating_capital']
# 将时间戳转为 YYYYMMDD 字符串
import datetime
date_str = datetime.datetime.fromtimestamp(timetag/1000).strftime('%Y%m%d')
# 获取财务数据
fin_data = ContextInfo.get_financial_data(field_list, [stock], date_str, date_str)
if fin_data is not None and not fin_data.empty:
# 假设返回的是 DataFrame
if isinstance(fin_data, pd.DataFrame):
# 根据返回结构取值,通常需要处理一下索引
try:
shares = fin_data.iloc[0][0]
return shares * current_price
except:
return 0
return 0
注:实盘交易或粗略筛选时,使用 get_last_volume 效率更高且代码更简洁。
Q&A
Q: 为什么要用 20 日均成交额而不是当天的成交额?
A: 单日的成交额容易受到突发事件、停牌复牌或市场情绪的干扰。使用 20 日(约一个月)的平均值可以更平滑地反映股票的真实流动性水平,避免误杀或误选。
Q: get_last_volume 返回的单位是什么?
A: 返回的是“股”数。例如某股票流通股本为 10 亿股,该函数返回 1,000,000,000。计算市值时直接乘以股价即可得到以“元”为单位的市值。
Q: 这段代码可以直接放在回测里跑吗?
A: 可以。代码中包含了 # -*- coding: gbk -*- 声明,且使用了标准的 QMT API。只需确保你在 QMT 客户端中已经下载了对应的历史行情数据(日线数据)。