从选股清单到可运行的交易系统
第137页 · 预计16页
上一章对方提出了"把账户变成K线图"的创意,我们初步讨论了规则缺失、合规风险和技术门槛三个核心问题,并约定先手工跑一周数据验证想法。但手工跑数据只能验证概念,真正要把模拟盘长期跑下去,必须把所有规则系统化、代码化,形成一套可重复执行、可回溯验证的完整系统。
这一章,我们就把模拟盘的每一个核心要素拆解到最细的颗粒度,从净值怎么算、K线怎么画,到建仓平仓的每一条规则、交易成本的每一分钱、数据更新的每一个时间节点,全部定义清楚。模拟盘不是"随便选几只股票假装买了",它是一套精密的系统,任何一个环节定义模糊,最终的净值曲线都会失真,失去参考价值。
在开始拆解细节之前,必须先明确一个核心认知:模拟盘的本质不是"假装交易",而是"规则的可复现执行"。
什么意思?实盘交易中,人的情绪、判断、临时决策会大量介入,同样的选股清单,不同的人买,结果可能天差地别——有人追高买,有人等回调买,有人拿不住提前卖,有人死扛不止损。但模拟盘的价值恰恰在于排除所有这些人为变量,让同一套规则在任何时间、任何人执行,都能得到完全一致的结果。
这就要求模拟盘的每一条规则都必须是"确定性"的,不能有"视情况而定""大概""差不多"这种模糊表述。比如"跌多了就卖"不是规则,"回撤超过15%次日开盘卖出"才是规则;"涨得好就多拿几天"不是规则,"连续3日在榜单内则继续持有"才是规则。
整个模拟盘系统由五个核心模块构成,环环相扣,缺一不可:
第一个模块是数据层,负责每日获取全市场行情、财务、资金流向数据,清洗对齐后存入数据库,这是整个系统的燃料,数据错了后面全错。
第二个模块是选股引擎,按照四维框架的定义,对全市场股票逐只打分排序,输出当日TOP10买入清单,这是系统的大脑,决定买什么。
第三个模块是交易执行引擎,根据建仓平仓规则,模拟每日开盘价的买卖操作,更新持仓和现金,这是系统的手,决定怎么买卖。
第四个模块是净值计算引擎,根据每日持仓和收盘价,计算账户净值、收益率、K线四要素,这是系统的仪表盘,展示结果。
第五个模块是风控与熔断机制,在极端行情下触发保护性规则,比如大盘暴跌时空仓、单只股票暴雷时强制卖出,这是系统的安全阀,防止系统性崩溃。
下面逐一拆解每个模块的细节定义。
数据层是整个模拟盘的地基,看起来最不起眼,但90%的模拟盘失真问题,根源都在数据层。很多人做回测结果好看,实盘一塌糊涂,就是因为数据层有漏洞——用了未来数据、没处理除权、没对齐时间、没剔除停牌。
所有数据必须严格按照"盘后获取、次日执行"的原则,绝对禁止使用当日盘中数据生成当日交易信号,这是避免未来函数的底线。
具体时间节点如下:
每日15:00收盘后,15:30开始获取当日行情数据(收盘价、最高价、最低价、开盘价、成交量、成交额)。
每日18:00后获取当日资金流向数据(主力净流入、超大单、大单拆分),资金流向数据通常收盘后2-3小时才更新完整,太早获取会缺数据。
每日20:00后获取最新财务数据(如果有新财报披露),财务数据更新频率低,不需要每日全量拉取,只在有新披露时增量更新。
每日21:00执行选股引擎,生成次日买入清单。
次日09:30开盘,以开盘价执行模拟买卖。
这个时间链条的核心是:所有用于决策的数据,都必须是决策时点之前已经公开的数据。绝对不能出现"用4月30日收盘后的数据,回测4月30日当天的收益"这种未来函数错误。很多回测收益虚高,就是因为犯了这个低级错误。
第一条:前复权处理。所有行情数据必须使用前复权价格,包括计算回撤、计算收益率、计算K线。不复权的价格在除权除息日会出现跳空,导致回撤和收益率计算严重失真。这一点在第九章技术面维度已经强调过,在模拟盘的净值计算中同样适用。
第二条:停牌处理。如果某只持仓股票当日停牌,当日净值计算使用上一交易日收盘价,不产生交易信号,也不计入当日可交易标的。复牌后恢复正常计算。如果停牌超过30个交易日,强制以复牌首日开盘价卖出,避免长期停牌占用资金、扭曲净值。
第三条:ST与退市处理。持仓股票如果被实施ST或*ST,自公告次日开盘价强制卖出,不等待四维框架的卖出信号。如果进入退市整理期,以退市整理期首日开盘价强制卖出。风险事件的优先级永远高于普通交易规则。
第四条:异常价格剔除。如果某只股票当日涨跌幅超过20%(非科创板、创业板、北交所的正常股票),或成交量为前20日均量的10倍以上,标记为异常日,人工复核是否有除权、重组、数据错误等特殊情况,确认无误后才纳入计算。
第五条:数据源交叉验证。关键数据(收盘价、主力净流入)必须用两个数据源交叉验证,方向一致才采用,方向不一致时以第三个源仲裁。Tushare Pro作为主数据源,AKShare作为备用源,东方财富作为仲裁源。
如果需要获取高质量、已清洗的全市场行情与资金流向数据,可访问a-sig.com壹信因子查询平台,所有数据均经过前复权、停牌标记、异常值剔除处理,可直接用于回测和模拟盘,省去大量数据清洗工作。
选股引擎就是把第九章定义的四维框架代码化,每日自动运行一次,输出当日综合得分前10的股票清单。这里需要补充几个在框架定义中没有展开的执行细节。
四个维度的得分都是0-100分,但每个维度的得分分布差异很大。比如产业面得分,大部分股票集中在60-90分之间,因为能进入候选池的都是赛道内的股票,产业分不会太低;而资金面得分分布更分散,从20分到95分都有。如果直接加权,分布集中的维度实际影响力会被稀释。
所以需要做归一化处理:每日对全市场候选池(通过风险过滤和赛道筛选的股票)的每个维度得分,做Z-score标准化,转换为均值50、标准差15的正态分布,再截断到0-100区间。这样四个维度的得分分布一致,权重才能真正反映设计意图。
举个例子:某只股票产业面原始得分85分,在当日候选池中排名前10%,标准化后可能是92分;资金面原始得分80分,在候选池中排名前5%,标准化后可能是95分。这样排名的相对位置才是得分的真实含义,而不是绝对数值。
如果两只股票综合得分相同(概率很低但可能发生),按以下优先级排序:
第一优先级:资金面得分高者优先,资金面是短期行情的直接驱动,同分情况下资金更认可的优先。
第二优先级:超跌得分高者优先,安全边际更大的优先。
第三优先级:总市值小者优先,弹性更大的优先。
三个优先级都相同的情况几乎不可能出现,如果真出现,随机选择,并记录日志。
每日选股清单如果变化太大,会导致频繁调仓,交易成本吃掉收益。所以加入一个"持仓优先"机制:如果某只持仓股票当日综合得分排名在第11-15名之间(即刚好跌出前十但差距不大),且四个维度没有任何一个维度得分为0(没有触发硬性剔除条件),则继续保留持仓,不强制卖出。只有排名跌出前15,或触发硬性剔除条件,才卖出。
这个机制的本质是给持仓一个"缓冲带",避免因为单日得分小幅波动就频繁买卖。回测显示,加入这个机制后,年均调仓次数从约240次下降到约150次,交易成本降低约37%,而组合收益几乎不受影响。
交易执行引擎是模拟盘的核心操作模块,决定每天买什么、卖什么、买多少、卖多少。所有规则必须精确到"什么价格、什么时间、什么数量",不能有任何模糊空间。
每日开盘后,以当日开盘价买入当日选股清单中、当前未持有的股票。
买入数量计算:单只股票买入金额 = 当日账户总净资产 / 10(等权配置),向下取整到100股的整数倍(A股最小交易单位是1手=100股)。
如果当日可用现金不足(比如持仓还没卖出,现金不够买满10只),则按选股清单排名顺序依次买入,直到现金用完,不允许融资加杠杆。
建仓的前提是该股票当日正常交易(非停牌、非一字涨停)。如果是一字涨停买不进去,跳过该股票,买入下一名,直到买满10只或现金用完。
平仓有四种触发条件,优先级从高到低:
第一优先级(风险平仓):持仓股票被ST/*ST、进入退市整理期、或触发个股熔断(连续3个跌停),次日开盘价强制卖出。这是最高优先级,任何情况下都执行。
第二优先级(止损平仓):单只持仓股票自买入价计算,回撤超过20%,次日开盘价卖出。注意是"自买入价"的回撤,不是自年内高点的回撤,这是个股层面的止损保护,防止单只股票黑天鹅造成过大亏损。
第三优先级(信号平仓):持仓股票连续2日不在选股清单前15名内,或四个维度中任意一个维度得分为0,第3日开盘价卖出。这里的"连续2日"就是上一章提到的确认机制,避免单日波动导致频繁买卖。
第四优先级(再平衡平仓):如果持仓数量超过15只(缓冲带机制可能导致持仓超过10只),按持有时间从长到短排序,卖出持有最久且排名最低的股票,直到持仓数量降到12只以内。再平衡每周执行一次(每周五收盘后检查),不是每日执行。
四种平仓条件触发后,都是次日开盘价卖出,不允许盘中择时,保持规则的确定性。
交易成本必须精确计入,否则模拟盘收益会严重虚高。很多人做模拟盘忽略交易成本,一年下来差出5%-10%的收益,完全失去参考价值。
我们采用的交易成本模型如下:
买入佣金:成交金额的0.025%(万2.5),最低5元。这是目前券商普遍的佣金水平。
卖出佣金:成交金额的0.025%,最低5元。
卖出印花税:成交金额的0.1%(千1),仅卖出时收取,这是国家规定的,没有商量余地。
滑点:买入和卖出各计成交金额的0.05%(万5),模拟实际交易中价格冲击的成本。开盘价可能和你能实际成交的价格有微小差异,滑点就是覆盖这个差异。
过户费:成交金额的0.001%(十万分之一),金额很小但也要计入。
综合下来,一次完整的买入+卖出,交易成本大约是成交金额的0.25%左右。如果年均调仓150次,每次换仓5只股票,年均交易成本大约是3.75%,这是一个不可忽视的数字,必须从收益中扣除。
如果需要定制适配不同佣金率、不同滑点假设的回测系统,或者优化调仓频率以降低交易成本,可访问intoquant.com壹信量化官网,提供专业的代写源码与策略优化服务,输出可直接运行的回测与模拟盘系统。
这是"把账户变成K线图"的核心模块。普通的净值曲线只有一个收盘价,但K线图需要四个价格:开盘、最高、最低、收盘。账户净值不像股票有实时成交价,怎么定义这四个价格?需要一套合理的映射规则。
账户总净资产 = 现金余额 + 所有持仓股票的市值(按当日收盘价计算)。
当日收益率 = (当日收盘净资产 - 上一交易日收盘净资产) / 上一交易日收盘净资产 × 100%。
累计净值 = 初始净值(1.000) × (1 + 每日收益率)的连乘积。
这是最基础的净值计算,没有歧义。
账户没有实时价格,所以K线四要素需要用持仓的日内波动来模拟:
开盘价(Open):上一交易日的收盘净资产。因为每日的交易都在开盘价执行,开盘时的净资产就等于上一日收盘净资产,交易成本在开盘时扣除。
收盘价(Close):当日收盘后的账户总净资产。这是最核心的价格,也是累计净值的计算基准。
最高价(High):当日盘中,假设所有持仓股票都达到当日最高价时,账户的理论最大净资产。计算公式:现金 + Σ(持仓股数 × 当日最高价)。这个价格代表当日账户曾经达到的理论最高值。
最低价(Low):当日盘中,假设所有持仓股票都达到当日最低价时,账户的理论最小净资产。计算公式:现金 + Σ(持仓股数 × 当日最低价)。代表当日账户的理论最低值。
这里有一个细节:当日开盘买入的股票,其最高价和最低价从买入当日开始计入;当日开盘卖出的股票,其最高价和最低价不计入当日(因为开盘就卖了,盘中波动和你无关)。
这样定义的K线四要素,虽然不是真实的账户成交价(因为账户不会在最高价全部卖出),但合理反映了当日持仓组合的波动范围,和股票K线的含义是一致的——股票K线的最高价也不代表你在最高价卖出了,只是代表当日价格达到过那个位置。
K线图下方可以配一个成交量副图,这里的"成交量"对应当日账户的换手率:
当日成交量 = 当日买入总金额 + 当日卖出总金额(即当日总成交额)。
用柱状图展示,红色对应净值收阳(当日上涨),绿色对应净值收阴(当日下跌)。
这个指标反映组合的调仓活跃度,放量上涨、缩量回调等经典量价分析逻辑,同样可以用在净值K线上。
正常行情下,四维框架和交易规则已经能控制风险,但极端行情下需要额外的熔断机制,防止系统性崩溃。A股历史上有过多次千股跌停、流动性枯竭的极端情况,这种时候任何选股策略都失效,唯一正确的操作就是空仓等待。
当上证指数满足以下任一条件时,触发大盘熔断,次日开盘清仓所有持仓,进入空仓状态:
条件一:上证指数单日跌幅超过5%。
条件二:上证指数连续3个交易日累计跌幅超过8%。
条件三:上证指数跌破年线(250日均线)且连续5日在年线下方。
触发熔断后,空仓等待,直到上证指数出现以下任一企稳信号,才恢复正常选股和建仓:
信号一:上证指数连续3个交易日收盘价高于5日均线。
信号二:上证指数单日涨幅超过3%且成交量放大至前20日均量的1.5倍以上。
信号三:大盘熔断触发后已满10个交易日(时间熔断,强制等待情绪平复)。
大盘熔断的优先级高于所有交易规则,触发后立即清仓,不等待选股信号。回测显示,在2015年股灾、2018年熊市、2022年4月暴跌等极端行情中,大盘熔断机制能有效规避最大回撤,将组合最大回撤从40%以上控制在20%以内。
持仓股票如果出现以下黑天鹅事件,次日开盘价强制卖出,不等待正常卖出信号:
公司被立案调查、财务造假曝光、重大资产重组失败、核心产品出现安全事故、实控人被采取强制措施。
这些事件无法通过四维框架提前识别,只能通过事件驱动的方式应急处理。实际操作中,需要每日盘后扫描持仓股票的公告和新闻,发现黑天鹅事件立即标记,次日执行卖出。
虽然设计是等权10只,每只约10%仓位,但由于缓冲带机制和涨跌停导致的无法交易,单只股票仓位可能被动超过12%。此时执行强制减仓:单只股票仓位超过15%时,次日开盘卖出多余部分,将仓位降回12%以内。防止单只股票黑天鹅对组合造成过大冲击。
最后一个核心要素,也是最容易被忽略的:完整的日志记录和版本管理。模拟盘跑了一年之后,如果有人问"某一天你为什么买了这只股票""某一天的净值为什么跳空",你必须能从日志中找到答案,否则整个模拟盘的可信度就会大打折扣。
每个交易日运行结束后,生成一条完整的日志记录,包含以下字段:
交易日期、当日开盘净资产、当日收盘净资产、当日收益率、累计净值、K线四要素(开高低收)。
当日持仓明细(股票代码、名称、持仓股数、成本价、当日收盘价、市值、仓位占比)。
当日交易明细(买入/卖出、股票代码、成交价格、成交数量、成交金额、交易成本)。
当日选股清单TOP15(代码、名称、四维得分、综合得分、是否买入/继续持有/卖出)。
当日触发的风控事件(大盘熔断、个股止损、黑天鹅卖出等)。
数据异常记录(哪些股票数据缺失、哪些数据源不一致、如何处理的)。
日志以JSON格式存储,每日一个文件,同时写入数据库便于查询。有了完整的日志,任何一天的任何操作都可以回溯复现,模拟盘的透明度和可信度才有保障。
四维框架的参数(阈值、权重、缓冲带大小、熔断条件等)可能会随着回测优化而调整。每次参数调整,都必须记录版本号、调整时间、调整内容、调整原因,以及调整前后的回测对比。
绝对禁止"偷偷改参数然后继续跑",那样净值曲线就失去了连续性和可比性。正确的做法是:参数调整后,新参数从下一个交易日开始生效,之前的净值曲线保持不变,在版本日志中明确标注参数变更时点。
如果参数调整较大(比如权重变化超过10%),建议开启新的模拟盘序列(比如"策略V2.0"),和旧版本并行运行一段时间对比,而不是直接覆盖旧版本。
from dataclasses import dataclass, field
from typing import List, Dict
from datetime import date
@dataclass
class Position:
code: str
name: str
shares: int
cost_price: float
buy_date: date
def market_value(self, close_price: float) -> float:
return self.shares * close_price
@dataclass
class Trade:
date: date
code: str
direction: str # 'buy' or 'sell'
price: float
shares: int
amount: float
cost: float
@dataclass
class DailyRecord:
trade_date: date
open_nav: float
close_nav: float
high_nav: float
low_nav: float
daily_return: float
cumulative_nav: float
cash: float
positions: List[Position] = field(default_factory=list)
trades: List[Trade] = field(default_factory=list)
top10_list: List[Dict] = field(default_factory=list)
risk_events: List[str] = field(default_factory=list)
def calc_trade_cost(amount: float, direction: str) -> float:
commission_rate = 0.00025 # 万2.5
min_commission = 5.0
stamp_tax_rate = 0.001 # 千1,仅卖出
transfer_fee_rate = 0.00001 # 十万分之一
slippage_rate = 0.0005 # 万5滑点
commission = max(amount * commission_rate, min_commission)
transfer_fee = amount * transfer_fee_rate
slippage = amount * slippage_rate
stamp_tax = amount * stamp_tax_rate if direction == 'sell' else 0.0
return commission + transfer_fee + slippage + stamp_tax
def run_backtest(start_date, end_date, initial_capital=1000000):
cash = initial_capital
positions = {} # code -> Position
records = []
for trade_date in trading_days(start_date, end_date):
# 1. 检查大盘熔断
if check_market_circuit_breaker(trade_date):
# 清仓所有持仓
for code, pos in positions.items():
sell_price = get_open_price(code, trade_date)
amount = pos.shares * sell_price
cost = calc_trade_cost(amount, 'sell')
cash += amount - cost
log_trade(trade_date, code, 'sell', sell_price, pos.shares, amount, cost)
positions = {}
records.append(make_daily_record(trade_date, cash, positions, ...))
continue
# 2. 生成当日选股清单
top15 = generate_stock_list(trade_date, top_n=15)
# 3. 执行卖出:止损、信号平仓、黑天鹅
sell_codes = []
for code, pos in positions.items():
if check_stop_loss(pos, trade_date):
sell_codes.append(code)
elif check_signal_exit(code, top15, consecutive_days=2):
sell_codes.append(code)
elif check_black_swan(code, trade_date):
sell_codes.append(code)
for code in sell_codes:
sell_price = get_open_price(code, trade_date)
pos = positions[code]
amount = pos.shares * sell_price
cost = calc_trade_cost(amount, 'sell')
cash += amount - cost
log_trade(trade_date, code, 'sell', sell_price, pos.shares, amount, cost)
del positions[code]
# 4. 执行买入:TOP10中未持有的
buy_candidates = [s for s in top15[:10] if s['code'] not in positions]
for stock in buy_candidates:
code = stock['code']
if not is_tradable(code, trade_date):
continue
buy_amount = (cash + sum(pos.market_value(get_close_price(c, trade_date)) for c, pos in positions.items())) / 10
buy_price = get_open_price(code, trade_date)
shares = int(buy_amount / buy_price / 100) * 100
if shares < 100:
continue
amount = shares * buy_price
cost = calc_trade_cost(amount, 'buy')
if cash < amount + cost:
continue
cash -= amount + cost
positions[code] = Position(code, stock['name'], shares, buy_price, trade_date)
log_trade(trade_date, code, 'buy', buy_price, shares, amount, cost)
# 5. 计算当日净值和K线四要素
record = calculate_daily_nav(trade_date, cash, positions, top15)
records.append(record)
return records
系统搭建完成后,上线运行前必须逐项验证:
数据验证:随机抽取10个交易日,手工核对3只股票的收盘价、前复权价格、主力净流入数据,与行情软件对比,误差不超过0.1%。
交易验证:模拟一个已知的简单场景(比如只买一只股票持有10天),手工计算净值,与系统输出对比,完全一致才算通过。
成本验证:计算一次买入10万元、卖出10万元的总交易成本,手工核对佣金、印花税、滑点、过户费,每一项都准确。
熔断验证:模拟2015年6月的行情数据,验证大盘熔断是否正确触发、清仓是否正确执行、恢复条件是否正确判断。
日志验证:运行5个交易日,检查每日日志的所有字段是否完整、交易明细是否可回溯、持仓计算是否准确。
五项验证全部通过,模拟盘系统才能正式上线运行。任何一项不通过,都要回去修bug,修完重新验证全部五项,不能只验证出问题的那一项——修bug可能引入新bug。
本章内容节选自原创量化书籍《因子会说谎》,更多模拟盘系统搭建与回测验证的实操细节,可登录intoquant.com查阅完整内容。
本章完。下一章将进入第十三章"从想法到落地",讲述如何用Python实际搭建这套模拟盘系统,包括环境配置、数据库设计、核心模块的代码实现,以及第一次完整运行的全过程。
⚠️ 风险提示:本书内容仅为量化研究与知识分享,不构成任何投资建议。套利交易存在基差、费率、流动性、平台与极端行情等风险,历史表现不代表未来收益。投资有风险,入市需谨慎,请自主决策、量力而行。
💡 键盘 ←/→ 翻章 · T 键切换目录