经典的 Jegadeesh-Titman 12 个月动量策略

Jegadeesh-Titman 动量策略基于横截面动量效应:在同一股票池中,过去一段时间收益更高的股票可能在随后一段时间继续保持相对强势。本策略在沪深 300 模拟成分股中直接使用输入行情携带的过去 12 个月收益率 ret_12m,每月末按该因子排序,构建前 10% 的等权多头组合。本文使用 DolphinDB Backtest 对现有策略脚本进行回放,并说明因子、交易规则、数据结构、引擎配置及实测结果。

1. 策略介绍

本策略以 12 个月累计收益率衡量股票动量,优先持有近期表现最强的股票。选股时同时使用历史成分、ST、上市日期和成交量过滤;持仓构建后通过 10% 偏离阈值减少不必要的调仓。下面分别介绍因子含义、交易标的和交易规则。

1.1 核心因子

核心因子为过去 12 个月收益率 ret_12m。通常它可表示为 P_t / P_{t-12M} - 1,其中 P_t 为当前价格,P_{t-12M} 为约 12 个月前的价格;值越大,说明该股票在过去 12 个月的相对涨幅越高。

策略先保留当日有效沪深 300 模拟成分股,剔除当日处于 ST 期间、上市不足 90 天、成交量不大于 0 以及过去 12 个月收益率为空的股票。对剩余股票按过去 12 个月收益率降序排列,取前 10% 作为新买入范围;若上一期持仓仍处于前 15% 的缓冲区,则保留它。该缓冲区只用于降低换手,不会将排名低于前 15% 的股票继续留在组合中。

1.2 交易标的

本例使用 300 只带 .SZ 后缀的模拟股票构建候选池,并假定这些股票均属于沪深 300 成分股。为简化测试流程、跳过成分股、ST 状态和上市天数等过滤环节,示例中将 ST 股票历史表设为空表,并把所有股票的上市日期统一设置为 2024 年 1 月 1 日。

策略运行在普通股票现金账户下,仅建立多头持仓,不涉及卖空、融资或借券。行情频率为日频,订单按当日收盘价提交;每根日线进入收盘处理阶段时,脚本先执行待卖目标,再执行待买目标。买入和部分卖出数量均按 100 股整数倍向下取整;若需要清仓,则允许卖出不足一手的剩余持仓。

接入真实数据时,候选池必须使用带有历史时点信息的指数成分、ST 状态和上市日期,过去 12 个月收益率也应基于信号日当时已经可见的复权价格计算。不得用当前成分股替代历史成分,也不得将事后修订的因子直接回填到历史信号日,以避免引入未来函数和幸存者偏差。

1.3 交易规则

  1. 回测首个有效交易日立即产生一次初始调仓信号;此后仅在每个月最后一个上交所交易日产生新的调仓信号。

  2. 在信号日取当日或之前最新的沪深 300 模拟成分快照,剔除 ST、上市不足 90 天、未在股票池中或成交量不大于 0 的股票。

  3. 读取可交易股票的 ret_12m,剔除缺失值后按收益率降序排列;前 10% 为新入选范围,原持仓仍在前 15% 缓冲区时继续保留。

  4. 按实际入选数量等权分配信号日账户权益,以信号日收盘价、手续费和 100 股整数倍计算各股票目标股数。

  5. 仅当股票新入选、完全退出或当前持仓相对目标数量的偏离超过 10% 时,才生成买卖目标。每根日线收盘时先卖后买;买入受可用现金约束。

  6. 若股票停牌、成交量为零、买入价达到涨停价或卖出价达到跌停价,则本次不能成交;不可交易造成的待处理目标会保留到后续可交易日,现金不足造成的未完成买入不会追单。

当候选股票存在但所有 ret_12m 都无效时,策略跳过本轮选股并保留既有持仓。若存在有效因子但没有股票通过筛选,策略会形成空目标,并在后续可卖时尝试退出原有持仓。

2. 策略回测

以下按照准备数据、定义策略、设置参数、创建引擎、回放行情和读取结果的顺序展开。本案例已在 DolphinDB Server 3.00.6.1、Backtest 3.00.6.6 与模拟撮合引擎插件(Matching Engine Simulator) 3.00.6.6 上运行。服务器需要安装与 DolphinDB Server 版本匹配的两个插件;创建引擎前,代码会检查插件是否已安装,并加载这两个插件。

2.1 准备数据

在本案例中,我们将通过一份模拟的日线行情数据来驱动回测。本次测试的样本包含了 300 只模拟股票在 4 个交易日内(2026 年 8 月 3 日至 8 月 6 日)的 1,200 条量价记录。模拟的数据中已经提前计算并内置了“过去 12 个月动量因子”,策略读取数据后就能直接使用该因子进行排名,节省前置计算步骤。

原始行情表及辅助表的主要结构如下:

表 1. 表 2-1 原始行情表及辅助表主要结构
原始表 列名 类型 说明
日线行情表 symbol SYMBOL 模拟股票代码,例如 000001.SZ
日线行情表 tradeTime TIMESTAMP 日线所属交易日
日线行情表 open / low / high / close DOUBLE 当日 OHLC 价格
日线行情表 volume LONG 成交量;不大于 0 时不可交易
日线行情表 amount DOUBLE 成交额
日线行情表 upLimitPrice / downLimitPrice DOUBLE 涨跌停限制价格
日线行情表 adj_factor INT 输入中的复权因子字段
日线行情表 ret_12m DOUBLE 输入的过去 12 个月收益率,动量排序因子
日线行情表 vwap DOUBLE 输入中的成交量加权均价
指数成分快照表 index_code / snap_date / con_code STRING / DATE / SYMBOL 沪深 300 模拟成分快照
ST 状态历史表 code / start_date / period_end STRING / DATE / DATE 模拟 ST 期间
上市信息表 code / list_date SYMBOL / DATE 模拟上市日期

为了验证策略的股票过滤逻辑,脚本在内存中快速搭建了几个测试用的数据环境:把所有候选股票设为指数成分股,并指定了极个别股票作为“ST 状态”和“次新股”的测试用例。另外,脚本会加载分红除权表,并将其内联传入回测配置的 stockDividend,用于测试引擎对公司行为(现金分红、送股)的自动处理。

完整数据准备代码如下:

actualStartDate = 2025.08.07
actualEndDate = 2026.08.06

data = loadText("/.../classicJTMomentum_marketData.csv")
data.replaceColumn!("volume", long(data.volume))

stockPool = exec distinct symbol from data
tpExplicit = set(array(STRING, 0))
tpIdxSnaps = table(take("000300.SH", size(stockPool)) as index_code,
    take(actualStartDate, size(stockPool)) as snap_date, stockPool as con_code)
tpStHist = table(["000010.SZ", "000011.SZ"] as code,
    [2025.10.01, 2026.03.02] as start_date,
    [2025.12.31, 2026.04.01] as period_end)
tpListInfo = table(stockPool as code, take(2024.01.01, size(stockPool)) as list_date)
update tpListInfo set list_date = 2025.07.01 where code == "000300.SZ"

分红除权表需在配置之前读入:

// 分红除权表
def getJTMomentumDividendCsvSchema() {
    return table(`symbol`endDate`annDate`recordDate`exDate`payDate`divListDate`afterTaxCashDiv`bonusRatio`capitalConversion`allotRatio`allotPrice as name,
        ["SYMBOL", "DATE", "DATE", "DATE", "DATE", "DATE", "DATE", "DOUBLE", "DOUBLE", "DOUBLE", "DOUBLE", "DOUBLE"] as type)
}
stocksDividend = loadText("/.../classicJTMomentum_stocksDividend.csv", ',', getJTMomentumDividendCsvSchema())

本案例的分红事件安排在回测行情覆盖的 4 个交易日之内:000152.SZ 于 2026.08.04 除权除息并派发现金红利 0.5 元/股,000004.SZ 同日送股 0.2 股/股。两只股票在首个信号日均入选前 10% 组合,因此分红会在回测期间实际影响持仓数量与账户权益。

2.2 编写策略逻辑

本策略的代码结构由一个初始化函数 initialize、一个核心回调函数 onBar,以及六个用于处理交易细节的辅助函数组成。

上下文建立与调仓信号: initialize 函数负责建立全局环境,如费率配置与状态标记。策略的主干逻辑由 onBar 函数驱动:每次更新时,它会接收包含全市场当日行情的字典 msg(以股票代码为键、日线数据为值)。 在调仓时机上,策略通过初始标记 initialRebalancePending=true 确保在回测首日立即建仓。随后,策略调用 getMarketCalendar("XSHG", ...) 获取交易日历,严格限制仅在每个月的最后一个交易日生成新的调仓目标。

仓位计算与资金分配: 在每月的调仓日,策略筛选出动量达标的 N 只股票,并采用 1/N 的等权重逻辑分配资金。目标股数的计算逻辑如下: 以信号日的账户总权益为基准平分资金,按当日收盘价及佣金率计算可买股数,并向下取整至 100 股的倍数: 目标股数 = floor(信号日权益 × 1/N ÷ [收盘价 × (1 + 佣金)] ÷ 100) × 100

订单执行机制: 在计算出每只股票的目标持仓数量后,策略会在当天的收盘阶段正式触发交易。为了确保账户有充足的资金用来建仓,系统的执行顺序严格遵循“先卖后买”原则: 首先,系统会集中处理所有需要减仓或清仓的持仓,将可用资金回笼; 随后,再利用回笼后的资金执行买入指令。值得注意的是,买入操作并不是随机分配的,而是严格按照股票的动量排名从高到低依次下单。如果遇到账户现金不足以买下所有目标股票的情况,程序会优先保障排名更靠前、动量表现更好的股票完成建仓。

交易规则验证(辅助函数):

为了使回测贴近真实市场规则,代码设计了四个辅助函数来对订单进行拦截与规整:

  • 委托数量校验:roundLot 将买入与部分卖出的委托数量强制向下规整为 100 股的整数倍;normalizeSellQty 则在处理清仓逻辑时,允许直接卖出包含零股在内的全部剩余持仓。

  • 流动性与涨跌停校验:isBuyableAt 和 isSellableAt 负责拦截无效订单。买入指令要求当日成交量大于 0 且价格未达到涨停限制;卖出指令同样要求成交量大于 0 且价格未跌破跌停限制。

def roundLot(code, rawQty) {
    return floor(rawQty / 100) * 100
}

def normalizeSellQty(code, positionQty, requestedQty) {
    positionQty_ = floor(positionQty)
    qty_ = floor(min(positionQty_, requestedQty))
    if (qty_ <= 0) { return 0 }
    if (qty_ >= positionQty_) { return positionQty_ }
    return roundLot(code, qty_)
}

def isBuyableAt(price, bar) {
    return bar.volume > 0 && (isNull(bar.upLimitPrice) || price < bar.upLimitPrice)
}

def isSellableAt(price, bar) {
    return bar.volume > 0 && (isNull(bar.downLimitPrice) || price > bar.downLimitPrice)
}

initialize(mutable context) 是回测引擎的初始化回调。每次创建新引擎时调用一次,用于建立本次回测需要的可变状态。

context:可变 DICTIONARY,由创建引擎时传入。函数在其中写入成本、调仓偏离阈值、最小交易金额、首次调仓标记、已选股票、待买卖目标以及股票池相关表。

def initialize(mutable context) {
    context["commission"] = 0.00015
    context["tax"] = 0.0005
    context["rebalanceDeviationThreshold"] = 0.10
    context["minimumTradeValue"] = 10000.0
    context["initialRebalancePending"] = true
    context["selectedStocks"] = []$STRING
    context["pendingBuyTargets"] = dict(STRING, LONG)
    context["pendingSellTargets"] = dict(STRING, LONG)
    context["stockPool"] = objByName("stockPool")
    context["tpIdxSnaps"] = objByName("tpIdxSnaps")
    context["tpStHist"] = objByName("tpStHist")
    context["tpListInfo"] = objByName("tpListInfo")
    context["rebalanceDates"] = objByName("rebalanceDates")
}

onBar(mutable context, msg, indicator) 是日线行情回调,也是策略的主执行函数。它先在信号日构造股票池和动量排名,形成待买卖目标;随后在每根日线的收盘阶段依次执行待卖、待买目标。

  • context:可变 DICTIONARY,包含引擎注入的当前日期、时间和引擎句柄,也包含初始化状态、股票池、调仓日期、选中股票和待处理目标。

  • msg:DICTIONARY,以股票代码为键,以该股票当日日线为值。函数从中读取交易时间、成交量、收盘价、涨跌停价和 ret_12m。

  • indicator:引擎为指标结果预留的参数。本策略没有配置独立指标,因此函数签名保留该参数,但策略逻辑没有读取它。

该函数没有显式返回值,主要作用包括更新候选池、动量排名、目标数量和待处理订单。其内部流程分为两部分:

  1. 在首次回调或月末,读取历史时点成分、ST 和上市信息,筛选可交易股票,按 ret_12m 排序并构造前 10% 加前 15% 缓冲的目标组合。

  2. 在每根日线结束时,先执行待卖目标,再按动量排序执行待买目标;触及涨跌停或无成交量时保留不可交易目标,现金不足时不追单。

完整策略逻辑代码如下:

def onBar(mutable context, msg, indicator) {
    curDate = date(msg.values()[0]["tradeTime"])
    // 首次立即建仓;其后仅在每月最后一个交易日产生调仓信号。
    rebalanceDates = context["rebalanceDates"]
    isMonthEnd = curDate in rebalanceDates
    isRebalanceDay = context["initialRebalancePending"] || isMonthEnd
    context["initialRebalancePending"] = false
    if (isRebalanceDay) {
        snaps = context["tpIdxSnaps"]
        latestPerIndex = select index_code, max(snap_date) as latest_date from snaps where snap_date <= curDate group by index_code
        indexMembers = set(exec con_code from ej(snaps, latestPerIndex, `index_code`snap_date, `index_code`latest_date))
        stHistory = context["tpStHist"]
        listingInfo = context["tpListInfo"]
        stCodes = set(exec code from stHistory where start_date <= curDate and (isNull(period_end) or period_end > curDate))
        eligibleCodes = set(exec code from listingInfo where list_date <= date(curDate - 90))
        eligiblePool = array(STRING, 0)
        for (s in context["stockPool"]) { if (((s in indexMembers) || (s in context["tpExplicit"])) && !(s in stCodes) && (s in eligibleCodes)) { eligiblePool.append!(s) } }
        context["tradablePool"] = set(eligiblePool)

        symbols = []$STRING
        momentum = []$DOUBLE
        for (istock in msg.keys()) {
            if (!(istock in context["tradablePool"]) || msg[istock]["volume"] <= 0) { continue }
            symbols.append!(istock)
            momentum.append!(double(msg[istock]["ret_12m"]))
        }
        // 用 isValid 掩码过滤掉 ret_12m 无效(NULL)的标的,避免再建一张中间表。
        validMask = isValid(momentum)
        validInput = table(symbols[validMask] as symbol, momentum[validMask] as `ret_12m)
        // 有候选标的但 ret_12m 全无效时,视为当日选股数据不可用,跳过本次调仓。
        selectionDataValid = !(symbols.size() > 0 && validInput.size() == 0)
        if (selectionDataValid) {
            previousSelected = context["selectedStocks"]
            selectedStocks = []$STRING
            if (validInput.size() > 0) {
                buyCount = max(1, int(validInput.size() * 0.1))
                bufferCount = max(buyCount, int(validInput.size() * 0.15))
                rankedSymbols = exec symbol from validInput order by ret_12m desc limit bufferCount
                // 前10%新买入;已持有股票只要仍处于前15%缓冲区便继续持有(rankedSymbols 已按收益率降序)。
                for (i in 0:rankedSymbols.size()) {
                    if ((i < buyCount) || (rankedSymbols[i] in previousSelected)) { selectedStocks.append!(rankedSymbols[i]) }
                }
            }
            context["selectedStocks"] = selectedStocks

            signalEquity = Backtest::getTotalPortfolios(context.engine).totalEquity[0]
            targetQuantities = dict(STRING, LONG)
            // 等权配置:每只入选股票目标市值相同,权重是标量。
            if (size(selectedStocks) > 0) {
                stockWeight = 1.0 / size(selectedStocks)
                for (s in selectedStocks) {
                    targetQuantities[s] = roundLot(s, signalEquity * stockWeight / (msg[s]["close"] * (1 + context["commission"])))
                }
            }

            // 仅对新入选、全额退出或目标偏离超过阈值的仓位下单。
            pendingBuys = dict(STRING, LONG)
            for (s in selectedStocks) {
                posInfo = Backtest::getPosition(context.engine, s)
                pos = 0
                if (size(posInfo) > 0) { pos = posInfo["longPosition"] }
                targetQty = targetQuantities[s]
                plannedQty = targetQty - pos
                isNewSelection = !(s in previousSelected)
                deviation = iif(targetQty > 0, double(abs(plannedQty)) / double(targetQty), 0.0)
                // 最小成交金额只在执行阶段校验一次(executePendingBuysAtClose 会先校验再判断可买性)。
                if (plannedQty > 0 && (isNewSelection || deviation > context["rebalanceDeviationThreshold"])) {
                    pendingBuys[s] = targetQty
                }
            }
            pendingSells = dict(STRING, LONG)
            for (s in msg.keys()) {
                posInfo = Backtest::getPosition(context.engine, s)
                if (size(posInfo) > 0 && posInfo["longPosition"] > 0) {
                    pos = posInfo["longPosition"]
                    targetQty = iif(s in targetQuantities, targetQuantities[s], 0)
                    plannedQty = pos - targetQty
                    isExit = !(s in targetQuantities)
                    deviation = iif(targetQty > 0, double(plannedQty) / double(targetQty), 0.0)
                    // 非清仓的最小成交金额同样只在执行阶段校验一次(executePendingSellsAtClose)。
                    if (plannedQty > 0 && (isExit || deviation > context["rebalanceDeviationThreshold"])) {
                        pendingSells[s] = targetQty
                    }
                }
            }
            context["pendingBuyTargets"] = pendingBuys
            context["pendingSellTargets"] = pendingSells
        }
    }

    // 每根日线收盘处理待执行目标:先卖后买;停牌/涨跌停时保留到下一可交易日重试。
    expectedCash = Backtest::getAvailableCash(context.engine)
    expectedCash = executePendingSellsAtClose(context, msg, expectedCash)
    executePendingBuysAtClose(context, msg, expectedCash)
}

2.3 设置回测参数

脚本集中设置回测区间、初始资金、成本和撮合参数,并通过 stockDividend 把分红除权表内联传给引擎。月末日期由上交所交易日历计算;在当前数据覆盖期内,首日初始调仓生效。

strategyName = "经典的JegadeeshTitman动量策略"
actualStartDate = 2025.08.07
actualEndDate = 2026.08.06

config = {
    startDate: actualStartDate,
    endDate: actualEndDate,
    strategyGroup: "stock",
    cash: 1000000.0,
    commission: 0.00015,
    tax: 0.0005,
    dataType: 4,
    outputOrderInfo: true,
    matchingMode: 2,
    orderBookMatchingRatio: 0.2,
    stockDividend: stocksDividend,
    enableMinimumPerTransactionFee: false
}

初始资金为 1,000,000 元;佣金率为 0.015%,卖出印花税率为 0.05%;单笔计划金额低于 10,000 元时不提交;当目标股数偏离现有持仓不超过 10% 时不调仓。outputOrderInfo=true 会使 tradeDetails 同时返回订单状态记录和成交记录,因此必须以 tradeQty > 0 区分实际成交。

stockDividend 为分红除权基本信息表(字段说明见 Backtest 插件文档),引擎据此在除权日调整持仓数量、在派息日计入现金分红。使用该参数时需注意:除权除息日 exDate 必须落在回测区间内,且股权登记日 recordDate 需早于 exDate,否则引擎会在创建或回放阶段报错。

2.4 创建回测引擎

Matching Engine Simulator 负责订单撮合,Backtest 负责事件回调、持仓、资金与绩效管理。脚本注册初始化和日线回调,并在创建前尝试删除同名旧引擎,避免重复使用旧持仓状态。

引擎通过 config 获取回测参数,通过 callbacks 找到策略函数。

callbacks = dict(STRING, ANY)
callbacks["initialize"] = initialize
callbacks["onBar"] = onBar

engineName = "JegadeeshTitmanMomentum12M"
try { Backtest::dropBacktestEngine(engineName) } catch(ex) { }
engine = Backtest::createBacktester(engineName, config, callbacks, false)

重复运行时,脚本会先尝试删除同名引擎,再创建新的回测实例。createBacktester 的第四个参数 false 表示本次未启用 JIT 优化;运行结果应使用相同插件版本、数据文件和参数以复现。

2.5 执行回测

将读取的日线行情直接送入引擎。回调按行情中的 tradeTime 推进:首个有效日生成初始组合并在收盘处理订单,后续日线继续处理待成交目标与组合估值。

行情发送完毕后,使用 Backtest::appendEndMarker 通知引擎回放结束。日线的时间戳为交易日标识,订单价格由策略提交时的收盘价 close 字段决定。

Backtest::appendQuotationMsg(engine, data)
Backtest::appendEndMarker(engine)

2.6 获取回测结果

处理结束标记后,分别查询收益概述、订单与成交明细、每日持仓和每日账户权益。

tradeDetails = Backtest::getTradeDetails(engine)
returnSummary = Backtest::getReturnSummary(engine)
dailyPosition = Backtest::getDailyPosition(engine)
totalPortfolios = Backtest::getDailyTotalPortfolios(engine)
  • returnSummary:查看收益、风险和交易统计。

  • tradeDetails:查看订单状态与成交;只有 tradeQty > 0 的记录表示实际成交。

  • dailyPosition:查看各日持仓、买卖数量和收盘估值。

  • totalPortfolios:查看每日现金、市值、账户权益、净值和费用。

3. 回测结果分析

使用上述脚本,回测处理 1,200 条行情,覆盖 300 只模拟股票和 2026-08-03 至 2026-08-06 共 4 个交易日。首个交易日触发初始调仓:300 只有效股票按 12 个月动量排序后,前 10% 对应 30 只股票,因而生成 30 个独立买入订单。outputOrderInfo=true 使订单状态和成交各保留一行,tradeDetails 共 60 行;其中 30 行 tradeQty > 0,对应 30 笔实际成交,而不是 60 笔成交。

首个信号日动量排名前五的股票如下。收益率按输入原始值展示,排序和目标数量均使用脚本中的原始精度:

表 2. 表 3-1 首个信号日动量排名前五股票
模拟股票 过去 12 个月收益率 信号日收盘价 成交量 目标/成交股数
000152.SZ 300.5602 87.5847 73,449 300
000004.SZ 80.3700 85.6788 39,604 300
000185.SZ 41.7942 98.0133 8,294 300
000025.SZ 31.4083 40.6171 90,348 800
000273.SZ 23.5185 17.4530 95,676 1,900

初始权益为 1,000,000.00 元,期末权益为 882,800.95 元,期末净值为 0.882801,区间总收益为 -11.7199%。按每日净值相对历史高点计算,最大回撤为 11.7199%。首个信号日入选的两只分红股中,000152.SZ 与 000004.SZ 于 2026-08-04 分别获得现金分红 0.5 元/股与送股 0.2 股/股,其影响已计入上述权益。回测摘要和实际成交统计如下:

表 3. 表 3-2 回测摘要和实际成交统计
指标 实测结果 说明
行情记录数 1,200 300 只股票 × 4 个交易日
独立订单数 30 均为 2026-08-03 产生的买入订单
实际成交笔数 30 以 tradeQty > 0 统计
订单状态记录数 60 每笔订单含状态与成交记录,不能直接当作成交笔数
总成交股数 48,000 30 笔买入成交的股数之和
期末权益(元) 882,800.95 含现金与期末持仓市值,已计入分红送股
区间总收益 -11.7199% 从初始资金到期末权益的变化
最大回撤 11.7199% 本次 4 日样本中的最大净值回撤
年化波动率 146.1725% 样本过短,仅为引擎计算值
夏普比率 -0.6839 样本过短且无有效基准,不作长期评价

结果反映的是 4 个交易日内的资金变化,而不是完整 12 个月动量策略的长期样本外表现。在用于策略研究前,应补齐足够长的连续复权行情,并校验所有 OHLC、涨跌停价、成交量与成交价格均为合理的正值。

因此,本次运行能够验证策略回调、初始选股、订单提交和结果获取链路已执行,但不能据此判断 12 个月动量信号在真实市场中是否有效,也不能把当前的年化风险指标或基准相关字段作为投资结论。

4. 总结

经典的 Jegadeesh-Titman 动量策略以过去 12 个月收益率为横截面排序因子,在历史时点股票池内选择前 10% 的强势股票,并用前 15% 缓冲区降低换手。策略按实际入选数量等权配置,以 100 股整数倍、最小交易金额、涨跌停和现金约束控制订单执行,通过月末信号与待处理目标完成调仓;同时在回测配置中内联 stockDividend,由引擎自动处理现金分红与送股对公司行为的影响。

本次模拟的日线行情仅有 4 个交易日,且存在零或非正价格字段,适合验证脚本链路,不足以评估动量因子的长期有效性。用于真实研究时,应使用覆盖至少形成期加持有期的连续历史数据、可追溯的历史成分与状态数据,并在清洗价格后再分析收益、回撤、换手和相对基准表现。