我自己写了一个开源的记账软件
多年前,我刚开始工作,领到工资时很兴奋,但是半年一过去,惊讶的发现自己存款没有增加反而还减少了。于是好奇一件事:这半年赚的钱都花哪儿去了?然后打开Excel,找出银行卡的明细账单算了一阵。最后反思了自己的消费习惯。
在这之后,我养成了每半年算一次账的习惯。但是,随着互联网金融的大发展,各种账户越来越多:微信、支付宝、QQ、微博……这些账户上都有余额和收支明细,一些账户同时还具有投资功能:可以买基金、理财产品等等。Excel处理少数几个账户还可以,一旦账户多了,用起来相当难受。
我开始寻找记账软件来解决问题。市面上的记账软件相当丰富,免费的收费的都有。《电脑报》上的一篇文章《记账这件小事:App挑选攻略》里对它们有大致的介绍。
但是我发现:这些记账工具分成了泾渭分明的两类:一般的App只处理生活收支的分析,不支持投资品;而各大投资平台提供的记账工具只处理投资分析和收益率计算,不处理生活收支。没哪个工具能同时把这两者处理好。
生活账户和投资账户能分开吗?
有一种说法是:生活账户和投资账户应当隔离,不能让投资影响生活。这话听起来很有道理,但在实践中,很多外界因素会使这个规矩被打破:如果用于生活消费的银行卡买基金恰好有优惠,要不要用这张卡来买?余额宝的普及让货币基金可以同时用于消费,使得生活与投资的界限进一步被模糊。而一旦涉及到外币,就更无法隔离了:如果你同时持有人民币和美元,那么美元现金相对于人民币就是一种投资(因为汇率会波动),这种情况下无论如何也不可能把生活账户和投资账户分开。
据此我认为:生活和投资的分离应当是在财务规划时考虑的事情,不应该是在使用物理账户时受到的约束。如果记账工具要求使用者分离自己的生活账户和投资账户,那就不是工具在服务人,而是要求人来适应工具。
可惜目前国内的主流记账工具都不能很好的支持这点。尤其是一旦出现了外币资产,无论是生活记账App还是投资分析工具,几乎通通扑街支持不了。
我想要一种记账工具能够整合我的所有资产,并在家庭资产维度上进行整体分析。否则,我要分别对不同资产用不同工具处理,然后导出数据再整合……那我还不如回去用Excel,至少在那里数据格式都是统一的。
账户被分开处理会带来一个问题:各账户之间由于转账而导致的关联如何被正确识别。比如从银行账户转出一笔钱到支付宝上,这笔交易会同时体现在银行和支付宝两个账户的资金明细上。一旦有一笔交易在两个账户上的记录不一致,就会造成多个统计结果的不准确。Excel虽然可以用统一数据格式记录所有账户数据,但是要用公式来校验这些转账数据也很困难。
我后来才知道,这个问题是会计行业中一个古老而悠久的问题,而且在700年前就已经被意大利商人解决了[1]。
从Beancount得到的启示:复式记账
偶然间,我听说了国外的一款开源记账软件Beancount[2],从该软件的文档中我得知了复式记账(Double-Entry Bookkeeping)方法。
和我们日常接触的单式记账(Single-Entry Bookkeeping)相比,复式记账中每一笔交易都会有两个账户参与其中,并且两个账户的资金变动数量相加等于0。这种方法从根本上避免了转账时两边记录不一致的问题。
但是,这看起来只适用于在自己拥有的账户之间的转账交易。对于其他交易(如消费),另一方并不是自己的账户,比如上图中去餐馆(Restaurant)消费,餐馆是消费的收款方,这种账怎么记呢?
Beancount的方法是:把广义的“账户”分成4种类型,其中Expenses和Income这两种账户不关心某个时间点上的余额(Balance),它们并不包含用户的资产或者债务,而是代表已经发生的某一类支出或者收入。比如上面图中的餐馆就是一个支出账户。如果把某一段时期的某个支出账户的“转入”数额加起来,就能得知该段时间在这个支出类别上支出的总额是多少。(TataruBook把这类代表支出或收入的账户称为“外部账户”)
Beancount的记账方法给了我很大的启示。我开始考虑是否全面使用Beancount进行记账。但是很快我发现几个问题:
- Beancount在做投资记账时,会按照“手”(lot)为单位分别计算每手投资品的成本和收益[3],这和大多数国人(包括我)的习惯是不一样的。我更希望看到在某一段时期内某只股票或基金的总体持仓和收益率是多少,而不希望对单个品种再拆成一手一手去计算。
- Beancount用自定义的文本格式保存记账数据。虽然文本可以人工阅读和编辑,但要批量读取和解析这些记账数据只能靠Beancount,各种分析报表的计算更是完全依赖于Beancount的功能。如果万一将来Beancount不能用了,那么这些历史记账数据如何维护就存在风险。(对于所有使用私有数据格式保存记账数据的软件,理论上都有这种隐患)
- Beancount的开发者似乎对当前的软件性能不满意,想要把核心的Python代码用C++重写[4]。这让我产生了担忧,因为这往往意味着软件的数据处理模型有可能在根本上存在效率问题。而且这个重写从2020年启动,似乎到现在也没完成,我怀疑是不是遇到了一些难以解决的设计困难。
这些问题使得我对选择Beancount产生了犹豫。我开始思考在私有数据格式和Excel之间,有没有一种更通用的数据处理工具。我想到了关系型数据库(SQL database)。恰好,Beancount的文档中也解释了为什么它没有使用关系型数据库[5]。我琢磨了它给出的这几条原因,发现这些都只不过是表面的困难。如果能仔细的设计数据表,并合理应用查询技巧,使用关系型数据库进行复式记账并非不可能。
关系型数据库的选型
首先我要选一种数据引擎来尝试做个原型。从数据库理论的专业教科书中,我找到了数据库引擎的流行程度排名[6]。在这份名单中,大部分的数据库都是采用的客户端-服务器架构,对于个人本地记账来说太重型了。但是名单中有一个名字引起了我的注意,它就是和Excel师出同门的Access。
我想:既然Excel能这么流行,那么Access应该在易用性上做得很不错吧?——结果,这个想法却害我走了一大步弯路。
商业软件居然没有开源软件好用?
Access提供了看似吸引人的图形化设计方式,但是对于正经要设计数据库的开发者来说,那东西几乎等于一个只能教少儿编程的玩具。很快我就抛弃了它的图形化设计工具,全部自己编写SQL语句。但是Access的代码编辑界面难用得不可思议,连代码的格式化都没有。你能想象一个复杂的视图定义显示成这样吗?
这倒还只是小问题,因为我可以换用其他的代码编辑器。更大的问题是Access的功能实在太弱了:不支持事务处理,不支持CTE(common table expression),不支持窗口函数……它还经常对我编写的SQL查询报语法错误,而我怎么都想不明白是因为什么。有时甚至先报错,过一会儿没改代码它又能运行了。
我还碰到过一个笑话:在Access的一处提示信息中,说字段中的数字类型是“已修复”。我半天没搞明白这是个什么东西,后来去看英文发现是“Fixed”(定点数字)……
一番折腾后我决定换其他数据库引擎。我选择了SQLite,它是世界上使用最为广泛的轻量级开源数据库。我甚至都不需要下载安装它,因为Python的标准库中就默认包含了SQLite引擎。
换成SQLite后,一切都舒服多了。SQLite支持事务处理,支持CTE,支持窗口函数,对SQL语句的解析非常稳定,速度又快,文档的质量也很高。我不禁产生了一个困惑:明明有这么好用的开源软件,为什么还会有人花钱买Access这么难用的商用软件?就为了它那玩具一般的图形界面?
SQLite的窗口函数还解决了一个关键的性能问题:在明细报表中需要展示每个账户在每笔交易之后的余额。如果使用传统的SQL查询,需要将该时间点之前所有交易的数额全部累加起来。如果对每个时间点都这样累加,计算的时间复杂度是O(n^2),一旦交易多了就会非常慢。但是使用窗口函数,可以减少大量的重复计算,使得复杂度变成O(n)。这项技术使得所有视图可以在数据被修改后无延迟的快速更新。
业界的相关研究
设计原型的过程中,我也开始好奇业界是否有人研究过如何用SQL数据库进行复式记账,我应该看看业界的最新成果是怎样的。
我找到了stackoverflow上一篇拥有200多个赞的文章[7],该文章发表于2019年,是一位在银行IT系统工作了几十年的澳大利亚资深数据库专家给出的数据表建模:
我仔细分析了他的建模,发现除了一些因银行业特有的审计要求而做出的特殊设计以外,整体上我的复式记账数据表设计和他是类似的思路。这证明在技术方向上我应该没有大的错误。
如何在数据库中纳入投资分析
当一笔交易涉及到投资的时候,情况会变得有些不一样。比如:如果用10000元人民币买了500股的股票,那么人民币账户上的变动是-10000人民币,股票账户上的变动是+500某只股票的份额。这时,由于两个账户上资产的单位不同,不能再用普通的复式记账规则来校验。这种情况下要怎么做,无论是复式记账法还是stackoverflow上的数据表设计都没有给出回答。
起初,我按照现实世界中的金融模型,把账户按照货币-投资品进行了两级分类:
每个投资品首先处于特定货币域中,以特定的货币计价:人民币买A股,美元买美股。这看起来很符合现实情况。
但是我很快发现了这个模型的缺陷:在每个货币域中,投资收益都是以这种货币来计算价值的。比如投资美股一年赚了多少钱会以美元来统计。但问题是美元相对于人民币也是投资品,也有汇率波动,如果想要计算以人民币计价,整体上投资赚了多少钱,这就困难了。有人可能觉得只要把挣的美元换算成人民币就行。可问题是按什么时间的汇率换算呢?——有的美元是年初挣的,有的美元是年末挣的,不同日期的汇率不一样。很多投资收益率算法与时间因子存在强关联(一年赚10%和十年赚10%的意义完全不同),所以在这个问题上不能随意处理。
更何况,美元不光是投资品,同时还是可用于消费的货币。每天花掉的那些美元又怎么影响投资分析呢?
我开始理解为什么主流的记账软件都搞不定外币了——因为这对于数据模型的设计非常有挑战。
这时我想起了自己给学生上课时讲过的一句话:“软件的设计不应该简单照搬现实世界模型,而应当基于软件本身的行为来做抽象。”
一番思考后,我发现从投资者关注的角度来看,所有非本位币现金的资产的本质特点都是“价格存在波动”。至于投资品属于哪个货币域,并不是投资分析的关键关注点。无论是A股股票、美股股票还是美元,它们都可以用人民币来计算价值。因此,只要规定一种资产是本位币(在TataruBook中叫做“标准资产”),把它作为投资分析时统一的度量“尺子”,其他的资产在数据模型中都可以统一当作投资品来处理。
采用了这套新的模型后,多种货币带来的问题迎刃而解。由于所有非标准资产都用相同的处理方式,不再区分现金和其他品种,这个模型还解决了如何记录“比特币买披萨”的问题。
在搞定了数据模型的设计后,我开始写前端的数据处理软件——毕竟不能每次记账时手工用SQL代码输入数据。这个前端软件也是普通用户看到的操作界面。我得想想给这个记账软件取个名字。
让塔塔露帮你记账
作为风靡世界的MMORPG《最终幻想14》的玩家,我对于游戏里精明能干会算账的拂晓血盟资金管理人塔塔露非常喜欢。于是我一拍脑袋,给记账软件取名叫TataruBook。并在代码仓库和文档里也使用了塔塔露作为“代言人”。
对于界面的设计我犹豫了很久。很多用户凭直觉会想要一眼就能看懂的图形界面。但悖论是:对于大多数习惯了用Excel的人来说,我如果把界面操作做得和Excel不一样,用户会抱怨“使用习惯为什么跟Excel不一样”;但我如果要做得和Excel一模一样,那等于我自己要复制Excel的功能。
其实选择了SQLite数据格式,就意味着数据库的通用操作界面已经有人做了。有大量的软件支持对SQLite数据库文件进行查看和编辑,比如DB4S[8]、SQLiteStudio[9]等等,甚至还有网站能支持在线查看SQLite数据文件[10]。由于TataruBook的记账数据和报表都存储在SQLite数据库文件中,因此在这些软件或工具中选择任何一个,都能查看到所有报表,以及进行简单的数据编辑。
除此之外还需要的功能有两个:一个是数据库文件的初始化,另一个是记账数据的批量导入。这部分功能其实对界面并没有多高的要求,因此我选择了使用命令行界面。但是,很快就有人抱怨:
我不会用命令行,怎么办?
“我双击运行TataruBook.exe,然后什么都没发生。”——对于知道怎么使用命令行程序的人来说,这听起来像笑话。但是确实有大量的普通人从来没有用过命令行界面。我应该针对他们给出更好的解决方案。
但按照前面的逻辑,如果我再做一套呈现数据的图形界面,显然是在重复造轮子,不是个合理的选择。因此我选择了另一种解决方式:右键快捷菜单。
在TataruBook的最新版本中,主要的操作都可以通过右键菜单来完成,不需要打开命令行窗口。你可以在Excel中编辑交易记录,然后复制若干行,再通过右键菜单的“TataruBook paste”指令直接将剪贴板中的交易记录添加到记账数据库文件中。
文档和代码一样重要
虽然复式记账在会计行业中已被广泛使用,但是在普通人群里,知道如何正确使用的并不多。对于不了解任何背景的普通人来说,他们第一眼看到每笔交易都有两个账户参与,只会感到疑惑,不知道怎么办。
为了让人快速了解使用方法,我使用Github Pages建立了TataruBook的文档网站[11],在文档里详细描述了TataruBook的安装方法,入门使用示例,所有的表和视图,投资收益率的算法,以及批量导入记账数据的方式。
写文档的过程同时也是对软件概念建模的校验。当概念空间没有做到逻辑自洽时,文档内容很容易出现前后矛盾的情况。为了确保文档质量,我邀请亲友从普通用户角度进行检视,并修改了60多个issue。文档完成以后,我对于TataruBook概念空间的完备性也更有信心了。
中文的文档完成后,我还写了英文版的文档——过程中也顺带学了不少的金融专业术语。之后,在肆萬、阿雨、優美等几位亲友的帮助下,TataruBook还推出了日文版的文档。
在多语言版本中,如何呈现使用示例是个麻烦的问题。因为如果在英文文档中使用国内的银行名称、微信、支付宝等等名词,老外很难看懂。TataruBook的办法是在文档里全部用《最终幻想14》的虚拟世界设定来示例,这样不管哪个国家的人理解起来都差不多。另外,《最终幻想14》游戏本身就有中日英3种语言的官方翻译,这给我的文档翻译也带来了方便。
Github Pages建网站虽然很快,但是国内的网络访问起来常常不流畅。为了解决这个问题,我在国内发布的软件包里包含了完整的离线版本文档。
使用TataruBook能给我带来什么好处?
你是否还记得自己以前开通了多少交通卡、饭卡、储值卡,每张卡里有多少钱?你是否还记得自己多年前在某个金融机构买的基金或理财产品?——不管你的个人资产以什么形式存放在哪里,TataruBook都会永远记得,而且能准确的给出资产整体统计和分布。甚至你和别人的口头债务,TataruBook都能帮你记住。
银行和其他金融机构一般都只提供最近一两年的资金明细查询,更久的就查不到了。但是,如果你定期把这些明细数据导入到TataruBook中,那么你的所有资金往来和投资交易记录都可以终身保存在SQLite数据库文件里,它们会记录你的生活。——十年前生日时去了哪里聚餐消费?五年前情人节买了什么礼物送给TA?即使在金融机构上查不到明细了,但在你自己的记账数据库中永远不会消失。
如果你做投资,那么TataruBook会保存你的每一笔交易记录,你可以查询历史上任一段时间自己在某个品种上的收益率,以及家庭整体的收益率。你也不用担心现在或将来有什么投资品种是TataruBook所不支持的,因为TataruBook的数据模型决定了只要投资品有价格,它就能被记录和分析。
你不用担心隐私问题,因为TataruBook只在本地工作不联网,TataruBook本身以及它所使用的Python、SQLite全部都是开源软件。TataruBook的记账数据和报表都存储在SQLite文件中,因此无论用哪种支持SQLite的工具打开,都能看到同样的报表数据。SQLite数据库格式是世界上使用最广泛的数据格式之一,官方保证至少维护到2050年,美国国会图书馆将SQLite作为长期保存数字内容的推荐格式[12]。所以,相比起使用其他私有数据格式的记账软件,使用TataruBook,你未来需要迁移数据的可能性微乎其微。
记账数据文件会占用多大硬盘空间呢?——我自己的记账数据现在包含了130多个账户和5000条交易记录,文件大小不到500k。所以,普通人一辈子的记账数据加起来大概率不会超过50M。
TataruBook的代码仓和发布页
Github: https://github.com/Goalsum/TataruBook
Gitee: https://gitee.com/goalsum/tatarubook
国内网络访问Github可能不流畅,推荐通过Gitee的发布页下载最新软件包。
参考
- 欧洲现存最早的复式记账记录是Amatino Manucci在13世纪末的会计记录。 https://en.wikipedia.org/wiki/Double-entry_bookkeeping
- https://beancount.github.io
- https://beancount.github.io/docs/trading_with_beancount.html
- https://beancount.github.io/docs/beancount_v3.html#current-problems
- https://beancount.github.io/docs/command_line_accounting_in_context.html#why-not-just-use-an-sql-database
- https://db-engines.com/en/ranking
- https://stackoverflow.com/questions/59432964/relational-data-model-for-double-entry-accounting
- https://sqlitebrowser.org
- https://sqlitestudio.pl
- https://sqliteviewer.app
- https://goalsum.github.io/TataruBook/index_cn.html
- https://www.sqlite.org/lts.html
- 点赞
- 收藏
- 关注作者
评论(0)