写收评最怕的不是行情判断错,而是判断本身没有边界。比如“这个位置不破,指数和个股的反弹还将延续”,这句话听起来很有道理,但真正落到操作上,至少缺少三个可计算的部分:这个位置具体是哪个价格、怎样才算“不破”、如果破了应该用什么方式防守。与其把收评写成涨跌预测,不如把它变成一个可以运行的规则系统。本文从“26年8月12日股市收评!这个位置不破指数个股反弹将还会延续!移动跟踪防守即可!”这句典型的收盘总结出发,拆解其中的技术判断,并用 Python 实现一个“收盘复盘脚本”,输出关键位置、移动防守线和当日信号。文中的行情数据为模拟数据,所有结果只用于演示规则,不构成投资建议。
1. 先理解收评里的“位置不破”和“移动跟踪防守”到底指什么
1.1 收评的三种写法,只有一种能沉淀成代码
日常看到的收评大概可以分成三类。
第一类是预测式,例如“明天大概率上涨”“下周行情值得期待”。这类写法的问题在于没有明确边界,第二天如果没涨,很难说它是错了还是需要更多时间验证,无法闭环。
第二类是观点式,例如“当前位置有支撑,反弹有望延续”。这句话点出了方向,但没有给出“支撑”的具体定义,也没有说明什么情况下观点失效,读者看完不知道该在哪个价格做决策。
第三类是规则式,例如“指数只要不跌破 3200 点,日线反弹结构保持;若收盘跌破 3200 点,则离场或降低仓位,并设置移动防守线”。这里有位置、有触发条件、有应对动作,最关键的是它能把每个概念翻译成程序里的变量和 if 条件。
写代码也好,写收评也好,真正有价值的是第三类。预测式观点很难验证,规则式判断可以被回测、被复盘、被优化。本文要做的,就是把标题里那句“位置不破、移动跟踪防守”改造成规则式判断。
1.2 “这个位置不破”里的“位置”是什么
技术分析里,“位置”通常指支撑位或压力位。支撑位的常见来源包括:
- 前 N 日最低点;
- 均线,比如 MA20、MA60;
- 密集成交平台;
- 跳空缺口边缘;
- 筹码分布峰值区域。
在代码中,最容易落地的是“前低”和“均线”。例如指数在一段时间内反复在 3200 点附近止跌,那么 3200 就是观察位。这里要注意,支撑位不是一条绝对精确的线,它更像一个价格区间。程序里如果只有一个固定数值,需要额外设置缓冲或判定标准。
“不破”也有多种定义。日线级别最常见的三种:
| 判定方式 | 含义 | 优点 | 缺点 |
|---|---|---|---|
| 收盘价不破 | 每日收盘价高于支撑位 | 信号稳定,过滤盘中噪音 | 反应慢,盘中大跌时无法及时提示 |
| 最低价不破 | 每日最低价高于支撑位 | 对风险敏感,能捕捉假破位 | 容易把盘中插针误判为破位 |
| 连续两日收盘不破 | 连续两天收盘价高于支撑位 | 过滤瞬时波动效果好 | 延迟明显,可能已经回吐较多利润 |
实际项目中,应该把“破位”和“跌破”区分开。比如盘中最低价瞬间跌破 3200,但收盘又拉回 3200 上方,这算不算破位?不同人有不同理解。代码要支持两种模式,而不是把定义写死。
1.3 “移动跟踪防守”是防守线的移动,不是固定止损
固定止损的思路是:买入后设置一个固定比例,比如亏 5% 就离场。价格涨到 10% 时,止损线还是亏损 5% 的位置,这会导致利润大幅回吐。
移动止损的思路正好相反:随着价格不断走高,把防守线也逐步上移,让防守线始终跟住当前价格。比如最高涨到 100 元,按回撤 5% 计算,防守线就是 95 元;后来最高涨到 110 元,防守线自动上移到 104.5 元。这样做的好处是,不需要猜测顶部在哪里,只要价格不跌回防守线,就默认趋势还在;一旦跌破防守线,说明反弹结构可能改变,立刻执行防守动作。
收评中的“移动跟踪防守即可”,本质上就是这么一套规则。它不是预言价格会涨到哪,而是告诉读者:只要防守线不被跌穿,就继续按反弹思路跟踪;跌穿了,就切换到防守状态。
2. 先把行情数据变成 DataFrame,再定义关键位置
2.1 准备数据:模拟数据与真实行情接口
在写策略之前,先要有一套“收盘数据”。最方便的方式是构造一个带开盘价、最高价、最低价、收盘价和成交量的 DataFrame。
import pandas as pd import numpy as np # 模拟数据仅用于演示,不代表真实行情 trade_days = pd.date_range("2026-07-28", periods=12, freq="B") mock = { "open": [3190, 3204, 3208, 3216, 3222, 3228, 3236, 3248, 3252, 3260, 3274, 3282], "high": [3206, 3215, 3224, 3223, 3238, 3241, 3254, 3263, 3261, 3275, 3286, 3294], "low": [3185, 3198, 3202, 3208, 3212, 3220, 3228, 3238, 3244, 3252, 3262, 3270], "close": [3201, 3210, 3218, 3217, 3232, 3235, 3249, 3257, 3256, 3270, 3280, 3288], "volume": [10000, 12000, 11000, 9000, 13000, 14000, 15000, 12500, 11800, 16000, 17000, 18000], } df = pd.DataFrame(mock, index=trade_days) print(df.tail())这里用了 12 个交易日的数据,日期从 2026-07-28 到 2026-08-14,其中一定包含 8 月 12 日这个示例日期。真实项目中不要用模拟数据跑结论,应该接行情数据源,比如 akshare、tushare 或自建数据库,并且要确认接口版本、返回字段和复权方式。
2.2 关键支撑位的自动计算:前低和均线
支撑位可以由程序自动计算,也可以手工预设。手工预设适合“某个整数关口”,但扩展性差,所以代码里先计算常见的两个指标。
# MA5 只用于演示,真实日线策略一般看 MA20、MA60 df["ma5"] = df["close"].rolling(5).mean() # 前 5 日低点,shift(1) 表示不包含当天 df["prior_low_5"] = df["low"].shift(1).rolling(5).min() print(df[["close", "ma5", "prior_low_5"]])重点说明一下rolling的窗口含义。rolling(5).min()会取最近 5 根 K 线的最低价,但如果不加shift(1),就会把当天最低价也包含进去,这在收盘复盘时会产生“收盘后才知道当天最低价”的问题。复盘场景下当天数据已经确定,所以用不用shift(1)取决于你是否希望支撑位包含当日极值。
如果希望支撑位更稳定,可以用 MA20;如果希望更贴近近期波动,可以用 MA10 或前低。不要同时塞太多指标,先从单一规则跑通。
2.3 把“不破位”写成可计算的判断列
有了收盘价、最低价和支撑位之后,就可以生成两个布尔列:一个表示“最低价是否守住支撑”,另一个表示“收盘价是否守住支撑”。
support = 3200 # 手工预设关键位置,也可以取前低或均线值 df["above_support"] = df["low"] > support df["close_above_support"] = df["close"] > support # 自定义:收盘和最低价都不破才算“位置守住” df["hold_structure"] = df["close_above_support"] & df["above_support"] print(df[["close", "low", "above_support", "close_above_support", "hold_structure"]])这里的support是写死的,后续可以改成动态计算,比如df["prior_low_5"]或df["ma5"]。用固定数值的好处是逻辑简单,坏处是市场波动变大后,固定位置可能很快失效。生产环境建议把支撑位来源抽象成函数。
3. 实现移动跟踪防守:从“最高点回撤”到“动态防守线”
3.1 移动防守线的计算公式
移动防守线最朴素的做法是:从入场后出现的最高价往下回撤固定比例。
trail_pct = 0.03 # 最高价回撤 3% 触发防守 # 直接基于全样本 cummax 的写法,只适合无中途离场的简单场景 df["max_high"] = df["high"].cummax() df["trail_stop"] = df["max_high"] * (1 - trail_pct) print(df[["high", "max_high", "trail_stop"]])cummax会从样本第一根 K 线开始累计最高价,所以如果中间发生过止损离场,再用这个全局最高价计算防守线就不准确。更合理的做法是在持仓循环中单独维护一个变量:只有持仓期间的最高价才能决定防守线。
3.2 生成买入、持有和离场信号
下面这段代码用逐根 K 线遍历的方式处理状态。空仓时,如果收盘价站上关键位置,就买入;持仓时,如果最低价跌破移动防守线,就离场。
position = 0 entry_price = 0.0 max_price_after_entry = 0.0 trail_stop = 0.0 signals = [] for idx, row in df.iterrows(): if position == 1: max_price_after_entry = max(max_price_after_entry, row["high"]) trail_stop = max_price_after_entry * (1 - trail_pct) # 最低价跌破防守线,按防守线离场 if row["low"] < trail_stop: exit_price = trail_stop signals.append((idx, "sell", exit_price)) position = 0 max_price_after_entry = 0.0 trail_stop = 0.0 continue if position == 0 and row["close"] > support: entry_price = row["close"] position = 1 max_price_after_entry = row["high"] trail_stop = max_price_after_entry * (1 - trail_pct) signals.append((idx, "buy", entry_price)) print("交易信号:") for sig in signals: print(sig)这段代码有两个地方需要结合真实场景理解。
第一,离场按trail_stop价格成交,是理想化假设。实际盘中跌破防守线时,卖出价格大概率低于防守线,所以回测结果会偏乐观。
第二,同一根 K 线内先触发离场、后面又满足买入条件时,程序用continue跳过了同一天的买入。这是避免在同一天反复交易,但也会漏掉一些真实可能出现的入场机会。实盘策略通常会在日线级别把交易信号放在次日执行,而不是当天收盘立即执行。
3.3 把信号翻译成收评文本
程序算出的是布尔值和数字,收评要的是人能直接读懂的句子。到这一步可以把最新状态格式化输出。
latest = df.iloc[-1] current_stop = df["trail_stop"].iloc[-1] status = "持有" if position == 1 else "空仓" print("收盘复盘输出:") print(f"关键位置:{support}") print(f"最新收盘:{latest['close']:.2f}") print(f"最新最低价:{latest['low']:.2f}") print(f"移动防守线:{current_stop:.2f}") print(f"当前状态:{status}")输出结果类似:
收盘复盘输出: 关键位置:3200 最新收盘:3288.00 最新最低价:3270.00 移动防守线:3195.18 当前状态:持有这里“持有”的含义是:价格仍在关键位置上方,且没有跌破移动防守线。它不代表明天一定上涨,只代表当前规则没有给出离场信号。
4. 做个简单回测,验证“不破就持有、破了就防守”是否可行
4.1 用资金曲线做一次最小回测
回测的目的不是证明策略很赚钱,而是验证规则在历史数据上是否会按预期产生买卖。最小回测可以用一个简单的资金账户模拟。
cash = 100000.0 hold_num = 0 buy_price = 0.0 max_high = 0.0 stop_line = 0.0 records = [] df["strategy_value"] = np.nan for idx, row in df.iterrows(): if hold_num > 0: max_high = max(max_high, row["high"]) stop_line = max_high * (1 - trail_pct) if row["low"] < stop_line: cash = hold_num * stop_line records.append(("sell", idx, stop_line)) hold_num = 0 max_high = 0.0 stop_line = 0.0 if hold_num == 0 and row["close"] > support: buy_price = row["close"] hold_num = cash / buy_price cash = 0.0 max_high = row["high"] stop_line = max_high * (1 - trail_pct) records.append(("buy", idx, buy_price)) df.loc[idx, "strategy_value"] = cash + hold_num * row["close"] print("交易记录:") for rec in records: print(rec) total_return = df["strategy_value"].iloc[-1] / 100000.0 - 1 print(f"策略总收益:{total_return:.2%}")这里用“现金 + 持仓市值”计算每个交易日的策略净值。买入时用全部现金,卖出时把持仓按防守线价格换成现金。
4.2 为什么回测结果不能直接当成预期收益
上面的回测没有包含以下因素:
- 手续费;
- 印花税;
- 滑点;
- T+1 限制;
- 停牌和涨跌停无法成交;
- 入场与离场信号发生在同一根 K 线的处理。
任何一个因素都足以明显改变结果。比如 A 股当日买入不能当日卖出,如果买入信号出现在盘中,当天无法验证是否成功成交,需要额外逻辑处理。因此,最小回测只适合验证规则是否正确,不适合用来估计未来收益。
另一个常见问题是样本量。10 多根 K 线只能算一个演示片段,连统计数据都谈不上。真实项目里做回测,至少要用几年日线数据,并且要做滚动样本外测试。
4.3 胜率和盈亏比要成对统计
如果需要统计胜率,不能只看最后的资金曲线,而要把买卖配对。
buy_prices = [] sell_prices = [] for rec in records: if rec[0] == "buy": buy_prices.append(rec[2]) elif rec[0] == "sell" and len(buy_prices) > len(sell_prices): sell_prices.append(rec[2]) profits = [] for bp, sp in zip(buy_prices, sell_prices): profits.append(sp / bp - 1) if profits: win_rate = sum(1 for p in profits if p > 0) / len(profits) avg_profit = sum(profits) / len(profits) print(f"交易笔数:{len(profits)}") print(f"胜率:{win_rate:.2%}") print(f"平均单笔收益率:{avg_profit:.2%}") else: print("交易笔数不足,无法统计胜率")胜率不是唯一指标。一笔赚 20%、两笔亏 5%,胜率只有三分之一,但策略仍然可能盈利。关键要看盈亏比和交易次数是否足够。
5. 常见问题:为什么算出来的防守位总和你理解的不一样
5.1 防守线跳来跳去,或者一直不动
现象:防守线有时突然跳到很低的位置,有时又长时间不变。
常见原因是cummax()在全样本上累计了历史最高点。如果程序在中途离场后又重新入场,离场前的最高价仍然参与计算,导致新持仓的防守线被顶得很高,策略变得非常迟钝。
正确做法是在每次入场时重置max_price_after_entry,只累计当前持仓期间的最高价。
# 错误示范:全样本累计最高价 df["trail_stop"] = df["high"].cummax() * (1 - trail_pct) # 正确思路:持仓期间单独维护变量 if position == 1: max_price_after_entry = max(max_price_after_entry, row["high"]) trail_stop = max_price_after_entry * (1 - trail_pct)5.2 盘中明明破位了,程序却没有给卖出信号
现象:最低价已经跌破防守线,但程序输出“持有”。
原因多半是判断条件用了收盘价,或者只检查了close < stop_line。盘中破位和收盘破位是两种不同定义,代码里要显式选择。
# 敏感型:最低价破位立即离场 if row["low"] < trail_stop: ... # 稳健型:收盘价破位才离场 if row["close"] < trail_stop: ...实盘建议至少同时输出两种模式的结果,再决定用哪一种。敏感型容易在震荡行情中反复止损,稳健型又可能让利润回吐更多。没有绝对优劣,只有是否匹配自己的风险偏好。
5.3 除权除息导致价格跳空,支撑位和防守线失效
股票分红送转后,价格会向下跳空。如果用的是未复权数据,昨天收盘价 20 元,今天可能变成 15 元,支撑位和均线都会被突然拉低。这样算出来的防守线不仅没有参考价值,还会产生错误信号。
解决方法是使用“前复权”数据。前复权会以当前价格为基准,把历史价格按除权因子调整,让价格曲线保持连续。接入行情接口前,一定要确认返回的是不是前复权数据,以及复权基准是哪一天。
5.4 历史数据明明支撑住了,程序却反复止损
现象:价格在支撑位附近多次下探,程序每次都触发离场,但价格后面又涨回来了。
这可能不是程序错误,而是参数和判定粒度的问题。固定百分比trail_pct = 0.03对波动小的品种可能太紧,对波动大的品种可能太松。可以考虑用 ATR 代替固定百分比,让防守线随市场波动自动调整。
# 简单 ATR 近似算法 df["tr"] = np.maximum( df["high"] - df["low"], np.maximum( abs(df["high"] - df["close"].shift(1)), abs(df["low"] - df["close"].shift(1)) ) ) df["atr"] = df["tr"].rolling(14).mean() df["trail_stop_atr"] = df["max_high"] - 2 * df["atr"]这种写法仍然偏简化,真正的 ATR 通常用 Wilder 平滑算法。先理解思路,再根据自己熟悉的方式实现。
6. 从“收评脚本”到“每日自动复盘”,生产环境还缺什么
6.1 学习环境与生产环境的差距
学习环境里,手动运行脚本、打印输出就够了。生产环境要处理的远不止“策略逻辑是否正确”,还包括数据稳定性、调度、通知、存储和异常恢复。
| 维度 | 学习环境 | 生产环境 |
|---|---|---|
| 数据 | 模拟数据或本地 CSV | 行情接口、数据库,需要异常重试 |
| 调度 | 手动运行 | cron、APScheduler 或任务平台 |
| 通知 | print 输出 | 邮件、企业微信机器人、IM 通知 |
| 存储 | 不落库 | sqlite、MySQL、ClickHouse |
| 日志 | 控制台 | 文件日志、日志采集、告警 |
| 权限 | 本机运行 | 密钥管理、接口权限、只读账号 |
| 风控 | 未考虑 | 仓位限制、连续止损熔断 |
| 合规 | 可以随意写 | 明确风险提示,不承诺收益 |
6.2 定时运行:先用 cron 跑起来
如果只在收盘后跑一次,cron 是最简单的方案。假设脚本路径是/data/review/review.py,每个交易日 15:10 执行:
10 15 * * 1-5 cd /data/review && /usr/bin/python3 review.py >> review.log 2>&115:10是为了给数据源留出更新缓冲。如果数据接口更新延迟,可以把时间调晚一点。日志必须重定向到文件,否则 cron 执行失败时看不到报错。
也可以把每日结果写到 sqlite,方便后续复盘。
import sqlite3 conn = sqlite3.connect("review.db") df.tail(1)[["close", "ma5", "trail_stop"]].to_sql( "daily_review", conn, if_exists="append", index=True ) conn.close()注意to_sql在不同 pandas 版本下对 sqlite3 连接的支持不完全一样。如果遇到报错,可以改用 SQLAlchemy 的create_engine("sqlite:///review.db")再调用to_sql。
6.3 输出自动收评时,风险提示不能省
自动化脚本一旦接入 IM 机器人,输出就会直接触达读者。此时不能只发“当前状态:持有”,还要附上规则说明和风险提示。例如:
收盘复盘 关键位置:3200 最新收盘:3288 移动防守线:3195 状态:持有 说明:以上结果为规则计算输出,仅用于技术复盘,不构成投资建议。不要把“持有”包装成“必涨”,也不要把“离场”解读成“马上暴跌”。自动化工具的价值是稳定执行规则,而不是替用户做收益承诺。
7. 写收评前可以套用的技术清单
7.1 收评复盘清单
每次写收评或调试复盘脚本时,对照这张表检查一遍,可以减少规则不明确的问题。
| 检查项 | 说明 |
|---|---|
| 行情周期 | 是日线、60 分钟还是周线,不同周期支撑位意义不同 |
| 支撑位来源 | 前低、均线、平台、筹码区还是手工预设 |
| 破位判定 | 收盘破位、盘中破位还是连续 N 日破位 |
| 防守线算法 | 最高价回撤、ATR 跟踪、均线跟随 |
| 参数周期 | MA20 还是 MA60,回撤 3% 还是 5% |
| 信号动作 | 买入、持有、减仓、离场,动作要明确 |
| 输出格式 | 人读文本要包含位置、价格、状态和风险提示 |
| 复盘归档 | 每天结果落库,事后能检查规则是否有效 |
7.2 下一步可以扩展的方向
从当前最小闭环出发,可以按顺序做四件事。
第一,把固定支撑位改成动态支撑位。用前低或均线替代写死的 3200,规则就不再依赖人工设定。
第二,加入成交量过滤。反弹延续的可靠性往往和量能有关,缩量上涨时可以把防守线收紧,放量突破时可以把防守线放宽。
第三,引入多周期判断。日线判断方向,60 分钟线寻找更精确的防守位置,能减少单周期噪音。
第四,做历史信号归档和滚动回测。把每天产生的信号存下来,定期统计胜率、盈亏比和最大回撤,再用更新的数据做样本外测试。
7.3 对新手最值得做的一个练习
如果你刚开始接触这类复盘脚本,不要急着写复杂策略。先用模拟数据跑通“支撑位判断 + 移动止损”这一最小流程,然后接真实历史数据,最后再考虑自动化。最应该坚持的习惯是:每一条收评都必须能回答三个问题——关键位置是多少、怎么算破位、破位之后防守线在哪。回答不了这三个问题,文字写得再流畅,也不是一份合格的技术复盘。
收盘复盘的核心,不是预测明天涨还是跌,而是把“反弹还能不能延续”转成“位置有没有守住、防守线有没有被跌破”两个可判断的问题。脚本能自动跟踪,人只需要在规则被触发时做出正确响应。能做到这一点,收评就不再是一句口号,而是一套可以反复验证的工程方法。