← 返回教程库

同一堆行情数据,做研究和做交易要的根本不是一个东西

最后更新 2026-08-19
📚 量化与 AI 投研 📖 仓库导览 · qlib ⏱ 约 15 分钟 R2 · 注意风险
你将学到
  • 能说清「取一行」和「取一列」这两种数据访问模式的区别,并判断自己手上的活属于哪一种
  • 能解释为什么投研系统愿意为「扫列快」牺牲掉「查最新一根快」,以及这个取舍会连带牺牲什么
  • 能看懂缓存的键代表什么,知道它为什么既省时间又会助长反复试探的坏习惯

你手上有一台不算差的电脑,硬盘里躺着几千只股票十几年的日线数据。你想做一件听起来毫不困难的事:给每只股票算一条「过去二十天均价」,然后看看价格站上这条线之后的一段时间,平均表现如何。

你写了个循环。跑起来,进度条爬得像蜗牛。你去泡了杯茶,回来还在爬。你开始怀疑是不是电脑太旧。

其实不是。真正的问题在更前面一步:你存数据的方式,是照着「看盘」的习惯来的,而你现在干的是「做研究」的活。 这两件事对数据的要求几乎是相反的,硬要用同一副架子去装,怎么都别扭。

这篇不讲怎么安装和配置什么东西,那是说明书的活。这篇要做的是:把一个投研框架的数据层拆开,看看它为什么被造成现在这个奇怪的样子——用的标本是微软开源的量化投研框架 qlib,但这套取舍的逻辑,换个工具一样成立。

先想想医院的档案室怎么摆

一家医院的病历,会被两种完全不同的人反复来查。

第一种是门诊医生。 病人坐在对面,医生要的是「这个人,今天,全部指标」——血压、血糖、肝功、上次开的药,一次性摊在眼前。他要的是一整行:一个人的所有列。他不关心隔壁那个病人的血压,一点都不关心。而且他要得急,病人在等。

第二种是做研究的人。 他要的是「过去十年,所有病人的血压这一项」。他不在乎某个具体的人到底怎么了,他要的是一整列:所有人的同一个字段,越长越好。而且他不是查一次就走——他今天算平均值,明天按年龄分组再算一遍,后天换个分组口径又算一遍。同一批数据,他要反复扫很多遍。

现在你是这家医院的档案管理员,你只能选一种摆法。

如果你照着门诊医生的需求摆,那就一个人一个档案袋,袋子里塞着他的全部化验单。医生报个名字,你抽一个袋子递过去,几秒钟的事。可研究员来了就惨了——他要全部人的血压,你得把上万个袋子一个一个打开,从每个袋子里翻出那一张血压单,再合起来。他要的东西其实很少,但你被迫把整个档案室翻了一遍。

如果你照着研究员的需求摆,那就按项目分:血压单全部归在一起,按时间排好;血糖单全部归在一起,也按时间排好。研究员报一句「血压,2015 到 2025」,你抱一摞过去,齐活。可门诊医生来了就麻烦了——他要一个人的全部指标,你得跑遍所有项目的柜子,每个柜子里挑出这个人的那一张。

没有两全的摆法。 你只能问一个问题:这个档案室,主要是给谁用的?

你手上的活,到底是哪一种

把医院换成市场,两种人依然存在,而且很可能都是你。

你在看盘、在下单的时候,你是门诊医生。 你要的是「这个品种,此刻,全部信息」——最新价、买卖盘、你的持仓成本、今天的涨跌幅。你要一整行,而且要快,因为价格在动。至于三年前的价格,此刻跟你半点关系没有。

你在验证一个想法的时候,你是研究员。 你要的是「所有品种,过去十年,收盘价这一列」。谁涨谁跌你现在不关心,你关心的是这一列数据放在一起呈现出什么规律。而且——这是关键——你一定会反复扫它

第二点值得展开说,因为它是整个设计的起点。

研究这件事的真实节奏,从来不是「取一次数、算一次、得出结论」。真实节奏是这样的:你先算个二十天均线看看,效果一般;换成三十天再看;觉得应该剔掉成交量太小的那部分再看;想到应该按行业分开看,又来一遍;发现结果好得可疑,怀疑是某一年拉高的,于是按年份切开再来一遍。

同一批原始数据,一天之内被你扫了几十遍。 每一遍你要的原始材料几乎完全一样,只是加工方式变了。

这就是投研场景的数据访问模式:读得极多,写得极少;取的是长长的列,不是短短的行;同一批数据被反复访问。 和交易场景那种「只要最新一根、要得急、来了就得处理」的模式,是两个物种。

所以一个专门为投研造的数据层,会毫不犹豫地倒向研究员那一边。它甚至会主动放弃「查最新一根很快」这个能力——反正那不是它的活。

这个判断对不写代码的人同样有用:你选任何一个数据工具之前,先问自己一句,我主要是要行,还是要列?把一个为交易造的工具拿去做研究,或者反过来,慢只是小事,更麻烦的是你会不自觉地被工具的脾气带着走,只做它擅长的那类分析。

到了系统里,档案室长什么样

qlib 的官方文档里画了一张目录结构图。剥掉技术外壳,它其实就是一间按研究员需求摆的档案室,有三个抽屉。

第一个抽屉:日历。 一份单独的文件,把所有交易日按顺序列出来。它不属于任何一只股票,它是全场共用的时间轴

第二个抽屉:名单。 哪些代码属于某个股票池,以及它们分别在哪段时间里属于。注意这里存的不是「今天的成分股」,而是带时间段的名单——这一点直接决定了你的回测会不会不小心用上了一份「事后才知道」的名单。

第三个抽屉:数据本身。 这里是重点:每一只股票的每一个字段,各自单独存成一个文件。 收盘价一个文件,开盘价一个文件,成交量一个文件。文件里面是什么?就是那一列数字,按日历顺序,一个挨一个,密密麻麻,中间不夹任何别的东西。

这就是「列式存储」四个字的全部含义——别被术语唬住,它说的就是「按项目分柜子,不按人分袋子」。

翻到源码里读取那一段,会看到一个很朴素的动作:先算出你要的起始日期在这一列里是第几个位置,然后直接跳到那个位置,再连着往下读一段。整个过程里,没有查找,没有比对,没有解析。

这一点值得停下来体会。为什么能做到?因为存的时候就定死了两条规矩:

  • 每个数字占的空间一模一样。所以「第 N 个数字在哪」是能直接算出来的,不用一个个数过去。
  • 第 N 个位置,就对应日历上的第 N 个交易日,这个约定绝对不能破。

第二条规矩的代价,在一个不起眼的地方露了出来:官方文档写明,股票停牌的那些天,价格和成交量会被写成空值,而不是干脆跳过不写。 为什么要浪费位置存一堆空的?因为一旦跳过,后面所有数字的位置全部前移,「第 N 个就是第 N 个交易日」这个约定当场作废,那个「直接跳过去读」的把戏也就玩不成了。

宁可留着空座位,也不能让队伍错位。 这是列式存储的命门:它换来了极快的扫列速度,代价是整个体系的正确性,全押在「对齐」这一件事上。任何一处错位,都不会报错,只会安安静静地把某只股票的收益率整体挪一天——而挪一天是什么后果,参见前视偏差

还有一个必须提醒的副产品。文档明确说了,这套数据里存的价格是复权之后的,而且是把每只股票首个交易日的价格归一化成 1 再往后推的。也就是说,你从里面取出来的「收盘价」,数值上跟当天真实成交的那个价格根本不是一回事;框架另外存了一个还原用的因子,让你需要时能倒推回原始价格。

这对研究员来说没问题——他要的是一条前后可比的收益序列。但如果你不知道这件事,拿着这个数就去讲「这个价位上方有套牢盘」,那你是在对着一个人为构造出来的数字讲故事。数据的每一次「为了好算而加工」,都会悄悄改变它能回答的问题类型。 这类坑不止一个,还有更多汇总在常见数据坑里。

为什么不能让你自己撸循环

档案室摆好了,接下来是取数的方式。

如果只提供「把某一列原封不动搬给你」这一种服务,会发生什么?

你要的从来不是原始的那一列。你要的是「过去二十天的均值」「今天相对五天前的涨幅」「这一列减去那一列」。于是流程变成:系统把原始列全部搬到你手上,你在自己这边写循环慢慢加工。

问题出在搬运量上。你最后可能只想要一个数,但为了算出它,几千只股票十几年的原始数据得先整个搬一趟。搬运本身就是最慢的那一步,而你想试的加工方式有几十种——搬了几十趟,每一趟搬的还是同一批东西。

qlib 的选择是提供一套表达式:你不再说「把收盘价给我」,你说的是「五日均收盘价」——写成一句短短的式子交上去,比如文档里举的 Mean($close, 5)

这个改变看着小,实际上换了一件事的性质:你从「要材料」变成了「点菜」。

「要材料」的时候,厨房只能照单搬货,它不知道你要干嘛,帮不上任何忙。「点菜」的时候,厨房知道你的完整意图,于是它可以做三件你自己做不到的事:

  • 只搬真正需要的。你要五日均值,它清楚只需要动收盘价这一列,别的柜子碰都不用碰。
  • 知道该多拿一点。要算二十天均值,那第一天的结果就得靠前面十九天垫着。系统清楚这个窗口有多长,会自动往前多取一段——这背后的道理和指标启动期是同一件事:任何带回看窗口的计算,开头那一截都是不作数的。自己撸循环时,这一截最容易被忘掉,然后你会得到一段莫名其妙的开局数据,还以为是市场真的这样。
  • 认得出你问过同样的问题。这一点留到下一节。

翻译成投资层的话:把一个想法压缩成一句能原样复述的式子,本身就是一种纪律。 一句式子可以存档、可以比对、可以在半年后原样再跑一遍看看还灵不灵;而「我当时是把均线调长了一点,好像还剔掉了几只」——这种记忆,半年后连你自己都还原不出来。

至于这种表达能力被用到极致之后会出什么事,那是另一篇的题目,见用表达式批量造因子,以及它带来的危险,概念层面的铺垫在公式化因子

缓存:为「反复扫同一批数据」而生

现在回到那个被反复强调的事实:研究员会把同一批数据扫几十遍。

你今天上午算过一次「五日均收盘价」,下午改了别的地方,又要用到它。这个数完全没变,凭什么要重算?

qlib 的缓存分了几层,思路都一样:把算过的结果留下来,下次直接用。 全场共用的日历和股票池名单被留在内存里,因为几乎每一次取数都要用到它们对齐时间;算出来的表达式结果被写到硬盘上,下次要同样的东西,直接读现成的。

真正值得琢磨的,是这些缓存怎么起名字

文档说得很清楚:表达式的缓存,名字来自「哪个品种 + 哪句表达式 + 什么频率」这几样东西一起算出来的一个指纹;整份数据集的缓存,名字来自「股票池配置 + 表达式清单 + 频率」的指纹。

缓存的名字,就是你那个问题的完整描述。 问题有一个字不同,指纹就不同,就是另一份缓存,就得重算。

这带来一条对所有人都成立的教训。缓存出错,几乎从来不是缓存本身坏了,而是你偷偷改了口径,却没让它出现在指纹里。你在外面某个地方改了一个筛选条件、换了个数据源、修正了一段历史数据,但描述这个问题的那句话一个字没变——系统就会一脸无辜地把旧答案端给你,而且端得飞快。

放到不写代码的场景里,一模一样:你维护着一张自选池的表格,上个月你悄悄调整了纳入标准,但表格的名字还叫「稳健池 v1」。三个月后你对着它复盘,你以为你在看同一个东西。凡是会变的东西,都必须出现在它的名字里。

还有一层缓存的副作用,值得单独警惕。

缓存让「再跑一遍」这件事变得极其便宜——从等一杯茶变成了眨个眼。便宜是好事吗?对效率是,对判断力未必。当试错成本降到接近零,人的行为会变:你不再是想清楚了才去验证,而是随手改个数就跑一遍,看哪次结果好看。跑上几百次之后,总有一次会好看的,而那次好看和市场规律毫无关系。

这就是数据窥探最舒服的温床——它不是靠作弊长出来的,是靠「太方便了」长出来的。 详见数据窥探。快,从来不是免费的;它把成本从你的时间,转移到了你的自制力上。

这套设计换来了什么,又赔上了什么

任何设计都是取舍,把账两边都摊开才算看懂了。

换来的:

  • 扫一整列极快,而且要多长有多长——这正是回测和因子研究的主要动作。
  • 同一批数据反复扫,第二遍之后几乎不花钱。
  • 「要什么」和「怎么算」被分开了,你的想法变成了可存档、可复述的一句话。

赔上的:

  • 不适合「给我最新一根」的实时场景。 按项目分柜子的档案室,要凑齐一个人的全部指标,得跑遍所有柜子。这不是缺陷,是它压根没打算做这件事——真正的实盘执行链路是另一套东西,freqtrade 那边的做法见它怎么保证每根 K 线只喂给你当时该有的数据
  • 改历史很贵。 复权因子调整了、某段数据修订了,牵动的往往是一整列。研究场景里数据是「定期整批更新」的,不是「随时改一个格子」。
  • 全押在对齐上。 日历、空值、位置,任何一处错位都不会报警,只会安静地把结论带偏。
  • 加工过的数就不是原始的数了。 复权价、归一化、填空值,每一步都是为了好算,每一步也都让它离「当时屏幕上显示的那个数」远了一点。

顺带说一句:财报那类「一个数字什么时候才被人知道」的问题,比价格序列复杂得多,光靠上面这套按日历排列的列是装不下的,它在这个框架里有一套专门的存法,见从数据层堵死偷看未来和概念篇Point-In-Time。整条流水线上数据层站在哪个位置,见qlib 是什么

它明确不负责的那些事

  • 它不保证数据是对的。 存得再整齐,源头错了就是错的。框架另外提供了体检的手段去查缺失和异常跳变,这恰恰说明脏数据是常态而不是意外。
  • 它不替你决定该看哪些数据。 要哪些字段、哪个池子、哪段时间,全是你的输入。
  • 它不告诉你算出来的东西有没有意义。 一句表达式算得又快又对,和这句表达式描述的规律真实存在,是两个毫不相干的问题。
  • 它不管你的数据合不合规、能不能商用。 数据的来源和使用授权,是你自己要处理的事。

小结

  • 交易要的是「一整行」——这个品种此刻的全部信息;投研要的是「一整列」——所有品种全历史的同一个字段。两种需求对应两种完全不同的存法,没有两全。
  • 投研数据层倒向后者:按字段分开存、按日历顺序排列、位置直接算出来就能跳过去读。代价是全部正确性押在对齐上,停牌日宁可留空座位也不能让队伍错位。
  • 提供表达式而不是原始数据,是为了让系统知道你的完整意图——这样它才能少搬东西、自动补足回看窗口、并认出重复的问题。对你的额外好处是:想法被固化成了可复述、可归档的一句话。
  • 缓存的名字就是问题的完整描述。口径变了名字没变,你会被飞快地喂一个旧答案。
  • 缓存让重跑变得极便宜,而便宜会改变人的行为——试错成本趋近于零的时候,最容易长出的东西是数据窥探。

一句话说完这篇:做研究和做交易,要的根本不是同一种数据——一个要横着看某一刻的全部,一个要竖着看某一项的全部,而档案柜只能照着一种需求来摆。

本文基于开源仓库的源码与文档快照做概念讲解,未实际运行该程序,也不构成任何投资建议或软件使用建议;相关边界见免责声明。想系统看这一卷的其他内容,回到量化与 AI 投研卷

内容有错、看不懂、或想看下一篇?告诉我们 →

本文为投资教育整理,不构成任何投资建议;不荐个股/基金、不预测点位、不承诺收益。关键数据与结论请结合权威来源自行验证,并见风险免责声明