上一篇我们看完了实盘通道的全景,也搭好了通道无关的下单抽象层。但在真金白银上场之前,还差最后一道工序——纸面交易(Paper Trading):用真实行情、真实信号、真实下单逻辑,但只用虚拟资金记账。它是回测与实盘之间唯一的桥梁:回测验证「想法在历史上成立」,纸面验证「系统在当下能跑」——信号准时吗、下单逻辑对吗、监控告警灵吗、你自己的手痒不痒。本篇讲透纸面交易系统的最小实现,以及从纸面切换到实盘前的完整检查清单。
本篇目标
- 能说清纸面交易与回测的本质区别:验证目标不同(历史有效性 vs 当前系统可靠性)
- 能实现一套最小的纸面交易系统:每日信号 → 记录委托 → 按真实行情撮合 → 记账复盘
- 能做每日复盘:信号对照、成交对照、净值跟踪
- 能逐项核对「切换实盘前的检查清单」,不凭感觉上实盘
- 能给自己立下小资金试水的纪律条款
核心概念
纸面交易验证的是「系统」,不是「策略」
回测已经回答了「策略逻辑在历史上是否有效」。纸面交易回答的是另一组问题:数据管道每天准时跑吗(第 42 篇)?信号生成与预期一致吗?下单代码路径没有 bug 吗?监控告警真的会响吗(第 43 篇)?行情断流时系统会怎样?——这些是「工程可靠性」问题,只有让系统在真实时间流里连续运行才能暴露。
时间尺度也有讲究:纸面期至少覆盖一个季度,最好半年。太短的纸面期碰不到足够多的市场状态(一次像样的回调都没遇到,风控代码等于没测过),也碰不到足够多的工程异常(接口变更、服务器重启、节假日安排)。
模拟盘与实盘的偏差来源
即便纸面交易,也要清醒它和实盘的剩余偏差:成交假设(纸面按收盘价/开盘价成交,实盘有限价排队、滑点、部分成交);情绪因素(纸面亏损不心疼,实盘手会抖——这是纸面永远无法测试的一项,只能靠纪律对冲);资金约束(纸面资金无限精确,实盘有整手、可用资金、涨跌停限制)。所以纸面通过≠实盘稳赚,它只证明「系统这条流水线能转」。
划重点:纸面交易的核心纪律是像真的钱一样记录。每笔委托记录时间、价格、数量、理由;每天收盘对照行情复盘;每周算一次净值与回撤。糊弄记账的纸面交易,等于用回测心态跑实盘系统——白费。第 45 篇的
PaperBroker就是为此准备的记账引擎。
切换清单:上实盘前的最后一道闸
从纸面切实盘不是「感觉差不多了」就切,而是逐项打勾:参数冻结了吗、纸面期够长吗、异常预案演练过吗、资金计划定了吗……本篇代码部分给出可执行的清单评估器——清单上任何一项是「否」,就不要切。这不是教条,是无数账户的学费换来的程序化冷静。
完整代码
本篇依赖:uv pip install pandas numpy。基于第 45 篇的 PaperBroker(此处内联精简版保持独立可跑)搭建完整纸面系统。
# ---------- 1. 纸面交易引擎(承接第45篇 PaperBroker,精简内联) ----------
import numpy as np
import pandas as pd
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
@dataclass
class Order:
symbol: str; side: str; price: float; volume: int
status: str = "pending"; filled_volume: int = 0
msgs: list = field(default_factory=list)
class PaperBroker:
def __init__(self, init_cash=100000.0, fee=0.001, slip=0.001):
self.cash = init_cash
self.pos = {}
self.fee, self.slip = fee, slip
self.orders = [] # 全部委托留痕
def _fill(self, symbol, side, price, volume):
fill_px = price * (1 + self.slip if side == "buy" else -self.slip)
cost = volume * fill_px * (1 + self.fee if side == "buy" else -self.fee)
self.cash -= cost
self.pos[symbol] = self.pos.get(symbol, 0) + \
(volume if side == "buy" else -volume)
o = Order(symbol, side, price, volume, "filled", volume,
[f"{datetime.now():%H:%M:%S} 成交价{fill_px:.3f}"])
self.orders.append(o)
return o
def buy(self, symbol, price, volume):
return self._fill(symbol, "buy", price, volume)
def sell(self, symbol, price, volume):
return self._fill(symbol, "sell", price, volume)
def position(self, symbol):
return self.pos.get(symbol, 0)
# ---------- 2. 每日信号 → 委托 → 记录(纸面主循环的单日切片) ----------
def get_market_data(symbol):
"""获取当日行情。akshare 优先,失败用模拟价(教学)"""
try:
import akshare as ak
df = ak.fund_etf_hist_em(symbol=symbol, period="daily")
row = df.iloc[-1]
return float(row["收盘"])
except Exception:
rng = np.random.default_rng(int(datetime.now().strftime("%H%M")))
return 4.0 * (1 + rng.normal(0, 0.01)) # 模拟当日收盘价
def daily_signal(px_history):
"""双均线信号:返回 1(持有多头) / 0(空仓)。只用历史数据"""
s = pd.Series(px_history)
return int(s.rolling(5).mean().iloc[-1] > s.rolling(20).mean().iloc[-1])
def run_paper_day(broker, symbol, px_history, trade_log):
"""纸面交易的完整单日流程"""
price = get_market_data(symbol)
sig = daily_signal(px_history) # ① 收盘后算信号
cur = broker.position(symbol)
want = sig * 10000 # 目标持仓股数
if want > cur: # ② 生成委托(次日口径简化为当日)
o = broker.buy(symbol, price, want - cur)
elif want < cur:
o = broker.sell(symbol, price, cur - want)
else:
o = None
# ③ 记录:日期/信号/价格/动作/现金/持仓
trade_log.append({
"date": datetime.now().strftime("%Y-%m-%d"),
"signal": sig, "price": round(price, 3),
"action": (o.side if o else "hold"),
"volume": (o.volume if o else 0),
"cash": round(broker.cash, 2),
"position": broker.position(symbol),
})
return trade_log
# ---------- 3. 每日复盘:净值跟踪与信号核对 ----------
def paper_review(broker, symbol, trade_log):
"""收盘后复盘:当前净值、当日动作、累计交易统计"""
price = get_market_data(symbol)
nav = broker.cash + broker.position(symbol) * price
df = pd.DataFrame(trade_log)
n_switch = (df["action"] != "hold").sum() if len(df) else 0
print(f"--- 纸面复盘 {datetime.now():%Y-%m-%d} ---")
print(f"当前净值: {nav:,.2f} (初始 100,000)")
print(f"累计信号切换: {n_switch} 次, 委托总数: {len(broker.orders)}")
if len(df):
print("最近3天动作:")
print(df.tail(3).to_string(index=False))
Path("logs").mkdir(exist_ok=True)
df.to_csv("logs/paper_trades.csv", index=False, encoding="utf-8-sig")
return nav
# ---------- 演示运行 ----------
broker = PaperBroker()
rng = np.random.default_rng(46)
px_history = list(4.0 * np.exp(np.cumsum(rng.normal(0.0002, 0.01, 60))))
trade_log = []
for day in range(3): # 模拟连续3个交易日
px_history.append(get_market_data("510300"))
trade_log = run_paper_day(broker, "510300", px_history, trade_log)
paper_review(broker, "510300", trade_log)
# ---------- 4. 切换实盘前的检查清单评估器 ----------
def readiness_check():
"""逐项评估。任何 NO → 不具备切换条件"""
checks = [
("纸面交易连续运行满 3 个月,无中断", None),
("纸面期间信号生成 0 次迟到/缺失", None),
("策略参数已冻结(写入 git commit,见第44篇)", None),
("实盘通道已开通并完成 1 笔最小单位真实委托", None),
("断线重连演练:手动断网后系统能恢复持仓状态", None),
("异常预案成文:接口失效/数据缺失/涨跌停的处理动作", None),
("实盘资金计划:金额 ≤ 总资产可亏损部分,且 3 个月不动用", None),
("风控在线:止损规则(第35篇)在纸面期间至少触发过 1 次", None),
("监控告警实测:人为制造一次异常,告警能送达", None),
("对账机制:每日策略持仓 vs 通道持仓比对", None),
]
print("=== 实盘切换检查清单 ===")
for i, (item, _) in enumerate(checks, 1):
ans = input(f"{i:2d}. {item} [y/n]: ").strip().lower()
if ans != "y":
print(f">>> 第 {i} 项未通过 —— 请补齐后再切换实盘")
return False
print(">>> 全部通过,可以进入小资金实盘阶段")
return True
# readiness_check() # 取消注释交互式运行
代码逐段解释
- PaperBroker:第 45 篇的精简复刻,加了
orders列表留存全部委托——纸面期的事后审计全靠它。 - 单日流程:
get_market_data拿当日真实(或模拟)收盘价 →daily_signal用含今天在内的历史算双均线信号 → 与当前持仓比对生成增量委托 → 全过程写入 trade_log。这个「信号-委托-记账」三步就是纸面系统的全部骨架,接上第 42 篇的定时任务,它每天自动转。 - 复盘函数:净值 = 现金 + 持仓×现价;统计信号切换次数与委托总数;最近三天动作打印出来肉眼核对;trade_log 落盘 CSV。每日一眼、每周汇总是纸面期的基本功课。
- 清单评估器:十项检查逐项 y/n,任何一项不过就明确拒切。注意第 7 项的资金计划与第 8 项「止损至少触发过一次」——没见过止损动作的风控代码,等于没上过膛的枪。
示例输出(示意,模拟行情下):
--- 纸面复盘 2026-09-28 ---
当前净值: 99,842.15 (初始 100,000)
累计信号切换: 2 次, 委托总数: 2
最近3天动作:
date signal price action volume cash position
0 2026-09-24 0 4.052 hold 0 100000.0 0
1 2026-09-25 1 4.061 buy 10000 58627.4 10000
2 2026-09-28 1 4.048 hold 0 58627.4 10000
常见坑与小练习
常见坑
- 纸面跑了三天就急着切实盘:没经历过回调、没触发过止损、没遇到过接口异常,等于裸考。三个月是最短纸面期,不是最长。
- 纸面系统与回测代码两套逻辑:信号计算在回测里一套写法、纸面里另一套重写,两者渐行渐远。正确做法是共用
strategies/里的同一份信号模块(第 41 篇的分层价值再次显现)。 - 记账不认真:漏记一天、价格手填错一次,复盘基础就毁了。记账必须由代码完成,人工只做核对。
- 清单变成走过场:答 y 之前不看证据。每一项都应该有物证:git log 里的 commit、logs/ 里的记录、真实的演练截图。
- 切实盘后立刻放大资金:小资金阶段的目的是验证「真实滑点下的系统行为」,不是赚钱。至少一个月的实盘观察期后,再谈资金规模。
小练习
- 把
run_paper_day接上第 42 篇的计划任务,实现「每个交易日 15:05 自动运行、写日志、更新心跳」的全自动纸面系统。思路提示:入口脚本调用 run_paper_day + write_heartbeat;数据用当日 akshare 实时接口或 15:05 拉的日线。
- 在
paper_review里加「信号核对」:单独重算当日信号与 trade_log 里记录的 signal 比对,不一致立即告警(这是防信号逻辑被无意改动的哨兵)。思路提示:用第 44 篇的思路给 strategies/ 的信号函数做独立复算,双路径比对。
- 把检查清单从 input 交互改成读 YAML 配置(每项附物证说明),输出一份可 git 管理的「切换评审报告」。
思路提示:
checks.yaml里每项含 passed/evidence 字段,脚本汇总成 markdown 报告——让「切实盘」这个决定本身也有版本记录。
本文策略仅用于教学,不构成投资建议,实盘前请充分测试。