精华帖子

手把手教你用BigQuant写第一个量化策略:高股息+低估值实战教程

由bq74n3yq创建,最终由bq74n3yq 被浏览 9 用户

一、假设已初步了解BigQuant平台

\

  1. 注册并登录BigQuant账号

  2. 了解BigQuant基础界面:Quant Agent、AI Studio、知识库、AI策略广场等等

  3. 数据覆盖:了解平台提供的数据种类、历史长度与可用频率。常用数据表举例:

    • 股票后复权日行情cn_stock_bar1d
    • 技术指标预计算表cn_stock_factors_ta
    • 估值数据(PE/PB/PS/市值等)cn_stock_valuation
    • 股票池列表(每日)cn_stock_instruments
    • 数据平台:https://bigquant.com/data/home
  4. Bigtrader:BigQuant平台提供的交易引擎库,支持模拟真实市场环境下的股票买卖操作。它允许用户定义初始化函数(initialize)、调仓逻辑函数(handle_data)等,从而构建和测试自己的投资策略。

  5. dai:数据查询接口,用于从BigQuant的大数据仓库中提取所需的历史数据。例如,通过SQL查询语句获取特定条件下的股票信息。

  6. from bigmodule import M:BigQuant特有的图形化编程模块,简化了复杂策略的搭建过程。M.* 模块提供了丰富的预置功能,如股票筛选器(cn_stock_basic_selector)、因子输入(input_features_dai)等,方便用户快速构建策略。

  7. 内置因子函数(如 m_ta_sma、m_lag、c_group_avg 等)、回测引擎接口、手续费模型、撮合规则(开盘价还是收盘价下单)等。

    \

二、策略详细流程


下面通过一个“高股息 + 低估值 + 行业相对低估 + 趋势过滤”的等权定期轮动策略来练习如何在BigQuant平台上创建和运行自己的交易策略 。

Step 0:确定投资目标

  • 追求绝对收益核心是 “多资产 + 对冲 + 风控纪律”,不管市场涨跌,追求正收益。通过资产配置多元化降低相关性;加入负相关资产或使用使用对冲工具(股指期货/期权)来实现对冲;严格止损止盈,如设置单票最大亏损-8%,整体回撤超-5%减仓;坚持现金为王机制。
  • 追求超额收益:你的策略在多大程度上“押注”了某个特定的投资风格或风险来源,比如动量因子抓住了上涨趋势就是赚钱的因子。还可以根据经济周期(复苏/过热/滞胀/衰退)切换板块实现行业轮动;聚焦高Alpha领域:如专精特新、国产替代、AI应用落地;集中持仓:前十大重仓占比>50%,敢于下注高确定性机会。
  • 还有策略直接投资低波资产,波动越低的股票,配得越重,避免高估值、高换手、高Beta股票,选择长期持有,减少情绪干扰和交易损耗,且实证表明低波动股票预期收益并不低于高波动股票。 因此,低波动投资策略在市场中越来越受到重视,尤其是在追求稳健收益的投资者中。

为尽可能兼顾两者——现实中可以采取多策略融合的折中方案。

\

Step 1:回测/交易环境初始化(只在回测开始时执行一次)

\

  • def initialize(context):设置手续费/滑点、设定参数(调仓周期、持仓数)、预先拉取并计算信号数据、放到 context 里缓存等。def initialize(context: bigtrader.IContext):的意思是:“定义一个叫initialize的函数,它接收一个由 BigQuant 提供的策略上下文对象(名为context),用于初始化策略。”其中:bigtrader.IContext是类型提示,帮助你写代码时获得智能补全和减少错误,不是必须的,但强烈推荐保留。
# @param(id="m5", name="initialize")
def m5_initialize_bigquant_run(context):
    """
    只执行一次:设置手续费、调仓参数、初始化计数器
    """
    from bigtrader.finance.commission import PerOrder
    context.set_commission(PerOrder(buy_cost=0.0003, sell_cost=0.0013, min_cost=5))

    # 策略参数
    context.rebalance_days = 6
    context.extension["bar_index"] = 0

\

  • def handle_data(context, data):每个交易 bar 都会执行(日频就是每天一次)用来做交易决策与下单:判断是否调仓日、读取当日信号、卖出不在目标池的股票、买入并调整到目标权重等。
  • context:策略运行的“全局状态/容器”。你在 initialize 里存的 context.data、context.stock_num,后面 handle_data 都能用;还包含账户信息、下单接口、日志等。
  • data:当前 bar 的行情数据与时间信息,常用 data.current_dt 得到当前日期。

\

# @param(id="m5", name="handle_data")
def m5_handle_data_bigquant_run(context, data):
    """
    每根K线执行:按调仓周期读取当日信号,卖出非目标、买入目标到目标仓位
    """
    import pandas as pd

    context.extension["bar_index"] += 1
    # 控制调仓频率:每 rebalance_days 的第 1 天调仓
    if context.extension["bar_index"] % context.rebalance_days != 1:
        return

    # 取当日信号(m4 已经把历史信号抽成了全量数据)
    today = data.current_dt.strftime("%Y-%m-%d")
    sig = context.data
    today_df = sig[sig["date"] == today]

    # 若当日无信号(例如停牌/数据缺失导致),则不操作
    if today_df is None or len(today_df) == 0:
        return

    target_instruments = set(today_df["instrument"].tolist())
    holding_instruments = set(context.get_account_positions().keys())

    # 先卖出:不在目标里的全部清仓
    for inst in holding_instruments - target_instruments:
        context.order_target_percent(inst, 0)

    # 再买入/调仓:按 position 字段等权
    for _, row in today_df.iterrows():
        pos = row.get("position", 0)
        pos = 0.0 if pd.isnull(pos) else float(pos)
        context.order_target_percent(row["instrument"], pos)
  • 其他函数:成交回报处理函数,每个成交发生时执行一次;委托回报处理函数,每个委托变化时执行一次;盘后处理函数,每日盘后执行一次

\

Step2:确定股票池/可交易过滤


常见股票池就是设置市场范围,如全 A / 沪深 / 指数成分(如沪深300、中证500/1000)。

基础过滤(强烈建议):非 ST(st_status=0);不停牌(suspended=0);上市天数 > 60/90(list_days);流动性门槛:成交额 amount;流通市值 float_market_cap 等

较复杂的价格异常过滤(可选):接近退市价、连续跌停等

# @module(position="140,40", comment="""股票池过滤:剔除ST/停牌/退市等基础过滤""")
m1 = M.cn_stock_basic_selector.v8(
    exchanges=["""上交所""", """深交所"""],
    list_sectors=["""主板"""],
    indexes=["""中证500""", """上证指数""", """创业板指""", """深证成指""", """上证50""", """科创50""", """沪深300""", """中证1000""", """中证100""", """深证100"""],
    st_statuses=["""正常"""],
    margin_tradings=["""两融标的""", """非两融标的"""],
    drop_suspended=True,
    m_cached=False,
    m_name="""m1"""
)

\

此处从 cn_stock_prefactors 取数,并做如下风险排除(先筛掉“垃圾股”):

\

st_status = 0               -- 非ST股

AND suspended = 0           -- 未停牌

AND list_sector NOT IN (3, 4) -- 排除科创板(3)、创业板(4)?(注:需确认编码含义)

AND is_risk_warning = 0     -- 无退市风险警示

AND list_days > 270         -- 上市超270天(约9个月),避开次新股

AND float_market_cap > 500000000  -- 流通市值 > 5亿,保证流动性

\

Step3:拉取数据→ 构建因子/特征 → 过滤/排序/打分 → 生成目标权重表

常见因子:

  • 价值:PE、PB、股息率等(偏均值回归)
  • 成长:营收/净利润同比、环比增长等
  • 质量:ROE、毛利率、负债率等
  • 动量/反转:过去 k 日/月收益、短期反转等
  • 波动/风险:历史波动率、回撤、Beta
  • 流动性/资金:换手、量比、主力净流入等

因子构造要点

  • 统一量纲:用截面 rank / zscore(如 c_rank、c_zscore)避免市值、价格等量纲差异影响。
  • 去极值与标准化(可选):clip、zscore;避免极端值主导排序。
  • 行业/市值中性(可选):用行业分组排序(c_group_pct_rank)或做中性化,减少风格暴露。
  • 避免未来函数:所有因子只能用当日及以前数据(m_lag/m_avg 等)

因子打分

规则/打分法(更稳、解释力):

每个因子做 rank 或 zscore;加权求和:score = w1rank1 + w2rank2 + ...按 score 排序取前 N

机器学习法(更灵活,需更严格验证):

用 StockRanker/树模型/线性模型等输出预测分数;时间序列走样本外检验,控制过拟合

\

BigQuant平台 中,用户可以通过 DAI(Data Access Interface) 拉取金融数据。其中最常用的两种方式是:DAI SQL(结构化查询语言)、DAI 表达式(Expression / 函数式写法)

DAI 表达式 DAI SQL


言归正传!在这个策略中,主要关注价值指标:

指标 核心计算公式 通俗理解
市盈率 (PE) 股价 ÷ 每股收益 \n(或:总市值 ÷ 净利润) 回本年限:假设利润不变,赚回本金要多少年。
市净率 (PB) 股价 ÷ 每股净资产 \n(或:总市值 ÷ 净资产) 家底折扣:买这公司,是打几折买它的家底。
股息率 每股分红 ÷ 股价 × 100% \n(或:分红总额 ÷ 总市值 × 100%) 租金回报:持有股票一年能拿多少“利息”。


① 估值与分红筛选(价值+红利因子):

AND pb IS NOT NULL AND pb <= 1.8 -- PB ≤ 1.8,低估值

AND pe_ttm IS NOT NULL AND pe_ttm > 0 -- PE为正(盈利公司)

AND dividend_yield_ratio IS NOT NULL AND dividend_yield_ratio > 0 -- 有分红

PB ≤ 1.8是典型的价值股门槛(银行、能源、公用事业常满足),分红确保是真金白银回馈股东,而非“伪成长”


② 行业相对估值(避免“伪便宜”):

c_group_avg(sw2021_level2, pe_ttm) AS ind_pe_median

QUALIFY

pe_ttm <= ind_pe_median * 1.2

sw2021_level2:申万二级行业,ind_pe_median:该行业内所有股票的 PE 中位数,要求个股 PE ≤ 行业中位数的 1.2 倍,偏向行业内相对便宜

→ 防止在整体高估的行业中误选“相对便宜但绝对贵”的股票(如2021年的白酒)

==直接让QuantAgent来帮你计算指标相关系数👉== ==https://bigquant.com/quantagent/share/482f0506-b672-4ea3-a80f-1fa303812778==


③ 技术面确认(弱动量/趋势过滤):

m_lag(close, 1) > bbi

  • m_lag(close, 1):昨日收盘价
  • BBI(Bull and Bear Index,多空指标)通常是将多条不同周期的移动平均线取平均,作为综合趋势指标。多空指标 = (MA3 + MA6 + MA12 + MA24) / 4
  • 昨日收盘 > BBI 筛选出股价处于短期均线上方的股票,趋势偏强,避免“价值陷阱”——便宜但持续下跌的股票


④ 排序与权重:

ORDER BY date, dividend_yield_ratio DESC

1.0 / $stock_num AS weight

  • 按股息率降序排序 → 优先选分红最高的
  • 等权重分配:每只股票权重 = 1/29 ≈ 3.45%

\

 # 预计算:提前算好整个回测期所有调仓日的股票池
    sql = """
    SELECT
        date,
        instrument,
        close,
        dividend_yield_ratio,
        pb,
        pe_ttm,
        float_market_cap,
        sw2021_level2,
        m_ta_sma(close, 3) AS ma3,
        m_ta_sma(close, 6) AS ma6,
        m_ta_sma(close, 12) AS ma12,
        m_ta_sma(close, 24) AS ma24,
        (m_ta_sma(close, 3) + m_ta_sma(close, 6) + m_ta_sma(close, 12) + m_ta_sma(close, 24)) / 4 AS bbi,
        c_group_avg(sw2021_level2, pe_ttm) AS ind_pe_median,
        1.0 / $stock_num AS weight
    FROM cn_stock_prefactors
    WHERE
        st_status = 0
        AND suspended = 0
        AND list_sector NOT IN (3, 4)
        AND is_risk_warning = 0
        AND list_days > 270
        AND float_market_cap > 500000000
        AND pb IS NOT NULL
        AND pb <= 1.8
        AND pe_ttm IS NOT NULL
        AND pe_ttm > 0
        AND dividend_yield_ratio IS NOT NULL
        AND dividend_yield_ratio > 0
    QUALIFY
        pe_ttm <= ind_pe_median * 1.2
        AND m_lag(close, 1) > bbi
    ORDER BY date, dividend_yield_ratio DESC
    """

    # 批量查询历史数据(避免handle_data重复查询)
    df = dai.query(
        sql,
        filters={"date": [context.add_trading_days(context.start_date, -30), context.end_date]},
        params={"stock_num": stock_num}
    ).df()

|

风格:偏“价值红利”选股(高股息 + 低PB/PE),再叠加“行业相对估值约束”和“站上BBI的趋势确认”。

组合构建:固定数量、等权、定期轮动,降低单票权重风险,交易规则简单透明。

收益来源:红利收益 + 价值修复 + 趋势过滤提升胜率。

\

Step4:回测交易逻辑→ 调仓周期、成交价、风控、仓位

\

回测时必须关注:

  • 基准对比:沪深300/中证500/1000等
  • 收益风险:年化收益、波动、Sharpe、最大回撤、Calmar
  • 稳定性:滚动窗口表现、分年度/分市场阶段表现
  • 换手与成本:手续费+滑点+冲击成本,是否吞噬收益
  • 归因:行业暴露、市值暴露、因子暴露(是否只是“买小盘/买高beta”)

bigtrader.run 是 BigQuant/BigTrader 的核心运行入口,通常用于启动策略回测、策略优化或实盘运行任务。它负责:

  • 装载策略代码与策略配置;
  • 初始化数据源、账户与风控模块;
  • 调度回测/实盘事件(如按时间序列推进 tick/bar、执行委托与撮合);
  • 记录日志、绩效与交易记录,并可导出结果。
performance = bigtrader.run(
    market=bigtrader.Market.CN_STOCK,
    frequency=bigtrader.Frequency.DAILY,
    start_date="2021-01-01",
    end_date="2025-11-18",
    capital_base=1000000,
    benchmark="000300.SH",
    initialize=initialize,
    handle_data=bigtrader.HandleDataLib.handle_data_weight_based,
    order_price_field_buy="open",
    order_price_field_sell="open",
)

通过高股息 + 低估值的组合,试图在控制回撤的同时获取稳定正收益,是典型的 “追求超额收益,同时兼顾一定低波动” 的策略。 调仓频率较低(每6天调仓一次 ≈ 每周一次),属于中低频策略,非高频交易,也非长期持有不动(持股周期约1~2周)。

三、执行并分析策略

执行策略并查看结果:

总体评价

  • 在2021–2025这轮熊市中表现优异:策略上涨117%,而沪深300下跌13% → 超额收益显著;

  • 风格稳健:低Beta、低波动、高股息特征明显;

  • 但超额能力不足:Alpha和信息比率偏低,说明主要靠市场反弹+低估值修复赚钱,而非选股能力。

    \

基准对比:选哪个指数更公平?

我的策略是全市场选股 + 高股息低估值,但持仓偏向中大盘(流通市值 > 5亿),行业集中在金融、能源、公用事业。因此选沪深300(000300.SH)作为基准部分合适,可以选中证500(000905.SH)更合适,覆盖中盘股,行业更均衡,更能反映“非龙头”股票表现。也可以直接对标“高股息”策略——中证红利指(000922.SH),成分股PB/PE/分红率与该策略高度相似。

\

收益来源需归因

  • 策略集中于银行、煤炭、电力等高股息行业,超额收益可能来自行业Beta而非选股Alpha。
  • 市值暴露以中盘为主(接近中证500),非小盘或高Beta策略
  • 核心因子暴露为红利+价值,需警惕因子长期有效性变化。


\

四、 检验与参数优化


写完一个量化策略只是整个量化投资流程的第一步。要将策略真正投入实盘、稳定盈利,还需要完成一系列严谨的回测工作。以下是典型的回测后续步骤:

\

  1. 数据检查与防“未来函数”
  • 确认使用的数据在当时是否可得:财报披露日、生效日、复权方式、指数成分调入调出时间
  • 检查是否误用未来价格/未来财务值/未来成分
  • 缺失值、异常值、幸存者偏差处理(退市、ST、停牌)

\

  1. 回测质量与稳健性评估
  • 不同区间、不同市场状态(牛/熊/震荡)分段回测
  • 参数敏感性(小范围扰动是否崩)
  • 蒙特卡洛/重排收益序列、不同调仓频率、不同滑点/费用假设下的稳定性

\

  1. 交易可执行性与微观结构
  • 下单规则:开盘/收盘/均价成交假设是否现实
  • 流动性约束:成交量比例、冲击成本、最小成交额、涨跌停无法成交
  • 换手与容量评估(资金放大后是否还有效)

\

  1. 风控与组合约束落地
  • 仓位上限、单票上限、行业/风格暴露约束、止损/止盈/回撤控制
  • 黑名单规则(ST、退市风险、异常波动)
  • 极端情景压力测试(跳空、熔断、流动性枯竭)

\

  1. 归因分析与可解释性
  • 业绩归因:收益来自选股还是择时、来自哪些行业/风格因子
  • 回撤归因:最大回撤发生原因与持仓集中度问题
  • 验证策略“赚钱逻辑”与统计显著性是否一致

\

  1. 模拟盘验证(Paper Trading)

  • AI Studio中直接提交模拟到 “我的策略”
  • 用真实成交规则跑一段时间,核对回测与实盘偏差(滑点、成交率、延迟)
  • 逐日对账:资金曲线、持仓、成交、费用

\

五、实盘落地

==具体实盘操作👉====BigTrader AI量化交易终端(股票实盘)==

附录

from bigquant import bigtrader, dai

def initialize(context: bigtrader.IContext):
    # 设置全局配置(一次性)
    
    context.set_commission(bigtrader.PerOrder(buy_cost=0.0003, sell_cost=0.0013, min_cost=5))
    
    # 定义策略参数(后续不变)
    rebalance_days = 6
    stock_num = 29
    context.logger.info("开始计算选股数据...")
    
    # 预计算:提前算好整个回测期所有调仓日的股票池
    sql = """
    SELECT
        date,
        instrument,
        close,
        dividend_yield_ratio,
        pb,
        pe_ttm,
        float_market_cap,
        sw2021_level2,
        m_ta_sma(close, 3) AS ma3,
        m_ta_sma(close, 6) AS ma6,
        m_ta_sma(close, 12) AS ma12,
        m_ta_sma(close, 24) AS ma24,
        (m_ta_sma(close, 3) + m_ta_sma(close, 6) + m_ta_sma(close, 12) + m_ta_sma(close, 24)) / 4 AS bbi,
        c_group_avg(sw2021_level2, pe_ttm) AS ind_pe_median,
        1.0 / $stock_num AS weight
    FROM cn_stock_prefactors
    WHERE
        st_status = 0
        AND suspended = 0
        AND list_sector NOT IN (3, 4)
        AND is_risk_warning = 0
        AND list_days > 270
        AND float_market_cap > 500000000
        AND pb IS NOT NULL
        AND pb <= 1.8
        AND pe_ttm IS NOT NULL
        AND pe_ttm > 0
        AND dividend_yield_ratio IS NOT NULL
        AND dividend_yield_ratio > 0
    QUALIFY
        pe_ttm <= ind_pe_median * 1.2
        AND m_lag(close, 1) > bbi
    ORDER BY date, dividend_yield_ratio DESC
    """

    # 批量查询历史数据(避免handle_data重复查询)
    df = dai.query(
        sql,
        filters={"date": [context.add_trading_days(context.start_date, -30), context.end_date]},
        params={"stock_num": stock_num}
    ).df()
    context.logger.info(f"数据计算完成,共 {len(df)} 条记录")
    
    # 数据预处理:筛选调仓日数据
    df = df.groupby('date').head(stock_num)
    df = bigtrader.TradingDaysRebalance(rebalance_days, context=context).select_rebalance_data(df)
    
    # 存入context供后续使用
    context.data = df
    context.logger.info(f"选股完成,每个调仓日将持有 {stock_num} 只股票")


performance = bigtrader.run(
    market=bigtrader.Market.CN_STOCK,
    frequency=bigtrader.Frequency.DAILY,
    start_date="2021-01-01",
    end_date="2025-11-18",
    capital_base=1000000,
    benchmark="000300.SH",
    initialize=initialize,
    handle_data=bigtrader.HandleDataLib.handle_data_weight_based,
    order_price_field_buy="open",
    order_price_field_sell="open",
)


performance.render()

\

{link}