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 交易规则
-
每个回测日开始时,若前一信号日已有待执行目标,则先按当日开盘价执行卖出和买入。
-
首个回测日被视为候选调仓日;之后仅在月末交易日进一步检查触发条件。
-
只有当模拟指数收盘价高于 200 日均线,并满足边沿触发条件时,才进入选股流程。
-
从固定股票池中剔除成交量不大于 0 和 20 日动量无效的股票,按 20 日动量降序选择前 10 只。
-
以信号日账户权益、收盘价和买入手续费锁定目标股数,普通股票向下取整至 100 股整数倍。
-
下一交易日先卖出未入选或超配股票,再按排名顺序买入不足目标的股票。
-
订单仅在成交量大于 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 天上市信息。
| 字段 | 类型 | 说明 |
|---|---|---|
| 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 日动量。
该函数没有显式返回值,主要副作用包括提交买卖订单、更新待调仓状态,以及写入相关记录。内部核心流程分为两步:
-
执行已留存调仓目标:如果存在更早交易日生成的待执行目标,先检查可卖数量和开盘交易条件,卖出超配持仓;再检查可用现金和涨停条件,买入低配股票。执行一次后清除待调仓标记。
-
评估市场状态与生成新目标:回调后半段仅在首日或月末评估市场状态。当满足指数收盘价高于 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%。由于数据来自模拟数据,这一差异不能用于评价真实市场中的相对表现。
| 指标 | 本次结果 | 解读 |
|---|---|---|
| 区间总收益 | -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. 附录
以下为本策略的完整策略脚本(包含策略函数、回测配置和引擎执行流程)以及配套的模拟数据文件:
