<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>PostgreSQL on 远思码技术实验室</title><link>https://yuansima.com/tags/postgresql/</link><description>Recent content in PostgreSQL on 远思码技术实验室</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Wed, 14 Oct 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://yuansima.com/tags/postgresql/index.xml" rel="self" type="application/rss+xml"/><item><title>TimescaleDB 量化行情库进阶：分区、压缩与连续聚合</title><link>https://yuansima.com/p/timescaledb-market-data-modeling/</link><pubDate>Wed, 14 Oct 2026 21:30:00 +0800</pubDate><guid>https://yuansima.com/p/timescaledb-market-data-modeling/</guid><description>&lt;p&gt;&lt;em&gt;远思为径，码动价值。欢迎来到远思码技术实验室的最新技术实践记录。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;在&lt;a class="link" href="https://yuansima.com/p/clickhouse-vs-dolphindb/" &gt;量化数据存储选型&lt;/a&gt;那篇里，我留下了一个结论：&lt;strong&gt;冷数据归档用 ClickHouse，热数据计算交给 DolphinDB&lt;/strong&gt;。但实验室真正跑日常研究的那套库，最后选了第三个选项——&lt;strong&gt;TimescaleDB&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;原因很朴素：A 股日线一年也就一百多万行，任何数据库都扛得住；真正稀缺的是&amp;quot;&lt;strong&gt;能不能用 SQL 直接把数据拼出来&lt;/strong&gt;&amp;quot;。TimescaleDB 是 PostgreSQL 扩展，意味着我可以用窗口函数、&lt;code&gt;DISTINCT ON&lt;/code&gt;、CTE、&lt;code&gt;generate_series&lt;/code&gt; 这些熟悉的东西，而不用为了一次取数去写一段 DolphinDB 脚本。&lt;/p&gt;
&lt;p&gt;这篇记录的是把它从&amp;quot;能存&amp;quot;做到&amp;quot;能长期跑&amp;quot;的几个关键点。&lt;/p&gt;
&lt;h2 id="一超表分区时间维度--空间维度"&gt;一、超表分区：时间维度 + 空间维度
&lt;/h2&gt;&lt;p&gt;上一篇文章里我们建了最简单的超表：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;CREATE&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;TABLE&lt;/span&gt; daily_bar (
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; symbol text &lt;span style="color:#66d9ef"&gt;NOT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NULL&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; trade_date date &lt;span style="color:#66d9ef"&gt;NOT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NULL&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;open&lt;/span&gt; double &lt;span style="color:#66d9ef"&gt;precision&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; high double &lt;span style="color:#66d9ef"&gt;precision&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; low double &lt;span style="color:#66d9ef"&gt;precision&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;close&lt;/span&gt; double &lt;span style="color:#66d9ef"&gt;precision&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; volume bigint,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; amount double &lt;span style="color:#66d9ef"&gt;precision&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; turnover double &lt;span style="color:#66d9ef"&gt;precision&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; pct_chg double &lt;span style="color:#66d9ef"&gt;precision&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; adj_factor double &lt;span style="color:#66d9ef"&gt;precision&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;DEFAULT&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;.&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;PRIMARY&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;KEY&lt;/span&gt; (symbol, trade_date)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;SELECT&lt;/span&gt; create_hypertable(&lt;span style="color:#e6db74"&gt;&amp;#39;daily_bar&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;trade_date&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; chunk_time_interval &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; INTERVAL &lt;span style="color:#e6db74"&gt;&amp;#39;1 year&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; if_not_exists &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;TRUE&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;日线量级下这样完全够用。但当我把分钟线也接进来（5 分钟 K 线，全市场一年约 &lt;strong&gt;1.6 亿行&lt;/strong&gt;）之后，问题出现了：&lt;strong&gt;单个 chunk 内部扫的是全市场&lt;/strong&gt;。查询&amp;quot;贵州茅台最近一年 5 分钟线&amp;quot;仍然要扫过所有股票的 5 分钟数据。&lt;/p&gt;
&lt;p&gt;解决办法是引入空间维度，按 &lt;code&gt;symbol&lt;/code&gt; 做哈希分区：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;SELECT&lt;/span&gt; create_hypertable(&lt;span style="color:#e6db74"&gt;&amp;#39;minute_bar&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;ts&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; partitioning_column &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;symbol&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; number_partitions &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;8&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; chunk_time_interval &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; INTERVAL &lt;span style="color:#e6db74"&gt;&amp;#39;7 days&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; if_not_exists &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;TRUE&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;建完之后，一张超表实际变成 &lt;code&gt;8 × 时间片&lt;/code&gt; 个物理子表。查询条件里带 &lt;code&gt;symbol&lt;/code&gt; 时，规划器直接裁掉 7/8 的分区。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;经验值：&lt;code&gt;number_partitions&lt;/code&gt; 别超过 CPU 核心数的两倍太多。分区太碎会让插入时的元数据开销超过收益，8 分片在实验室的 8 核机器上正好。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="二压缩按-symbol-分段按时间排序"&gt;二、压缩：按 symbol 分段，按时间排序
&lt;/h2&gt;&lt;p&gt;时序数据最大的浪费是重复。同一只股票、连续的交易日，&lt;code&gt;symbol&lt;/code&gt; 完全重复，价格变化很小。TimescaleDB 的列式压缩把&amp;quot;同一 segment 内的行&amp;quot;转成数组存储，压缩比通常在 5~20 倍。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;ALTER&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;TABLE&lt;/span&gt; minute_bar &lt;span style="color:#66d9ef"&gt;SET&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; timescaledb.compress,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; timescaledb.compress_segmentby &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;symbol&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; timescaledb.compress_orderby &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;ts DESC&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;-- 7 天以前的数据自动压缩
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;SELECT&lt;/span&gt; add_compression_policy(&lt;span style="color:#e6db74"&gt;&amp;#39;minute_bar&amp;#39;&lt;/span&gt;, INTERVAL &lt;span style="color:#e6db74"&gt;&amp;#39;7 days&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;两个参数的选择逻辑：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;compress_segmentby&lt;/code&gt; 决定了&amp;quot;哪些行会被放在一起&amp;quot;。因为绝大多数查询都是按股票取一段连续时间，所以分段键选 &lt;code&gt;symbol&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;compress_orderby&lt;/code&gt; 决定段内排序。时间倒序排列，让&amp;quot;最新 N 条&amp;quot;的查询能快速定位。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实验室实测（8 核 / 32GB / NVMe，A 股 5 分钟线 1 年）：&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;状态&lt;/th&gt;
 &lt;th&gt;表大小&lt;/th&gt;
 &lt;th&gt;单股 1 年查询耗时&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;未压缩&lt;/td&gt;
 &lt;td&gt;6.3 GB&lt;/td&gt;
 &lt;td&gt;210 ms&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;压缩后&lt;/td&gt;
 &lt;td&gt;780 MB&lt;/td&gt;
 &lt;td&gt;240 ms&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;压缩后 + 带 symbol 条件&lt;/td&gt;
 &lt;td&gt;780 MB&lt;/td&gt;
 &lt;td&gt;35 ms&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;压缩后写入变慢（约 1.4 倍），但我们的写入集中在盘后批处理，完全无所谓。&lt;strong&gt;如果你的场景是盘中高频写入 + 实时查询，压缩窗口不要设得太短&lt;/strong&gt;——被压缩的 chunk 不能再直接 &lt;code&gt;INSERT&lt;/code&gt;，新数据会跑到新的 chunk 去，这是设计使然而不是缺陷。&lt;/p&gt;
&lt;h2 id="三连续聚合日线自动滚成月线"&gt;三、连续聚合：日线自动滚成月线
&lt;/h2&gt;&lt;p&gt;研究里最烦的是&amp;quot;每次都要手写一段 group by 把日线变月线&amp;quot;。连续聚合（Continuous Aggregate）把这一步变成库里的物化视图，增量刷新：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;CREATE&lt;/span&gt; MATERIALIZED &lt;span style="color:#66d9ef"&gt;VIEW&lt;/span&gt; monthly_bar
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;WITH&lt;/span&gt; (timescaledb.continuous) &lt;span style="color:#66d9ef"&gt;AS&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;SELECT&lt;/span&gt; symbol,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; time_bucket(&lt;span style="color:#e6db74"&gt;&amp;#39;1 month&amp;#39;&lt;/span&gt;, trade_date) &lt;span style="color:#66d9ef"&gt;AS&lt;/span&gt; trade_month,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;first&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;open&lt;/span&gt;, trade_date) &lt;span style="color:#66d9ef"&gt;AS&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;open&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;max&lt;/span&gt;(high) &lt;span style="color:#66d9ef"&gt;AS&lt;/span&gt; high,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;min&lt;/span&gt;(low) &lt;span style="color:#66d9ef"&gt;AS&lt;/span&gt; low,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;last&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;close&lt;/span&gt;, trade_date) &lt;span style="color:#66d9ef"&gt;AS&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;close&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;sum&lt;/span&gt;(volume) &lt;span style="color:#66d9ef"&gt;AS&lt;/span&gt; volume,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;sum&lt;/span&gt;(amount) &lt;span style="color:#66d9ef"&gt;AS&lt;/span&gt; amount
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;FROM&lt;/span&gt; daily_bar
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;GROUP&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;BY&lt;/span&gt; symbol, trade_month
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;WITH&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NO&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;DATA&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;SELECT&lt;/span&gt; add_continuous_aggregate_policy(&lt;span style="color:#e6db74"&gt;&amp;#39;monthly_bar&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; start_offset &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; INTERVAL &lt;span style="color:#e6db74"&gt;&amp;#39;3 years&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; end_offset &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; INTERVAL &lt;span style="color:#e6db74"&gt;&amp;#39;1 day&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; schedule_interval &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; INTERVAL &lt;span style="color:#e6db74"&gt;&amp;#39;1 hour&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;注意 &lt;code&gt;end_offset =&amp;gt; INTERVAL '1 day'&lt;/code&gt;：&lt;strong&gt;只刷新到昨天为止&lt;/strong&gt;。当月数据还在变，如果聚合到当天，物化结果就是一个会不断被改写的&amp;quot;半成品&amp;quot;，容易在回测里用到未完成的月线。查询时当月数据仍然走实时聚合（TimescaleDB 会自动 union 未物化的部分）。&lt;/p&gt;
&lt;p&gt;真实用法是这样：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;-- 查询某只股票的月线序列，物化部分 + 实时部分自动拼接
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;SELECT&lt;/span&gt; trade_month, &lt;span style="color:#66d9ef"&gt;open&lt;/span&gt;, &lt;span style="color:#66d9ef"&gt;close&lt;/span&gt;, volume
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;FROM&lt;/span&gt; monthly_bar
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;WHERE&lt;/span&gt; symbol &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;600519&amp;#39;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;AND&lt;/span&gt; trade_month &lt;span style="color:#f92672"&gt;&amp;gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;2020-01-01&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;ORDER&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;BY&lt;/span&gt; trade_month;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="四增量写入幂等-upsert-与复权因子"&gt;四、增量写入：幂等 upsert 与复权因子
&lt;/h2&gt;&lt;p&gt;盘后同步任务每天会重跑，必须保证幂等。&lt;code&gt;ON CONFLICT DO UPDATE&lt;/code&gt; 是标准解法：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;INSERT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;INTO&lt;/span&gt; daily_bar (symbol, trade_date, &lt;span style="color:#66d9ef"&gt;open&lt;/span&gt;, high, low, &lt;span style="color:#66d9ef"&gt;close&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; volume, amount, turnover, pct_chg, adj_factor)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;VALUES&lt;/span&gt; (:symbol, :trade_date, :&lt;span style="color:#66d9ef"&gt;open&lt;/span&gt;, :high, :low, :&lt;span style="color:#66d9ef"&gt;close&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; :volume, :amount, :turnover, :pct_chg, :adj_factor)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;ON&lt;/span&gt; CONFLICT (symbol, trade_date) &lt;span style="color:#66d9ef"&gt;DO&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;UPDATE&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;SET&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;close&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; EXCLUDED.&lt;span style="color:#66d9ef"&gt;close&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; volume &lt;span style="color:#f92672"&gt;=&lt;/span&gt; EXCLUDED.volume,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; pct_chg &lt;span style="color:#f92672"&gt;=&lt;/span&gt; EXCLUDED.pct_chg,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; adj_factor &lt;span style="color:#f92672"&gt;=&lt;/span&gt; EXCLUDED.adj_factor;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这里有个容易忽略的设计：&lt;strong&gt;存未复权价 + 复权因子&lt;/strong&gt;，而不是直接存前复权价。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;前复权价会在每次除权后整体重算，历史数据被&amp;quot;改写&amp;quot;，导致同一天的因子值在不同时间点算出来不一样；&lt;/li&gt;
&lt;li&gt;业界惯例（也是 akshare 的 &lt;code&gt;hfq&lt;/code&gt; 接口）是存后复权或存 &lt;code&gt;adj_factor&lt;/code&gt;，复权在查询层做，历史数值永远不变。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;查前复权价时在 SQL 里乘一下即可：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;SELECT&lt;/span&gt; trade_date,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;close&lt;/span&gt; &lt;span style="color:#f92672"&gt;*&lt;/span&gt; adj_factor &lt;span style="color:#f92672"&gt;/&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;MAX&lt;/span&gt;(adj_factor) OVER () &lt;span style="color:#66d9ef"&gt;AS&lt;/span&gt; close_qfq
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;FROM&lt;/span&gt; daily_bar &lt;span style="color:#66d9ef"&gt;WHERE&lt;/span&gt; symbol &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;000001&amp;#39;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;ORDER&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;BY&lt;/span&gt; trade_date;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
 &lt;blockquote&gt;
 &lt;p&gt;反面教材：早期我用&amp;quot;每天重写整段前复权序列&amp;quot;的方式同步数据，结果同一个因子在 3 月跑出来 IC 是 0.031，6 月重跑变成 0.027。差的那一点就是除权。&lt;strong&gt;数值漂移是量化研究里最隐蔽的 bug&lt;/strong&gt;。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="五保留策略与冷热分层"&gt;五、保留策略与冷热分层
&lt;/h2&gt;&lt;p&gt;分钟线不能无限堆。实验室的策略是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;-- 3 年前的分钟线直接删除
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;SELECT&lt;/span&gt; add_retention_policy(&lt;span style="color:#e6db74"&gt;&amp;#39;minute_bar&amp;#39;&lt;/span&gt;, INTERVAL &lt;span style="color:#e6db74"&gt;&amp;#39;3 years&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;日线则不删，因为它小到可以忽略（15 年 A 股日线约 2 GB，压缩后 300 MB 左右）。&lt;/p&gt;
&lt;p&gt;如果要做真正的归档，可以把老 chunk 的压缩数据 &lt;code&gt;COPY&lt;/code&gt; 导出到对象存储（实验室用的是内网的 MinIO），再用 &lt;code&gt;drop_chunks&lt;/code&gt; 释放：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;-- 导出后再删除
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;COPY&lt;/span&gt; (&lt;span style="color:#66d9ef"&gt;SELECT&lt;/span&gt; &lt;span style="color:#f92672"&gt;*&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;FROM&lt;/span&gt; minute_bar &lt;span style="color:#66d9ef"&gt;WHERE&lt;/span&gt; ts &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;2021-01-01&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;TO&lt;/span&gt; PROGRAM &lt;span style="color:#e6db74"&gt;&amp;#39;gzip &amp;gt; /mnt/minio-archive/minute_bar_2020.csv.gz&amp;#39;&lt;/span&gt; CSV HEADER;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;SELECT&lt;/span&gt; drop_chunks(&lt;span style="color:#e6db74"&gt;&amp;#39;minute_bar&amp;#39;&lt;/span&gt;, older_than &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; INTERVAL &lt;span style="color:#e6db74"&gt;&amp;#39;5 years&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="六一个取数函数统一到矩阵"&gt;六、一个取数函数，统一到矩阵
&lt;/h2&gt;&lt;p&gt;数据库再好，研究时用的还是 DataFrame。实验室封装了一个按需取数的函数，把&amp;quot;复权、停牌、字段选择&amp;quot;都收在这里：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# src/data/loader.py&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; __future__ &lt;span style="color:#f92672"&gt;import&lt;/span&gt; annotations
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;import&lt;/span&gt; pandas &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; pd
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; sqlalchemy &lt;span style="color:#f92672"&gt;import&lt;/span&gt; create_engine
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;DSN &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;postgresql+psycopg2://quant:quant@127.0.0.1:5432/quant&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;load_bars&lt;/span&gt;(symbols: list[str], start: str, end: str,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; freq: str &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;1d&amp;#34;&lt;/span&gt;, adjust: str &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;qfq&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; engine&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;None&lt;/span&gt;) &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;取行情长表，自动按 adjust 做复权。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; adjust: &amp;#39;none&amp;#39; | &amp;#39;qfq&amp;#39;(前复权) | &amp;#39;hfq&amp;#39;(后复权)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; 返回列: symbol, trade_date, open, high, low, close, volume, amount
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; engine &lt;span style="color:#f92672"&gt;=&lt;/span&gt; engine &lt;span style="color:#f92672"&gt;or&lt;/span&gt; create_engine(DSN, pool_pre_ping&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;True&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; table &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;daily_bar&amp;#34;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; freq &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;1d&amp;#34;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;minute_bar&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; tcol &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;trade_date&amp;#34;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; freq &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;1d&amp;#34;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;ts&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sql &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;f&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; SELECT symbol, &lt;/span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;tcol&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt; AS trade_date, open, high, low, close,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; volume, amount, adj_factor
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; FROM &lt;/span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;table&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; WHERE &lt;/span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;tcol&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt; BETWEEN :start AND :end
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; AND symbol = ANY(:symbols)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; ORDER BY symbol, &lt;/span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;tcol&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; df &lt;span style="color:#f92672"&gt;=&lt;/span&gt; pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;read_sql(sql, engine, params&lt;span style="color:#f92672"&gt;=&lt;/span&gt;{&lt;span style="color:#e6db74"&gt;&amp;#34;start&amp;#34;&lt;/span&gt;: start, &lt;span style="color:#e6db74"&gt;&amp;#34;end&amp;#34;&lt;/span&gt;: end, &lt;span style="color:#e6db74"&gt;&amp;#34;symbols&amp;#34;&lt;/span&gt;: symbols})
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; adjust &lt;span style="color:#f92672"&gt;in&lt;/span&gt; (&lt;span style="color:#e6db74"&gt;&amp;#34;qfq&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;hfq&amp;#34;&lt;/span&gt;) &lt;span style="color:#f92672"&gt;and&lt;/span&gt; &lt;span style="color:#f92672"&gt;not&lt;/span&gt; df&lt;span style="color:#f92672"&gt;.&lt;/span&gt;empty:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; df &lt;span style="color:#f92672"&gt;=&lt;/span&gt; df&lt;span style="color:#f92672"&gt;.&lt;/span&gt;sort_values([&lt;span style="color:#e6db74"&gt;&amp;#34;symbol&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;trade_date&amp;#34;&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; grp &lt;span style="color:#f92672"&gt;=&lt;/span&gt; df&lt;span style="color:#f92672"&gt;.&lt;/span&gt;groupby(&lt;span style="color:#e6db74"&gt;&amp;#34;symbol&amp;#34;&lt;/span&gt;, sort&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;False&lt;/span&gt;)[&lt;span style="color:#e6db74"&gt;&amp;#34;adj_factor&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ratio &lt;span style="color:#f92672"&gt;=&lt;/span&gt; grp&lt;span style="color:#f92672"&gt;.&lt;/span&gt;transform(&lt;span style="color:#e6db74"&gt;&amp;#34;last&amp;#34;&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; adjust &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;qfq&amp;#34;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1.0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; col &lt;span style="color:#f92672"&gt;in&lt;/span&gt; [&lt;span style="color:#e6db74"&gt;&amp;#34;open&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;high&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;low&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;close&amp;#34;&lt;/span&gt;]:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; df[col] &lt;span style="color:#f92672"&gt;=&lt;/span&gt; df[col] &lt;span style="color:#f92672"&gt;*&lt;/span&gt; df[&lt;span style="color:#e6db74"&gt;&amp;#34;adj_factor&amp;#34;&lt;/span&gt;] &lt;span style="color:#f92672"&gt;/&lt;/span&gt; ratio
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; df&lt;span style="color:#f92672"&gt;.&lt;/span&gt;drop(columns&lt;span style="color:#f92672"&gt;=&lt;/span&gt;[&lt;span style="color:#e6db74"&gt;&amp;#34;adj_factor&amp;#34;&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;ratio&lt;/code&gt; 那一行就是前复权的全部秘密：把当前复权因子归一，最新的价格保持原始数值，历史价格被等比缩小。&lt;/p&gt;
&lt;h2 id="七踩过的坑清单"&gt;七、踩过的坑清单
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;ALTER TABLE hypertable&lt;/code&gt; 的坑&lt;/strong&gt;：超表上加索引必须建在超表上（&lt;code&gt;CREATE INDEX ON daily_bar (...)）&lt;/code&gt;），不要手动往 chunk 上建，重建 chunk 之后索引就丢了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;主键必须包含时间分区列&lt;/strong&gt;。TimescaleDB 的约束要求唯一索引包含分区列，&lt;code&gt;PRIMARY KEY (symbol)&lt;/code&gt; 会直接报错。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;time_bucket&lt;/code&gt; 与交易日历不是一回事&lt;/strong&gt;。&amp;ldquo;按月&amp;quot;聚合出来的是自然月，2 月只有 15 个交易日，用它算月收益没问题，但用它算&amp;quot;每月调仓&amp;quot;就会和交易日错位。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;别在压缩 chunk 上做大量 UPDATE&lt;/strong&gt;。压缩块和解压块的写入路径不同，反复更新老数据会把压缩收益吃光。需要修正历史数据时，一次性 &lt;code&gt;decompress_chunk&lt;/code&gt; → 更新 → &lt;code&gt;compress_chunk&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;vacuum&lt;/code&gt; 不是万能的&lt;/strong&gt;。时序表几乎只增不改，但 &lt;code&gt;drop_chunks&lt;/code&gt; 之后还是建议看一眼 &lt;code&gt;autovacuum&lt;/code&gt; 的触发情况。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="结语"&gt;结语
&lt;/h2&gt;&lt;p&gt;数据层做到这里，已经不是&amp;quot;能存数据&amp;quot;的层次，而是&amp;rdquo;&lt;strong&gt;取数不成为研究瓶颈&lt;/strong&gt;&amp;quot;：单股分钟线 35 ms、全市场日线截面 1 秒内出结果。&lt;/p&gt;
&lt;p&gt;数据齐了、因子有了，接下来是最容易自欺欺人的一环：&lt;strong&gt;回测&lt;/strong&gt;。下一篇从零写一个向量化回测引擎，把手续费、滑点、涨跌停、T+1 全部显式建模。&lt;/p&gt;
&lt;p&gt;远思为径，码动价值。基础设施的耐心，最终都会体现在回测曲线是否可信上。&lt;/p&gt;</description></item></channel></rss>