银行Python的口述历史(2021)

Hacker News Top 新闻

摘要

本文深入探讨了大型投资银行使用的定制化Python生态系统,重点介绍了一个名为Minerva的虚构系统,以及一个名为Barbara的全局对象数据库。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/06/25 23:14

# Bank Python 的口述历史 来源:https://calpaterson.com/bank-python.html 2021年11月 大型投资银行所使用的 Python 的奇特世界 从住宅区看到的金丝雀码头景象高端金融是个异域国度,他们在那里行事方式截然不同今天我将带你透过锁眼(https://youtu.be/1VKcwhyEeDs?t=105)一瞥一群不为公众所熟知的软件系统,我称之为“Bank Python”。Bank Python 的实现实际上是对整个 Python 生态系统的专有分支,许多(但并非所有)最大型的投资银行都在使用。Bank Python 与大多数人所熟知和喜爱的(或憎恨的)普通 Python 相比,差异显著。 有成千上万的人在这些系统上——或者说,系统内部——工作,但在公开网络上关于它们的信息却不多。当我试图在对话中解释 Bank Python 时,人们往往将我的话当作偏执狂的胡言乱语而置之不理。这一切听起来实在太离谱了。 我将讨论一个虚构的、综合的、想象中的 Bank Python 系统,名为“Minerva”。子系统的名称会被更改,尽管我会尽量准确,但某些细节会有所美化——当然,我并非了解每一个细节。我甚至可能犯一些错误。希望我能把握住大方向。 ## Barbara:伟大的键值存储 关于 Minerva,首先需要知道的是它构建在一个全局的 Python 对象数据库之上。 ```python import barbara # 打开到默认数据库“ring”的连接 db = barbara.open() # 取出某个债券 my_gilt = db["/Instruments/UKGILT201510yZXhhbXBsZQ=="] # 计算该债券的当前价值(根据银行的建模人员) current_value: float = my_gilt.value() ``` Barbara 是一个简单的键值存储,具有层级化的键空间。它极其简单:仅由 `pickle`(https://docs.python.org/3/library/pickle.html)和 `zip`(https://docs.python.org/3/library/zlib.html)构成。 Barbara 有多个“ring”,即命名空间,但默认的 ring 基本上就是整个银行的一个全局对象数据库。从默认的 ring 中,你可以取出交易数据、金融工具数据(如上所示)、市场数据等等。日常使用的大部分数据都来自 Barbara。 应用程序也常将其内部状态存储在 Barbara 中——直接写入和读取数据类,仅使用非常简单的锁定和事务(如果有的话)。Minerva 脚本无法访问文件系统,脚本获取到的少量数据必须放入 Barbara。 在内部,Barbara 节点会在其 ring 内复制写入,有点像 Dynamo(https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf)和 BigTable(https://research.google.com/archive/bigtable-osdi06.pdf)的工作方式。当你调用 `barbara.open()` 时,它会连接到最近的可用的默认 ring 实例。在该实例内部,读取和写入是强一致的。来自其他实例的读取和写入会很快出现,但并非立即。如果一致性很重要,你只需确保始终连接到特定实例即可——这种做法在不必要时是不鼓励的。Barbara 出奇地健壮,很可能因为它如此简单。完全失败极为罕见,降级状态也仅稍多一些。 来自默认 ring 的一些示例路径: | 路径 | 描述 | |------|------| | /Instruments | 金融工具目录(债券、股票等) | | /Deals | 交易目录(已发生的交易) | | /FX | 外汇部门的通用区域 | | /Equities/XLON/VODA/ | 与沃达丰股票相关的事务目录 | | /MIFID2/TR/20180103/01 | 某个业务流程中的中间对象 | Barbara 还具有一些“叠加”功能: ```python # 连接到多个 ring:按键按提供的 ring 名称顺序“叠加” db = barbara.open("middleoffice;ficc;default") # 从 'middleoffice' ring 中获取 /Etc/Something(如果存在), # 否则尝试 'ficc',最后尝试默认 ring some_obj = db["/Etc/Something"] ``` 你可以将 ring 列成一个栈,每次读取会先尝试第一个 ring,如果该键不存在,则尝试第二个 ring,依此类推。写入可以始终发往第一个 ring,也可以发往该键已存在的最高层 ring(由我未展示的配置决定)。 有一些充分的理由不使用 Barbara。如果你的数据集很大,最好考虑其他方案——也许是传统的 SQL 数据库或 kdb+(https://en.wikipedia.org/wiki/Kdb%2B)。Barbara 对象(压缩后)的软大小限制约为 16MB。压缩后的 pickle 已经很小,所以这实际上是一个相当大的尺寸。Barbara 确实支持对象属性的二级索引,但如果二级索引在你的程序中非常重要,那么最好也考虑其他方案。 ## Dagger:金融工具的有向无环图 投资银行的一项重要工作是估算金融工具的价值——“资产定价”。例如,债券的价值等于你持有它所能获得的所有资金,再根据债券发行方破产的风险进行一定折扣。债券(概念上!)可能是最简单的工具,而更有趣的是对其他“衍生”金融工具的估值,例如信用违约互换、利率互换以及真实工具的合成版本。它们都基于某种“基础”工具,但以不同方式支付。 衍生品如何估值的具体细节并不重要,只需知道既有大量细节,也有大量衍生品。工具之间的依赖关系形成一个有向无环图。某些衍生金融工具的层次结构可能如下所示: 一些金融工具的价值源自其他工具,这使它们成为衍生品。你可以得到衍生品的衍生品,而某些衍生品的价值源自多个基础工具。 Dagger 是 Minerva 中的一个子系统,用于帮助理清这些数据依赖关系。你可以这样编写一个类: ```python class CreditDefaultSwap(Instrument): """信用违约互换在债券违约时支付一定金额""" def __init__(self, bond: Bond): super().__init__(underliers=[bond]) self.bond = bond def value(self) -> float: # 返回(缓存的)估值,根据某个资产定价模型 return ... ``` Dagger 会追踪基础工具图中的边,并在基础工具价值发生变化时自动通过 Barbara 重新定价衍生品。如果某家公司的坏消息公布,信用机构下调其信用评级,那么债券部门的人将通过 Dagger 更新相关的 `Bond` 对象,Dagger 将自动重新估值所有受影响的对象。这可能意味着数百种其他衍生工具。信用降级可能会相当刺激。 单个工具被组合成头寸。Position 类看起来有点像这样: ```python class Position: """头寸是一个工具及其数量""" def __init__(self, inst: Instrument, quantity: float): self.inst = inst self.quantity = quantity def value(self) -> float: # 返回(缓存的)估值,基本上是 # self.inst.value() * self.quantity return ... ``` 再次注意,头寸也是你可以估值的东西。当其所包含对象的价值发生变化时,它的价值也会发生变化。它也会被 Dagger 自动重新估值。 一组头寸被称为“book”,这在金融领域是一个超载的词,但在此上下文中只是一组头寸: ```python class Book: """Book 是一组头寸""" def __init__(self, contents: Set[Valuable]): # Valuable 类型在 Python 术语中是“protocol”, # 在 Java 术语中是“interface”——任何具有 value() 的东西 self.contents = contents def value(self) -> float: # 再次返回(缓存的)估值,大致是: # sum(p.value() for p in self.contents) return ... ``` Book 可以包含其他 Book。从最小的债券交易台到整个银行的一个总 Book,存在一个嵌套 Book 的层次结构。要对银行进行估值,你可以执行: ```python # 这是整个银行的顶层 Book,递归包含银行内所有其他内容 bank = db["/Books/BigBankPlc"] # 这将打印整个银行的估值 print(bank.value() ``` 当然,这是理想情况。实际上,CFO 可能会使用不同的系统来生成账目。但子公司 Book 的估值仍然被广泛使用。 如果你了解 Excel,你会开始注意到相似之处。在 Excel 中,电子表格单元格也会根据其依赖关系进行更新,同样是一个有向无环图。Dagger 允许人们将 Excel 式的建模计算放入 Python,为其编写测试,控制版本控制,而无需处理像 `CDS-OF-CDS EURO DESK 20180103 Final (final) (2).xlsx` 这样的文件。Dagger 是将金融模型从 Excel 转移到编程语言、并置于测试和版本控制之下的关键技术。 Dagger 不仅处理估值,还处理银行用于控制各种可能发生的坏事情的风险指标。例如,Dagger 使得相对容易地找到所有关于(比如说)据传即将破产的 Compu-Global-Hyper-Mega-Net Plc 的头寸。这包括所有期权、期货、信用工具,并全部“净额结算”以找到整个银行对该公司的完整头寸。再也不会因为对问题次级贷款机构的敞口而措手不及! ## Walpole:银行级作业运行器 到目前为止,我说过很多数据都存储在 Barbara 中。现在要爆个料:源代码也在 Barbara 中,而不是在磁盘上。保持镇定。它存储在一个名为 `sourcecode` 的特殊 Barbara ring 中。 不将源代码放在文件系统上打破了许多假设。这样的程序如何运行?答案是 Walpole,银行级作业运行器。Walpole 是一个通用作业运行器,类似于一个巨大的 Jenkins 与一个巨大的 systemd 的结合体。 与 Minerva 中的许多事物一样,Walpole 并非按团队部署:只有一个单一的银行级实例。Walpole 既适用于长期运行的服务,也适用于周期性作业,甚至用于构建。周期性作业在银行中非常常见:有许多许多的日终或周终作业需要运行,以更新数据、检查事项、发送电子邮件摘要等。 Walpole 提供了运行软件所需的所有常规功能。它可以在软件崩溃时重启,并在持续崩溃时发送警报。它存储日志。它理解作业之间的依赖关系(很像 systemd),因此如果生成你的作业所需数据的作业失败,你的作业甚至不会尝试启动,而是触发更多警报。 一个真正的优势是 Walpole 大大降低了部署门槛。任何人都可以将作业放入 Walpole——你只需要一个小的 ini 式配置文件,说明脚本运行的时间、主函数的位置,你的整个应用程序就部署完成了,无需进一步协商。 这是一件大事,因为在大型银行中协商任何事情都是一件令人沮丧的事:硬件的交付周期可能长达数月。而让人们同意你,所需的时间则更长。 当前存在的“云原生计算”的一大缺点在于它非常非常复杂。它通常比旧式的非云计算更复杂。要在 Minerva 之外部署你的应用程序,你现在需要了解一些关于 k8s、Cloud Formation 或 Terraform 的知识。这是一套与普通程序员(更不用说金融建模人员)截然不同的技能,几乎没有重叠。相反,任何人都可以搞定一个 ini 文件。 ## MnTable:无处不在的表库 我总觉得编程语言很少内置表数据结构,这很遗憾。程序员有一种不幸的倾向,即偏向哈希表——尤其在 Python 和 JavaScript 中,它们被广泛使用,以至于很难找到不是由哈希表构成的东西。 哈希表有一些严重的缺点。首先,大多数实现仅驻留在内存中,并且稀疏地存在,这使得即使处理中等规模的数据集也很麻烦;这是 Python 程序在实践中经常遇到的问题。更重要的是,它们要求你事先知道访问模式,而且最好是通过单个主键访问。 表则相反(https://calpaterson.com/how-a-sql-database-works.html):它们内存密集,易于从磁盘读写。它们可以使用 b 树索引来允许通过任何路径高效访问;因此你永远不必在程序中间反转字典,仅仅为了能够通过键以外的其他方式访问。它们支持批量操作,并可以利用惰性求值。 在开源领域,流行的库是 pandas(https://pandas.pydata.org/),但 pandas 有一些严重的缺点: 1. 在 Minerva 最初实现时,它还不存在 2. 它的效率不如你期望的那么高,尤其是在内存方面 3. 对于大于内存的数据集,表现不佳 4. (可以说)它的 API 过于复杂 因此,Minerva 中有一个专有的表库:MnTable。 ```python # 创建一个新表,包含三个指定类型的列 t1 = mntable.Table([('counterparty', str), ('instrument', str), ('quantity', float)]) # 向表中添加一些数据(原地操作,表默认是不可变的) t1.extend( [ ['Cleon Partners', 'xlon:voda', 1200.0], ['Cleon Partners', 'xlon:spd', 1200.0], ['Blackpebble', 'xlon:voda', 1200.0], ], in_place=True) # 返回一个新表(不改变原表),仅包含沃达丰的数据。 # 这是惰性的,只有在你查看时才会求值 t1.restrict(instrument='xlon:voda') ``` MnTable 在 Bank Python 中无处不在。一些实现是用 C++ 编写的(这在金融软件中并不少见),一些则是 sqlite3 的薄包装。有许多许多程序以 MnTable 开始,对其应用一些操作列表,然后将结果表转发到其他地方。 这很方便,因为银行中数据无处不在,且大部分是“中等”规模:在千兆字节范围内。有很多关于高频交易者的讨论,但大多数金融从业者并不关注 tick 级数据,甚至不关注日内数据。“中等规模”意味着你不能为每一行创建一个对象,但也不至于大到需要分布式计算集群。 ## 痛苦的度量 如果说使用任何金融软件都是纯粹无拘无束的快乐,那就大错特错了。Minerva 也不例外。 新入职者需要异常长的时间才能上手——而且那还是在他们没有因为看到那套特殊的、强制的内部 IDE 而愤然辞职的情况下(我差点就这么做了)。即使入职数月后,新员工仍在学习非常基本的新事物:有太多不同的东西。 随着时间的推移,Bank Python 与开源 Python 之间的差距越来越大。技术两面都在更新,显然外部比内部快得多,但它们并没有变得更接近。

相似文章

@cyrilXBT: https://x.com/cyrilXBT/status/2053291096076145097

X AI KOLs Timeline

本文介绍了一种通过模型上下文协议(MCP)集成 Claude Code,将 Obsidian 笔记库转变为商业操作系统的方法。文章详细阐述了其架构、文件夹结构以及利用本地文件访问功能自动化研究、内容创作和项目管理的五个专用系统。

@ByteMohit: https://x.com/ByteMohit/status/2063493300884246598

X AI KOLs Timeline

一篇关于构建AgentForge的详细技术文章,AgentForge是一个基于Python的开源agent框架,涵盖了会话运行时、工具合约、审批层和持久化等组件,强调agent由其运行时定义,而不仅仅是模型。

BerkeleyDB 的架构

Lobsters Hottest

伯克利 DB 是一款嵌入式数据库库,本文详细概述了其架构和设计哲学,从它在加州大学伯克利分校的起源到其模块化结构。