第 05 篇
简单市值轮动策略
从「每天对固定几只股票做均线判断」升级到「每 10 天在全 A 股里重选一次市值最小的 5 只股票持有」——真正的选股 + 轮动。本页新引入三样东西:rundaily 定时循环、context.currentdt 取当前时间、用 getindexstocks + getfundamentals 从全市场选股。素材见 raw/collections/jq-getting-started/05-23-简单市值轮动策略md.md。
这是什么
「简单小市值轮动策略」的完整实现课。策略一句话:每隔 10 个交易日,等金额持有市值排名最小的前 5 只股票,卖出其他股票。它展示了如何把「每隔 N 天做一次」的周期控制、全市场选股、动态换仓这三件事用聚宽代码做出来。
核心要点
策略定义与初始化
- 策略:每隔若干交易日,等金额持有市值最小的前几只股票,卖出其他股票;间隔定为 10 天。
- 初始化参数:
g.stocksnum = 5(持有最小市值股票数)、g.period = 10(轮动频率,多少天交易一次)、g.days = 1(记录策略进行到第几天)。 - 股票范围 = 全市场,但全市场不断变化(新股上市、退市),所以股票范围要在每个周期内现算,不能在初始化里定死。
run_daily —— 周期循环的另一种写法
run_daily(daily, time='every_bar')把自定义函数插入 initialize,让它按周期循环,效果等同 handle_data。套路:def initialize(context): ... 初始化 ... run_daily(daily, time='every_bar') # 注册周期函数 def daily(context): ... 每个周期要做的 ...time参数控制循环行为:'every_bar'按当前周期循环——按天回测就每天开盘运行,按分钟回测就每分钟开始运行。time 还有其他取值(见 API「定时运行」节)。- 推荐用 run_daily,JQ 上的 handle_data 将被弃用。
每 10 天交易一次的周期控制
- 基本回测只提供每日循环,「每十天一次」要用程序实现:维护计数变量
g.days,if g.days % g.period == 1:才执行选股买卖,否则pass;每天末尾g.days += 1。
选股(找出全市场市值最小的 5 只)
- 取当前时间:
date = context.current_dt.strftime('%Y-%m-%d')。 - 确定范围:
scu = get_index_stocks('000001.XSHG') + get_index_stocks('399106.XSHE')——上证指数 + 深证综指成分并起来 ≈ 全 A 股。 - 查市值排序:
get_fundamentals(query(valuation.code, valuation.market_cap).filter(valuation.code.in_(scu)).order_by(valuation.market_cap.asc()), date=date),按市值从小到大排。 - 取前 5:
buylist = list(df['code'][:g.stocksnum])(df 是 pandas DataFrame)。
换仓与买卖
- 卖出:遍历当前持仓
context.portfolio.positions,凡不在 buylist 里的 →order_target(stock, 0)清仓(原来持有、现在不是最小市值前 5,就该卖掉)。 - 买入:
position_per_stk = context.portfolio.cash / g.stocksnum(现金平分 5 份),对 buylist 里每只order_value(stock, position_per_stk)。
机制 / 论证
- 为什么全市场要在周期内现取:市值轮动依赖「当下市值排名」,而上市/退市让市场构成不断变——任何写死的股票范围都会过期。这跟第 4 章「固定两只股票」形成对比:轮动策略的对象本身是动态的。
- 计数取模实现低频动作:
g.days % g.period == 1把「每天运行」降频成「每 10 天第 1 天运行」——在不支持任意间隔的按日循环里,这是标准手法;也顺便说明 run_daily 的 time 参数只控制触发,低频业务逻辑仍要自己写。 - 卖出条件不是「跌了就卖」而是「不在名单就卖」:策略定义的是「持有市值最小前 5」,任何离开前 5 的持仓都违反定义,所以要调仓。这是轮动策略的核心动作模式:先对账(已持有 vs 该持有),差多少补多少。
可操作
- 可复用骨架(参数与逻辑):
initialize:设g.stocksnum=5、g.period=10、g.days=1;run_daily(daily, time='every_bar')。daily(context):if g.days % g.period == 1:做选股换仓,否则pass;末尾g.days += 1。- 选股:
date=context.current_dt.strftime('%Y-%m-%d')→scu = get_index_stocks('000001.XSHG') + get_index_stocks('399106.XSHE')→get_fundamentals(query(valuation.code).filter(valuation.code.in_(scu)).order_by(valuation.market_cap.asc()), date=date)→buylist = list(df['code'][:g.stocksnum])。 - 换仓:不在 buylist 的持仓
order_target(stock, 0);per = cash/g.stocksnum,每只order_value(stock, per)。
- 参数:5 只最小市值股、10 天轮动、全 A 范围(上证指数 000001.XSHG + 深证综指 399106.XSHE)、等金额买入。
- 教训:任何「动态选股」策略都要把「当前持仓 ∩ 目标名单」做差,名单外一律卖掉——否则旧持仓会赖着不走。
术语
- 市值轮动:定期重选市值最小的一批股票持有。
- run_daily / time='every_bar':自定义周期函数(handle_data 的推荐替代)。
- g.days % g.period == 1:取模计数实现「每隔 N 天」。
- get_index_stocks:取指数成分股;000001.XSHG = 上证指数、399106.XSHE = 深证综指。
- get_fundamentals + valuation.market_cap:查市值表。
- context.current_dt:策略当前时间。
不确定 / 待验证
- raw 正文一处写
context.portfolio.position、另一处写positions(后者是正写);g.stocksbnum是g.stocksnum的笔误。→ 落地按 positions/stocksnum 修正。 - 「上证指数 + 深证综指 = 全 A 股市场所有股票」是原素材的说法(不含北交所等,2015 年语境);范围口径 [需要验证]。
get_fundamentals按日取valuation.market_cap的排序结果会包含停牌/ST 股(本篇未过滤,过滤在 2.0 篇引入)。
相关
- 市值轮动策略 2.0 — 本篇的模块化与均衡化升级
- run_daily 与任务节奏控制(定时任务) — 定时任务/周期控制
- 聚宽行情与财务数据获取 API — get_fundamentals / 指数成分取数
- 股票池(交易对象范围与过滤) — 股票池与选股范围
- 聚宽新手入门教程(JoinQuant 平台实操) — 本教程总览
更新 2026-09-06