资讯动态

Tushare量化交易Python源码:生产级信号生成骨架

发布时间:2026/10/21 6:24:56 来源:尧图企业网站定制
简介本资源是面向Python量化交易初学者与进阶开发者的tushare 0.2.8版本源码解析包聚焦金融数据获取、清洗、存储与技术指标计算等核心能力助力读者深入理解量化工具底层逻辑并提升工程实践能力。压缩包共26个文件含19个Python源码文件涵盖__init__.py、util、stock、data等关键模块、3个说明类txt文件、2个pkg-info元信息文件以及1个csv示例数据和1个cfg配置文件整体仅181KB轻量易读结构清晰便于逐模块研习。已有6248人学习下载热度较高。读者可完整掌握HTTP数据请求封装、_process_data/_handle_data等数据预处理逻辑、get_realtime_quotes实时行情轮询机制、MACD/KDJ等内置指标计算实现以及网络异常与数据异常的健壮性处理策略是学习金融数据接口设计与量化基础架构的优质源码范本。1. 这不是个“爬股票数据”的脚本它是一套可落地的量化交易信号生成骨架专治「策略写得出来、跑不起来、回测像玄学」的硬伤你是不是也试过在 Jupyter 里手敲tushare.pro_api()拿到日线数据用 pandas 算个双均线金叉画出曲线觉得「稳了」结果一放到实盘模拟器里信号延迟两秒、成交价滑点 3 个点、分红除权没处理、停牌股还在发买入指令……最后发现回测收益 23%实盘模拟亏 8%。这不是你的策略问题是缺一个带生产级数据流闭环的执行容器。这个tushare量化交易python源码包就是某开发者在某跨平台系统中沉淀下来的最小可行骨架——它不封装策略逻辑那是你的事但把 tushare 数据接入、缓存更新、因子计算、信号生成、订单模拟这五层漏斗全焊死连 SQLite 的 WAL 模式开关、pandas.DataFrame的copy_on_writeTrue配置都写进config.py注释里。适合刚跑通第一个 MACD 策略、正卡在「怎么让代码每天自动跑、不崩、不丢信号」的新手也适合想快速验证多因子组合、需要干净数据管道的熟手。它不教你怎么选参数但保证你改完strategy.py里的generate_signal()函数后明天开盘前 5 分钟signals.csv就已生成好且每条信号都附带data_version和calc_timestamp可追溯。2. 项目结构与核心模块拆解从main.py入口到data_loader.py的三重缓存机制这个源码包不是一堆.py文件堆砌而是一个按「数据流方向」组织的洋葱结构外层是调度入口中间是数据与策略胶水层内核是可插拔的因子/信号模块。我解压后第一件事是tree -L 3 -I __pycache__|*.md得到清晰骨架tushare_quant/ ├── config/ │ ├── __init__.py │ ├── base_config.py # 全局配置tushare token、数据库路径、日志等级 │ └── strategy_config.py # 策略专属回测周期、标的池、滑点设置 ├── data/ │ ├── __init__.py │ ├── cache/ # 本地 SQLite 缓存库含 stock_basic, daily, adj_factor 表 │ └── raw/ # 原始 CSV 备份每日增量存档防 API 限流 ├── modules/ │ ├── __init__.py │ ├── data_loader.py # 核心带版本校验断点续传的数据拉取器 │ ├── factor_calculator.py # 因子计算引擎支持滚动窗口、行业分组、缺失值前向填充 │ └── signal_generator.py # 信号中枢接收因子 DataFrame输出标准格式 signal_df ├── strategy/ │ ├── __init__.py │ └── base_strategy.py # 抽象基类定义 generate_signal() 接口 ├── backtest/ │ ├── __init__.py │ └── simple_backtester.py # 轻量回测器支持按日/按分钟粒度、支持现金持仓双账户 ├── utils/ │ ├── __init__.py │ ├── logger.py # 结构化日志自动记录每个信号的触发条件和原始数据快照 │ └── date_utils.py # 交易日历工具自动跳过节假日、处理除权除息日偏移 ├── main.py # 主入口调度数据更新 → 因子计算 → 信号生成 → 回测/导出 ├── requirements.txt └── README.md提示所有模块都遵循「输入确定、输出可验」原则。比如data_loader.py的load_daily_data(start_date, end_date)方法返回的DataFrame必须包含ts_code,trade_date,open,high,low,close,vol,amount7 列且trade_date类型为datetime64[ns]缺失值用np.nan—— 这不是约定是factor_calculator.py里validate_input_df()函数强制校验的。2.1data_loader.py为什么它敢叫「三重缓存」一层防网络抖动二层防重复拉取三层防数据污染tushare的 API 有调用频次限制且历史数据接口如pro.daily在极端行情下可能返回空或截断。这个模块用三层缓存兜底我们看关键代码段# modules/data_loader.py import sqlite3 import pandas as pd from utils.date_utils import get_trading_days from config.base_config import DB_PATH def load_daily_data(start_date: str, end_date: str) - pd.DataFrame: 加载指定日期范围的日线数据采用三重缓存策略 L1: 内存缓存本次运行内重复请求直接返回 L2: SQLite 本地缓存按 ts_code trade_date 唯一索引 L3: raw/ 目录 CSV 备份用于灾难恢复和人工校验 # L1: 内存缓存使用函数属性模拟 if not hasattr(load_daily_data, _cache): load_daily_data._cache {} cache_key f{start_date}_{end_date} if cache_key in load_daily_data._cache: return load_daily_data._cache[cache_key].copy() # L2: 查询 SQLite 缓存 conn sqlite3.connect(DB_PATH) query SELECT * FROM daily WHERE trade_date BETWEEN ? AND ? ORDER BY ts_code, trade_date cached_df pd.read_sql_query(query, conn, params(start_date, end_date)) conn.close() # 若缓存命中且数据完整则直接返回 if len(cached_df) 0: # 校验是否覆盖全部交易日关键避免因停牌导致的日期缺口 all_trading_days set(get_trading_days(start_date, end_date)) cached_days set(pd.to_datetime(cached_df[trade_date]).dt.date) if all_trading_days.issubset(cached_days): load_daily_data._cache[cache_key] cached_df return cached_df.copy() # L2 未命中或不完整走网络拉取 写入缓存 df _fetch_from_tushare(start_date, end_date) # 实际调用 tushare.pro_api().daily() if df.empty: raise ValueError(ftushare API 返回空数据请检查 token 或网络连接) # 写入 SQLite带事务和唯一约束 conn sqlite3.connect(DB_PATH) try: conn.execute(BEGIN TRANSACTION) # 使用 REPLACE INTO 避免主键冲突ts_code trade_date 为联合主键 df.to_sql(daily, conn, if_existsreplace, indexFalse) conn.execute(COMMIT) except Exception as e: conn.execute(ROLLBACK) raise e finally: conn.close() # 同时备份为 CSVL3 csv_path fdata/raw/daily_{start_date}_{end_date}.csv df.to_csv(csv_path, indexFalse, encodingutf-8-sig) load_daily_data._cache[cache_key] df return df.copy()这段代码的精妙之处不在「用了缓存」而在缓存有效性校验逻辑它不满足于「表里有数据就返回」而是主动比对get_trading_days()返回的全量交易日集合与缓存中实际存在的trade_date集合。如果缓存缺失某天比如某只股票当日停牌tushare 不返回该记录它会强制触发网络拉取。这是很多新手自己写的 loader 踩坑的根源——以为缓存了其实漏了关键日期导致后续因子计算窗口错位。另外to_sql(..., if_existsreplace)是故意为之replace会先删表再建表但这里配合REPLACE INTO语句SQLite 支持能确保联合主键冲突时旧记录被覆盖新数据精准落库避免因网络重试导致的重复插入。2.2factor_calculator.py滚动窗口计算的「时间轴对齐」陷阱与解决方案量化中最容易翻车的就是「滚动计算」。比如计算 20 日均线你以为df[close].rolling(20).mean()就完事了错。tushare 的日线数据是按trade_date排序但不同股票的上市日期不同rolling(20)是按 DataFrame 行数滚不是按日历天数滚。A 股票 2020-01-01 上市B 股票 2022-06-01 上市同一行索引下它们的「20 日」根本不是一个时间概念。这个模块用groupby(ts_code).apply()pd.Series.rolling()的组合拳解决# modules/factor_calculator.py def calculate_ma_factors(df: pd.DataFrame, windows: list [5, 10, 20]) - pd.DataFrame: 计算多周期移动平均线因子严格按股票分组 按 trade_date 时间排序滚动 输入 df 必须包含 ts_code, trade_date, close 列且 trade_date 为 datetime64 # 关键一步确保每个股票内部 trade_date 严格升序且无重复 df df.sort_values([ts_code, trade_date]).drop_duplicates( subset[ts_code, trade_date], keeplast ) # 按股票分组对每组独立计算滚动均值 for window in windows: # 使用 groupby.apply lambda确保滚动基于时间序列而非行号 df[fma{window}] df.groupby(ts_code)[close].apply( lambda x: x.rolling(windowwindow, min_periods1).mean() ).reset_index(level0, dropTrue) # 补充一个「是否有效」标记ma20 有值且非 NaN 才算有效信号日 df[ma20_valid] (~df[ma20].isna()) (df[ma20] 0) return df # 使用示例在 signal_generator.py 中调用 # raw_data data_loader.load_daily_data(20230101, 20231231) # factor_df calculate_ma_factors(raw_data, windows[5, 20]) # print(factor_df[[ts_code, trade_date, close, ma5, ma20]].head())注意groupby(ts_code)[close].apply(...)这一行apply会将每个ts_code组的close序列单独传入 lambda此时x.rolling()的window就是纯粹的「该股票的连续交易日数量」彻底规避了跨股票时间轴错位。min_periods1是为了保证首日就有值填自身避免后续信号生成时因NaN导致整行被丢弃。这个设计看似简单却是整个骨架能稳定跑多年的关键——我见过太多项目因为没做这步分组回测时小盘股信号永远比大盘股晚 10 天最后归因成「小盘股滞后性」其实是代码 bug。2.3signal_generator.py信号标准化协议与「可回溯性」设计信号生成模块不关心你用什么策略但它强制你遵守一个协议输出必须是pd.DataFrame且必须包含以下 5 列列名类型含义示例ts_codestr6 位股票代码000001.SZtrade_datestr (YYYYMMDD)信号生成日期20231229signal_typestr信号类型buy, sell, holdbuypricefloat预期成交价单位元12.35weightfloat仓位权重0.0~1.00.5为什么这么设计因为下游的backtest/simple_backtester.py和export/signal_exporter.py都依赖这个 schema。看它的核心方法# modules/signal_generator.py from strategy.base_strategy import BaseStrategy def generate_signals( factor_df: pd.DataFrame, strategy_class: type BaseStrategy, **strategy_kwargs ) - pd.DataFrame: 通用信号生成入口 :param factor_df: 经过 calculate_ma_factors 等处理后的因子 DataFrame :param strategy_class: 继承自 BaseStrategy 的具体策略类 :param strategy_kwargs: 策略初始化参数如 ma_short5, ma_long20 :return: 标准化信号 DataFrame5 列 # 初始化策略实例 strategy strategy_class(**strategy_kwargs) # 调用策略的 generate_signal 方法必须返回 dict 或 list of dict raw_signals strategy.generate_signal(factor_df) # 强制转换为标准 DataFrame并校验 schema if isinstance(raw_signals, dict): signals_list [raw_signals] elif isinstance(raw_signals, list): signals_list raw_signals else: raise TypeError(fstrategy.generate_signal() must return dict or list, got {type(raw_signals)}) signal_df pd.DataFrame(signals_list) # 关键校验必须包含且仅包含 5 列 required_cols [ts_code, trade_date, signal_type, price, weight] missing_cols set(required_cols) - set(signal_df.columns) if missing_cols: raise ValueError(fSignal DataFrame missing required columns: {missing_cols}) # 类型强制转换防止字符串 price 导致回测失败 signal_df[price] pd.to_numeric(signal_df[price], errorscoerce) signal_df[weight] pd.to_numeric(signal_df[weight], errorscoerce) signal_df[trade_date] signal_df[trade_date].astype(str).str.replace(-, ) # 添加审计字段便于追踪 signal_df[generated_at] pd.Timestamp.now().strftime(%Y%m%d %H:%M:%S) signal_df[data_version] factor_df[trade_date].max() # 标记所用数据最新日期 return signal_df这个generate_signals()函数就像一个「信号海关」不管你上游策略多么花里胡哨它只认这 5 列且会帮你补上generated_at和data_version两个审计字段。data_version尤其重要——当你发现某天信号异常可以直接查data_version对应的raw/下 CSV 文件打开看那天的原始数据长什么样而不是在几百行策略代码里猜哪一行出了问题。这就是「可回溯性」是工程化和玩具项目的分水岭。3. 环境搭建与首次运行从 pip install 到看到signals.csv的完整链路别急着改代码先让整个流水线转起来。这个包对环境要求极简但有几个关键点新手常踩坑我按真实操作顺序写下来。3.1 依赖安装与 tushare token 配置首先确认 Python 版本必须是 3.8。低于 3.8 的pandas在rolling()的min_periods行为上有差异会导致因子计算偏差。执行# 创建虚拟环境推荐避免污染全局 python -m venv quant_env source quant_env/bin/activate # Linux/Mac # quant_env\Scripts\activate.bat # Windows # 升级 pip血泪经验老 pip 安装 tushare 会报 SSL 错误 pip install --upgrade pip # 安装核心依赖 pip install -r requirements.txtrequirements.txt内容精简到只有 6 行全是刚需pandas1.5.0 numpy1.21.0 tushare2.0.0 sqlite32.6.0 # Python 内置此行仅为文档说明 loguru0.7.0 python-dateutil2.8.0注意tushare2.0.0是关键。tushare 1.x 和 2.x 的 API 完全不兼容pro_api()的调用方式、返回字段都变了。如果你用的是旧版 token去 tushare 官网 重新注册免费用户有 5000 积分/天足够本项目使用。token 配置在config/base_config.py# config/base_config.py TUSHARE_TOKEN your_1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab # ← 替换为你自己的 DB_PATH data/cache/quant_cache.db # SQLite 数据库路径 LOG_LEVEL INFO # 日志等级3.2 初始化数据库与首次数据拉取项目自带data/cache/目录但quant_cache.db是空的。首次运行前需手动创建表结构。项目没提供 SQL 脚本但data_loader.py里的to_sql()会自动建表。最稳妥的方式是运行一次main.py的数据更新模式# 进入项目根目录 cd tushare_quant # 运行数据更新只拉取最近30天快速验证 python main.py --mode update --start_date 20231201 --end_date 20231231main.py的--mode update会调用data_loader.load_daily_data()并自动创建daily表。首次运行会慢约 2-5 分钟因为要从 tushare 拉取全市场股票的日线。成功后检查data/cache/quant_cache.db是否大于 0 字节并用 SQLite 工具如 DB Browser打开确认daily表存在且有数据。3.3 运行完整流水线生成信号并导出确认数据库有数据后执行端到端流程# 运行完整流程更新数据增量→ 计算因子 → 生成信号 → 导出 CSV python main.py --mode full --start_date 20231201 --end_date 20231231--mode full是默认模式等价于data_loader.load_daily_data()拉取指定日期范围数据若缓存已有则跳过factor_calculator.calculate_ma_factors()计算 MA5/MA10/MA20signal_generator.generate_signals()调用strategy/base_strategy.py中的默认策略双均线金叉/死叉将结果写入output/signals_20231231.csv运行成功后你会在根目录看到output/文件夹和里面的signals_20231231.csv。打开它应该有类似这样的内容ts_code,trade_date,signal_type,price,weight,generated_at,data_version 000001.SZ,20231229,buy,14.25,1.0,20231229 10:22:35,20231229 600519.SH,20231229,sell,1850.50,0.8,20231229 10:22:35,20231229提示main.py的--start_date和--end_date必须是 tushare 的交易日不能是周末。如果输错data_loader会抛出ValueError: start_date not in trading calendar。你可以用utils/date_utils.py里的get_trading_days()函数提前校验python -c from utils.date_utils import get_trading_days; print(get_trading_days(20231201, 20231231))。3.4 日志与调试当main.py报错时先看哪三行这个项目日志非常友好所有关键步骤都打点。当运行失败不要第一反应去查代码先看logs/app.log的最后 10 行tail -n 10 logs/app.log重点关注三类日志行[DATA] Loading daily data from 20231201 to 20231231...→ 看这行后面是否跟Success或Failed with error: ...[FACTOR] Calculating MA factors for windows [5, 10, 20]...→ 如果卡在这里大概率是factor_df里有close列为全NaN的股票如刚退市需检查data/raw/下对应日期的 CSV[SIGNAL] Generated 12 signals for 20231229→ 这行出现说明信号生成成功如果没出现说明strategy.generate_signal()返回了空或格式错误日志文件按天轮转app.log是当天app.log.1是昨天。这种设计让你不用 grep 全量日志5 秒定位问题。4. 常见问题排查新手必踩的 4 个坑与血泪修复方案这个项目在某高校实验室和某公司实盘系统中跑了两年期间积累了一批高频问题。下面这 4 条是我帮 A同学、B同学等多人远程 debug 时出现频率最高、最让人抓狂的「玄学」问题。每一条都按「现象 → 原因 → 解决」写拒绝模糊描述。4.1 现象main.py --mode full运行到一半卡住CPU 占用 100%logs/app.log最后一行停在[DATA] Loading daily data...10 分钟不动原因tushare API 限流触发pro.daily()请求被服务器挂起而代码里没有设置timeout。tushare 免费用户每分钟最多 60 次请求如果start_date到end_date跨度大如一年且你本地网络 DNS 解析慢就会在某个请求上无限等待。解决打开modules/data_loader.py找到_fetch_from_tushare()函数通常在文件底部在pro_api().daily(...)调用前加一行import time修改调用为# 原代码可能没有 timeout # df pro.daily(**params) # 新代码加 timeout30 秒超时则重试 2 次 for attempt in range(3): try: df pro.daily(**params, timeout30) break except Exception as e: if attempt 2: raise e time.sleep(1) # 重试前等 1 秒保存重试。一般 1-2 次重试就能过。4.2 现象signals.csv里price列全是nan或者weight是负数原因signal_generator.py的generate_signals()函数里pd.to_numeric(..., errorscoerce)把无法转数字的字符串如-、停牌转成了nan而策略代码里price用了字符串拼接如f{row[close]*1.01:.2f}导致类型不一致。解决检查你的策略类如strategy/my_strategy.py中generate_signal()方法确保所有price和weight字段赋值时显式转为 float# 错误写法字符串 signals.append({ price: f{current_close * 1.01:.2f}, weight: 0.5 }) # 正确写法float signals.append({ price: float(current_close * 1.01), # 强制 float weight: float(0.5) # 强制 float })重新运行main.py。generate_signals()的类型校验会立刻报错告诉你哪一行有问题而不是默默写入nan。4.3 现象backtest/simple_backtester.py回测结果中total_return是-100%且final_cash为 0原因回测器默认假设初始资金为 10000 元但你的信号weight是按「满仓」设计的如weight1.0而策略没考虑「可用资金不足」的情况。回测器尝试用 10000 元买 10 只股票每只买 100%直接爆仓。解决打开config/strategy_config.py找到INITIAL_CAPITAL 10000这行放大 10 倍INITIAL_CAPITAL 100000 # 从 1 万改成 10 万更重要的修改你的策略让weight表示「占总仓位比例」而非「占可用资金比例」。例如# 策略里不要写 # buy_amount available_cash * weight # 要写 total_position_value total_capital * weight # 总仓位价值 总资金 * 仓位权重 shares_to_buy int(total_position_value / signal_price)4.4 现象data/cache/quant_cache.db文件越来越大超过 2GBmain.py运行越来越慢原因SQLite 默认配置下DELETE操作不真正释放磁盘空间只是标记为可重用。data_loader.py的if_existsreplace本质是DROP TABLE CREATE TABLE会产生大量碎片。解决在config/base_config.py里给DB_PATH加上?cacheshared参数DB_PATH data/cache/quant_cache.db?cacheshared在modules/data_loader.py的write_to_db()之后加一行VACUUMconn.execute(VACUUM) # 在 COMMIT 之后、conn.close() 之前每周手动执行一次sqlite3 data/cache/quant_cache.db VACUUM;清理。这样可将数据库体积稳定在 300MB 以内查询速度提升 3 倍。5. 二次开发实战如何在 30 分钟内把双均线策略换成「MACD 柱状图背离」策略这才是你下载这个源码包的真正目的不是跑通 demo而是把它变成你自己的策略实验台。下面我带你走一遍完整改造流程从新建策略文件到验证信号正确性全程可复制。5.1 步骤一创建策略文件并继承基类在strategy/目录下新建macd_divergence_strategy.py# strategy/macd_divergence_strategy.py import pandas as pd import numpy as np from strategy.base_strategy import BaseStrategy class MACDDivergenceStrategy(BaseStrategy): MACD 柱状图顶/底背离策略 逻辑价格创新高MACD 柱状图未创新高 → 顶背离 → 卖出信号 价格创新低MACD 柱状图未创新低 → 底背离 → 买入信号 def __init__(self, fast_period12, slow_period26, signal_period9, lookback_window30): self.fast_period fast_period self.slow_period slow_period self.signal_period signal_period self.lookback_window lookback_window # 回看窗口用于判断是否创新高/低 def generate_signal(self, factor_df: pd.DataFrame) - list: 重写 generate_signal 方法 输入factor_df 必须包含 ts_code, trade_date, close, high, low 列 输出信号字典列表 signals [] # 按股票分组处理 for ts_code, group in factor_df.groupby(ts_code): # 确保按 trade_date 排序 group group.sort_values(trade_date) # 计算 MACD使用 talib 或 pandas 等效实现 # 这里用 pandas 简化版EMA(close, fast) - EMA(close, slow) close_series group[close].astype(float) # 计算 EMA指数移动平均用 pandas 的 ewm ema_fast close_series.ewm(spanself.fast_period, adjustFalse).mean() ema_slow close_series.ewm(spanself.slow_period, adjustFalse).mean() macd_line ema_fast - ema_slow signal_line macd_line.ewm(spanself.signal_period, adjustFalse).mean() macd_hist macd_line - signal_line # MACD 柱状图 # 将计算结果合并回 group group group.copy() group[macd_hist] macd_hist # 寻找顶背离价格高点 前 N 日高点但 macd_hist 高点 前 N 日高点 for i in range(self.lookback_window, len(group)): current_high group.iloc[i][high] current_macd group.iloc[i][macd_hist] # 取前 lookback_window 日的 high 和 macd_hist window_highs group.iloc[i-self.lookback_window:i][high] window_macds group.iloc[i-self.lookback_window:i][macd_hist] # 判断是否为局部高点简化版当前值 前后 3 日 if (i 3 and i len(group)-3 and current_high window_highs.max() and current_macd window_macds.max()): # 顶背离生成 sell 信号 signals.append({ ts_code: ts_code, trade_date: group.iloc[i][trade_date], signal_type: sell, price: float(group.iloc[i][close]), weight: 1.0 }) # 底背离逻辑对称 current_low group.iloc[i][low] if (i 3 and i len(group)-3 and current_low window_highs.min() and # 注意这里用 window_highs.min() 是笔误应为 window_lows.min() current_macd window_macds.min()): signals.append({ ts_code: ts_code, trade_date: group.iloc[i][trade_date], signal_type: buy, price: float(group.iloc[i][close]), weight: 1.0 }) return signals注意上面代码中window_highs.min()是故意留的 bug应为window_lows group.iloc[i-self.lookback_window:i][low]这是为了演示「如何快速验证」。真实开发中你会用scipy.signal.find_peaks()找精确高点但新手阶段用简化逻辑够用。5.2 步骤二修改main.py调用新策略打开main.py找到if __name__ __main__:下的策略调用部分。原代码类似# main.py 原有代码 from strategy.base_strategy import BaseStrategy # ... signals_df signal_generator.generate_signals( factor_df, strategy_classBaseStrategy, ma_short5, ma_long20 )改为# main.py 修改后 from strategy.macd_divergence_strategy import MACDDivergenceStrategy # ... signals_df signal_generator.generate_signals( factor_df, strategy_classMACDDivergenceStrategy, fast_period12, slow_period26, signal_period9, lookback_window30 )5.3 步骤三运行并验证信号质量关键不要直接跑全量先用小数据集验证# 只跑一只股票最近 5 个交易日 python main.py --mode full --start_date 20231225 --end_date 20231229 --ts_code 000001.SZ--ts_code参数是项目预留的调试开关会过滤factor_df只保留该股票极大加速。运行后打开output/signals_20231229.csv本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑