今天,公众号将为全网读者带来QMT量化实战系列! 公众号将为大家多维度、多策略、多场景来讲述QMT量化平台的实践应用。同时,我们对每段代码都做了解读说明,愿你在Quant的道路上学有所获! 此系列将由浅入深,每月1~2期,大家敬请期待! 今日复现策略:《支持自定义筛选与排序的多因子策略》
策略逻辑解读 🧠 数据获取通过Tushare获取数据,合成因子
- """获取股票列表"""
- pick_date = ContextInfo.pick_date
- # 获取股票每日指标
- daily_basic = pro.daily_basic(trade_date=pick_date)
- # 获取股票历史列表
- bak_basic = pro.bak_basic(trade_date=pick_date)
- # 合并两个数据
- merged_df = daily_basic.merge(bak_basic, on=['trade_date', 'ts_code'], how='inner', suffixes=('', '_bak'))
- # 去除重复列
- merged_df = merged_df[[col for col in merged_df.columns if not col.endswith('_bak')]]
- # 计算上市天数
- merged_df['listed_days'] = (
- pd.to_datetime(str(pick_date), format='%Y%m%d', errors='coerce') -
- pd.to_datetime(merged_df['list_date'].astype(str).str.strip(), format='%Y%m%d', errors='coerce')).dt.days
复制代码
因子清单因子字段 | 中文描述 | ts_code | TS股票代码 | trade_date | 交易日期 | close | 当日收盘价 | turnover_rate | 换手率(%) | turnover_rate_f | 换手率(自由流通股) | volume_ratio | 量比 | pe | 市盈率 | pe_ttm | 市盈率(TTM) | pb | 市净率 | ps | 市销率 | ps_ttm | 市销率(TTM) | dv_ratio | 股息率(%) | dv_ttm | 股息率(TTM)(%) | total_share | 总股本(万股 / 亿股) | float_share | 流通股本(万股 / 亿股) | free_share | 自由流通股本(万) | total_mv | 总市值(万元) | circ_mv | 流通市值(万元) | name | 股票名称 | industry | 所属行业 | area | 所属地域 | total_assets | 总资产(亿) | liquid_assets | 流动资产(亿) | fixed_assets | 固定资产(亿) | reserved | 公积金 | reserved_pershare | 每股公积金 | eps | 每股收益 | bvps | 每股净资产 | list_date | 上市日期 | undp | 未分配利润 | per_undp | 每股未分配利润 | rev_yoy | 收入同比(%) | profit_yoy | 利润同比(%) | gpr | 毛利率(%) | npr | 净利润率(%) | holder_num | 股东人数 |
🔍 筛选逻辑 通过因子筛选股票,因子列表的因子都能用于筛选 - # 排除所有名称中包含“ST”或“st”的股票
- stocks_info = stocks_info[~stocks_info['name'].str.contains('ST', case=False, na=False)]
- # 排除上市天数小于365天的股票
- stocks_info = stocks_info[stocks_info['listed_days'] >= 365]
- # 排除创业板
- stocks_info = stocks_info[~stocks_info['ts_code'].str.startswith('30')]
- # 排除科创板
- stocks_info = stocks_info[~stocks_info['ts_code'].str.startswith('68')]
- # 保留北交所
- #stocks_info = stocks_info[stocks_info['ts_code'].str.startswith(('8', '9','4'))]
- # 保留深证
- stocks_info = stocks_info[stocks_info['ts_code'].str.startswith(('00'))]
- # 排除北交所
- #stocks_info = stocks_info[~stocks_info['ts_code'].str.startswith(('8', '9','4'))]
复制代码
📊 排序逻辑支持多因子自定义排序,并允许为各因子分配独立权重,实现更精细的打分和排序逻辑。 单因子
- # 按总市值从小到大排序 权重1
- factors = [ {'指标': 'total_mv', '次序': '从小到大', '权重': 1}]
复制代码
双因子
- # 按总市值和流通市值排序 权重分别是1和2
- factors = [ {'指标': 'total_mv', '次序': '从小到大', '权重': 1}, {'指标': 'circ_mv', '次序': '从小到大', '权重': 2},]
复制代码
多因子 - # 按总市值、流通市值、收盘价排序 权重分别是10、20、2
- factors = [ {'指标': 'total_mv', '次序': '从小到大', '权重': 10}, {'指标': 'circ_mv', '次序': '从小到大', '权重': 20}, {'指标': 'close', '次序': '从小到大', '权重': 2},]
复制代码
🔁 交易逻辑卖出逻辑- 根据因子排序找出每个持仓的当前排名
- 如果股票排名 ≥ sell_rank 或不在候选列表,全仓卖出
- """卖出方法"""
- sell_codes = []
- code_list= list(stocks_info['ts_code'])
- positions = get_trade_detail_data(ContextInfo.account, 'stock', 'position')
- pos_codes = [f"{p.m_strInstrumentID}.{p.m_strExchangeID}" for p in positions]
- daily_data = ContextInfo.get_market_data_ex([],pos_codes, period = "1d", start_time = ContextInfo.trade_date, end_time = ContextInfo.trade_date,count=1)
- for item in positions:
- code = f"{item.m_strInstrumentID}.{item.m_strExchangeID}"
- volume = item.m_nVolume
- # 交易日收盘价
- price = round(daily_data[code]['close'].iloc[0],2)
- # 当前排名
- current_rank = code_list.index(code) if code in code_list else 9999
- # 当前排名大于卖出排名就卖出
- if current_rank>=ContextInfo.sell_rank:
- logger.info(f'[触发卖出] 标的:{code} 卖出数量:{item.m_nVolume} 最新价格:{price} 当前排名:{current_rank} 卖出排名:{ContextInfo.sell_rank}')
- passorder(24, 1101, ContextInfo.account, code, 11,price , volume, "示例", 2, "投资备注",ContextInfo)
- sell_codes.append(code)
复制代码 买入逻辑- 获取当前持仓股票代码集合
- 从目标列表剔除已持仓和待卖股票,得到买入候选池
- 计算最大可买入数量,确保持仓总数不超过预设上限
- 根据账户总资产均分资金,计算每只股票的买入金额及对应买入股
- """买入方法"""
- # 当前持仓代码集合
- current_positions = get_trade_detail_data(ContextInfo.account, 'stock', 'position')
- current_codes = {f"{p.m_strInstrumentID}.{p.m_strExchangeID}" for p in current_positions}
- # 需要买入的股票代码(不在当前持仓也不在卖出列表)
- buy_candidates = set(target_codes) - set(current_codes) - set(sell_codes)
- # 最多还能买多少只股票 如果buy_candidates的数量小于max_new_buys那就都买
- max_new_buys = min(len(buy_candidates), max(ContextInfo.stock_num - len(current_codes), 0))
- buy_count = 0
- for code in buy_candidates:
- if buy_count >= max_new_buys:
- break
- # 获取账户总资产
- total_asset = get_trade_detail_data(ContextInfo.account, 'stock', 'account')[0].m_dBalance
- # 计算单个股票买入金额
- order_amount = round(total_asset / ContextInfo.stock_num, 2)
- # 获取交易日行情数据
- daily_data = ContextInfo.get_market_data_ex([], [code], period="1d", start_time=ContextInfo.trade_date, end_time=ContextInfo.trade_date, count=1)
- if code not in daily_data or daily_data[code].empty:
- logger.warning(f"[跳过买入] 无法获取行情数据:{code}")
- continue
- price = round(daily_data[code]['close'].iloc[0], 2)
- if price > 0:
- quantity = max(int(order_amount / price) // 100 * 100, 0)
- if quantity > 0:
- passorder(23, 1101, ContextInfo.account, code, 11, price, quantity, "示例", 2, "投资备注", ContextInfo)
- logger.info(f'[触发买入] 标的:{code} 买入数量:{quantity} 买入价格:{price}')
- buy_count += 1
- #print_account(ContextInfo)
- else:
- logger.warning(f"[跳过买入] 价格为0:{code}")
复制代码
➕ 后续扩展完整代码获取,联系作者开通合作券商账户即可免费加入知识星球(限时) 需要进量化交流群的添加作者微信
|