200 日均线牛熊择时策略

200 日均线牛熊择时策略以沪深 300 指数收盘价与其 200 日均线的关系判断市场状态,并在满足触发条件时,从股票池中按 20 日动量选择排名靠前的股票,按等权方式配置,并在下一交易日开盘调仓。本文基于 DolphinDB Server 3.00.6.1 环境下的脚本执行结果,说明该策略的核心逻辑、数据准备方式、回测流程与结果解读。

本文所有行情、基准与回测结果均为模拟数据,不对应真实沪深 300 指数、真实成分股或真实市场收益。

1. 策略介绍

该策略将 200 日均线作为市场状态过滤条件,将 20 日动量作为横截面选股因子。每个交易日先执行上一信号日锁定的目标仓位;在首个回测日或满足月末条件时,如模拟指数收盘价高于其 200 日均线,则在可交易股票中选择 20 日动量最高的股票,按等权目标持仓在下一交易日开盘调仓。

1.1 核心因子

市场状态因子为模拟沪深 300 指数的 200 日简单移动平均线。策略通过指数收盘价是否位于 200 日均线上方来判断是否允许触发选股。

idx_ma200 = mavg(close * adj_factor, 200)
idx_close = close * adj_factor
shouldTriggerCur = idx_close > idx_ma200

其中,idx_close > idx_ma200 表示当前模拟指数位于 200 日均线之上。为了保证回测首日已有可用的 200 日均线值,脚本在回测开始日前保留 230 个上交所交易日作为预热窗口。预热行情仅用于指标计算,不直接送入回测引擎。

横截面排序因子为每只模拟股票的 20 日动量:

mom20 = (close * adj_factor) / move(close * adj_factor, 20) - 1

在触发日,策略会剔除动量无效或成交量不大于0的股票,按 mom20 降序选择最多 10 只股票。若实际入选 N 只,则每只目标权重为 1/N。

策略仅在 idx_close > idx_ma200 时产生选股和调仓目标,并未在指数跌破 200 日均线时自动将仓位降为0,也未写入清仓指令。

1.2 交易标的

本例使用 300 个模拟日线数据,标的代码后缀字符串为 .SH。其中 000300.SH 被单独作为模拟指数,其余 299 只构成静态模拟股票池。它们仅是脚本中的编号,不代表真实证券、真实沪深 300 成分股或可交易标的。

策略采用股票账户、只做多、初始现金 1,000,000 元。日线行情包含开高低收、成交量和成交额。由于模拟不提供真实涨跌停、ST、上市和分红信息,代码中作了如下简化处理:

  • 涨跌停价设为 NULL

  • 全部股票设为非 ST

  • 上市日统一设为 2000-01-01

  • 不模拟分红、拆股、停牌和复权变化

  • 复权因子(adj_factor)固定为 1.0

若用于真实研究,必须使用每个历史时点真实可获得的指数成分、ST 状态、上市日期、停复牌状态、复权因子与公司行为数据,不能使用当前口径替代历史口径。

1.3 交易规则

  1. 每个回测日开始时,若前一信号日已有待执行目标,则先按当日开盘价执行卖出和买入。

  2. 首个回测日被视为候选调仓日;之后仅在月末交易日进一步检查触发条件。

  3. 只有当模拟指数收盘价高于 200 日均线,并满足边沿触发条件时,才进入选股流程。

  4. 从固定股票池中剔除成交量不大于 0 和 20 日动量无效的股票,按 20 日动量降序选择前 10 只。

  5. 以信号日账户权益、收盘价和买入手续费锁定目标股数,普通股票向下取整至 100 股整数倍。

  6. 下一交易日先卖出未入选或超配股票,再按排名顺序买入不足目标的股票。

  7. 订单仅在成交量大于 0 且价格可交易时提交;本例模拟涨跌停价为空,因此不会额外阻挡订单。

每轮调仓只在执行日尝试一次,不会在后续交易日自动追单。虽然代码保留了延迟卖出状态变量,但当前版本没有把跌破 200 日均线映射为卖出意图,因此持仓不会因熊市条件自动退出。

2. 策略回测

以下内容按准备数据、策略逻辑、回测参数、创建引擎、回放行情和读取结果的顺序展开。本案例已在 DolphinDB Server 3.00.6.1、Backtest 3.00.6.6 与 MatchingEngineSimulator 3.00.6.6 上运行。服务器需要安装与 DolphinDB Server 版本匹配的两个插件;创建引擎前的代码会检查并加载插件。

2.1 准备数据

下载附录中的 ma200AllBars.csv 文件,作为回测的案例数据。回测区间为 2025 年 8 月 11 日至 2026 年 8 月 10 日,先将对应区间的 csv 数据导入,<path> 改为实际存储 csv 文件的真实路径。

actualStartDate = 2025.08.11
endDate = 2026.08.10
benchmarkCode = "000300.SH"

ma200AllBars = loadText("<path>/ma200AllBars.csv")
ma200IndexBars = select * from ma200AllBars where symbol == benchmarkCode
ma200StockBars = select * from ma200AllBars where symbol != benchmarkCode
stockPool = exec distinct symbol from ma200StockBars
ma200Bars = ma200StockBars.unionAll(ma200IndexBars, false)

为演示指数成分、ST 和上市过滤接口,脚本将剩余 299 只模拟股票设为静态指数成分、空 ST 历史以及统一的已满 90 天上市信息。

表 1. 表 2-1 数据字段
字段 类型 说明
symbol STRING 模拟代码,例如 000001.SH 或 000300.SH
tradeTime TIMESTAMP 交易日零点时间戳,仅作为日线标识
open DOUBLE 模拟开盘价(OHLC 价格)
low DOUBLE 模拟最低价(OHLC 价格)
high DOUBLE 模拟最高价(OHLC 价格)
close DOUBLE 模拟收盘价(OHLC 价格)
volume LONG 模拟成交量
amount DOUBLE 模拟成交额
upLimitPrice DOUBLE 本例均为空值,不模拟涨停
downLimitPrice DOUBLE 本例均为空值,不模拟跌停
adj_factor DOUBLE 固定为 1.0,不模拟复权调整
vwap DOUBLE amount / volume × adj_factor,成交量无效时为空
mom20 DOUBLE 20日动量,用于横截面选股
idx_ma200 DOUBLE 200日均线,回调中只读取 000300.SH 的值
idx_close DOUBLE 指数收盘价,回调中只读取 000300.SH 的值

指标在完整预热加回测序列上按代码和时间顺序计算,再截取回测开始日之后的数据送入引擎。

data = select symbol, tradeTime,open, low, high, close, long(volume) as volume, amount, up_limit as upLimitPrice, down_limit as downLimitPrice, adj_factor, double(close / move(close, 20) - 1) as mom20,
    double(mavg(close, 200)) as idx_ma200,
    double(close) as idx_close
    from ma200Bars context by symbol csort tradeTime
data = select * from data where tradeTime >= timestamp(actualStartDate) order by tradeTime

2.2 编写策略逻辑

策略由 roundLot、normalizeSellQty、isBuyableAt、isSellableAt、initialize 和 onBar 组成。前四个函数处理申报数量和成交条件,initialize 初始化交易状态,onBar 处理每天的调仓执行和新信号。

initialize 中的关键初始状态如下:

context["commission"] = 0.00015
context["tax"] = 0.0005
context["initialRebalancePending"] = true
context["prevTriggerState"] = false
context["selectedStocks"] = []$STRING
context["stockWeights"] = dict(STRING, DOUBLE)
context["pendingRebalance"] = false
context["pendingTargetQtys"] = dict(STRING, LONG)

回调函数:onBar(mutable context, msg, indicator) 是日线行情回调,也是策略的主执行函数。每个交易日调用一次,先执行上一信号日留下的调仓目标,再判断当日是否需要生成新信号。由于当日信号基于当日收盘数据生成,因此该信号将在下一交易日执行,而不会使用当日开盘价成交。

  • context:可变 DICTIONARY,包含引擎注入的当前日期、时间和引擎句柄,也包含初始化状态、筛选参数、成本参数及日志表。

  • msg:DICTIONARY,以股票代码为键,当日该股票的日线及因子记录为值。函数从中读取开盘价、收盘价、涨跌停价、成交量等行情特征以评估交易条件和计算股数。

  • indicator:引擎为指标结果预留的参数。此处读取了用于大盘择时的沪深 300 指数收盘价与 200 日均线,以及用于排序的个股 20 日动量。

该函数没有显式返回值,主要副作用包括提交买卖订单、更新待调仓状态,以及写入相关记录。内部核心流程分为两步:

  1. 执行已留存调仓目标:如果存在更早交易日生成的待执行目标,先检查可卖数量和开盘交易条件,卖出超配持仓;再检查可用现金和涨停条件,买入低配股票。执行一次后清除待调仓标记。

  2. 评估市场状态与生成新目标:回调后半段仅在首日或月末评估市场状态。当满足指数收盘价高于 200 日均线、当日未重复触发且满足边沿条件后,才构造候选表并选择动量排名前 10 只股票。随后按信号日账户权益和收盘价计算目标股数,并保存为下一交易日要执行的目标。特殊逻辑边界:当前策略并不是“每月只要站上均线就再平衡一次”。如果指数持续位于均线之上且账户已有持仓,后续月末不会重复触发。同时,跌破均线时也不会自动退出持仓。

def onBar(mutable context, msg, indicator) {
    curBarTs = msg.values()[0]["tradeTime"]
    curDate = date(curBarTs)
    syntheticDivDiag_ = objByName("ma200Diag")
    if (size(syntheticDivDiag_) > 0) {
        syntheticDivDates_ = exec date(time) from syntheticDivDiag_
            where diagType == "synthetic_dividend_bar"
        if (curDate in syntheticDivDates_) { return }
    }

    // === 延迟调仓执行(次日开盘:信号日锁定目标下单数量,执行日 open 撮合 + cash 兜底)===
    if (context["pendingRebalance"]) {
        if ((curDate != context["rebalanceDate"])) {
            tgtQtys_ = context["pendingTargetQtys"]   // 读信号日锁定的 target_qty
            expectedCash_ = Backtest::getAvailableCash(context.engine)
            sellCashMult_ = 1.0 - context["commission"] - context["tax"]
            rebalThr_ = context["rebalanceThreshold"]
            // 第一轮:卖出不在选中列表的(清仓全卖)+ 超配的减仓(偏差超阈值才动)
            // T+1 资产 sellable = 昨日剩余持仓 - 当日已卖;T+0 = 当前总持仓
            for (istock in msg.keys()) {
                bar = msg[istock]
                posInfo = Backtest::getPosition(context.engine, istock)
                pos = 0
                pos_sellable = 0
                if (size(posInfo) > 0) {
                    pos = posInfo["longPosition"]
                    pos_sellable = max(0, posInfo["lastDayLongPosition"] - posInfo["todaySellVolume"])
                }
                isSelected = (istock in context["selectedStocks"])
                if (!isSelected && pos > 0 && pos_sellable > 0 && isSellableAt(bar["open"], bar)) {
                    Backtest::submitOrder(context.engine, (istock, context.tradeTime, 5, bar["open"], pos_sellable, 3), "rebalance")
                    expectedCash_ = expectedCash_ + pos_sellable * bar["open"] * sellCashMult_
                } else if (isSelected && pos > 0 && pos_sellable > 0 && isSellableAt(bar["open"], bar)) {
                    targetQty = iif(istock in tgtQtys_, tgtQtys_[istock], 0)
                    if (pos > targetQty && (pos - targetQty) > 0 && double(pos - targetQty) / double(pos) > rebalThr_) {
                        sellQty_ = normalizeSellQty(istock, pos, min(pos_sellable, pos - targetQty))
                        if (sellQty_ > 0) {
                            Backtest::submitOrder(context.engine, (istock, context.tradeTime, 5, bar["open"], sellQty_, 3), "rebalance")
                            expectedCash_ = expectedCash_ + sellQty_ * bar["open"] * sellCashMult_
                        }
                    }
                }
            }
            // 第二轮:按排名顺序加仓+新买入(新建仓全额买入;加仓偏差超阈值才动)
            for (istock in context["selectedStocks"]) {
                if (!(istock in msg)) { continue }
                bar = msg[istock]
                if (!isBuyableAt(bar["open"], bar)) { continue }
                posInfo = Backtest::getPosition(context.engine, istock)
                pos = 0
                if (size(posInfo) > 0) { pos = posInfo["longPosition"] }
                targetQty = iif(istock in tgtQtys_, tgtQtys_[istock], 0)
                if (targetQty > pos) {
                    plannedQty = targetQty - pos
                    if (pos > 0 && targetQty > 0 && double(plannedQty) / double(targetQty) <= rebalThr_) { continue }
                    affordableCash_ = min(expectedCash_, Backtest::getAvailableCash(context.engine))
                    affordableQty = roundLot(istock, affordableCash_ / (bar["open"] * (1 + context["commission"])))
                    buyQty = min(plannedQty, affordableQty)
                    buyQty = roundLot(istock, buyQty)
                    if (buyQty > 0) {
                        Backtest::submitOrder(context.engine, (istock, context.tradeTime, 5, bar["open"], buyQty, 1), "buy")
                        expectedCash_ = expectedCash_ - buyQty * bar["open"] * (1 + context["commission"])
                    }
                }
            }
            context["pendingRebalance"] = false
        }
    }
    // === 延迟卖出执行(下根 bar 入口取出意图,用 bar.open 价提单) — 跟 buy 对称(业界 next_open 标准) ===
    if (context["pendingSellsDate"] != NULL && curDate != context["pendingSellsDate"]) {
        for (istock in msg.keys()) {
            if (!(istock in context["pendingSells"])) { continue }
            bar = msg[istock]
            if (bar["volume"] <= 0) { continue }
            sellQty_pend = context["pendingSells"][istock]
            labelPend_ = context["pendingSellsLabel"][istock]
            posInfo_s = Backtest::getPosition(context.engine, istock)
            pos_total_s = 0
            pos_sellable_s = 0
            if (size(posInfo_s) > 0) {
                pos_total_s = posInfo_s["longPosition"]
                pos_sellable_s = max(0, posInfo_s["lastDayLongPosition"] - posInfo_s["todaySellVolume"])
            }
            sellQty_actual = normalizeSellQty(istock, pos_total_s, min(sellQty_pend, pos_sellable_s))
            if (sellQty_actual > 0 && isSellableAt(bar["open"], bar)) {
                // sell limit 用 min(open, upLim) 兜底:涨停日 bar.open 浮点精度可能微超 prev*1.1=upLim,
                // DDB 引擎按 sell limit > upLim 直接拒(A 股规则:sell 价不能高于涨停价)
                sellLimit_ = iif(isNull(bar["upLimitPrice"]), bar["open"], min(bar["open"], bar["upLimitPrice"]))
                Backtest::submitOrder(context.engine, (istock, context.tradeTime, 5, sellLimit_, sellQty_actual, 3), labelPend_)
            }
        }
        context["pendingSells"] = dict(STRING, INT)
        context["pendingSellsLabel"] = dict(STRING, STRING)
        context["pendingSellsDate"] = NULL
    }

    // === 选股逻辑(信号日选股,下根 bar 开盘下单)===
    rebalDate = curDate
    isRebalanceDay = (transFreq(monthEnd(rebalDate), "XSHG") == rebalDate)
    initialRebalance_ = context["initialRebalancePending"]
    isRebalanceDay = (isRebalanceDay || initialRebalance_)
    context["initialRebalancePending"] = false
    if (curDate != context["lastTriggerDayReset"]) {
        context["triggeredToday"] = false
        context["lastTriggerDayReset"] = curDate
    }
    shouldTriggerCur = isValid(indicator["000300.SH"]["idx_close"]) && isValid(indicator["000300.SH"]["idx_ma200"]) && (indicator["000300.SH"]["idx_close"] > indicator["000300.SH"]["idx_ma200"])
    posSnapshot_ = Backtest::getPosition(context.engine)
    hasAnyPosition_ = (size(posSnapshot_) > 0) && (exec sum(longPosition) from posSnapshot_)[0] > 0
    triggerEdgeOK_ = shouldTriggerCur && !context["triggeredToday"] && (!context["prevTriggerState"] || !hasAnyPosition_)
    context["prevTriggerState"] = shouldTriggerCur
    isRebalanceDay = (isRebalanceDay && triggerEdgeOK_)
    if (isRebalanceDay) { context["triggeredToday"] = true }

    if (isRebalanceDay) {
        // 取当日最新一期指数成分股快照
        snapsT_ = context["tpIdxSnaps"]
        latestPerIdx_ = select index_code, max(snap_date) as latest_date from snapsT_ where snap_date <= curDate group by index_code
        idxMembers_ = set(exec con_code from ej(snapsT_, latestPerIdx_, `index_code`snap_date, `index_code`latest_date))
        tpExplicit_ = context["tpExplicit"]
        // 剔除当日处于 ST 状态的股票
        stHist_ = context["tpStHist"]
        stCodes_ = set(exec code from stHist_ where start_date <= curDate and (isNull(period_end) or period_end > curDate))
        // 剔除上市不足 N 天的新股
        listInfo_ = context["tpListInfo"]
        cutoffDate_ = date(curDate - 90)
        eligibleCodes_ = set(exec code from listInfo_ where list_date <= cutoffDate_)
        pool_ = array(STRING, 0)
        for (s_ in context["stockPool"]) { if (((s_ in idxMembers_) || (s_ in tpExplicit_)) && !(s_ in stCodes_) && (s_ in eligibleCodes_)) { pool_.append!(s_) } }
        context["tradablePool"] = set(pool_)
        // --- 横截面选股 ---
        pipelineDataValid_ = true
        selectedStocks = []$STRING
        stockWeights_ = dict(STRING, DOUBLE)
        symbols = []$STRING
        m_mom20_list = []$DOUBLE
        for (istock in msg.keys()) {
            if (!(istock in context["tradablePool"])) { continue }
            bar = msg[istock]
            if (bar["volume"] <= 0) { continue }  // 跳过停牌股

            symbols.append!(istock)
            m_mom20_list.append!(double(indicator[istock]["mom20"]))
        }

        // 组装并处理pipeline
        poolT = table(symbols as symbol, m_mom20_list as `mom20)
        if (poolT.size() > 0) {

            rankInputSize0_ = poolT.size()
            rankValidT0_ = select * from poolT where isValid(mom20)
            if (rankInputSize0_ > 0 && rankValidT0_.size() == 0) { pipelineDataValid_ = false }
            poolT = select * from rankValidT0_ order by mom20 desc limit 10
            selectedStocks = exec distinct symbol from poolT
            if (selectedStocks.size() > 0) {
                avgWeight_ = 1.0 / selectedStocks.size()
                for (s in selectedStocks) { stockWeights_[s] = avgWeight_ }
            }
        }
        if (pipelineDataValid_) {
            context["selectedStocks"] = selectedStocks
            context["stockWeights"] = stockWeights_
        }
        context["selectionDataValid"] = pipelineDataValid_
        // --- 横截面选股结束 ---
        if (context["selectionDataValid"]) {
        // 信号日:用信号日 close 价锁定目标下单数量
        // target_qty = portfolio_value × pct × weight / (close × (1+commission))
        // 执行日次根 bar open 撮合 + cash 兜底(业务规则:信号日锁数量,避免跳空污染)
        signalEquity_ = Backtest::getTotalPortfolios(context.engine).totalEquity[0]
        current_portfolio_pct_ = context["currentPortfolioPct"]
        weights_ = context["stockWeights"]
        tgtQtys_ = dict(STRING, LONG)
        for (s_ in context["selectedStocks"]) {
            w_ = weights_[s_]
            tgtQtys_[s_] = roundLot(s_, signalEquity_ * current_portfolio_pct_ * w_ / (msg[s_]["close"] * (1 + context["commission"])))
        }
        context["pendingTargetQtys"] = tgtQtys_
        context["signalEquity"] = signalEquity_
        context["pendingRebalance"] = true
        context["rebalanceDate"] = curDate
        }
    }
}

2.3 设置回测参数

回测使用日频股票策略组、100 万元初始资金、万分之 1.5 双边手续费和千分之 0.5 卖出税。订单以当日 open 字段提交,保留订单状态信息,不启用单笔最低手续费。

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

rebalanceThreshold 设为 0,因此只要持仓数量与目标数量存在差异,且满足可卖、可买和现金条件,就会尝试调整。currentPortfolioPct 初始化为 1.0,表示信号触发后按全部账户权益建立等权目标仓位。

2.4 创建回测引擎

Backtest 负责事件回调、账户、持仓和绩效统计;MatchingEngineSimulator 负责订单撮合。脚本假定所需插件已在服务器中可用,并注册初始化和日线回调。

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

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

每次运行前会尝试删除同名残留引擎;结果取出后再次删除。因此脚本不会将回测引擎或行情结果持久化到 DFS。

2.5 执行回测

脚本将已按 tradeTime 排序、且已剔除预热段的日线一次性送入引擎。预热数据仅用于 20 日动量和 200 日均线计算,回测引擎不接收预热行情。回放末尾复制一行数据并将代码改为 END,以通知引擎结束回放。

Backtest::appendQuotationMsg(engine, data)

endRow = select top 1 * from data
update endRow set symbol = "END"
Backtest::appendQuotationMsg(engine, endRow)

信号日的收盘数据只用于确定股票、权重和目标股数,订单在下一交易日由回调以开盘价提交。日线时间戳为零点并不代表在零点成交。

2.6 获取回测结果

回放结束后,脚本提取交易明细、收益汇总、每日持仓和每日账户权益,并以模拟 000300.SH 的 T-1 收盘价为基期构建基准净值。

tradeDetails = Backtest::getTradeDetails(engine)
returnSummary = Backtest::getReturnSummary(engine)
dailyPosition = Backtest::getDailyPosition(engine)
totalPortfolios = Backtest::getDailyTotalPortfolios(engine)

update returnSummary set benchmarkReturn = bmReturn
  • returnSummary:查看收益、年化波动率、夏普比率、最大回撤、换手率和日胜率

  • tradeDetails:查看订单方向、委托与成交时间、价格、数量、订单状态和标签

  • dailyPosition:查看每日持仓数量、买卖数量与估值

  • totalPortfolios:查看每日现金、市值、总权益、净值、累计收益和费用

脚本只补写了 benchmarkReturn,没有基于完整日收益序列重新计算 beta、alpha 或年化超额收益。因此相关字段若为 0,不能解释为策略对真实沪深 300 的风险暴露或超额收益结论。

3. 回测结果分析

本次在给定环境运行完整脚本后,初始权益为 1,000,000.00 元,区间总收益为 -6.3208%。由于数据来自模拟数据,这一差异不能用于评价真实市场中的相对表现。

表 2. 表 3-1 回测结果
指标 本次结果 解读
区间总收益 -6.3208% 期末权益相对100万元初始资金的累计变化
年化收益 -6.5732% 引擎按回测日收益年化后的结果
年化波动率 376.8546% 模拟数据价格路径波动很高,不能映射为真实市场风险水平
夏普比率 -0.0174 风险调整后收益接近于零
最大回撤 56.5919% 期内净值相对先前高点的最大下跌,风险暴露较高
回撤比率 -0.1161 引擎返回的收益与回撤相关统计
换手率 0.6146% 引擎返回的换手统计口径
日胜率 51.1013% 每日收益为正的交易日占比

期末账户中,现金为 253,972.20 元,股票市值为 682,819.69 元,总费用为 1,065.11 元。已实现盈亏为 222,274.49 元,浮动盈亏为-285,482.61 元,合计总盈亏为 -63,208.11 元。这意味着虽然期末收益为负,且未平仓持仓在期末存在较大的浮动亏损。

结果解读重点:一方面,20 日动量是在独立的模拟价格路径上计算,并未被设定为与未来收益存在因果关系;另一方面,当前代码没有在跌破 200 日均线时退出持仓,也不会在持续牛市中每月机械再平衡。因此最大回撤和相对基准落后,不能仅归因于 200 日均线规则本身,也反映了当前退出与再平衡逻辑的限制。

4. 总结

该策略展示了一个完整的 DolphinDB Backtest 研究流程:从处理模拟数据、预热计算 20 日动量与 200 日均线、以 200 日均线过滤触发时点、按 20 日动量做横截面选股、等权锁定目标数量、次日开盘执行交易,到最终读取账户与基准结果。

本次执行录得 5.0887%的模拟区间收益,但同期模拟基准为 17.5914%,且最大回撤达到 64.5988%。这些数值仅反映当前这一路随机模拟输入下的回测路径。若要用于真实投资研究,应至少补充以下能力:

  • 使用具有历史可得性的真实行情、指数成分、ST、上市、停牌、复权与公司行为数据

  • 明确实现跌破 200 日均线后的减仓或清仓规则

  • 补充持续牛市条件下的再平衡规则

  • 完善仓位上限、成本模型与滑点假设

  • 进行样本外检验与参数稳健性测试

5. 附录

以下为本策略的完整策略脚本(包含策略函数、回测配置和引擎执行流程)以及配套的模拟数据文件: