🚀 AI 一键生成 joinquant 策略代码

JoinQuant 量化实战:VR 成交量比率指标选股策略

joinquant | 发布时间: 2025/12/24
以下内容由 EasyQuant 生成。

问题描述

帮我写一个 VR 成交量比率的量化策略

解决方案

这是一个基于 VR (Volume Ratio,成交量比率) 指标的量化交易策略。

策略原理

VR 指标主要通过分析股价上升日和下降日的成交量比率,来判断市场的人气和资金流向,从而辅助判断股价的顶部和底部。

VR 计算公式:
$$VR = \frac{AVS + \frac{1}{2}CVS}{BVS + \frac{1}{2}CVS} \times 100$$

其中:

  • $AVS$:N日内股价上涨日(收盘价 > 前收盘价)的成交量之和。
  • $BVS$:N日内股价下跌日(收盘价 < 前收盘价)的成交量之和。
  • $CVS$:N日内股价平盘日(收盘价 = 前收盘价)的成交量之和。
  • $N$:通常取 26 日。

交易逻辑:

  1. 买入信号:当 VR 值低于 70(低位区域),认为市场超卖,人气低迷,可能是底部,执行买入。
  2. 卖出信号:当 VR 值高于 250(高位区域),认为市场超买,人气过热,可能是顶部,执行卖出。
  3. 股票池:选取沪深300成分股。
  4. 仓位管理:等权重买入,满仓操作。

策略代码 (JoinQuant)

# -*- coding: utf-8 -*-
import jqdata
import pandas as pd
import numpy as np

def initialize(context):
    """
    初始化函数,设定基准、手续费、全局变量等
    """
    # 设定沪深300作为基准
    set_benchmark('000300.XSHG')
    # 开启动态复权模式(真实价格)
    set_option('use_real_price', True)
    # 过滤掉order系列API产生的比error级别低的log
    log.set_level('order', 'error')
    
    # 股票类每笔交易时的手续费是:买入时佣金万分之三,卖出时佣金万分之三加千分之一印花税, 每笔交易佣金最低扣5块钱
    set_order_cost(OrderCost(close_tax=0.001, open_commission=0.0003, close_commission=0.0003, min_commission=5), type='stock')
    
    # --- 策略参数设置 ---
    g.index_security = '000300.XSHG' # 股票池:沪深300
    g.vr_window = 26                 # VR指标计算周期
    g.buy_threshold = 70             # VR低于该值买入 (超卖)
    g.sell_threshold = 250           # VR高于该值卖出 (超买)
    g.max_hold_count = 10            # 最大持仓数量
    
    # 每天开盘前运行
    run_daily(before_market_open, time='before_open')
    # 每天开盘时运行
    run_daily(market_open, time='open')

def before_market_open(context):
    """
    开盘前运行函数
    """
    # 获取当天的沪深300成分股
    g.security_list = get_index_stocks(g.index_security)

def calculate_vr(security_list, window, end_date):
    """
    批量计算VR指标
    返回: Series (index为股票代码, value为VR值)
    """
    # 获取历史数据:需要 window + 1 天的数据来计算涨跌(比较今天和昨天)
    # fields: close, volume
    h_data = history(window + 1, '1d', ['close', 'volume'], security_list)
    
    vr_values = {}
    
    for stock in security_list:
        try:
            close = h_data[stock]['close']
            volume = h_data[stock]['volume']
            
            # 如果数据长度不足,跳过
            if len(close) < window + 1:
                continue
                
            # 计算每日涨跌情况
            # diff > 0: 上涨, diff < 0: 下跌, diff = 0: 平盘
            price_diff = close.diff().iloc[1:] # 去掉第一个NaN
            current_vol = volume.iloc[1:]      # 对应的成交量
            
            # AVS: 上涨日成交量
            avs = current_vol[price_diff > 0].sum()
            # BVS: 下跌日成交量
            bvs = current_vol[price_diff < 0].sum()
            # CVS: 平盘日成交量
            cvs = current_vol[price_diff == 0].sum()
            
            # 计算VR
            denominator = bvs + 0.5 * cvs
            
            if denominator == 0:
                vr = 1000 # 分母为0,说明没有下跌和平盘,极端强势,给一个大数值
            else:
                vr = (avs + 0.5 * cvs) / denominator * 100
            
            vr_values[stock] = vr
            
        except Exception as e:
            # 遇到停牌或其他数据异常跳过
            continue
            
    return pd.Series(vr_values)

def market_open(context):
    """
    主交易逻辑
    """
    # 1. 计算股票池中所有股票的VR值
    vr_series = calculate_vr(g.security_list, g.vr_window, context.previous_date)
    
    if vr_series.empty:
        return

    # 2. 生成买入和卖出列表
    # 卖出条件:VR > 250 (超买) 或者 股票不在成分股里了
    current_positions = list(context.portfolio.positions.keys())
    stocks_to_sell = []
    
    for stock in current_positions:
        # 如果股票不在当天的计算结果里(可能停牌或被剔除),或者VR过高
        if stock not in vr_series.index:
            stocks_to_sell.append(stock)
        elif vr_series[stock] > g.sell_threshold:
            stocks_to_sell.append(stock)
            log.info("卖出 %s, VR值: %.2f (触发超买阈值)" % (stock, vr_series[stock]))
        elif stock not in g.security_list:
            stocks_to_sell.append(stock)
            log.info("卖出 %s, (被剔除出成分股)" % (stock))

    # 执行卖出
    for stock in stocks_to_sell:
        order_target_value(stock, 0)

    # 3. 买入逻辑
    # 筛选出 VR < 70 的股票
    buy_candidates = vr_series[vr_series < g.buy_threshold].sort_values()
    
    # 当前持仓数量
    current_hold_count = len(context.portfolio.positions)
    # 剩余可买位置
    slots_available = g.max_hold_count - current_hold_count
    
    if slots_available > 0 and not buy_candidates.empty:
        # 获取可用资金
        cash = context.portfolio.available_cash
        # 每只股票分配资金 (简单平均)
        cash_per_stock = cash / slots_available
        
        for stock in buy_candidates.index:
            if slots_available <= 0:
                break
            
            if stock not in current_positions:
                order_value(stock, cash_per_stock)
                log.info("买入 %s, VR值: %.2f (触发超卖阈值)" % (stock, vr_series[stock]))
                slots_available -= 1

策略说明

  1. 数据获取:使用 history 函数获取过去 N+1 天的收盘价和成交量。我们需要 N+1 天是因为计算当天的涨跌需要对比前一天的收盘价。
  2. VR 计算
    • 利用 Pandas 的 diff() 方法快速计算价格变化。
    • 利用布尔索引筛选出上涨、下跌和平盘的成交量并求和。
    • 处理了分母为 0 的极端情况。
  3. 买卖执行
    • 卖出:优先检查持仓股票,如果 VR 值过高(>250)或股票被调出沪深300成分股,则清仓。
    • 买入:在卖出操作释放资金后,筛选 VR 值过低(<70)的股票。如果有多个标的符合条件,按 VR 值从小到大排序(优先买入超卖最严重的),直到达到最大持仓数量限制。
  4. 风险控制:设置了最大持仓只数 (g.max_hold_count),防止资金过于集中在某一只股票上。

Q&A

Q: 为什么 VR 指标的参数通常设置为 26?
A: 26 是技术分析中常用的参数(如 MACD、Ichimoku Cloud 等也常用 26),代表大约一个月的交易日(加上周末)。这是一个经验值,您可以根据回测结果调整为 12 或 24。

Q: 这个策略适合什么市场环境?
A: VR 策略属于反转策略(Mean Reversion)。它在震荡市中表现较好,能够捕捉到底部反弹和顶部回调。在单边极强的牛市中,VR 可能会过早发出卖出信号(钝化);在单边熊市中,可能会过早抄底(接飞刀)。建议结合均线或其他趋势指标过滤信号。

Q: 如何优化这个策略?
A:

  1. 增加趋势过滤:例如,只在股价位于 60 日均线之上时才允许买入(即使 VR 很低),以避免在崩盘式下跌中抄底。
  2. 调整阈值:不同板块的 VR 波动范围不同,可以针对不同行业设置动态阈值。
  3. 止损:代码中目前仅依靠 VR 高位止盈,建议加入固定比例止损(如亏损 10% 止损)。