用联想记忆网络为量化模型注入可解释的市场记忆
我做量化研究这几年最常被非业内朋友问的一个问题是市场到底有没有记忆很多教科书模型把价格当成随机游走恨不得告诉你“过去不重要”。但在实盘里蹲久了你一定会遇到那种“似曾相识”的走势同样的形态、同样的放量、同样的情绪然后行情像复读机一样再走一遍。2024年诺贝尔物理学奖颁给了人工神经网络的基础研究看到这个消息我脑子里立刻搭上了一条线如果把市场状态编码成一个高维向量再给模型一个“联想记忆库”让它自动找出历史中最像当前走势的那一段用后续路径做预测这不就是一个带市场记忆的量化预测模型吗这篇文章我准备把整套思路从原理讲到实现再讲到回测和踩坑。内容适合有一定Python基础、想从物理学视角打开量化思路的朋友也适合那些觉得LSTM、Transformer像黑箱想要一个更可解释记忆机制的开发者。我会用虚构的行情数据做演示不构成任何交易建议重点是让大家理解“市场记忆”可以落地而不是一个玄学概念。1. 为什么诺奖思路能用来做量化预测1.1 传统量化模型的两条路线都缺“记忆”过去几年主流量化策略大致可以分成两条路线。第一条是线性/多因子模型典型做法是找一堆宏观指标、价量因子、情绪指标用回归或者集成学习预测未来收益。这类模型的优点是解释性强、回测速度快但缺陷非常明显它假设因子和收益之间的关系在长时间内保持稳定不太擅长处理市场状态切换。比如同样一个动量因子在趋势市里有效在震荡市里可能就是反复打脸的根源。模型本身没有“状态记忆”它不会主动问一句当前行情跟历史上哪个阶段最像第二条路线是深度学习尤其是LSTM、Transformer这类序列模型。它们确实有记忆机制LSTM有门控结构Transformer有注意力都可以在理论上记住长周期信息。但实际用下来“记忆”被压缩在成千上万个网络参数里你很难说清楚模型到底记住了什么。训练需要的数据量也很大换成股票、期货的日线、分钟线样本量往往不够结果就是在训练集上炫技、在样本外失灵。这两条路线的问题正好指向一个共同的需求我们真正需要的不是“把所有历史数据硬塞进一个复杂函数”而是设计一个显式的、可检查的记忆结构让模型能够像人一样“先想起类似的剧本再推演可能的后续”。1.2 市场“记忆”的统计证据市场上到底有没有记忆我觉得可以从三个层面看。第一个层面是收益率的自相关性。虽然日收益率本身的自相关通常很弱但在某些阶段、某些品种上滞后几期的自相关系数会显著偏离零。第二个层面是波动率聚集效应大波动后面经常跟着大波动平静期后面还是平静期。这种波动率的时间聚集性非常明显很多模型用GARCH类方法去拟合本质上就是在描述波动率的“记忆”。第三个层面是更长期的重标极差特征。通过对时间序列做重标极差分析很多金融序列会表现出长期记忆性也就是说今天发生的事情可能对几十甚至上百个交易周期之后还有微弱影响。这些证据说明市场不是完全随机游走至少存在某种“状态持续性”。但传统记忆模型大多只是线性地刻画自相关比如“今天的收益率和昨天有关系”这种记忆太单薄。真实市场中更有价值的记忆是“模式级别的联想”当前这十几根K线组合成一种形态历史上出现过差不多的形态之后往往出现类似的路径。这种联想能力恰恰是今年诺奖相关研究里最核心的东西。1.3 诺奖给我们的工具联想记忆网络2024年诺贝尔物理学奖的研究方向我理解下来最有交易价值的就是“联想记忆”。这类网络通过一个能量函数描述整个系统的状态网络会把训练阶段见过的模式存储在权重矩阵里。当你输入一个不完整、有噪声的当前状态网络会通过能量最小化过程不断迭代最终塌缩到某个已存储的完整模式。这个过程很像一个老刑警看到案发现场的一只鞋印脑子里立刻回忆起几个类似的旧案然后重构出完整的案发经过。从物理角度看每个存储的历史模式就是能量景观中的一个低洼点也叫吸引子当前的市场状态就是一颗滚到山坡上的小球它会沿着能量梯度滚到某个吸引子附近。这个机制和市场记忆天然契合我们不需要市场完全重复历史只需要找到当前状态最容易被哪个“历史剧本”吸引然后利用那个剧本的后续数据做预测。也正因为这个思路来自物理学它天然自带一个优势所有操作都有明确的能量函数和更新规则模型可解释性远好于纯黑箱深度学习。你完全可以打开权重矩阵看看哪些特征维度在主导记忆匹配这对量化研究来说太重要了。2. 构建带市场记忆的量化预测模型整体设计2.1 任务定义与模型假设我们真正要预测的是什么我的做法不是预测精确点位而是预测“未来H根K线之后的一段收益率分布”。举个例子最近20根15分钟K线构成一个状态我希望知道未来5根K线向上还是向下能涨多少、跌多少。我把预测结果转化成信号只在模型置信度较高时交易。这里有一个重要前提市场状态的可重复性。如果每个历史状态都是独一无二的联想记忆就没有意义。好在实际行情里类似于“持续放量突破”“缩量横盘后突然跳水”“冲高回落形成双顶”这类模式会反复出现只是细节不完全相同。联想记忆网络要做的就是从噪声中抓出不变量。2.2 模块拆解状态编码器、记忆库、联想检索、决策器整个系统可以拆成四个模块。状态编码器的任务是把原始量价数据变成固定维度的状态向量。这里不能直接把所有行情数据堆进网络否则维度爆炸、记忆串扰严重。我通常会让特征维度保持在64到128之间。记忆库存放的是经过处理的历史状态向量以及每个状态对应的未来收益路径。联想检索模块用一个能量函数计算当前状态和历史模式之间的匹配程度选出最相似的若干模式。决策器则根据这些模式的后续表现生成交易信号。这四个模块合在一起才是一个完整的市场记忆模型。你可以在任意一个模块上做替换。比如状态编码器可以从简单特征换成CNN记忆库可以用聚类中心代替原始样本决策器也可以换成强化学习。但核心思想不变先记住再联想最后做决策。2.3 为什么不用现成的LSTM或Transformer很多朋友会问既然要做序列预测直接上LSTM不是更省事吗我的回答是LSTM是隐式记忆联想记忆网络是显式记忆。隐式记忆的问题是当你看到一个错误预测时很难定位是哪个环节出了问题显式记忆则可以随时检查“当前状态被匹配到了哪个历史模式”。在市场数据样本量有限的情况下显式记忆库也更容易避免过拟合。Transformer的注意力机制其实和联想记忆有相通之处甚至可以说现代联想记忆网络和注意力在数学上是等价的。但Transformer的参数量很大训练需要大量数据和算力而联想记忆网络可以直接从历史状态库中检索不需要训练端到端的巨大模型。当然如果数据量足够、计算资源充裕把联想记忆库作为Transformer的显式记忆层往往会让模型的整体表现更强。我在自己的模拟项目X里就尝试过混合结构效果相当不错。3. 手写一个可运行的“市场记忆”模型3.1 数据准备与特征工程先用虚构数据走通整个流程。假设我们研究的对象是某商品期货的15分钟线特征我选了四类最近20根K线的对数收益率序列最近20根K线的滚动波动率分位最近20根K线的成交量变化率当前价格相对20根K线之前的动量。这些特征合起来可以拼成一个向量。为了方便演示我先把连续特征离散化成二值状态向量。经典联想记忆网络更适合处理正负1的二值状态所以每个特征我以滚动均值加0.5倍标准差为阈值高于阈值记为正低于阈值记为负中间区域用一个正负随机噪声打断避免太多零值。生产实践中你可以直接把特征变为连续值用现代连续联想记忆网络实现但今天的基础版本先用二值说明原理。下面是状态构建的简化代码import numpy as np def build_state_from_window(data_window): 把一个行情窗口转成二值状态向量 data_window: 包含 close, volume 的二维数组 returns np.diff(np.log(data_window[:, 0])) vol np.std(returns) momentum data_window[-1, 0] / data_window[0, 0] - 1.0 volume_change data_window[-1, 1] / np.mean(data_window[-5:, 1]) - 1.0 threshold np.mean(returns) 0.5 * vol feat_return np.where(returns threshold, 1.0, -1.0) feat_vol 1.0 if vol np.percentile(vol, 50) else -1.0 feat_momentum 1.0 if momentum 0 else -1.0 feat_volume 1.0 if volume_change 0 else -1.0 state np.concatenate([ feat_return, np.array([feat_vol, feat_momentum, feat_volume]) ]) return state这里偷懒把波动率分位简化成了单个二值特征真实场景建议把波动率切成多个区间做one-hot编码这样模型能更细腻地感知不同的波动状态。关键是同一个窗口计算出的特征必须保持时间一致性不能混入未来信息。3.2 记忆库存储用权重矩阵记录历史模式联想记忆网络的存储方式很简单如果是二值状态向量可以用外积求和构建权重矩阵。假设我们有M个历史状态向量每个向量长度是D那么权重矩阵W就是D×D矩阵。每一个状态模式p都会对W增加一项p乘以p的转置最后把对角线清零防止某个神经元自激。这一操作容易理解成“把每个历史样本刻进权重矩阵”其实更准确的说法是“叠加记忆痕迹”。它的好处是存储和检索都很快坏处是容量有限模式太多时会互相干扰。我在实际使用中发现直接拿上千个原始状态去存储会导致严重的记忆串扰所以后来我先把状态做聚类只保留少量“原型状态”再把原型存进记忆库。这个细节后面会详细讲。class MarketMemory: def __init__(self, dim): self.dim dim self.W np.zeros((dim, dim)) def store(self, patterns): patterns: shape (M, dim)每行一个正负1状态 for p in patterns: self.W np.outer(p, p) np.fill_diagonal(self.W, 0) def recall(self, state, steps30): 异步更新联想返回稳态向量 s state.copy() for _ in range(steps): old_s s.copy() order np.random.permutation(self.dim) for i in order: val self.W[i] s s[i] 1.0 if val 0 else -1.0 if np.array_equal(s, old_s): break return s异步更新是我实测中比较容易收敛的方式。同步更新有时会让网络在两个状态之间来回震荡就像在两个吸引子中间的鞍点跳来跳去。真实物理系统里每个神经元的更新本来就有先后所以异步更天然。3.3 联想检索从回忆到预测模型回忆出一个稳定状态后我们要把它映射回历史模式并取出对应的未来收益。方法很简单算稳定状态和所有历史模式的汉明距离取距离最近的K个模式。然后根据这K个模式对应的未来收益做加权平均就是一个预测结果。这里有一个容易犯糊涂的点既然记忆库里存的是状态向量为什么还要再算汉明距离因为联想迭代的稳定点不一定精确等于某个已存储模式它可能会混合几个模式的共性。用汉明距离做最近邻检索相当于把“硬回忆”变成“软匹配”更稳健。def predict(self, state, futures, k5): stable self.recall(state) dists [(np.sum(stable ! pattern), i) for i, pattern in enumerate(self.patterns)] dists.sort(keylambda x: x[0]) top_idx [idx for _, idx in dists[:k]] pred np.mean([futures[i] for i in top_idx], axis0) return pred, top_idx注意futures必须严格对应当前记忆库中的历史样本并且只能用该时间点之后的数据不能在回测时混入未来。3.4 引入温度参数从确定性回忆到概率预测经典联想记忆的更新是硬判决输出永远是正负1。但市场预测天然有不确定性我更希望模型输出一个概率分布而不是一把梭的单一结果。这时候可以用统计力学里的玻尔兹曼分布一个状态的能量越低被选中的概率越高。温度T控制分布的集中度T越小模型越倾向于只选择最优记忆T越大历史剧本的权重越平均。实际操作中我会计算当前状态与每个候选历史模式的距离转换成相似度分数再用带温度的softmax归一化得到每个历史模式的权重。T1.0时是一个常见起点如果发现预测结果太离散可以适当把T调低到0.5如果过拟合严重就上调到1.5让模型多看一些不同的历史剧本。def softmax_memory_scores(state, patterns, temperature1.0): # 距离越小相似度越高 sims np.array([-np.sqrt(np.mean((state - p) ** 2)) for p in patterns]) sims sims / temperature exp_sims np.exp(sims - np.max(sims)) return exp_sims / exp_sims.sum()温度参数是这个模型里我最喜欢调的超参数因为它从物理角度解释了“探索与利用”的平衡低温度是贪婪地利用最相似历史高温度是探索更多可能性。交易里这也对应了“置信度”的概念温度高时信号应该更分散仓位也应该更小。4. 回测设计与实测结果4.1 从预测到交易信号模型输出的是未来H根K线的预期收益率。我会定义两个阈值一个针对预期收益方向一个针对置信度。如果预测方向向上的概率高于65%并且匹配到的前K个历史剧本中超过一半是上涨的就开多相反开空。同时只交易能量很低的状态意味着当前行情和记忆库里的历史模式比较贴近如果当前状态在历史中找不到像样的近邻就空仓等待。为了控制风险单笔交易仓位固定为资金量的20%并设置2%的止损线。模型本身不做高频交易频率大概每天几次到几十次取决于品种和K线周期。4.2 三种典型行情下的表现我在某连续期货合约的15分钟线上做了模拟回测结果如下数据为虚构演示行情类型胜率夏普比率最大回撤备注强趋势行情52%1.2-8%盈利主要来自趋势延续但中途震荡信号容易亏损箱体震荡行情67%2.1-3%模型很容易找到类似的历史箱体表现最好突发剧烈波动38%-0.5-15%历史记忆几乎失效需要额外加记忆新鲜度过滤这个结果很符合模型特性联想记忆在状态重复率高的行情里最有效在真正的历史罕见突发事件里会失效。所以后来我加了一个过滤条件如果当前状态到最近历史模式的距离超过了全部分布95%分位数模型自动停止交易。加了过滤之后突发行情的最大回撤从-15%降到了-6%代价是少赚了一些极端反弹的利润。4.3 对比基线线性回归与LSTM为了验证模型不是自我感动我拿同一份数据跑了一个简单线性回归和一个两层LSTM。整体指标如下模型年化收益夏普比率最大回撤参数规模可解释性线性回归8%0.8-12%少高LSTM15%1.4-18%数十万低市场记忆模型12%1.6-9%数千高市场记忆模型的年化收益不如LSTM但夏普比率更高、回撤更小参数规模只有数千这在小样本数据上是很明显的优势。我更看重的是可解释性每次信号都能查出来“它参考了哪几个历史剧本”这给复盘带来了巨大便利。我认为这类模型更适合作为研究框架而不是一个封闭的预测黑箱。5. 排坑实录构建市场记忆模型容易踩的5个坑5.1 记忆容量不够网络变成“复读机”或“失忆”第一个大坑是直接把所有历史状态都塞进记忆库。经典联想记忆网络容量很有限大概只能可靠存储状态维度0.14倍的独立模式数量。如果你的状态维度是128理论上只能存18个左右互不干扰的模式超过这个数量网络就会串扰召回时老是回到同一个错误原型。我的解决办法是先对历史状态做K-means聚类把K设定在30到100之间只把聚类中心作为存储模式。这相当于把大量历史剧本浓缩成几十种“典型剧本”。实践下来识别效果反而比存储上千个原始样本更好因为它去掉了噪声记住的是模式骨架。5.2 数据泄漏记忆库里混进了未来信息这是回测中最容易犯、也是后果最严重的错误。比如你构建特征时用了整个样本期的均值和标准差做标准化或者用未来一段时间的收益去平滑当前特征模型就会在不知情的情况下“偷看答案”。我用滚动窗口重写了状态编码逻辑每次生成状态向量时只允许使用截至当前时间的数据。记忆库的更新也严格按时间顺序进行每个回测点只会把t时刻之前的历史状态存入记忆库。如果发现回测结果好得不正常先检查有没有未来函数而不是先庆祝。真正常见的泄漏源是特征归一化、缺失值填充、成交量滑动平均窗口越界这几个地方。5.3 同步更新不收敛如果你用同步更新也就是每一轮同时更新所有维度网络很可能在两个状态之间来回震荡。这是因为同步更新相当于离散动力学中的高增益反馈容易跳过稳定点。我改用异步更新每轮随机打乱神经元的更新顺序收敛速度会稍微慢一点但稳定性好很多。另一个方案是把符号函数换成sigmoid让状态平滑过渡也能减少振荡。5.4 全零状态陷阱当某些特征窗口内变化极小时编码出来的状态向量可能出现大量0或接近0的维度。联想记忆网络对这些维度的权重更新几乎不起作用导致预测结果退化成随机选择。我的经验是不要使用直接映射到0的编码方式要么做one-hot要么把0用很小的随机噪声填充。同时可以给每个神经元加上一个小的偏置项让无信息维度保持一个微小激活避免全网络死锁。5.5 市场记忆会过时但权重不会自动衰减经典联想记忆网络一旦存储完成权重是固定不变的。真实市场恰恰相反十年前的行情模式在今天可能已经部分失效。我给记忆库加了一个时间衰减机制每次存入新状态时把旧权重矩阵乘以一个0.99到0.999的衰减系数。这样最近的状态会在记忆矩阵中占据更大权重老剧本慢慢退场。这个“遗忘曲线”的思路非常朴素但对实盘效果影响很大。模型终于不再死抱着久远的历史不放。6. 扩展方向从离散记忆到连续记忆与注意力机制6.1 连续联想记忆和Transformer的关系如果你觉得二值状态表达力不够可以考虑升级到连续状态联想记忆。它的核心思想是状态向量可以是任意实数能量函数对状态连续可导迭代更新时使用softmax激活函数。数学推导后会发现这种连续联想记忆的检索过程几乎等价于Transformer的注意力机制查询向量对应当前状态记忆库中的模式对应键和值温度参数对应注意力缩放因子。换句话说你完全可以把现代联想记忆网络当成一个可解释的注意力层把它接到深度神经网络里使用。我在另一个图像处理Demo里实验过这种混合结构确实比纯Transformer更稳定。量化场景里连续版本更适合直接从原始特征向量做预测不需要强行二值化也保留了概率输出的物理意义。6.2 多时间尺度记忆单一状态的联想记忆只能覆盖一个时间尺度。比如15分钟K线的状态可能只记得短期形态但记不住日线级别的市场环境。后来我扩展到了双记忆库一个存分钟级状态一个存日线级状态最后综合两者结果。这套结构解决了“日内信号在大趋势面前被秒打脸”的问题。如果你要做更复杂的策略可以尝试用三个甚至四个时间尺度让模型同时具备微观记忆和宏观记忆。6.3 极端行情下的记忆失效与应对最后一个想说的扩展方向是“承认未知”。市场记忆模型最大的软肋是黑天鹅因为它本质上依赖重复出现的模式。我给模型加了一个“未知状态”判断当输入状态和历史所有模式的相似度都低于某个阈值时模型输出置信度直接归零并强制空仓。策略交易频率下降了一些但净值曲线平滑度提升得非常明显。我个人的体会是这类模型最值钱的地方不是预测精度本身而是它能把“市场状态识别”这件事显式化。过去做实盘复盘我要靠肉眼翻K线现在每次信号都有一个明确的“历史相似剧本”列表我可以直接跳回去看当时的资金面、消息面、板块表现。很多次我发现模型找到的历史相似阶段在宏观环境上也有惊人的重合这种感觉比单纯跑出一串漂亮回测数字更让人踏实。最后分享一个我自己一直在用的小技巧给记忆库加时间衰减之后记得定期检查衰减系数对结果的影响。如果系数太小记忆会被更新鲜但未必更重要的短期波动冲掉如果系数太大模型又会变成一个固执的历史复读机。我目前的经验值是分钟级模型用0.995日线级模型用0.999每周做一次参数滚动优化。市场记忆不是越强越好懂得忘记才是在量化里活得久的关键。