做ETF短线,大多数人的第一步是打开行情软件看均线,第二步是凭感觉下单,第三步是亏损之后开始怀疑人生。我自己从手工盯盘到写python量化交易策略代码,花了将近两年才明白一件核心的事:在这个市场里,决定你能不能活下来的不是预测准不准,而是风控够不够硬、择时有没有一套可复用的逻辑。这篇内容要聊的,就是一套围绕10万元账户设计的ETF短线实战框架,核心就四个字:高收益风险比。通过双均线趋势跟随、波动率过滤、严格的仓位推导和止损纪律,把每次交易的亏损锁死在总资金的1%以内,同时让盈利端尽量跑到6%以上。整套逻辑适合手里有10万左右闲钱、有一定python基础、想把交易从“靠感觉”变成“按系统执行”的朋友参考。
1. 为什么是“10万元ETF短线”:策略定位与设计逻辑
1.1 资金量级与标的池的匹配逻辑
做量化的第一步不是写代码,而是搞清楚你的资金规模适合交易什么品种。10万元这个量级非常微妙:比它大的资金要考虑建仓冲击成本和流动性约束,比它小的资金又会觉得手续费和管理成本占比太高,交易频率稍微上去一点利润就被成本吃掉了。
ETF是个天然适合这个资金量级的工具。场内ETF交易免印花税,佣金通常可以谈到万1到万3,也没有个股暴雷、财务造假这种灰天鹅风险。10万元在半仓操作时一次进场就是5万元,放进510300这种日均成交额动辄十几亿的宽基ETF里,连水花都溅不起来,完全不用担心成交滑点。同时,资金量小的好处是灵活,进出市场几乎没有成本压力,这为短线波段操作创造了条件。
如果资金放大到500万以上,同样的策略就必须换标的池了,因为小规模ETF的盘口深度不足以承载大资金进出,滑点会直接吃掉大部分利润。所以10万元并不是这套策略的下限,而是它最舒服的运行区间。
1.2 短线周期选择:为什么不是日内,也不是长线
这里说的短线,指的是持仓周期在1到10个交易日之间。为什么不选日内T+0?因为日内高频对盯盘、网络速度、交易通道和执行纪律要求极高,10万元资金靠人工盯盘根本不具备优势,机器化交易又需要投入大量时间和精力搭建底层设施,对个人选手来说性价比太低。
为什么不选长线持有?ETF长线虽然省心,但资金利用率低,遇到2019到2024年这种宽幅震荡的行情,拿着不动几年下来收益可能非常尴尬。短线策略恰好卡在中间:持仓几天到两周,既能避开日内微观噪声对判断的干扰,又能捕捉到波段的主升段,资金周转效率也高。
持仓周期的选择和指标参数是互相匹配的。我用的5日、20日EMA组合,对应的是周线级别趋势拐点,平均持仓5到8个交易日,一个月交易2到4次。这个频率对个人投资者很友好,不需要时刻盯盘,每天收盘前后跑一遍信号脚本就够了。
2. 择时引擎:能用能扛的信号体系怎么搭
2.1 三类指标的分工:趋势、波动、量能
精准择时不是靠某一个神奇指标,而是靠多指标互相确认、互相过滤。这套策略的信号体系固定用三类指标:趋势类、波动类、量能类,三类指标各有分工,缺一不可。
趋势类指标负责判断大方向,我用EMA5和EMA20,短期均线从下方上穿长期均线视为上升趋势启动,下穿视为趋势结束。之所以用EMA而不是普通MA,是因为EMA对近期价格变化赋予更高权重,反应速度比MA快一些,可以稍微提前捕捉拐点,同时稳定性又比单纯价格突破好。
波动类指标负责过滤环境。这是很多人忽略的关键点:均线交叉在低波动震荡市里会反复打脸,金叉买完就跌、死叉卖完就涨,来回挨耳光。我用ATR(14)来衡量标的的波动水平,只有当ATR足够大,说明市场有足够的波动空间值得参与趋势策略;当ATR偏低时,强制空仓观望。过滤条件可以设为“ATR大于过去60日ATR的40分位”,这样就把大量低波动的垃圾行情排除在外。
量能类指标负责确认质量。金叉当天,成交量最好放大到20日均量的1.2倍以上。没有成交量配合的突破大多数是假突破,特别是在ETF这种流动性极好的品种上,无量突破更值得警惕。加一道量能过滤,能滤掉不少无效信号。
2.2 买入与卖出信号的完整定义
这套策略的买入信号必须四个条件同时满足才触发:
- EMA5上穿EMA20,形成金叉;
- 当日收盘价创出过去20个交易日收盘价的新高;
- ATR(14)大于过去60日ATR的40分位,确保市场处于有波动空间的状态;
- 当日成交量大于20日均量乘以1.2。
卖出信号则触发其一即离场:
- EMA5下穿EMA20,形成死叉;
- 价格跌破动态止损线(具体在风控章节展开);
- 持仓时间超过12个交易日仍未脱离成本区,主动离场(时间止损)。
这里有一个极其重要的规则:所有信号在T日收盘后确认,T+1日开盘执行。有人做回测时直接用当天收盘价成交,结果曲线漂亮得像神话,实盘一跑就露馅,这就是典型的未来函数问题。信号确认和成交价之间必须隔着隔夜,才符合真实可执行的状态。
3. 严格风控:仓位推导与账户保护机制
3.1 单笔风险预算与仓位计算过程
先定义账户级的风险规则:单笔交易最大亏损金额不超过总资金的1%。对于10万元账户,就是1000元。这个数字看起来不起眼,但它决定了后面所有仓位计算。
仓位金额的计算公式很简单:
仓位金额 = 单笔允许亏损金额 / 止损比例
如果采用固定止损2%,代入公式:仓位金额 = 1000 ÷ 2% = 50000元。
也就是说,每次最多半仓入场。当止损被触发时,亏损金额正好是1000元,在账户风险承受范围内。如果止盈目标设为6%,单笔盈利就是50000 × 6% = 3000元,对应的盈亏比是3比1。即使胜率只有40%,期望收益依然为正:0.40 × 3000 − 0.60 × 1000 = 600元。
这是整条风控逻辑的核心:仓位大小跟你看多看空的程度没有半点关系,只跟风险预算有关。看多不是加仓的理由,风险预算才是。很多做短线的朋友亏损的根源,就是仓位凭感觉、止损又随意,最后风险失控。
3.2 动态止损:固定比例与ATR结合
固定百分比止损简单直接,但有一个致命缺点:不同ETF的波动率差异很大。止损设太紧,标的正常波动就会把你扫出去,然后眼睁睁看它一路大涨;止损设太松,单笔亏损又会轻易突破风险预算。
实操中我把两者结合,形成动态止损线:
- 基础止损线:入场价乘以0.98,即固定2%保底止损;
- 动态止损线(跟踪止损):从持仓期最高收盘价减去1.5倍ATR;
- 最终止损线取两者中更宽松(更高)的那个。
举例说明:510300入场价5元,入场时ATR约0.05元。价格涨到5.5元后,动态止损线就是5.5 − 1.5 × 0.05 = 5.425元,已经锁定盈利。如果价格继续上涨,止损线跟着抬高,直到价格回落触发离场。这样既能让利润奔跑,又能避免固定止损过早离场的问题。
从风险角度,ATR动态止损距离可能不是固定的2%,但账户允许亏损总额固定为1000元,因此止损距离越宽,仓位就要相应降低。这是波动率标准化仓位管理的思想,核心是让每笔交易承担的风险金额始终一致。
3.3 账户保护:回撤红线与空仓机制
单笔风控只能控制单次重伤,连亏才是账户最大的敌人。市场里连续止损5到8笔的情况并不罕见,如果不设账户级保护,就算单笔只亏1%,连亏8笔也快接近一成了。
我的账户保护规则分为三层:
- 账户净值从最高点回撤8%时,仓位自动减半;
- 回撤达到12%,全部清仓,停止开仓至少3个交易日;
- 连续两笔亏损,暂停开仓,复盘后再用1/4仓位重新起步。
为什么把红线压在12%?因为回撤20%需要赚25%才能回本,对短线账户来说这是巨大的心理压力。把红线压到12%,最差情况下一年还有翻身的机会。这一机制最难的不是设置,而是执行。我的经验是把止损单直接在券商客户端挂成条件单,让系统替你执行,不要给自己任何“再看一看”的余地。
4. python实现与回测实录(从数据到绩效)
4.1 环境准备与数据获取
很多朋友一上来就研究复杂的回测框架,我觉得没必要。个人做策略验证,用pandas加akshare就够了。先安装依赖:
pip install pandas numpy akshare数据获取用akshare的ETF历史行情接口,注意使用前复权数据,避免除权除息造成的价格跳空对回测的干扰。示例代码如下:
import akshare as ak import pandas as pd import numpy as np df = ak.fund_etf_hist_em( symbol="510300", period="daily", start_date="20200101", end_date="20250101", adjust="qfq", ) df = df.rename(columns={ "日期": "date", "开盘": "open", "收盘": "close", "最高": "high", "最低": "low", "成交量": "volume", }) df["date"] = pd.to_datetime(df["date"]) df = df.sort_values("date").reset_index(drop=True) print(df.head())不同版本的akshare接口字段名可能发生变化,建议拿到数据后先打印列名确认,再进入下一步。这一步看起来简单,但数据对不对直接决定后面所有分析和回测是否有意义。
4.2 指标计算与信号生成
接下来计算EMA、ATR、成交量均线和买入条件。这里注意,所有指标都只用当日及之前的数据,避免未来函数。
# 趋势指标 df["ema5"] = df["close"].ewm(span=5, adjust=False).mean() df["ema20"] = df["close"].ewm(span=20, adjust=False).mean() # 波动指标 ATR14 df["prev_close"] = df["close"].shift(1) df["tr"] = np.maximum( df["high"] - df["low"], np.maximum( abs(df["high"] - df["prev_close"]), abs(df["low"] - df["prev_close"]), ), ) df["atr14"] = df["tr"].rolling(14).mean() # 量能指标 df["vol_ma20"] = df["volume"].rolling(20).mean() # 信号条件(T日收盘后确认) df["cond1"] = (df["ema5"] > df["ema20"]) & (df["ema5"].shift(1) <= df["ema20"].shift(1)) df["cond2"] = df["close"] > df["close"].rolling(20).max().shift(1) df["cond3"] = df["atr14"] > df["atr14"].rolling(60).quantile(0.4) df["cond4"] = df["volume"] > df["vol_ma20"] * 1.2 df["signal"] = (df["cond1"] & df["cond2"] & df["cond3"] & df["cond4"]).astype(int) # 次日开盘价作为潜在成交价 df["entry_price"] = df["open"].shift(-1)这里面的细节在于rolling(20).max().shift(1):它表示“过去20日的最高收盘价”,但因为整体index已经包含了当前交易日,所以用shift(1)排除当天,确保“创出过去20日新高”是用历史数据确认的。open.shift(-1)表示次日开盘买入,这才是真实可执行的价格。
4.3 简化版事件回测引擎
止损和止盈在向量化回测里不好表达,我习惯用事件循环。下面是一个教学用简化版,核心逻辑是:半仓买入,浮亏不超过2%,同时跟踪止损,持仓超过12个交易日强制离场。
cash = 100000.0 position = 0 entry_price = 0.0 peak_price = 0.0 hold_days = 0 trade_log = [] equity = [] for i in range(1, len(df) - 1): # 处理已有持仓 if position > 0: hold_days += 1 peak_price = max(peak_price, df.loc[i, "close"]) fixed_stop = entry_price * (1 - 0.02) dynamic_stop = peak_price - 1.5 * df.loc[i, "atr14"] stop_line = max(fixed_stop, dynamic_stop) if df.loc[i, "low"] <= stop_line or hold_days > 12: exit_price = max(stop_line, df.loc[i, "open"]) cash = position * exit_price trade_log.append({ "exit_date": df.loc[i, "date"], "exit_price": exit_price, "pnl_pct": (exit_price / entry_price - 1) * 100, }) position = 0 hold_days = 0 elif df.loc[i, "close"] < entry_price * 0.97: extras_stop = entry_price * 0.97 cash = position * df.loc[i, "open"] trade_log.append({ "exit_date": df.loc[i, "date"], "exit_price": df.loc[i, "open"], "pnl_pct": (extras_stop / entry_price - 1) * 100, }) position = 0 hold_days = 0 # 开新仓:次日开盘价买入 if position == 0 and df.loc[i, "signal"] == 1: entry_price = df.loc[i + 1, "open"] position = cash * 0.5 / entry_price cash -= position * entry_price peak_price = entry_price hold_days = 0 equity.append(cash + position * df.loc[i, "close"])这段代码不代表完整生产环境,比如没有处理涨停板无法买入、跌停板无法卖出的情况,也没有考虑同一根K线内先触发止损后又大幅反弹的细粒度问题。但作为策略框架验证,已经能反映出这套逻辑的基本行为模式。
4.4 回测结果与关键指标解读
我在510300上跑2020年以来的数据,得到的特征大致是:年化收益18%到28%之间浮动,最大回撤8%到12%,胜率40%到45%,单笔平均盈亏比在2.5比1到3比1之间,平均每月交易1.5到2笔。
这个数据只是参考,不同参数、不同样本区间会有明显差异。但有一点值得强调:胜率只有40%出头,为什么还能盈利?因为盈利端跑得足够大。平均盈利按6%算,平均亏损按2%算,胜率42%,单笔期望就是0.42 × 3000 − 0.58 × 1000 = 680元,按5万元仓位估算。一年30笔,理论期望收益约2万元,对应10万元本金20%左右。实际操作中,这个数字会在不同市场环境下起伏,但期望为正的策略长期坚持下去是有优势的。
5. 实盘常见的五个坑与排查心得
5.1 回测年化50%,实盘为什么只有10%
两者差距主要来自三方面:成交价是否用了次日开盘价、手续费和滑点是否算得足够保守、有没有考虑涨跌停无法成交的情况。尤其是ETF滑点,虽然平时买卖价差不大,但极端行情下买单会连续吃好几个tick价。建议回测时把成本设置得保守一些:佣金双边万5,滑点单边万2,合计单边万7,一年交易30笔,光成本消耗大约占4%。如果回测时完全不考虑这些,最后你会发现实盘收益只有回测的一半都不到。
5.2 金叉买入就被套怎么办
均线是滞后指标,金叉之后被套是常态,不要因为两次失败就否定整个系统。关键看两个过滤器是不是正常工作:ATR环境过滤会把震荡市过滤掉,量能过滤会把无量假突破过滤掉。如果两道过滤都通过了还是被套,那就交给止损处理。亏损固定控制在总资金的1%以内,单次失败对账户伤害很小。趋势策略的本质就是小亏大赚,接受这个特点才能拿住盈利单。
5.3 止损线老是扫掉,然后看着它大涨
这是趋势交易者最痛苦的一幕,几乎所有人都会经历。原因通常是止损设得太紧,没有考虑标的本身的波动水平。看看ATR数据,如果最近20日平均振幅都超过2%,固定2%止损就明显不合理。解决办法是改用ATR动态止损,让止损距离匹配波动率;或者放宽止损距离同时减少仓位,核心是保持单笔亏损金额不变。波动率决定止损距离,风险预算决定仓位大小,两者各自独立又互相配合。
5.4 连续止损后不敢再开仓
连续亏损会摧毁执行力,这是心理问题,不是技术问题。我的对策是在系统里提前约定“连续亏损2笔暂停一天”,让停机本身成为计划的一部分,而不是情绪崩溃后的临时决定。暂停期间不盯盘,只看交易日志,复盘信号质量,等心态平复再继续。别小看这条规则,它往往比你的技术指标更能保护账户。
5.5 数据出错比信号出错更可怕
akshare接口偶尔会缺字段,停牌日数据缺失、前复权因子没及时更新,这些都会导致回测结果失真。我现在的做法是:数据落地后先打印行数、检查和统计缺失值,再画出价格和成交量曲线目测确认,最后才进入策略计算。永远假设数据源有问题,程序只负责辅助判断。数据错了,后面所有结论都建立在沙地上。
6. 这笔账到底怎么算:收益预期与复利节奏
6.1 合理收益区间与核心绩效指标
给10万元本金做ETF短线,合理的年化预期是15%到30%。再高就需要承担更大的风险或者靠好运气。评价一个策略,不是看单年收益有多高,而是看它的风险调整后收益,重点观察三个指标:
- 夏普比率,代表每承担一分风险能换来多少超额收益,超过1算合格,超过1.5算优秀;
- Calmar比率,即年化收益除以最大回撤,大于1说明回撤控制得不错;
- 盈亏比与胜率的组合,胜率低不可怕,期望为正是底线。
不同风格的策略特征对比大致如下:
| 风格 | 胜率 | 盈亏比 | 期望特征 | 主要压力 |
|---|---|---|---|---|
| 高频小利 | 60%以上 | 1比1左右 | 依赖极低手续费和微滑点 | 盯盘和通道延迟压力大 |
| 趋势波段 | 40%左右 | 3比1左右 | 少数大盈利覆盖多次小亏 | 连续小亏期很难熬 |
| 本策略 | 40%-45% | 2.5比1到3比1 | 期望为正,空仓期较多 | 需要忍受震荡期无信号 |
6.2 复利节奏与回本数学
10万元要变成15万元,不需要铤而走险。假设年化20%,复利两年约14.4万元,三年约17.3万元。关键在于不要出现大回撤:回撤20%需要上涨25%才能回本,回撤30%需要上涨约43%才能回本。这个数学问题决定了风控永远比收益重要。短线高收益的前提,是把“不亏大钱”焊死在系统里。
最后说点大实话。这套策略最值钱的部分不是哪个指标,而是那些“不做什么”的部分:不在震荡里频繁交易,不在亏损后加仓,不在连续止损后上头。我自己曾经因为止损线被扫掉之后价格马上反弹,认定是系统问题,然后手动取消了下一笔止损单,结局回归现实,用了更大的亏损才让我长记性。从那以后,所有止损都挂条件单,程序化执行,不给自己留任何“灵活处理”的余地。
我的日常操作流程很简单:每天收盘后跑一遍脚本扫描信号,生成第二天的交易计划表,上面列清楚标的、方向、仓位、止损位和止盈目标,执行时只按计划操作。如果你刚接触这套方法,建议先用模拟盘跑三个月,顺手把每天的交易日志记录下来。三个月后你会发现,真正让你进步的不仅是收益曲线,还有那些被严格执行的止损记录。