☰
Tushare量化交易Python源码:数据、回测与每日信号流水线
2026/10/26 11:24:27 网站建设 项目流程

简介:面向量化交易初学者与Python开发者的一份tushare 0.2.8源码学习材料,围绕金融数据接口的获取、清洗、存储与实时行情等核心环节展开,适合希望读懂数据接口实现、提升网络请求与异常处理能力的人群。压缩包共26个文件,约181KB,以19个py源码为主体,另含3个txt说明、2个pkg-info元数据、1个csv样例和1个cfg配置,便于按包结构梳理模块职责与调用关系。已有6257人学习下载,具备一定参考热度。读者可结合源码理解数据请求与解析流程、数据处理函数、本地CSV/Excel存储、实时行情轮询及技术指标计算等实现思路,并从中借鉴接口设计、异常处理与金融数据工程的组织方式,用于课程实践、项目参考或量化分析入门。

1. 早上 8 点 40 的那次跑批,才是这套源码的真正用法

早上 8 点 40,定时任务把昨天的日线补进本地库;8 点 50,全市场选股结果生成好推到手机;9 点 20 之前,你只需要确认一下有没有要下单的标的。这套 tushare 量化交易 Python 源码干的就是这件事——以 tushare 作为行情数据源,把数据拉取、因子计算、策略回测、每日信号生成串成一条能定时跑、能重复跑、结果可复现的流水线。它适合两类人:一类是 Python 已经能写点脚本、想把“看盘感觉”变成可回测规则的交易者;另一类是手里有策略想法、缺一套数据加回测骨架的开发者。源码不是黑盒交易机器人,它更像一套骨架,策略、风控、执行三个位置都留了明确的改造入口,改起来不用推倒重来。

2. 项目骨架与技术选型:从数据接口到回测引擎的一条链路

这套源码的技术选型非常“务实”,没有上重型框架。核心依赖就四五个:tushare 负责取数,pandas 负责所有的表格运算和时间序列对齐,numpy 做向量化计算,matplotlib 出净值曲线,存储侧用 SQLite 或 Parquet 落本地文件,定时任务交给系统 crontab 或者 apscheduler。整套东西在笔记本上就能跑起来,不需要额外部署数据库服务,这对个人开发者来说是决定性的——量化项目最容易死在“环境还没搭好就没兴趣了”。

架构上,它走的是经典的四层切分:数据层、策略层、回测层、执行层。数据层只关心“拿到干净的行情”,策略层只关心“给定数据输出信号”,回测层只关心“按规则撮合并算绩效”,执行层只关心“今天该买什么该卖什么”。这样切的好处是回测和实盘共用同一个策略函数,避免出现“回测一套逻辑、实盘另写一套”的翻车现场——血泪经验,两边代码一旦分叉,对不上账就是时间问题。

2.1 目录结构:data / strategy / backtest / trade 四层怎么分

源码的目录大致长这样,压缩包解开后先对照着看一遍,后面读代码会快很多:

quant-tushare/ ├── config/ │ ├── config.yaml # 数据源、数据库路径、日志级别 │ └── strategy.yaml # 策略参数:均线周期、持仓数量、费率 ├── data/ │ ├── raw/ # 原始日线,按年分 Parquet │ └── cache/ # 中间计算结果,可随时删 ├── quant/ │ ├── datasource/ # 数据层:只跟 tushare 打交道 │ │ ├── api.py # 接口封装、限流、重试 │ │ ├── loader.py # 拉取 + 复权 + 字段裁剪 │ │ └── storage.py # 落库、增量更新、读取 │ ├── strategy/ # 策略层:纯计算,不碰网络 │ │ ├── base.py # 策略基类,约定输入输出 │ │ └── ma_cross.py # 双均线示例策略 │ ├── backtest/ # 回测层:撮合、持仓、绩效 │ │ ├── engine.py # 主循环 │ │ ├── broker.py # 手续费、滑点、T+1 │ │ └── metrics.py # 年化、回撤、夏普 │ ├── trade/ # 执行层:生成每日清单 │ │ ├── signal.py │ │ └── notify.py │ └── utils/ │ ├── logger.py │ └── calendar.py # 交易日历,判断是否开市 ├── scripts/ │ ├── update_data.py # 增量更新行情 │ ├── run_backtest.py # 跑回测 │ └── daily_signal.py # 生成每日信号 ├── notebooks/ # 研究用的临时脚本,可删 ├── tests/ ├── requirements.txt └── README.md

四层的边界是硬的:datasource里不允许出现策略逻辑,strategy里不允许发网络请求。这条纪律看起来刻板,但它决定了后面能不能把回测结果和实盘信号对齐。我见过太多项目把取数和计算揉在一个函数里,最后想加个缓存、想换个数据源,牵一发动全身。

notebooks/这个目录别小看,源码里留它是为了研究阶段快速试因子,试完把稳定的逻辑搬进strategy/,临时脚本就扔在那不动。生产代码和研究代码物理隔离,能省掉大量“这段到底跑不跑”的困惑。

2.2 依赖与配置:一份 requirements 和一个 config.yaml 说清的事

依赖清单很短,装的时候照着来就行:

# requirements.txt 的核心几项 tushare>=1.2.89 # 行情数据接口 pandas>=1.5.0 # 表格与时间序列 numpy>=1.23.0 matplotlib>=3.5.0 PyYAML>=6.0 # 读配置 pyarrow>=10.0.0 # Parquet 读写 loguru>=0.6.0 # 日志,比 logging 少写样板代码

安装就是常规两步,建议单独建虚拟环境,避免和系统里的 pandas 版本打架:

python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install -r requirements.txt

配置文件是整个项目的“参数面板”,token 千万不要写死在代码里提交上去:

# config/config.yaml datasource: token: ${TS_TOKEN} # 从环境变量读,不落盘 rate_limit: 480 # 每分钟调用次数上限,按账号权限调整 retry: 3 # 失败重试次数 retry_wait: 5 # 重试间隔秒数 storage: db_path: data/quant.db raw_dir: data/raw keep_years: 10 # 本地保留多少年历史 backtest: start: '20190101' end: '20241231' init_cash: 1000000 commission: 0.00025 # 佣金万2.5 stamp_tax: 0.001 # 卖出印花税千1 slippage: 0.001 # 滑点千1 max_positions: 10

${TS_TOKEN}这种写法需要在加载时做一次环境变量替换,源码里通常用一个load_config()函数处理。环境变量在 shell 里设一次即可:

export TS_TOKEN=你的token字符串

rate_limit这个参数是给数据层用的,不同权限的账号每分钟能调的接口次数不一样,写死一个偏保守的值最安全。slippage和commission直接决定回测收益的真实程度,很多人回测年化 80%、实盘年化 8%,八成就是这两项没设或者设得太乐观。

2.3 一次完整跑批的命令链路

从零到有信号,三条命令按顺序走,每条都是独立可重跑的:

# 1. 增量更新行情,只补本地缺失的交易日 python scripts/update_data.py --start 20190101 --end 20241231 # 2. 跑回测,输出净值曲线和绩效指标 python scripts/run_backtest.py --strategy ma_cross --config config/strategy.yaml # 3. 生成最近一个交易日的选股信号 python scripts/daily_signal.py --strategy ma_cross --date 20241231

第一条命令的--start是本地水位线的兜底值,实际起点由数据库里已有的最大交易日决定,不会重复拉。第二条命令的--strategy对应quant/strategy/下的模块名,加新策略就是加一个文件加一个同名参数。第三条命令输出的是一个 CSV,字段通常是ts_code, name, signal, price, reason,reason里写清触发了哪条规则,方便第二天复盘时知道当时为什么买。

这三条命令的组合方式决定了项目能不能自动跑:放到 crontab 里,第一条每天 17:30 跑(收盘后数据更新),第二条每周跑一次,第三条每个交易日 8:50 跑。定时任务里记得用绝对路径和虚拟环境的 python,否则 crontab 里找不到模块是经典问题。

3. 数据层拆解:tushare 拉数、复权与增量更新

数据层是整个项目最脏最累的部分,也是最值得先读透的部分。策略写得再花哨,数据错了全白搭。这一层的目标很明确:给定一个日期区间,本地库里就有对齐好的、复权正确的、没有缺口的日线数据,其他模块只管读,不管怎么来的。

3.1 接口封装:限流、重试和一处集中管理

tushare 的 pro 接口是有调用频率限制的,全市场几千只票一只一只拉,不做限流必然被拒。源码里通常会在datasource/api.py里包一层:

# quant/datasource/api.py import os import time import tushare as ts from loguru import logger class TSApi: def __init__(self, config): token = os.getenv(config['datasource']['token'].strip('${}')) if not token: raise RuntimeError('TS_TOKEN 环境变量未设置') self.pro = ts.pro_api(token) self.rate_limit = config['datasource']['rate_limit'] self.retry = config['datasource']['retry'] self.wait = config['datasource']['retry_wait'] self._last_call = 0.0 def _throttle(self): # 简单匀速限流:保证两次调用间隔不小于 60/rate_limit 秒 interval = 60.0 / self.rate_limit delta = time.time() - self._last_call if delta < interval: time.sleep(interval - delta) self._last_call = time.time() def call(self, func_name, **kwargs): self._throttle() for i in range(self.retry): try: df = getattr(self.pro, func_name)(**kwargs) if df is None: return None return df except Exception as e: logger.warning(f'{func_name} 第{i+1}次失败: {e}') time.sleep(self.wait * (i + 1)) # 退避,越往后等越久 logger.error(f'{func_name} 重试{self.retry}次仍失败, 参数={kwargs}') return None

_throttle用的是最朴素的匀速限流,比令牌桶简单,但对单机脚本够用。call里的重试是线性退避,不是指数退避,因为 tushare 的限流通常是分钟级的,等太久反而拖慢整体进度。getattr(self.pro, func_name)这种写法让调用方只传字符串,加新接口不用改封装层。

参数上要注意rate_limit别设太满。账号标称每分钟 500 次,你就写 480,留一点余量给网络抖动和重试。我有次把限流关掉想快点拉完,结果触发了临时封禁,等了半小时才恢复,这就是图快的代价。

3.2 日线拉取与复权:别拿不复权的数据算收益

拉日线有两个坑:一是字段太多,全拉下来内存和磁盘都吃不消;二是复权口径,不复权的价格遇到除权除息会产生假跌,均线策略直接失真。

# quant/datasource/loader.py import pandas as pd # 只保留策略真正要用的列,减少存储和内存压力 KEEP_COLS = ['ts_code', 'trade_date', 'open', 'high', 'low', 'close', 'pre_close', 'vol', 'amount'] def fetch_daily(api, ts_code, start, end): df = api.call('daily', ts_code=ts_code, start_date=start, end_date=end) if df is None or df.empty: return pd.DataFrame(columns=KEEP_COLS) df = df[KEEP_COLS].copy() df['trade_date'] = pd.to_datetime(df['trade_date'], format='%Y%m%d') # 取复权因子,按后复权对齐,保证历史价格不随新的除权变化 adj = api.call('adj_factor', ts_code=ts_code, start_date=start, end_date=end) if adj is not None and not adj.empty: adj['trade_date'] = pd.to_datetime(adj['trade_date'], format='%Y%m%d') df = df.merge(adj[['trade_date', 'adj_factor']], on='trade_date', how='left') df['adj_factor'] = df['adj_factor'].ffill().fillna(1.0) for col in ['open', 'high', 'low', 'close', 'pre_close']: df[col] = df[col] * df['adj_factor'] return df.sort_values('trade_date').reset_index(drop=True)

这段代码的关键在复权因子的处理。adj_factor在某些日期可能缺失,用ffill()向前填充是常见做法,首日缺失则fillna(1.0)兜底。乘上因子得到的是后复权价格,历史价格不会因为新的分红送股而变化,回测才可复现。如果你只用close不复权,遇到 10 送 10 那天,价格直接腰斩,均线策略会给出一个完美的假卖出信号。

另一个细节是pre_close也要复权,因为算涨跌幅、算涨跌停要用它。vol和amount不复权,成交量金额本来就是实际值。

3.3 落库与增量更新:用最大交易日做水位线

全市场拉一遍动辄几十分钟,不可能每次重跑。增量更新的逻辑是:查本地库里每只票的最大交易日,从它的下一天开始拉。

# quant/datasource/storage.py import sqlite3 import pandas as pd def get_last_date(conn, ts_code): cur = conn.execute( 'SELECT MAX(trade_date) FROM daily WHERE ts_code = ?', (ts_code,)) row = cur.fetchone() return row[0] if row and row[0] else None def upsert_daily(conn, df): if df.empty: return 0 # 主键 (ts_code, trade_date),重复写入直接覆盖,保证可重入 rows = df.where(pd.notnull(df), None).values.tolist() conn.executemany( 'INSERT OR REPLACE INTO daily ' '(ts_code, trade_date, open, high, low, close, pre_close, vol, amount, adj_factor) ' 'VALUES (?,?,?,?,?,?,?,?,?,?)', rows) conn.commit() return len(rows)

配套的建表语句:

CREATE TABLE IF NOT EXISTS daily ( ts_code TEXT NOT NULL, trade_date TEXT NOT NULL, open REAL, high REAL, low REAL, close REAL, pre_close REAL, vol REAL, amount REAL, adj_factor REAL DEFAULT 1.0, PRIMARY KEY (ts_code, trade_date) ); CREATE INDEX IF NOT EXISTS idx_daily_date ON daily(trade_date);

INSERT OR REPLACE让写入天然幂等,同一天跑十次结果一样,这对定时任务太重要了——某次跑到一半崩了,重跑不会产生重复行。idx_daily_date这个索引别省,按日期切片查全市场是回测里最高频的操作,没索引在几十万行上能慢十倍。

水位线用MAX(trade_date)而不是“上次运行时间”,是因为补历史数据、手动插数据都会破坏时间假设,只有数据本身说的算。我一般还会在update_data.py里加一个--force参数,强制重拉最近 N 天,用来修数据。

3.4 数据校验:缺口、停牌和异常值

拉完不校验,等于没拉。最常检查的三件事:

# quant/datasource/loader.py 追加 def check_daily(df, calendar): issues = [] # 1. 缺口:交易日历里有、数据里没有的日期 have = set(df['trade_date']) expect = set(calendar[(calendar >= df['trade_date'].min()) & (calendar <= df['trade_date'].max())]) missing = sorted(expect - have) if missing: issues.append(f'缺口 {len(missing)} 天,首个 {missing[0]}') # 2. 异常涨跌:复权后单日涨跌超过 21% 基本可判定为脏数据 pct = df['close'].pct_change() abnormal = df[pct.abs() > 0.21] if not abnormal.empty: issues.append(f'异常波动 {len(abnormal)} 条') # 3. 零成交:停牌日 vol 为 0,回测里不能当成可交易日 zero = df[df['vol'] == 0] if not zero.empty: issues.append(f'零成交 {len(zero)} 天') return issues

缺口要区分两种情况:全天停牌导致的无数据是正常的,但如果是数据接口漏了,就必须补。常见做法是拿交易日历对比,缺口天数超过阈值(比如连续 3 天)就记日志人工看。异常涨跌在复权后超过 21% 基本不可能是真实行情,多半是复权因子缺失或者单位错乱。零成交日在选股时要直接过滤掉,不然回测会在一个根本买不进的票上成交,收益全是假的。

这三项检查的输出写进日志文件,每天跑完扫一眼,比事后发现回测结果离谱再回头查要省事得多。

4. 策略与回测内核:从信号到净值曲线的那几行关键代码

策略和回测是这套源码里最“像量化”的部分,但代码量其实不大。真正决定成败的是接口约定和撮合细节,而不是策略本身有多复杂。

4.1 策略基类:一个方法约定输入输出

所有策略继承同一个基类,只实现generate_signal,输入是行情 DataFrame,输出是同索引的信号序列:

# quant/strategy/base.py from abc import ABC, abstractmethod import pandas as pd class BaseStrategy(ABC): def __init__(self, params: dict): self.params = params @abstractmethod def generate_signal(self, df: pd.DataFrame) -> pd.Series: """返回 -1/0/1 组成的 Series,index 与 df 对齐 -1 卖出,0 持有不动,1 买入 """ raise NotImplementedError def prepare(self, df: pd.DataFrame) -> pd.DataFrame: # 预留钩子:算因子、加指标,子类可覆盖 return df

这个约定的价值在于:回测引擎和实盘信号生成都调generate_signal,同一份代码,同一个输入,结果必然一致。prepare是给因子计算留的,把耗时计算和信号生成分开,方便缓存中间结果。params从strategy.yaml读,改参数不改代码。

4.2 双均线策略:二十行写完一个能跑的示例

# quant/strategy/ma_cross.py from .base import BaseStrategy class MaCross(BaseStrategy): def prepare(self, df): fast = self.params.get('fast', 5) slow = self.params.get('slow', 20) df['ma_fast'] = df['close'].rolling(fast).mean() df['ma_slow'] = df['close'].rolling(slow).mean() return df def generate_signal(self, df): sig = 0 signals = [] for fast, slow in zip(df['ma_fast'], df['ma_slow']): if fast > slow: sig = 1 elif fast < slow: sig = -1 signals.append(sig) return __import__('pandas').Series(signals, index=df.index)

逻辑很直白:快线上穿慢线给买入信号,下穿给卖出信号。rolling(fast).mean()里的fast和slow从配置读,改周期不用动代码。信号用 0/1/-1 三态而不是布尔值,是为了给后面加“清仓”“减半仓”留扩展空间。

注意prepare里的均线是在复权后的close上算的,如果哪天有人把复权逻辑改坏了,这个策略会第一时间表现为“信号乱跳”,而不是收益慢慢变差,这种敏感性在调试时反而好用。

4.3 回测引擎:手续费、滑点、T+1 一个都不能省

回测主循环的核心是“按信号撮合、按规则扣费”:

# quant/backtest/broker.py class Broker: def __init__(self, cash, commission, stamp_tax, slippage): self.cash = cash self.commission = commission self.stamp_tax = stamp_tax self.slippage = slippage def buy(self, price, shares): # 买入按滑点上浮,佣金双边收 exec_price = price * (1 + self.slippage) cost = exec_price * shares fee = max(cost * self.commission, 5) # 最低5元 if cost + fee > self.cash: return None self.cash -= (cost + fee) return {'price': exec_price, 'shares': shares, 'fee': fee} def sell(self, price, shares): # 卖出按滑点下浮,多收一笔印花税 exec_price = price * (1 - self.slippage) amount = exec_price * shares fee = amount * self.commission + amount * self.stamp_tax self.cash += (amount - fee) return {'price': exec_price, 'shares': shares, 'fee': fee}

buy里的max(cost * commission, 5)是 A 股真实规则,很多简化回测直接按比例算,小资金账户会明显高估收益。sell多收的印花税只在卖出方向收,这是 A 股和很多海外市场不一样的地方,漏掉就是系统性高估。

T+1 的处理不在Broker里,而在引擎主循环的持仓状态上:当天买入的票记一个buy_date,卖出前检查buy_date < today。涨跌停的处理是另一个细节,开盘一字板买不进、跌停卖不出,严格回测里需要判断开盘价是否等于涨停价,不严格的话至少要在日志里标注这笔成交是“理想成交”。

回测引擎的主循环大致是:按交易日遍历,每天先更新持仓市值,再调用策略拿信号,然后按信号调仓,最后记录净值。净值序列是后面所有绩效指标的输入,也是画曲线的数据源。

4.4 绩效指标:年化、回撤、夏普的口径要写清

# quant/backtest/metrics.py import numpy as np def performance(nav, periods_per_year=252): ret = nav.pct_change().dropna() total = nav.iloc[-1] / nav.iloc[0] - 1 years = len(nav) / periods_per_year annual = (1 + total) ** (1 / years) - 1 if years > 0 else 0 drawdown = (nav / nav.cummax() - 1).min() sharpe = (ret.mean() / ret.std() * np.sqrt(periods_per_year) if ret.std() > 0 else 0) return {'total_return': total, 'annual_return': annual, 'max_drawdown': drawdown, 'sharpe': sharpe}

年化用的复合口径而不是简单年化,years用交易日数除以 252 估算,比按自然日算更贴近实际交易节奏。最大回撤用净值除以历史新高再取最小值,负值表示回撤幅度。夏普用日收益均值除标准差再乘根号 252 年化,无风险利率没扣,简单策略这么算够用;如果拿去和别人比,记得先确认对方扣没扣无风险利率,口径不一致比出来的排名毫无意义。

指标里最容易被忽略的是total_return和annual_return的区别,短周期回测(比如三个月)年化后会变成一个夸张的数字,评审时只看年化容易自欺欺人。

5. 避坑与排查:跑不通、对不上、跑得慢的三类现场

这套源码真正让人花时间的不是写策略,是排查。下面五条都是实际跑的时候大概率会撞上的,按“现象 → 原因 → 解决”记下来,能省不少来回。

5.1 现象:拉数报权限不足或频繁超时

原因:tushare 的接口按积分和权限分级,某些接口需要更高权限;另一个常见原因是每分钟调用超限,尤其在全市场循环拉取时,rate_limit设得太满或者根本没设。

解决:先单独调一次目标接口确认权限,报抱歉,您没有访问该接口的权限就是权限问题,换接口或用已有权限的字段自己算。超时的话把rate_limit降到标称值的 80%,重试间隔从 5 秒起线性递增。全市场拉取建议分批,每批 500 只,批与批之间 sleep 几秒,别一口气冲到底。

5.2 现象:回测年化高得离谱,实盘完全不是一个量级

原因:九成是未来函数或者成本没算。未来函数最常见的是用了当天收盘价生成信号、又用当天收盘价成交;成本漏算则是没设手续费、滑点、印花税,或者设了但方向搞反。

解决:信号在 T 日生成,成交必须放在 T+1 日的开盘价或均价,这条规则写进引擎主循环,不给策略留“当天买当天卖”的口子。成本检查用一个最小例子验证:单笔 10 万买入,佣金应该是100000 * 0.00025 = 25元,如果算出来是 0,说明配置没生效。滑点至少给千分之一,做小盘股的给千二。

5.3 现象:回测信号和每日清单对不上

原因:两边用的数据版本不同。回测跑的是三个月前拉的库,每日信号用的是今天刚更新的库,中间的除权除息让复权价格变了,均线自然不一样。还有一种可能是prepare里的窗口长度依赖历史长度,回测给足了 250 天,实盘只取了 60 天。

解决:每日信号生成前强制跑一次增量更新,保证库是最新的。prepare里的滚动窗口统一从配置读,并且在取数时多取slow * 2天的前置数据,算完再截断到目标区间。诊断方法很简单:把同一天的回测信号和实盘信号打出来做 diff,哪只票不一致就单独查它的数据。

5.4 现象:跑全市场回测慢到无法接受

原因:逐票循环里每次都在查库,几千只票就是几千次查询;或者每次都用pd.read_sql拉全表再过滤,内存和 IO 双重浪费。

解决:一次性把区间内的全市场数据读进内存,用groupby('ts_code')分组处理,别在循环里查库。数据量大的话按年切片,一年一个 Parquet 文件,读的时候只读需要的年份。指标计算尽量向量化,rolling().mean()比 for 循环快两个数量级,实在要循环就用numba或者干脆接受慢、放到晚上跑。

5.5 现象:日期格式错乱、时区偏移、切片切出空结果

原因:tushare 返回的trade_date是YYYYMMDD字符串,数据库里存成 TEXT,pandas 里又变成 datetime,三种形态混用,比较的时候静默返回空。时区问题多见于用了datetime.now()又做日期比较的场景。

解决:约定一条铁律——进数据库之前全部转成YYYY-MM-DD字符串,读出后立刻pd.to_datetime,中间不保留其他形态。日期比较统一用 pandas 的 datetime 类型,不用字符串比大小。涉及“今天”的逻辑统一走交易日历模块,别直接datetime.today(),周末和节假日跑出来的结果会让你怀疑人生。

6. 二次开发:把单策略脚本改成多策略框架的三个改造点

源码跑通之后,大部分人的下一步是加自己的策略。这里给三个改造方向,按投入产出比排序。

第一个改造点是策略注册。现在加策略要改run_backtest.py里的分支判断,改成用装饰器自动注册:

# quant/strategy/registry.py STRATEGIES = {} def register(name): def wrapper(cls): STRATEGIES[name] = cls return cls return wrapper # 使用:在策略文件顶部加一行 @register('ma_cross') class MaCross(BaseStrategy): ...

这样--strategy参数直接查字典,加策略只加文件,不动入口脚本。

第二个改造点是参数扫描。单策略跑一次只能看一组参数,改成网格扫描后输出一张表:

改造项单策略现状多策略/多参数目标
参数来源strategy.yaml 单份参数网格生成器
结果存储终端打印SQLite 结果表
评估方式看年化年化 + 回撤 + 夏普综合排序
复现方式手动改配置参数哈希做 run_id

结果表至少存run_id, strategy, params_json, annual, drawdown, sharpe, start, end,同一个run_id能复现出完全一样的净值曲线。参数扫描最容易犯的错是拿全样本调参再报全样本收益,正确做法是留一段样本外,调参只用样本内。

第三个改造点是执行层解耦。daily_signal.py现在直接打印 CSV,改成输出到一个signals表,再写一个独立的推送模块读表发消息。这样换推送渠道、加人工确认环节,都不用碰信号生成逻辑。风控也可以挂在这一层:单票最大仓位、行业集中度、当日最大回撤熔断,全部在生成清单之后、推送之前过滤一遍。

验证改造有没有改坏,方法很土但有效:改造前后用同一份数据、同一组参数跑一次,把净值曲线叠在一起看,差异超过千分之一就要查。我吃过一次亏,改注册机制的时候不小心把默认参数从 20 改成了 10,回测收益直接翻倍,差点当成策略突破。

从那以后我每次动框架代码,都强制先跑一遍“同一天、同一只票”的信号 diff,确认输出没变化再谈收益。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询