简介:本资源面向量化交易初学者与Python数据处理实践者,提供一套基于聚宽平台的SQL数据中转与QMT信号落地的轻量级技术方案,解决金融数据从聚宽接口提取、结构化存储至本地中转数据库的关键链路问题。压缩包共3个文件(19KB),含Python自动化脚本myqmt_sql.py(负责连接、查询与写入)、XML格式配置文件(定义信号字段结构与映射规则)及.rzrk专用信号逻辑文件(封装QMT买卖条件与SQL执行策略),三者协同实现信号解析、数据持久化与后续分析基础搭建。目前已有202人学习下载,适合希望快速理解聚宽SQL接口调用流程、构建本地量化数据中台或复用信号逻辑进行策略回测的开发者。
1. 聚宽SQL数据库中转方案:不是简单导出导入,而是构建可审计、可回溯、可复用的数据流转链路
你有没有遇到过这种场景:在聚宽平台写好策略逻辑,跑出信号后想立刻推送到本地量化系统做实盘?或者需要把聚宽的分钟级行情、财务因子、行业分类等结构化数据,稳定同步到自建的PostgreSQL/MySQL里,供多个Python进程并发读取、做特征工程、训练模型?这时候直接用聚宽SDK反复拉接口——慢、限流、无事务、难监控;手动导Excel再导入——字段错位、时间戳乱码、无法增量、一出错就得重来。这份“聚宽SQL数据库中转”资源,本质是一套轻量但完整的数据管道(Data Pipeline)落地模板:它不依赖聚宽官方API的实时推送(聚宽未开放该能力),而是通过定时任务+SQL脚本+标准化中间表设计,把聚宽数据“搬”出来、“理”清楚、“存”稳当。适合正在搭建本地量化基础设施的个人开发者、小团队研究员,以及需要将聚宽数据纳入企业级数据中台的工程师。它解决的不是“能不能拿”,而是“怎么拿得准、拿得稳、拿得可持续”。
2. 中转架构选型与核心组件拆解:为什么用SQLite做中转枢纽而非直连聚宽?
2.1 为什么放弃“聚宽SDK → 直连MySQL”这种看似最短路径?
很多新手第一反应是:聚宽有Python SDK,我直接jqdata.get_price()拿到DataFrame,再用pandas.to_sql()怼进MySQL不就完了?血泪经验告诉你,这条路在真实生产中大概率翻车。原因有三:
第一,连接稳定性不可控:聚宽SDK底层HTTP请求受网络抖动、服务端限流影响极大,一次get_fundamentals()调用超时或返回空数据,整个ETL流程就中断,且无重试机制;
第二,数据一致性难保障:to_sql默认if_exists='append',但聚宽数据存在重复日期(如复权价更新)、字段类型漂移(某日market_cap突然变object),直接追加会导致主键冲突或类型报错;
第三,调试黑匣子:出问题时你只能看到“ConnectionError”或“DataError”,无法定位是聚宽接口异常、本地网络问题,还是SQL语句本身有歧义。
提示:这不是SDK不好,而是它设计初衷是交互式研究,不是工业级数据管道。把研究工具当生产组件用,迟早要交后悔药。
2.2 SQLite作为中转库的四大不可替代性
本方案选择SQLite作为中转数据库(而非直接写入目标库),是经过多次压测和线上验证的务实选择:
| 特性 | 说明 | 对应解决的问题 |
|---|---|---|
| 零配置、免服务 | 无需安装MySQL/PostgreSQL服务,单个.db文件即数据库,sqlite3.connect('jqdata_mid.db')一行启动 | 避免开发环境部署数据库服务的权限、端口、密码管理成本 |
| ACID事务强保障 | 所有INSERT/UPDATE操作天然包裹在事务中,即使程序崩溃,.db文件也不会出现半截数据 | 解决“写一半断电导致数据损坏”的灾难场景 |
| 原子化表切换 | 可先写入临时表stock_daily_tmp,校验无误后RENAME TO stock_daily,对外服务零感知 | 实现“全量更新不阻塞查询”的关键技巧 |
| 跨平台二进制兼容 | .db文件在Windows/macOS/Linux下可直接拷贝使用,无字符集、页大小等兼容性问题 | 支持从聚宽云服务器下载中转库,本地直接分析 |
常见做法是:每天凌晨2点触发中转脚本 → 从聚宽拉取T-1日全市场日线、财务、估值数据 → 写入SQLite临时表 → 校验行数、空值率、日期范围 → 事务提交并重命名表 → 最后用sqlite3命令行工具导出为CSV/SQL dump,供下游MySQL/PostgreSQL导入。整个过程不依赖任何外部服务,一个Python脚本+一个DB文件就能闭环。
2.3 中转表结构设计原则:字段对齐、时间分区、冗余留痕
聚宽原始数据字段名(如close,pe_ratio,pub_date)与通用量化库习惯(close_price,pe_ttm,ann_date)常有差异。中转表不追求1:1映射,而是按下游消费方需求反向设计。例如:
stock_daily表必含字段:trade_date TEXT NOT NULL(统一为'YYYYMMDD'字符串,避免SQLite日期函数陷阱)、security_id TEXT NOT NULL(聚宽代码,如'000001.XSHE')、open REAL,high REAL,low REAL,close REAL,volume INTEGER,money REAL(金额单位统一为元,非万元);- 强制添加审计字段:
etl_batch_id TEXT(格式为jq_20240520_020000,标识本次中转批次)、etl_source_version TEXT(记录聚宽SDK版本号,如3.12.0,用于追溯数据源变更); - 财务表按公告日期分区:
fin_report表中ann_date为分区键,查询“2023年报”时只需扫描ann_date >= '20240101' AND ann_date <= '20240430',避免全表扫描。
这种设计让中转库既是数据容器,也是元数据日志——哪天数据异常?哪个字段开始为空?哪个证券代码突然消失?查etl_batch_id和etl_source_version就能快速定位。
3. 核心中转脚本实现:从聚宽拉取→清洗→写入SQLite的完整Python流程
3.1 环境准备与依赖声明
本方案仅需三个Python包,全部可通过pip install一键安装,无编译依赖:
jqdatasdk>=3.10.0:聚宽官方SDK(注意:必须≥3.10.0,低版本不支持get_fundamentals_continuously连续获取);pandas>=1.5.0:数据清洗主力,利用pd.concat()合并多日数据、df.drop_duplicates()去重;sqlite3:Python标准库,无需额外安装。
pip install jqdatasdk pandas注意:聚宽SDK需提前在官网注册账号并获取
username和password,首次运行会提示输入。凭证明文存储在~/.jqdata/config.json,生产环境建议用环境变量加密(见4.3节)。
3.2 主流程脚本:jqdata_to_sqlite.py
以下代码是中转脚本的核心骨架,已去除业务无关的logging和异常包装,保留最简可运行逻辑:
# jqdata_to_sqlite.py import jqdatasdk as jq import pandas as pd import sqlite3 from datetime import datetime, timedelta import sys # 1. 初始化聚宽连接(实际使用时请用环境变量) jq.auth("your_username", "your_password") # ⚠️ 生产环境务必替换! # 2. 定义中转库路径和表名 DB_PATH = "jqdata_mid.db" DAILY_TABLE = "stock_daily_tmp" # 临时表,校验后重命名 FIN_TABLE = "fin_report_tmp" # 3. 计算数据日期范围:取T-1日(避免T日数据未全) end_date = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d") start_date = "2020-01-01" # 全量起始日,可按需调整 # 4. 拉取日线数据(分批避免内存溢出) def fetch_daily_data(): all_stocks = jq.get_all_securities(types=['stock'], date=end_date).index.tolist() batch_size = 500 dfs = [] for i in range(0, len(all_stocks), batch_size): batch = all_stocks[i:i+batch_size] df = jq.get_price( security=batch, start_date=start_date, end_date=end_date, frequency='daily', fields=['open', 'close', 'high', 'low', 'volume', 'money'], skip_paused=True, fq='post' ) if not df.empty: # 重置索引,将level_0(security)和level_1(date)转为列 df = df.reset_index() df.rename(columns={'level_0': 'security_id', 'level_1': 'trade_date'}, inplace=True) # 格式化trade_date为YYYYMMDD字符串 df['trade_date'] = pd.to_datetime(df['trade_date']).dt.strftime('%Y%m%d') dfs.append(df) return pd.concat(dfs, ignore_index=True) if dfs else pd.DataFrame() # 5. 写入SQLite临时表 def write_to_sqlite(df, table_name): conn = sqlite3.connect(DB_PATH) try: # 使用replace模式清空临时表(确保每次都是干净状态) conn.execute(f"DROP TABLE IF EXISTS {table_name}") df.to_sql(table_name, conn, if_exists='replace', index=False, dtype={'trade_date': 'TEXT', 'security_id': 'TEXT'}) conn.commit() print(f"✅ 写入{table_name}完成,共{len(df)}行") finally: conn.close() if __name__ == "__main__": print(f"🚀 开始中转:{start_date} 至 {end_date}") daily_df = fetch_daily_data() write_to_sqlite(daily_df, DAILY_TABLE) print("🎉 中转完成!下一步:校验并重命名表")参数说明与关键逻辑:
skip_paused=True:跳过停牌股票,避免返回大量NaN;fq='post':强制后复权,保证价格序列连续,这是量化回测的底线要求;df.reset_index():聚宽get_price()返回MultiIndex DataFrame,必须转为普通列才能写入SQLite;dtype={'trade_date': 'TEXT'}:显式指定日期为TEXT类型,规避SQLite自动转为NUMERIC导致的排序错误(如'20240101' < '20240201'在TEXT下正确,在NUMERIC下可能被当作整数比较);if_exists='replace':每次运行都重建临时表,杜绝历史脏数据干扰。
3.3 财务数据中转:处理公告日期与财报周期的错位难题
聚宽财务数据(get_fundamentals)的pub_date(公告日)与end_date(报告期截止日)常不一致。例如2023年年报end_date='2023-12-31',但pub_date可能在2024年3月才发布。若只按pub_date拉取,会漏掉未公告的财报;若只按end_date拉,又会拉到未发布的“幽灵数据”。本方案采用双维度拉取+状态标记:
def fetch_financial_data(): # 先拉取所有已公告的财报(pub_date在近3年内) pub_cond = jq.fundamentals.income_statement.pub_date >= '2021-01-01' # 再拉取所有报告期为近3年的财报(end_date在近3年内),无论是否公告 end_cond = jq.fundamentals.income_statement.end_date >= '2021-01-01' # 合并两个条件,去重 q = jq.query(jq.fundamentals.income_statement).filter( (pub_cond) | (end_cond) ).order_by(jq.fundamentals.income_statement.pub_date.desc()) df = jq.get_fundamentals(q, date=end_date) if not df.empty: # 添加状态字段:'published' or 'unpublished' df['report_status'] = df['pub_date'].apply( lambda x: 'published' if pd.notna(x) and x <= end_date else 'unpublished' ) # 格式化日期字段 df['pub_date'] = pd.to_datetime(df['pub_date']).dt.strftime('%Y%m%d').fillna('') df['end_date'] = pd.to_datetime(df['end_date']).dt.strftime('%Y%m%d') return df此逻辑确保:已公告数据100%覆盖,未公告但报告期合理的数据也进入中转库,并打上report_status标签,下游可按需过滤。
4. 避坑指南:中转过程中高频踩坑的5个真实场景与根治方案
4.1 现象:get_price()返回空DataFrame,日志显示{'error': 'no data'}
原因:聚宽对start_date到end_date区间内无交易日的证券(如新上市未满一年的股票)会静默返回空,而非报错。若代码未检查df.empty,后续reset_index()会直接抛AttributeError。
解决:在fetch_daily_data()中增加空值防御:
if df.empty: print(f"⚠️ 股票批次{i}-{i+batch_size}无数据,跳过") continue4.2 现象:SQLite写入后SELECT * FROM stock_daily查不到数据,但sqlite3命令行能查到
原因:Python脚本中conn.close()后未显式conn.commit(),而SQLite默认autocommit=False,连接关闭时未提交的事务被回滚。
解决:所有写操作后必须conn.commit(),并在finally块中关闭连接(如3.2节代码所示),杜绝遗漏。
4.3 现象:财务表中total_revenue字段部分为None,部分为0.0,下游计算revenue_growth时报ZeroDivisionError
原因:聚宽原始数据中,total_revenue为None表示“未披露”,0.0表示“确认为零”,二者语义完全不同。但pandas.read_sql()默认将NULL转为np.nan,与0.0混在一起难以区分。
解决:在写入前显式标记:
df['total_revenue'] = df['total_revenue'].where(pd.notna(df['total_revenue']), -999999.0) # -999999.0作为“未披露”特殊值,下游可识别4.4 现象:etl_batch_id在不同机器上生成重复(如两台服务器同时运行脚本)
原因:datetime.now().strftime("%Y%m%d_%H%M%S")精度为秒,若脚本启动时间间隔小于1秒,批次ID相同,导致临时表重命名冲突。
解决:加入毫秒和主机标识:
import socket batch_id = f"jq_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{int(datetime.now().microsecond/1000):03d}_{socket.gethostname()[:4]}"4.5 现象:聚宽SDK认证失败,报错Authentication failed: invalid username or password
原因:聚宽账号密码在jq.auth()中明文硬编码,Git提交后泄露;或密码含特殊字符(如@、/)被URL解析错误。
解决:改用环境变量+URL编码:
import os from urllib.parse import quote username = quote(os.getenv("JQ_USERNAME", "")) password = quote(os.getenv("JQ_PASSWORD", "")) jq.auth(username, password)然后运行前执行:
export JQ_USERNAME="your_user" && export JQ_PASSWORD="your_pass" && python jqdata_to_sqlite.py5. 中转库交付与下游集成:如何把SQLite变成你的量化数据中枢
5.1 生成可交付的SQL Dump文件(供MySQL/PostgreSQL导入)
SQLite本身不支持mysqldump式导出,但可用其内置.dump命令生成标准SQL文本。以下脚本将中转库转换为带CREATE TABLE和INSERT语句的.sql文件,下游数据库可直接source:
#!/bin/bash # generate_dump.sh DB_FILE="jqdata_mid.db" OUTPUT_SQL="jqdata_$(date +%Y%m%d_%H%M%S).sql" # 导出所有表(排除sqlite_master等系统表) sqlite3 "$DB_FILE" ".tables" | \ grep -v "sqlite_" | \ xargs -I {} sqlite3 "$DB_FILE" ".dump {}" >> "$OUTPUT_SQL" # 替换SQLite特有语法为通用SQL(如去掉AUTOINCREMENT,改为SERIAL) sed -i 's/INTEGER PRIMARY KEY AUTOINCREMENT/INTEGER PRIMARY KEY/g' "$OUTPUT_SQL" sed -i 's/TEXT/varchar(20)/g' "$OUTPUT_SQL" # 简单类型映射,按需调整 echo "✅ SQL dump生成完成:$OUTPUT_SQL"提示:此脚本生成的SQL文件可直接在MySQL 8.0+或PostgreSQL 12+中执行。若下游是旧版MySQL,需额外处理
IF NOT EXISTS语法。
5.2 与Pandas无缝对接:用read_sql_query替代read_csv
中转库的最大价值是让下游分析代码极度简洁。对比两种写法:
旧方式(CSV):
# 读CSV要处理路径、编码、日期解析、缺失值 df = pd.read_csv("stock_daily.csv", parse_dates=['trade_date'], date_parser=lambda x: pd.to_datetime(x, format='%Y%m%d')) df = df.fillna(0) # CSV无NULL概念,需手动填0新方式(SQLite):
# 一行读取,类型、NULL、索引全自动 conn = sqlite3.connect("jqdata_mid.db") df = pd.read_sql_query( "SELECT * FROM stock_daily WHERE trade_date >= '20240101'", conn, parse_dates=['trade_date'] # SQLite中trade_date是TEXT,仍需parse_dates ) conn.close()更进一步,可封装为函数:
def get_stock_daily(start_date: str, end_date: str) -> pd.DataFrame: """安全获取日线数据,自动处理连接和关闭""" with sqlite3.connect("jqdata_mid.db") as conn: return pd.read_sql_query( "SELECT * FROM stock_daily WHERE trade_date BETWEEN ? AND ?", conn, params=(start_date, end_date) )5.3 构建数据质量看板:用SQL快速验证中转结果
中转完成后,别急着用,先跑三条SQL验证数据健康度。这是我在某高校量化实验室带学生时强制推行的“三查”习惯:
| 检查项 | SQL语句 | 合格标准 | 不合格后果 |
|---|---|---|---|
| 日期完整性 | SELECT MIN(trade_date), MAX(trade_date), COUNT(DISTINCT trade_date) FROM stock_daily; | MIN=20240101,MAX=20240519,COUNT=100(假设应有100个交易日) | 缺失交易日导致回测跳空 |
| 证券覆盖度 | SELECT COUNT(*) FROM (SELECT DISTINCT security_id FROM stock_daily); | ≥4500(A股总数) | 新股未纳入,策略失效 |
| 空值率 | SELECT AVG(CASE WHEN close IS NULL THEN 1.0 ELSE 0.0 END) AS null_rate FROM stock_daily; | < 0.001(<0.1%) | 复权失败或接口异常 |
把这三条SQL存为check_quality.sql,每次中转后执行:
sqlite3 jqdata_mid.db < check_quality.sql从那以后我每次写完中转脚本,都强制走一遍这三查——哪怕只是本地测试,也绝不跳过。数据质量没有“差不多”,只有“全对”或“全错”。希望帮到你。
本文还有配套的精品资源,点击获取