QLIB接入tushare实现A股行情增量更新实战指南

发布时间:2026/10/5 3:07:28
QLIB接入tushare实现A股行情增量更新实战指南 QLIB这套框架有个挺有意思的现象文档和官方Example都极其完善从下载数据到跑通LightGBM因子模型一路顺畅但只要你把数据集从Yahoo Finance换成国内A股立刻就能感受到什么叫“折腾才刚刚开始”。最大的分水岭就在数据上——QLIB对输入数据的格式要求非常严格而tushare返回的数据又完全是另一套风格。这篇文章就是把我自己从“tushare拉数据”到“QLIB能正常跑因子回测”的完整链路记录下来重点解决每天收盘后怎么增量更新行情这件事顺带把复权、交易日历、bin文件重新dump这些坑都填平。1. 为什么QLIB对行情数据如此“挑剔”1.1 三张表对齐QLIB数据体系的核心结构QLIB不是一个只吃K线的回测框架它把数据拆成了三个相互关联的存储对象calendar、instrument、feature。你可以把它们想象成图书馆的三个柜台calendar告诉你图书馆哪天开门交易日instrument告诉你书架上有哪些书股票代码和上市退市区间feature则记录每本书每天的内容变化OHLCV等行情指标。calendar表本质就是一个纯日期列表存的是所有交易日格式像2024-01-02、2024-01-03这样。instrument表则记录了每只股票的代码、市场板块、上市日期、退市日期、行业分类QLIB依赖它来判断某只股票在某一天是否“存在”。feature表是核心每只股票对应一个独立文件里面存着该股票按交易日排列的行情和因子字段。这个三表结构意味着你从tushare拉下来的数据不能直接塞给QLIB必须先转换成围绕这三张表的格式。很多新手把tushare返回的DataFrame直接存成CSV然后用D.instruments()去读结果发现股票列表为空、因子跑出来全是NaN——就是因为instrument表的信息没有同步维护。1.2 QLIB对文件名和字段名的“强迫症”如果你打开QLIB官方提供的示例数据目录会发现文件名长这样$SH600000.csv $SH600036.csv $SZ000001.csv注意两个细节文件名带$符号且股票代码前面有市场前缀。SH代表上交所SZ代表深交所这是QLIB区分同代码股票比如600000在SH和BJ如果重号的话的关键约定。而feature文件的列名要求是标准格式字段说明open开盘价high最高价low最低价close收盘价volume成交量factor复权因子vwap均价可选turnover换手率可选QLIB自带的dump_bin.py脚本在读取CSV时会严格检查索引列是否为DatetimeIndex字段名是否在配置的include_fields里。如果你用tushare直接导出列名是中文或trade_date、vol这类dump过程就会报字段不匹配的错误。1.3 复权价格QLIB因子计算的前提热词里专门有“tushare如何计算复权”这确实是绕不开的坎。QLIB在计算技术因子如MA、RSI、BOLL时输入的价格序列会被当作真实可交易价格来处理。如果因为除权除息导致价格出现断崖式跳空比如10转10股价从20元瞬间变10元那么基于价格计算的因子第二天会出现巨大的假突变模型会学到完全错误的信息。所以送入QLIB的价格必须是复权价。QLIB核心开发者建议使用后复权价原因是前复权会随时间推移不断修正历史价格每次除息后所有历史数据都会变化这会导致你昨天建好的因子库和今天重建的因子库整体不一致。后复权则锚定过去历史价格不随新除权事件改变天然具备稳定性更适合做长时间的因子研究和回测。拿tushare的复权因子表adj_factor来说一只股票上市首日因子为1之后每次除权除息因子发生变化。后复权价格的计算逻辑是后复权收盘价 不复权收盘价 × 当日累计因子。tushare的adj_factor接口返回的本身就是日频的累计复权因子直接用不复权价格乘上这个因子就是后复权价。而pro_bar(adjhfq)则是tushare直接帮你算好的后复权价省去了手动乘的步骤。注意QLIB官方只要求你给的价格是“合理可用的价格”不强制必须是后复权。但如果你用前复权每次除权后增量更新历史数据会被重写导致不同批次dump出来的bin文件价格基线不一致这在回测时非常容易埋雷。我强烈建议统一用后复权。2. tushare取数前必须确认的三件事2.1 token权限与接口门槛tushare是社区常见的A股数据接口但它的“积分制”经常让新人措手不及。daily基础行情接口需要一定积分而pro_bar的复权参数、adj_factor复权因子、trade_cal交易日历都有各自的调用权限。具体门槛数值一直在调整我建议以tushare官网文档为准这是最权威的信息来源。实际操作中我遇到的情况是积分不够时调用pro_bar(adjhfq)会返回空DataFrame或直接抛异常但接口文档又没写清楚。我的排查经验是先单独调一次pro.daily()能返数据再往上叠加adj参数。如果daily能返回而pro_bar无法返回大概率就是积分权限问题。2.2 pro_bar复权参数与复权因子接口的选择tushare里取日线行情有两条路daily和pro_bar。daily返回的是不复权数据pro_bar是在daily基础上封装了一层支持adj参数可选qfq前复权和hfq后复权。我的建议是直接用pro_bar(adjhfq)因为它在底层已经处理了停牌、上市首日、涨跌停等特殊情况返回的字段也更规整。另外如果你需要自己存一份复权因子可以单独调adj_factor接口存进MySQL或直接合并到CSV的factor列。QLIB在dump_bin时会把factor字段也写进bin文件后续算因子时会用到。这里有一个容易犯的错把adj_factor直接当成“因子”传给QLIB。adj_factor的确是复权因子也是QLIB里factor列的首选数据但对tushare返回的复权行情来说pro_bar(adjhfq)的close已经包含了复权调整此时如果再把factor列填进去会导致QLIB的某些复权相关操作产生叠加效应。稳妥的做法是要么只用pro_bar的复权价factor列填1要么只用不复权价factor列填adj_factor然后让QLIB内部通过factor字段进行价格调整。我个人的习惯是后复权价 factor列填1简单直接回测逻辑清晰。2.3 交易日历别用节假日猜沪深交易所的交易日不是简单的“周一到周五剔除周末”元旦、春节、国庆这种长假会导致某周的某一天不开市偶尔还会有临时休市。tushare提供的trade_cal接口返回的is_open1记录就是准确的交易日清单。在增量更新脚本里交易日历的作用很关键你要决定“今天数据更新到哪一天”。如果当天是交易日但数据还没出来盘后数据通常要等到下午5点到7点才稳定脚本需要判断并跳过等下一个周期再拉。如果用普通工作日判断很可能遇到“今天周三是节假日休市但程序傻乎乎去拉数据拉回来一坨空值往CSV里写把全量数据表污染了”的情况。3. 全量初始化把过去N年A股行情倒进QLIB3.1 目录结构与配置文件在动手跑全量之前先建好目录。我习惯把数据根目录放在qlib_data下面里面分csv和bin两层qlib_data/ ├── csv/ │ ├── calendars/ │ │ └── day.txt │ ├── instruments/ │ │ ├── all.txt │ │ └── stocks.txt │ └── features/ │ ├── $SH600000.csv │ ├── $SH600036.csv │ └── ... └── bin/ ├── calendars/ ├── instruments/ └── features/然后QLIB的配置入口指向bin目录即可。配置文件一般是~/.qlib/qlib_init.conf或者在代码里直接设置from qlib.config import REG_CN from qlib.constant import REG_CN import qlib qlib.init(provider_uriqlib_data/bin, regionREG_CN)3.2 全量拉取脚本我用的是tushare的pro接口先拉股票列表再逐只拉历史日线。下面这个脚本就是以2020年至今为区间做全量初始化的示例import tushare as ts import pandas as pd import os from datetime import datetime ts.set_token(你的token) pro ts.pro_api() start_date 20200101 end_date datetime.now().strftime(%Y%m%d) os.makedirs(qlib_data/csv/features, exist_okTrue) # 1. 股票列表 stock_basic pro.stock_basic(exchange, list_statusL, fieldsts_code,symbol,name,area,industry,list_date) stock_list stock_basic[ts_code].tolist() # 2. 交易日历 cal pro.trade_cal(exchangeSSE, start_datestart_date, end_dateend_date, is_open1) trade_days cal[cal_date].tolist() # 3. 写入calendar with open(qlib_data/csv/calendars/day.txt, w) as f: for d in sorted(trade_days): f.write(d \n) # 4. 逐只拉行情并写CSV for ts_code in stock_list: symbol ts_code.split(.)[0] exchange ts_code.split(.)[1] if exchange SH: symbol_with_prefix SH symbol elif exchange SZ: symbol_with_prefix SZ symbol else: continue # 跳过北交所或扩展市场按需处理 df ts.pro_bar(ts_codets_code, adjhfq, start_datestart_date, end_dateend_date, factors[tor, vr]) if df is None or df.empty: continue df df.rename(columns{trade_date: date}) df[date] pd.to_datetime(df[date]) df df.sort_values(date) df df.set_index(date) df df[[open, high, low, close, vol, amount, factor]] df.columns [open, high, low, close, volume, amount, factor] df.to_csv(fqlib_data/csv/features/${symbol_with_prefix}.csv, indexTrue)这个脚本有几个容易出错的细节pro_bar在传adjhfq时如果还传factors[tor,vr]返回的字段里会多出换手率和成交量比率正好可以补全技术因子所需的数据一步到位。QLIB官方示例里字段名是volume不是vol所以导入前要重命名。别小看这个字段名问题dump_bin.py在include_fields里配置了volume你给的CSV里却叫vol它会直接跳过这个字段最后bin文件里volume全为空。我在全量拉取时把amount也保留下来了虽然QLIB默认因子不一定用到但后续自定义因子比如典型价格、动量会需要成交额。3.3 调用dump_bin.py生成bin文件CSV准备完毕后用QLIB官方的dump_bin.py将文本数据转成二进制格式python scripts/dump_bin.py dump_all --csv_path qlib_data/csv --qlib_dir qlib_data/bin --include_fields open,high,low,close,volume,amount,factor注意几点dump_bin.py是QLIB源码scripts目录下的脚本直接从GitHub克隆QLIB仓库就能找到。--include_fields这个参数在较新的QLIB版本里才强制要求老版本默认只转open、high、low、close、volume等核心字段。如果你用了自定义字段务必在include_fields里显式列出来。dump过程会同时生成calendars/day.txt和instruments/all.txt但instrument文件里的内容是从csv/instruments/all.txt读入的。所以如果你自己在csv/instruments/all.txt里写了股票列表dump时它会一并转过去。如果没有这个文件dump会报错找不到instrument定义。3.4 验证初始化结果转完bin后可以用QLIB读取来验证import qlib from qlib.data import D from qlib.config import REG_CN qlib.init(provider_uriqlib_data/bin, regionREG_CN) print(D.calendar(start_time2024-01-01, end_time2024-01-31)) print(D.instruments(marketcsi300)) df D.features([SH600000], [$close, $volume], start_time2024-01-01, end_time2024-01-31) print(df.head())如果df能正常返回非空数据说明bin文件没问题。如果返回全空或NaN优先检查是不是calendar和feature的日期区间没对齐——比如股票2020年1月才上市但你从2020年1月开始拉数据而instrument表里该股票的start_time写成了2019年1月QLIB在2020年1月这个点上会认为股票还没上市数据一概不返回。4. 每日增量更新核心逻辑与完整脚本4.1 增量更新的本质不是“追加”而是“重建”先说一个容易误解的点QLIB的bin文件不支持增量追加dump_bin.py的工作原理是把CSV全量读进来再全量写入bin。所以“每日更新行情”这件事实际步骤是用tushare拉取最新一天的行情增量数据。将增量数据合并到已有的CSV文件里。重新调用dump_bin.py让bin文件整体重建。只要数据量停留在几千只股票、几年历史的规模重建一次bin文件的耗时大概在1~3分钟左右完全在可接受范围内。但如果你的数据系统已经积累了十几年的高频数据那就要考虑分区整理或只dump近期区间。不过对于大多数个人量化研究重建bin的策略最简单可靠。4.2 增量更新脚本的组件拆解我需要把这个脚本拆成四个核心函数分别处理不同的逻辑第一步获取最近交易日和待更新日期def get_trade_days(pro, end_date): cal pro.trade_cal(exchangeSSE, start_date20240101, end_dateend_date, is_open1) days sorted(cal[cal_date].tolist()) return days这里的关键逻辑是从calendar文件里读出最后一个交易日然后再到trade_cal返回的列表里找这个日期后面的所有交易日。只有这些“缺失的交易日”才需要去拉行情。第二步增量拉取指定区间的行情def fetch_incremental(pro, ts_code, start_date, end_date): df ts.pro_bar(ts_codets_code, adjhfq, start_datestart_date, end_dateend_date, factors[tor, vr]) if df is None or df.empty: return pd.DataFrame() df df.rename(columns{trade_date: date}) df[date] pd.to_datetime(df[date]) df df.sort_values(date).set_index(date) df df[[open, high, low, close, vol, amount, factor]] df.columns [open, high, low, close, volume, amount, factor] return df第三步合并到本地全量CSV这一步比想象中容易出问题。因为tushare的pro_bar返回的是整个区间的全部交易日数据如果某一天数据因为晚到或缺数而没返回那合并后就会产生“空洞”。所以合并时不能简单concat而应该用combine_first或按日期做merge去重。def merge_with_local(incremental_df, csv_path): if os.path.exists(csv_path): old_df pd.read_csv(csv_path, index_coldate, parse_datesTrue) else: old_df pd.DataFrame() combined pd.concat([old_df, incremental_df]) combined combined[~combined.index.duplicated(keeplast)] combined combined.sort_index() combined.to_csv(csv_path, indexTrue) return combined第四步重新dump_bin和全量初始化一样增量更新后的最后一步也是调用dump_bin.py。python scripts/dump_bin.py dump_all --csv_path qldb_data/csv --qldb_dir qldb_data/bin --include_fields open,high,low,close,volume,amount,factor如果你希望脚本自动执行而不手工敲命令可以把这个调用放到subprocess里或者直接用Python调用QLIB的dump函数。我个人更喜欢直接subprocess因为它能复用现有环境变量和Python路径少踩一些坑。4.3 完整增量脚本参考把上面几段拼起来一个可落地的最小脚本大概长这样import os import subprocess import pandas as pd import tushare as ts from datetime import datetime ts.set_token(你的token) pro ts.pro_api() CSV_FEATURES_DIR qlib_data/csv/features CALENDAR_FILE qlib_data/csv/calendars/day.txt CSV_ROOT qlib_data/csv BIN_ROOT qlib_data/bin # 1. 读取本地已有的最后一个交易日 with open(CALENDAR_FILE, r) as f: last_local_date f.readlines()[-1].strip() today datetime.now().strftime(%Y%m%d) # 2. 获取实际交易日 cal pro.trade_cal(exchangeSSE, start_datelast_local_date, end_datetoday, is_open1) all_days sorted(cal[cal_date].tolist()) if last_local_date in all_days: all_days all_days[all_days.index(last_local_date) 1:] if not all_days: print(没有需要更新的新交易日) exit(0) incremental_start all_days[0] incremental_end all_days[-1] # 3. 获取股票列表这里只更新全市场有交易的股票可适当过滤 stock_basic pro.stock_basic(list_statusL, fieldsts_code) ts_codes stock_basic[ts_code].tolist() failed_codes [] # 4. 逐只拉增量数据并合并 for ts_code in ts_codes: symbol, exchange ts_code.split(.) if exchange SH: csv_name f${exchange}{symbol}.csv elif exchange SZ: csv_name f${exchange}{symbol}.csv else: continue csv_path os.path.join(CSV_FEATURES_DIR, csv_name) try: inc_df fetch_incremental(pro, ts_code, incremental_start, incremental_end) if not inc_df.empty: merge_with_local(inc_df, csv_path) except Exception as e: failed_codes.append(ts_code) print(f处理 {ts_code} 失败: {e}) # 5. 重新dump_bin cmd [ python, scripts/dump_bin.py, dump_all, --csv_path, CSV_ROOT, --qlib_dir, BIN_ROOT, --include_fields, open,high,low,close,volume,amount,factor ] subprocess.run(cmd, checkTrue) print(f增量更新完成更新区间 {incremental_start} - {incremental_end}) if failed_codes: print(f失败股票数: {len(failed_codes)})这个脚本在运行时有个值得注意的点pro.stock_basic(list_statusL)只返回当前上市状态的股票如果某只股票在最近一个月发生暂停上市、重新上市、退市整理等情况它就不会出现在列表里那么这只股票的增量行情就漏了。我的处理方式是每周末单独跑一次“全市场股票列表对比”把新上市/退市的股票同步到instrument表里并做一次更长期限的补偿拉取。4.4 更新耗时估算以全市场5000只股票、增量1个交易日计算循环里每次pro_bar调用大概耗0.3秒左右总耗时约25~30分钟。这个速度基本取决于tushare接口的延迟和你的积分等级积分越高单次调用返回越快。如果嫌太慢可以改成多线程并发拉取但要小心tushare每分钟调用次数的限制建议控制在每分钟60次以内否则容易触发频率限制。5. 部署上线定时任务、边界情况与数据校验5.1 用cron定时执行增量脚本写好后放到服务器上用crontab调度。A股收盘是15:00盘后数据一般在17:30~20:00之间稳定考虑到tushare的数据更新延迟我建议把任务安排在每天21:00之后运行比如0 21 * * 1-5 cd /path/to/project /usr/bin/python incremental_update.py logs/update_$(date \%Y\%m\%d).log 21这里的重点是把标准输出和错误都重定向到日志文件否则你很难发现在某一天因为一个接口异常导致后面的股票全部更新失败。日志里最好带上每只股票的处理状态和耗时方便事后排查。5.2 边界情况新股、停牌、退市增量更新最怕的其实不是“拉不到数据”而是“只拉到了部分数据”且数据里混着异常值。新股上市新股在stock_basic里会以list_statusL存在但一定存在一个“上市首日”之前的空白期。如果增量区间覆盖了上市首日pro_bar会自动返回从上市首日开始的数据合并时没问题。但如果新股上市首日是在上一个交易日之后而我们增量区间没覆盖到它就会被漏掉。我的做法是每周一做一个全量stock_basic对比把新出现的股票连带上市首日到上周五的历史数据一次性补全。停牌tushare的pro_bar对停牌日通常会返回空也就是说增量区间里某天停牌那天的数据就是不存在的。合并CSV后这个日期对应的行不会出现在文件里这是正常的。QLIB的D.features在读取时遇到缺失交易日会用NaN填充或前向填充取决于你的数据处理器配置。退市/风险警示退市股在list_statusD里如果你只用list_statusL退市股不会进入增量更新范围但历史bin里已有的数据不会受影响。风险警示股票ST、*ST和正常股票一样更新QLIB不会因为你给它喂ST股就报错只是你构建训练集时要自行过滤。5.3 数据完整性校验增量更新后bin文件是全新生成的理论上只要CSV是完整的bin就不会有问题。但CSV的完整性需要单独校验。我习惯在更新脚本最后加一段校验逻辑# 随机抽10只股票检查最新日期是否为预期交易日 import random sample_codes random.sample(ts_codes, 10) latest_expected all_days[-1] for code in sample_codes: symbol, exchange code.split(.) csv_path os.path.join(CSV_FEATURES_DIR, f${exchange}{symbol}.csv) df pd.read_csv(csv_path, index_coldate, parse_datesTrue) if str(df.index.max().date()) ! latest_expected: print(f{code} 最新日期为 {df.index.max().date()}期望 {latest_expected})这段代码虽然简单但在生产环境里非常有价值。它能在你还没跑回测前就发现“某只股票最新交易日缺失”的问题。否则等你用D.features拉数据训练模型时才发现某只股票缺了几天排查成本会高很多。5.4 磁盘占用与bin目录管理bin文件是二进制格式比CSV压缩了不少但如果积累了多年全市场日线数据磁盘占用还是会慢慢涨起来。我的经验值是5000只股票 × 10年日线 × 7个字段bin目录大约在3~5GB左右CSV目录会更大。建议在服务器上每个月压缩一次历史CSV或者在dump_bin之后直接删除成色较旧的CSV文件只保留最近一年的CSV做增量合并参考。不过有一点要留神dump_bin.py在重新dump时依赖CSV目录里的全部数据如果CSV删得太早后续增量更新时merge_with_local读到旧数据会缺失bin重建出来的历史也不完整。所以要么保留全部CSV要么在删除前确认bin里已有数据是完整的且不要再做全量重建。6. 复权计算的细节与常见误区6.1 tushare复权因子和手动计算的方法tushare的adj_factor接口返回的是「复权因子」它不是涨跌幅而是一个连乘累积的系数。想要从原始不复权价格算后复权价公式是后复权价 不复权价 × 当日复权因子举个例子某股票上市首日收盘价10元复权因子是1发生一次10转10后若除权日当天不复权收盘价为5元复权因子会变成2后复权价就是10元。这样价格序列就没有“跳空”了技术指标的计算结果也更连贯。pro_bar(adjhfq)实际上就是底层做了这个乘法但如果你需要自定义复权方式比如想用“后复权价 真实成交量”的组合就可以手动实现df_raw pro.daily(ts_code600000.SH, start_date20240101, end_date20240401) df_factor pro.adj_factor(ts_code600000.SH, start_date20240101, end_date20240401) df pd.merge(df_raw, df_factor, ontrade_date, howleft) df[close_hfq] df[close] * df[adj_factor]6.2 “前复权”在增量更新里的危害比你想的更大前面已经提过前复权价会随着每次除权而修改所有历史价格。这在一次性全量导入时没有问题但在增量更新场景下非常危险比如今天是4月1日你用pro_bar(adjqfq)拉了1月1日到4月1日的数据存进QLIB。等到4月15日发生一次高送转你增量拉4月2日到4月15日的数据此时tushare返回的前复权价会把这个除权事件应用到整个历史区间导致你CSV文件里1月1日到4月1日的那部分历史价格突然变化。但你的增量合并逻辑是按日期去重的旧文件里的历史日期不会因为新数据而更新于是CSV里出现了“前半段是旧前复权价、后半段是新前复权价”的割裂状态。bin重建后因子计算就会用到这种前后矛盾的序列结果混乱。后复权则不存在这个问题因为历史价格锚定过去除权事件只会影响除权日及之后的价格增量合并时旧历史数据完全不需要变。这就是为什么我在整个流程里坚持用adjhfq的根本原因。6.3 除权日当天应该保留哪一条价格线增量更新时如果除权日恰好落在增量区间内pro_bar返回的除权日价格已经是复权后的价格直接写入CSV没问题。但如果用的是daily接口做手动复权要特别注意除权日当天原始收盘价和复权因子的对应关系除权日当天的adj_factor已经反映了除权所以计算后复权价时直接用当日因子相乘即可不需要额外“跳过”除权日。另外除权除息日当天开盘竞价往往有大幅波动有些框架会建议在回测时剔除除权日当天或前一天的信号以免因子突变诱发虚高绩效。但这是策略层面的处理数据层只需保证复权价连续即可。7. 运行过程中的踩坑记录与解决思路这里记录几个我实际运行这套增量更新流程时遇到的问题都属于“不踩一次真的想不出来”的坑。7.1 dump_bin后读取bin股票数量少了我全量dump后用D.instruments(marketall)一看发现只有3000多只股票而tushare的stock_basic里明明有5000多。排查了很久才发现是CSV文件里有些股票数据为空比如上市交易日当天没有行情返回导致dump_bin.py直接跳过了该文件。它在日志里会打印类似“skip empty feature $SZ000001.csv”的提示只是很容易被忽略。解决方法是在生成CSV时遇到空DataFrame手动创建一个只有索引、没有行数据的空表或者写入一行NaN占位确保文件不为空。更推荐的做法是全量脚本跑完后检查一遍csv/features目录下的文件数量和stock_basic的数量比对及时补拉。7.2 新旧版本dump_bin参数不一致QLIB的dump_bin.py在不同版本里对dump_all的参数要求有变化。老版本只要指定--csv_path和--qlib_dir默认会把CSV目录下所有列都dump进去新版本要求你用--include_fields显式列字段。如果你直接用网上的旧示例命令跑新版本源码会看到类似“error: the following arguments are required: --include_fields”的报错。这个看报错就能解决但如果你使用的是别人封装好的镜像或docker容器里QLIB版本可能很老字段参数又不一样。最稳妥的方式是直接python scripts/dump_bin.py dump_all --help看当前版本的help输出再按参数说明执行。7.3 时区与日期格式的隐性坑tushare返回的trade_date是字符串格式YYYYMMDDQLIB的calendar文件里也是这种格式的日期字符串。但在合并进DataFrame时我把date列转成了pd.to_datetime这会自动加时区00:00:00。如果后续用D.features查询时传入的start/end也是字符串QLIB会统一按当天零点处理一般没问题。但如果你本机是UTC8而服务器是UTC日期索引的datetime在读写CSV时可能会发生偏移导致某天的数据被判断成前一天或后一天。为了避免这个问题我在写CSV前就把时间统一设置为pd.to_datetime(df.index).tz_localize(None)去掉时区信息。7.4 内存不足导致dump中断全量dump时如果CSV文件特别大且dump_bin.py默认读取所有CSV进内存可能直接OOM。我的解决思路是分市场dump先只dump上证再只dump深证。具体做法是在csv/features目录下建子目录每个子目录放一部分股票然后用dump_bin.py分别执行最后把calendars和instruments目录里的公共文件拷贝到最终bin目录。这个方法有点绕但对个人服务器16G内存的环境来说能省掉很多麻烦。8. 我最后想说的几个建议这套“QLIB tushare 每日增量更新”的流程我在个人量化环境里已经稳定跑了小半年。除了一开始那些格式问题之外后续真正让我觉得值得继承的经验其实就三条。第一复权方式一定要在数据入口就锁死。不管是自己写合并逻辑还是用别人封装好的导入工具把adjhfq这类设定写死在配置里并且每一次增量更新都校验一下最新价格和tushare官网的复权价是否一致能避免非常多的隐性误差。第二CSV层与bin层分开看待。CSV是“源数据”bin是“派生数据”。所有清洗、合并、去重都尽可能在CSV层完成bin只作为QLIB运行时的高效读取格式。这样就算bin文件坏了你也可以随时从CSV重建反过来如果只改bin不改CSV下次增量更新时你的修改会被覆盖掉。第三监控要简单但有效。我每天只看两个指标更新日志里“失败股票数”是否为0以及随机抽样的20只股票的最新交易日是否等于当天交易日。这两个指标能拦住绝大部分更新异常。如果你连随机抽样都觉得麻烦那就在更新任务后加一行SQLSELECT COUNT(*) FROM stock_feature WHERE date latest_trade_date只要数量在合理区间内基本就说明更新成功了。最后分享一个我习惯用的小技巧每次增量更新完成后用QLIB跑一个最简单的小因子比如$close的MA5做一次数据完整性自查。如果这个基础因子能正常算出来说明从tushare到QLIB这条链路是通的再往上叠加复杂因子时才不会怀疑是数据层的锅。这套流程本质上就是把“取数—落盘—转格式—刷新”四件事固定下来后面无论策略怎么变数据底座都不用再折腾了。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询