2021年11月

https://lug.ustc.edu.cn/planet/2020/08/keeping-account-with-beancount/

本文首发于 https://charlesliu7.github.io/blackboard/2019/07/24/beancount/

偶尔看到了复式记账这个概念,对精细记账的我而言很受用,选择 Beancount 这样的开源工具的原因莫过于账本数据完全由自己掌握,而不是被各大 APP 所保管。本文从一次个人实践的角度来说明一下复式记账的使用。

本篇文章是一个从零开始的个人实践记录,涵盖 文件组织 -> 基本账本书写 -> 爬取一卡通数据并自动记录,供同样使用 Beancount 的同学做参考,但此实践并不一定完全合乎其他人的使用习惯,如果有其它记录策略也是可以的。本文内容基于读者对复式记账和 Beancount 语法有一定了解的情况下撰写的,关于复式记账的概念和一些诸多基本功能介绍,可以参考阅读以下文章:

开始!

安装使用Permalink

Beancount 是一个 Python 实现的开源工具,在本地即可运行,首先从 PyPI 获取:

pip install beancount fava

其中 beancount 是核心包,包含核心的命令行工具;fava 是网页可视化工具。 这里有一个fava 示例账本 ,对应的 Beancount 源代码可以在 Bitbucket 上下载 本文的示例账本以及可视化可以在该仓库查看。

克隆该仓库,在命令行中使用 fava main.beancount

$ fava main.beancount
Running Fava on http://localhost:5000

打开浏览器即可看到可视化账本。

文件结构Permalink

Beancount 支持 include 语法来拓展账簿,个人采用按时间划分文件,辅之特殊事件(比如旅游)单独记录的方法,目录结构如下:

.
├── 2018
│   └── ...
├── 2019
│   └── 01.beancount
│   └── 02.beancount
│   └── 03.beancount
│   └── 04.beancount ; 注释用分号
│   └── xx.event.beancount ; 单独针对某一特别事件的账本,比如旅游
│   └── 05.beancount
│   └── 06.beancount
│   └── 07.beancount
├── accounts.beancount ; 记录初始账户信息
├── main.beancount ; 主文件

账本书写Permalink

账户信息设置Permalink

首先要定义账户,即文件 accounts.beancount,Beancount 系统中预定义了五个分类:

  • Assets 资产:本人按照账户类型:国家:金融机构名字:具体账户的策略划分,时间是开户时间,比如:2017-01-01 open Assets:CN:Bank:BoC:C1234 CNY ; 学校银行卡 2017-01-01 open Assets:CN:Card:USTC CNY ; 一卡通 2017-01-01 open Assets:CN:Web:AliPay CNY ; 支付宝 2017-01-01 open Assets:CN:Web:WeChatPay CNY ; 微信支付 有一类针对 AA 付款或者个人向自己借款的账户,需要专门记录。2017-01-01 open Assets:Receivables:X ; 对 X 的应收款项
  • Liabilities 负债:本人主要是信用卡和向他人借款的账户,比如:2017-01-01 open Liabilities:Payable:X ; 对 X 的债务 2017-01-01 open Liabilities:CreditCard:CN:BoC:C1111 CNY ; 信用卡 2017-01-01 open Liabilities:CreditCard:CN:Huabei CNY ; 花呗
  • Equity 权益(净资产):目前只有一个用于平衡开户的时候账户资金的权益。1990-01-01 open Equity:Opening-Balances
  • Expenses 支出:支出就非常的多样化,可以根据自己需求分门别类,比如:2017-01-01 open Expenses:Clothing ; 包括上衣,裤子和装饰,袜子,围巾,帽子 2017-01-01 open Expenses:Shoes ; 鞋 2017-01-01 open Expenses:Food:Dinner 2017-01-01 open Expenses:Food:Lunch 2017-01-01 open Expenses:Food:Breakfast 2017-01-01 open Expenses:Food:Fruits 2017-01-01 open Expenses:Food:Nightingale ; 校门口夜宵 2017-01-01 open Expenses:Food:Drinks 2017-01-01 open Expenses:Food:Snack ; 杂食、零食 等等……
  • Income 收入:收入也可以根据自己的实际收入来源来建立账户,比如:2017-01-01 open Income:Salary:XXX 2017-01-01 open Income:Salary:Others 2017-01-01 open Income:Others

主文件设置Permalink

然后设置主文件 main.beancount 内容,主文件任务是设置全局变量,然后去涵盖各个子账本:

option "title" "取个霸气的名字吧" ; 账簿名称
option "operating_currency" "CNY" ; 账簿主货币
option "operating_currency" "USD" ; 可以添加多个主货币

include "accounts.beancount" ; 包含账户信息

; 每个月的账本
include "2020/06.beancount"
include "2020/07.beancount"

账户初始余额设置Permalink

在开始记账前,要设置每个账户的余额信息,采用以下方法来给每个账户设置余额/借记账单:

2019-01-01 pad Assets:Bank:CN:BoC:C1111 Equity:Opening-Balances ; 从 Opening-Balances 中划取 XX 帐到银行卡中
2019-01-02 balance Assets:Bank:CN:BoC:C1111    +xxx.xx CNY ; 银行卡余额为 xxx.xx

该语句的含义是无论 Assets:Bank:CN:BoC:C1111 之前余额多少,在 2019 年 1 月 2 日开始之前都调整到 xxx.xx CNY,差额从 Equity:Opening-Balances 来。注意两行之间差一天的时间,balance 断言界定为当天开始;一般储蓄卡余额为正,信用卡余额为负。

记账Permalink

  • 基本记账,记账语法为:YYYY-mm-dd * ["Payee"] "Narration" posting 1 posting 2 posting 3 ... 比如:2019-01-01 * "Walmart" "在超市买两件衣服和晚餐" Expenses:Clothing 20 USD Expenses:Clothing 10 USD Expenses:Food:Dinner 10 USD Liabilities:CreditCard:US:Discover -40 USD
  • 多货币转换使用 @@ 作为货币转换即可,货币 Beancount 会进行汇率计算,比如:2019-01-01 * "日本航空" "纽约-东京" Expenses:Transport:Airline 1000 USD @@ 110000 JPY Liabilities:CreditCard:JP:Rakuten -110000 JPY
  • 账户结息:账户的利息肯定难以每日都记录,本人采用 pad+balance 断言,每隔一段时间结算一下。
  • 分期付款:这是个常见的购买方式,需要单独设置开一个 Liabilities Account,手续费记利息支出,每个月账单出现的时候转移一下。 Beancount 提供了一个插件 plugin "beancount.plugins.forecast 专门用来处理分期、订阅情况,可以用于每月费用的自动生成。

核账Permalink

本人选择每个月还款日核实一下账本,在 Fava 左侧 Balance Sheet 或者 Holdings 里可以看到各个账户当前的状况,如果和实际的账户金额有出入的话就需要点进对应账户查看每笔交易的情况,看看是否漏记或者错记。

用 Importer 自动记录一卡通消费Permalink

综述Permalink

Importer 个人理解的作用是将整理好的账单文本转化为 Beancount 记录的形式,即格式化 (表格, JSON 等) 账单 -> Importer -> Beancount 记录,Importer 在其中起到一个消费记录格式转化作用。

Beancount 作者对 Importer 有详细的文档叙述,即 Importing External Data in Beancount。Beancount 官方也有基于机器学习的智能 importer beancount/smart_importer

而本人的需求是:

  1. 利用校园一卡通门户系统获取每日的一卡通使用记录,并生成 CSV 记录。
  2. 基于 CSV 的账单生成 beancount 文件。
  3. 能够自行定制规则来实现对不同消费的分类。

将当日的一卡通消费生成为 CSVPermalink

爬取一卡通数据的代码为 crawler.py ,其作用为爬取当日的一卡通消费记录,并自定义规则区分早、午、晚餐,生成符合 Beancount 格式的 CSV。(代码可以直接运行)

import requests
from datetime import datetime
from bs4 import BeautifulSoup
import json
import codecs
import csv

name = 'XXX'  # 姓名
stu_no = 'PBXXXXXXXX'  # 学号
pwd = 'user_pwd'  # 统一身份认证密码

if __name__ == '__main__':
    # 利用统一身份认证登陆校园一卡通门户系统
    casurl = 'https://passport.ustc.edu.cn/login?service=http%3A%2F%2Fecard.ustc.edu.cn%2Fcaslogin'
    caspost = {'username': stu_no, 'password': pwd}  # 统一身份认证
    msg = ''
    s = requests.session()
    try:
        r = s.post(casurl, caspost)
    except Exception as e:
        msg = '{0} - INFO: USTC ecard CAS登陆失败 {1}'.format(datetime.now().strftime('%Y-%m-%d %H:%M:%S'), e)
    remaining = 0
    if not name in r.text:
        msg = '{0} - INFO: USTC ecard CAS登陆失败 NOOOOOOOO!!!!!!!!'.format(datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
        print(msg)
    else:
        msg = '{0} - INFO: USTC ecard CAS登陆成功'.format(datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
        print(msg)
        paylist = s.get('https://ecard.ustc.edu.cn/paylist')
        b = BeautifulSoup(paylist.text, features="lxml")
        token = b.findAll('input')[-1].get_attribute_list('value')[0]
        data = s.post(url='https://ecard.ustc.edu.cn/paylist/ajax_get_paylist', data={'date': '', 'page': ''}, headers={'origin': 'https://ecard.ustc.edu.cn', 'referer': 'https://ecard.ustc.edu.cn/paylist', 'sec-fetch-mode': 'cors', 'sec-fetch-site': 'same-origin', 'x-csrf-token': token, 'x-requested-with': 'XMLHttpRequest'})
        b = BeautifulSoup(data.text, features="lxml")
        table = b.find('table')
        th_index = []
        for th in table.findAll('th'):
            th_index.append(th.getText())
        year, month, day = datetime.now().year, datetime.now().month, datetime.now().day
        # 根据自己定义的规则判定早餐、午餐、晚餐
        payinfo = {'breakfast': {'loc': '', 'type': '科大餐饮', 'value': 0.0, }, 'lunch': {'loc': '', 'type': '科大餐饮', 'value': 0.0, }, 'dinner': {'loc': '', 'type': '科大餐饮', 'value': 0.0, }, 'transferin': {'loc': '一卡通充值', 'type': '', 'value': 0.0, } }
        flag = True
        for tr in table.findAll('tr'):
            line = []
            for td in tr.findAll('td'):
                line.append(td.getText())
            if line and flag:
                remaining = float(line[3])
                flag = False
            if not line:
                pass
            elif line[0] == '圈存机充值' and int(line[1]) == 0:
                payinfo['transferin']['value'] = float(line[4])
            elif line[0] == '消费':
                linetime = datetime.strptime(line[5], '%Y-%m-%d %H:%M:%S')
                if linetime > datetime(year, month, day, 6) and linetime < datetime(year, month, day, 10): # 判定为早餐
                    if line[6] in payinfo['breakfast']['loc']:
                        pass
                    else:
                        payinfo['breakfast']['loc'] += (line[6] + ' ')
                    payinfo['breakfast']['value'] += float(line[4])
                elif linetime > datetime(year, month, day, 10) and linetime < datetime(year, month, day, 14): # 判定为午餐
                    if line[6] in payinfo['lunch']['loc']:
                        pass
                    else:
                        payinfo['lunch']['loc'] += (line[6] + ' ')
                    payinfo['lunch']['value'] += float(line[4])
                elif linetime > datetime(year, month, day, 16) and linetime < datetime(year, month, day, 20): # 判定为晚餐
                    if line[6] in payinfo['dinner']['loc']:
                        pass
                    else:
                        payinfo['dinner']['loc'] += (line[6] + ' ')
                    payinfo['dinner']['value'] += float(line[4])
                elif linetime < datetime(year, month, day, 0):
                    break
                else:
                    mtmp = '{0} - INFO: 未知消费 {1}'.format(datetime.now().strftime('%Y-%m-%d %H:%M:%S'), line)
                    print(mtmp)
            else:
                mtmp = '{0} - INFO: 异常消费 {1}'.format(datetime.now().strftime('%Y-%m-%d %H:%M:%S'), line)
                print(mtmp)
        mtmp = '{0} - INFO: 卡内余额 {1}'.format(
            datetime.now().strftime('%Y-%m-%d %H:%M:%S'), remaining)
        print(mtmp)

        # CSV Part
        today = datetime.now().strftime('%Y-%m-%d')
        headers = ['记账日期', '收款人', '交易摘要', '人民币金额', '类别']
        csvinfo = []
        if payinfo['transferin']['value'] > 0:
            csvinfo.append({headers[0]: today, headers[1]: payinfo['transferin']['type'], headers[2]: payinfo['transferin']
                            ['loc'], headers[3]: "%.2f" % -payinfo['transferin']['value'], headers[4]: 'Transferin'})
        if payinfo['breakfast']['value'] > 0:
            csvinfo.append({headers[0]: today, headers[1]: payinfo['breakfast']['type'], headers[2]: payinfo['breakfast']
                            ['loc'], headers[3]: "%.2f" % payinfo['breakfast']['value'], headers[4]: 'Breakfast'})
        if payinfo['lunch']['value'] > 0:
            csvinfo.append({headers[0]: today, headers[1]: payinfo['lunch']['type'], headers[2]: payinfo['lunch']['loc'], headers[3]: "%.2f" % payinfo['lunch']['value'], headers[4]: 'Lunch'})
        if payinfo['dinner']['value'] > 0:
            csvinfo.append({headers[0]: today, headers[1]: payinfo['dinner']['type'], headers[2]: payinfo['dinner']
                            ['loc'], headers[3]: "%.2f" % payinfo['dinner']['value'], headers[4]: 'Dinner'})
        with open(today+'.csv', 'w') as f:
            f_csv = csv.DictWriter(f, headers)
            f_csv.writeheader()
            f_csv.writerows(csvinfo)

代码执行完毕后会生成 20XX-XX-XX.csv,例如 2020-07-02.csv

记账日期收款人交易摘要人民币金额类别
2020-07-02科大餐饮一卡通充值-200.00Transferin
2020-07-02科大餐饮西区芳华园餐厅5.00Breakfast
2020-07-02科大餐饮西区芳华园餐厅10.00Lunch
2020-07-02科大餐饮西区芳华园餐厅10.00Dinner

准备 Importer ConfigPermalink

Beancount Importer Config 文件为 importers/ustc_card_importer.py

#!/usr/bin/env python
import os
import sys
import beancount.ingest.extract
from beancount.ingest.importers import csv

beancount.ingest.extract.HEADER = ''

def dumb_USTCecard_categorizer(txn):
    # At this time the txn has only one posting
    try:
        posting1 = txn.postings[0]
    except IndexError:
        return txn

    # Guess the account(s) of the other posting(s)
    if 'breakfast' in txn.narration.lower():
        account = 'Expenses:Food:Breakfast'
    elif 'lunch' in txn.narration.lower():
        account = 'Expenses:Food:Lunch'
    elif 'dinner' in txn.narration.lower():
        account = 'Expenses:Food:Dinner'
    elif 'transferin' in txn.narration.lower():
        account = 'Assets:CN:Bank:BoC:C1234'
    else:
        return txn
    # Make the other posting(s)
    posting2 = posting1._replace(
        account=account,
        units=-posting1.units
    )
    # Insert / Append the posting into the transaction
    if posting1.units < posting2.units:
        txn.postings.append(posting2)
    else:
        txn.postings.insert(0, posting2)
    return txn

CONFIG = [
    # USTC canteen
    csv.Importer(
        {
            csv.Col.DATE: '记账日期',
            csv.Col.PAYEE: '收款人',
            csv.Col.NARRATION1: '交易摘要',
            csv.Col.AMOUNT_DEBIT: '人民币金额',
            csv.Col.NARRATION2: '类别'
        },
        account='Assets:CN:Card:USTC',
        currency='CNY',
        categorizer=dumb_USTCecard_categorizer,
    ),
]

语法说明参见 Beancount 系列二: Importer 设置

执行命令生成 beancount 账单。

bean-extract ustc_card_importer.py 2020-07-02.csv

得到账单:

**** /path/to/2020-07-02.csv

2020-07-02 * "科大餐饮" "一卡通充值; Transferin"
    Assets:CN:Card:USTC        200.00 CNY
    Assets:CN:Bank:BoC:C1234  -200.00 CNY

2020-07-02 * "科大餐饮" "西区芳华园餐厅; Breakfast"
    Assets:CN:Card:USTC      -5.00 CNY
    Expenses:Food:Breakfast   5.00 CNY

2020-07-02 * "科大餐饮" "西区芳华园餐厅; Lunch"
    Assets:CN:Card:USTC  -10.00 CNY
    Expenses:Food:Lunch   10.00 CNY

2020-07-02 * "科大餐饮" "西区芳华园餐厅; Dinner"
    Assets:CN:Card:USTC   -10.00 CNY
    Expenses:Food:Dinner   10.00 CNY

校园卡消费可以直接使用该 importer。支付宝账单、信用卡账单等也可以通过导出 CSV 账单的方式利用自己编写的 importer 导入。

自动化Permalink

上述过程需要执行多个命令和脚本,利用 crontab 在每日睡前 (23:30) 执行一遍代码即可自动化记录消费。

$ python crawler.py>>log.log
$ cd importers
$ python ustc_card_importer_pipeline.py # 注意这里需要修改要记录的账本文件

Done!

FavaPermalink

  • Fava 可视化网页中提供了编辑功能,对于多文件的编辑,默认打开的是主文件,要想修改编辑器默认打开的文件,需将 2019-07-11 custom "fava-option" "default-file" 这个设置放在想要设定的文件里。
  • Fava 系统中也提供了添加记录的功能,但添加的记录默认写入了主文件里,根据Fava insert-entry optionsdefault-file could also set the insertion file 作者似乎不 care 添加在哪个文件里这个问题,但依然可以利用 insert-entry 关键字变相设置一下,比如将 2019-01-01 custom "fava-option" "insert-entry" ".*" 断言写在 2019/01.bean 文件的末尾,所有在 2019-01-01 之后的记录,通过 Fava 添加记录的话,该记录会 write 在这个断言之前。
  • Fava 是不带有密码功能的,根据 Make fava password-protected 作者认为这不应该是 Fava 应该做的工作;利用 Apache 或者 Nginx 的认证功能可以满足这个需求。
  • 可视化工具 Fava 也支持 Importer,可以通过设置:2017-01-01 custom "fava-option" "import-config" "./importers/path/to/importer.py" 2017-01-01 custom "fava-option" "import-dirs" "./importers/path/to/csv_tmp/" 在 Fava 界面侧栏看到 Importer,并手动导入数据。注 :Importer 在 Fava 中使用的时候 metadata 会被去除。
  • Fava 还支持自定义 side bar link,即:2099-01-01 custom "fava-sidebar-link" "This Week" "/jump?time=day-6+-+day" 2099-01-01 custom "fava-sidebar-link" "This Month" "/jump?time=month" 2099-01-01 custom "fava-sidebar-link" "3 Month" "/jump?time=month-1+-+month%2B1" 2099-01-01 custom "fava-sidebar-link" "Year-To-Date" "/jump?time=year+-+month" 2099-01-01 custom "fava-sidebar-link" "All dates" "/jump?time="

https://geekplux.com/posts/accounting-earnings-report-graph-theory

如果你去搜索“为什么记账”,大概会有无数条理由。大多数会提到:记录自己的开支、寻找可削减的开支等。但我认为记账的功效不止于此,它其实是量化自己的一步,也是自身财务管理的一步。如果你想要投资炒股,那记账更是你理解公司财报的重要一步。

如果说金融业的本质是合理资源配置(通俗来说就是“让钱去该去的地方”)的话,那记账就是自己财富资源配置的基础。

复式记账#

先简单说说我的记账史:我大三开始记账,一开始用一款叫 Toshl 的 APP,多端同步、界面大方(现在这家公司仍活的挺好,登录上还能看到我几年前的账单)。当时它没有自动同步功能,我靠纯手动记账坚持了大概一年半。直到我的信用卡多起来,可以自动同步信用卡账单的网易有钱进入我的视线,我在安全和便捷的博弈中倾向了便捷。靠网易有钱一直坚持记账了两年多。这期间不仅记余额、日常花销,还包括基金、股票的仓位等,让我对资产负债表慢慢有了概念。

Beancount#

到后来我遇到 Beancount,发现这才是适合我这种强迫症的终极大杀器。同时我也接触到了复式记账这种更科学的记账方式,对资产=负债+权益也有了充分的理解。

珠玉在前,关于什么是 beancount、什么是资产、为什么用 beancount 记账、为什么要采用复式记账等等,都在 Beancount複式記賬Beancount —— 命令行复式簿记等文章中有详细介绍,我这里简单概括一下:

复式记账就是保证每笔账都要有至少两个(复数个)账户,且这些账户的和为零的记账方式。

简单举个例子:我们去便利店买了一瓶可乐,那么如果按普通的记账方式,账单上的记录为:

2020-01-01,买可乐,银行卡余额 -5 元

而复式记账为:

2020-01-01,银行卡余额 -5 元 花销:饮料 +5 元

而同样的一笔交易,对于便利店来说是:

2020-01-01, 货物:可乐 -1 瓶 公司账户余额 +5 元

可以看到,复式记账必须是多个“账户”,因为要保证加起来为 0。“花销”是可以作为一个账户的,这样你只要看这个账户就知道你总共花了多少钱。货物也可以是一个账户,因为货物是有价值的。

另一个层面,可以发现记账不仅可以记钱,还可以记任何你想记录的“货币”,比如例子中的可乐、会员卡积分、股票等等,实现量化一切。

优越性#

下图是用 fava 生成的网页,美观大方。

复式记账最大的好处在于能保证账目的一致性和完整性,这样让你对自己的资产情况一目了然。再加上 beancount 是基于纯文本记账(纯文本自由且强大),所以能实现更复杂的功能。比如可以用 beancount 记录自己资产的拆借、折旧:

  • 拆借:这在我们日常生活中特别常见,朋友垫付、买房贷款、拼团购物、信用卡消费,这都算拆借(同理,公司和公司之间、公司和银行之间等也都有拆借的行为)。
  • 折旧:就是你买入一个东西之后它随时间不停贬值的过程。比如买入一台电脑或手机,二手再卖出就不可能再按原价卖出了,而且转手价钱的高低取决于你使用了多久。所有我们需要针对自己会贬值的资产定期折旧(同理,公司购入 100 台电脑或椅子作为资产,这些资产也需要定期折旧)。

上面举的这两种,具体例子可以到我前面提到的文章中找。总之它们都会涉及到多个账户多条账目,用普通的记账方式,很容易就搞不清楚钱到底花在哪,而用 beancount 复式记账可以清晰掌握,并生成损益表和资产负债表。

除此之外还有很多强大的功能,大家可以自行探索。

账单与财报#

我们通常说一家公司的财报,其实主要说它的两个表:损益表资产负债表。当你记账久了就发现,公司的这两个表,和我们个人的没什么两样

上文中我提到的拆借折旧这两个就是很典型的例子,我们自身也有,只是我们很少像公司一样严格的记账。如果你有经营一家公司,你会很好的理解这一点,而如果你是一个只有工资一项收入的工具人,那你得稍微转换一下思路:

  • 损益表体现我们能挣多少钱。把我们付出的时间、体力、脑力算作成本,把工资算作收益,很容易得出相对时间内我们自身的毛利率是多少、经营成本是多少……想赚更多钱就要提升利率(工作效率、工作含金量),降低成本(自动化部分工作)。
  • 资产负债表体现我们有多少钱。应收账款、存货、固定资产、无形资产;应付账款、流动负债、长期负债等等,你仔细想想,这些是不是都可以在我们身上找到一一对应呢?

更严谨的记账可以帮你理解公司财务报表,帮助你在股票投资中避险;而对公司财报更深的理解,也能帮助你优化自身的资产结构,开源节流。两者相得益彰,这是我自己深有体会的。

当理解的很深的时候,甚至可以跳脱出财务的范畴,比如做技术也经常说技术债,其实就是把“技术“当成一家”公司“,你每行代码可以理解成给这家公司盖楼,数据可以算这家公司的无形资产。你写的不好的代码,相当于给”这栋楼“注入不良资产,产生的畸形数据之后还得清理,算是产生了”流动负债“……长期不维护的代码,也会”折旧“……以上。

量化一切#

通过上文的赘述,是不是你已经有点眉目了,发现不管主体是公司还是个人,不管记录的东西是金钱还是其它,都可以量化。

之前在和一个朋友讨论记账时,他说:”我花 10 分钟去记账,它带给我的收益我不知道有多少,唯一增加的就是账本厚度了“,我突然想到,如果时间算作一个账户,那么他这句话可以记作:

2020-01-01 时间 -10 分钟 账本厚度 +5 行

时间就是金钱,这句话不是空谈,我们所做的每一件事,其中都包含时间成本。进而再想,发现不止是时间可以量化,而是任何事物都可以。比如

2020-01-01 去跑步运动 时间 -40 分钟 体力值 +10 2020-01-02 学习编程 时间 -60 分钟 体力值 -10 编程技术 +30 2020-01-10 上班 时间 -60 分钟 银行卡余额 100 元 收入 -100 元 编程技术 +5

从这个账单中,可以看出,跑步能让你体力充沛,更好地学习。同时工作一小时对编程技术的提升远没有自己单独坐下来用心去学的多。另外为什么时间和收入都是负数呢,你可以这么理解,你的时间本来是无限的(不确定你能活到几百岁),你从中拿走了几十分钟用来做事。收入同理,你可以理解成你的钱本来是无限的(不确定你能成为亿万还是兆万富翁),你从中拿走了 100 元放入了银行。多挣钱你就可以理解成从自己无限的可能中尽可能地多拿钱出来到银行里,因为只有拿出来才能花……

总之,什么都是可以量化的,只要你设定一个价格。比如刚才例子中的 40 分钟跑步 = 体力值+10,这些怎么定义都取决于你。如果你看过《奇特的一生》就发现作者就是近乎苛刻地将自己一生的时间都做了量化。当然我不是建议你去这么做,也不是说完全量化会有多么好,只是说明有这种可能,让你更好的理解“万物皆有价”,让你更好的理解你的一切。

记账与图论#

复式记账保证了每一条账目,都会有至少两个账户和至少一条交易。而图这种数据结构是点和边的集合。如果我们把账户当作节点,把交易当作边,即:

账户 = 节点,交易 = 边

就很容易构成了一个图。

图论是研究事物之间关系的科学,万物之间都是有千丝万缕联系的,任何有联系(联接)的东西都可以抽象成图这种数据结构。抽象成图之后,更好做分析,比如分析哪个节点影响力最大,哪条路径最为关键,预测哪个方向会出现更多节点等等(对网络科学感兴趣的,可以看网络科学宗师巴拉巴西写的 《链接》)。

我是在读了Accounting for Computer Scientists这篇文章之后大彻大悟的。这篇文章算是写给程序员的记账指南,一路从单笔记账写到财报。

上图通过把账单抽象成图之后,很明显可以看到自己钱的流向:合伙人给公司账户 5000$,其中 500 用来买公司需要的家具,5块还了信用卡,由于买食物刷了 13 块,所有还欠信用卡 8 块。但最最神奇的是,不管你切断其中的哪条边,总能保证两个子图加起来的总和为 0,不信你可以试一试,这就是复式记账的威力。

而通过给节点上色,则很容易分辨出资产(绿色)、损益(蓝色)、启动资金(粉红)。具体的接下来再怎么分析,可以看我刚才提到的文章。

总之将账单抽象成图,是一个我以前没有想到过的思路,兴许将来的某一天我可以将自己的账单用Force-directed graph 画出来玩玩。说不定能看出“哪笔支出是造成我现在没钱的原因”,“哪些债务影响了我在某类别上的支出”,或者直接预测出未来我将会在哪方面花更多的钱,防微杜渐。

尾#

记账,尤其是用 beancount 记账,让我变身小会计,让我能理解公司的财报,让我对很多事情有了量化的思维。量化自己的好处就是能随时把数据拿来做个像模像样的分析,同时也能把自己当作一家公司来运营,这点我在我的文章里都提到过很多次了。

兴许在将来的哪一天,我可以对着自己的账单发现某些可优化的点,实现财务自由呢。玩笑话说归说,但把计算机思维运用到生活中我已经在无形中尝试过很多了,还真有用。

https://sspai.com/post/59777#!

开通少数派第一天,分享一篇博客旧文,关于beancount的教程。

记了几年账,工具用过随手记、Excel 甚至 Evernote。通常只记收支,不记收支对应的账户变化。其实知道这种方式很粗浅,容易错记、漏记,并且只能分析收支,无法跟踪个人财务现状。结果则是,虽然记了几年,却很少分析,个人财务状态也没有实质改善。

几个月前看到 byvoid 分享 Beancount,发现是一款记账神器。我从 7 月 1 日正式使用,如今也有 3 个多月,分享一些经验心得。

一、普通记账 vs 复式记账

Beancount 记账方法叫复式记账。

把只记录收支的方法称为普通记账(估计是多数人在用的方法)。那么复式记账,除了记录收支,还需记录账户(支付宝、银行卡等)的变动。以一个最简单的例子感受二者的区别:

假设:7 月 1 日,打车花费 30 元,使用银行卡支付。

普通记账一般包括日期、收支分类和金额,如下:

2019-08-28: 交通-打车 -200元

复式记账把账户变化也一并记账,如下:

2019-08-28:
    交通-打车    200元
    银行卡      -200元

复式记账会记录每笔交易的资金流动,各账户变化「有正有负,正负相等」。这便是复式记账的基本原理,称之为「会计恒等式」。这种方式能够保证记账准确无误,也能提供更详细的财务分析。

这句话中的账户是广义的,也可理解为分类,「银行卡」和「交通 - 打车」都是账户。下文中出现账户,若无特别说明,均指广义的账户。

二、Beancount 入门

复式记账是方法论,而 Beancount 则是支持复式记账的工具,Beancount 有以下三个优点:

  1. 完整个人财务数据比较敏感,Beancount 本地存储,不用担心数据泄露。
  2. 纯文本账本,不依赖特定软件,便于数据迁移。
  3. Beancount 是开源软件。

接下来介绍 Beancount 的基础使用。

安装

beancount 是个 Python 项目,安装好 python 后,执行:

pip install beancount
pip install fava

Fava 是关联软件,为 Beancount 提供一个更漂亮的 Web 界面(如图 1/2/3),建议同时安装。

账本示例

Beancount 的使用非常简单,概括为两步:

第一步:使用文本文件按一定格式记账。

下面是个完整示例,直接保存为 moneybook.bean(Beancount 的文件扩展名为.bean

;【一、账本信息】
option "title" "我的账本" ;账本名称
option "operating_currency" "CNY" ;账本主货币

;【二、账户设置】
;1、开设账户
1990-01-01 open Assets:Card:1234 CNY, USD ;尾号1234的银行卡,支持CNY和USD
1990-01-01 open Liabilities:CreditCard:5678 CNY, USD ;双币信用卡
1990-01-01 open Income:Salary CNY ;工资收入
1990-01-01 open Expenses:Tax CNY ;交税
1990-01-01 open Expenses:Traffic:Taxi CNY ;打车消费,只支持CNY
1990-01-01 open Equity:OpenBalance ;用于账户初始化,支持任意货币

;2、账户初始化
2019-08-27 * "" "银行卡,初始余额10000元"
    Assets:Card:1234           10000.00 CNY
    Equity:OpenBalance        -10000.00 CNY

;【三、交易记录】
2019-08-28 * "杭州出租车公司" "打车到公司,银行卡支付"
    Expenses:Traffic:Taxi        200.00 CNY
    Assets:Card:1234            -200.00 CNY

2019-08-29 * "" "餐饮"
    Assets:Card:1234           -1100.00 CNY
    Liabilities:CreditCard:5678 1100.00 CNY
    
2019-08-31 * "XX公司" "工资收入"
    Assets:Card:1234           12000.00 CNY
    Expenses:Tax                1000.00 CNY
    Income:Salary             

第二步:命令行执行 fava moneybook.bean,看到如下结果:

 $ fava moneybook.bean                                                                                                                                              
Running Fava on http://localhost:5000

再到浏览器中打开 http://localhost:5000 就能看到账本,下图分别是损益表、资产负债表、日记账的截图。其它栏目自行探索。

图1 - 损益表

图 1 - 损益表

图2 - 资产负债表

图 2 - 资产负债表

图3 - 日记账

图 3 - 日记账

记账格式

使用 Beancount,关键就是了解它的记账格式。从示例可知,账本包括三个部分:

  • 账本信息:比如账本名称,账本主货币。
  • 账户设置:包括开设账户和账户初始化。
  • 交易记录:日常记账。

下面依次介绍。

账本信息

一般设置账本的名称和主货币即可。

option "title" "我的账本" ;账本名称
option "operating_currency" "CNY" ;账本主货币

账户设置

【开户】

记账之前,得有账户,开户格式如下:

开启日期 open 账户名 货币类型

Beancount 中账户名支持层级,以英文冒号: 分隔,如 Assets:Card:1234。但第一层必须是以下五个账户之一,日常交易中涉及到的账户,一定可以归于其中某一类:

  • 收入(Income):工资、投资收益等。
  • 支出(Expenses):衣、食、住、行等。
  • 资产(Assets):储蓄卡余额、支付宝余额、股票账户余额、房子、车子等。
  • 负债(Liabilities):信用卡欠款、房贷、车贷等。
  • 权益(Equity):这个账户比较特殊,在账户初始化、误差处理等少数场合使用。

开启日期可使用真实日期,若忘了了,我习惯使用自己的生日或开始复式记账的前一天(2019-06-30)。

开设账户时货币类型不是必须的,但建议加上,记录交易时货币不一致 Beancount 会报错。货币可设多个,用英文逗号(,)分隔。

最后,如果一个账户不再使用,比如注销信用卡。可用 close 命令关闭账户。如下:

关闭日期 close 账户名

【初始化】

当我们开始记账时,一般资产和负债都不会是 0,因此需要对资产和负债账户进行初始化。初始化的格式与交易明细完全一致,请参考正文的交易明细介绍。

唯一不同,初始化需要用到 Equity 账户。如示例中的 Assets:Card:1234,初始金额 10000 元来自 Equity:OpenBalance

交易明细

账户设置中的初始化,和交易明细有关,因此先介绍交易明细的记录格式。如下:

日期 * "交易方" "交易备注"
    账户   金额 货币
    账户   金额 货币

其中

  • * 号表示这笔交易是确定的,没有疑问。若是 ! 号,表示存疑,但一般用不上。
  • 交易方和交易备注,均可省略。
  • 货币必须与账户设置中对应的货币类型一致。比如账户设置为美元账户,消费时出现人民币,Beancount 会报错。

此外,账户后的金额是带有符号的,如下:

  • 支出账户:一般为正数。表示花费多少钱。
  • 收入账户:一般为负数。表示收入多少钱。投资收入账户可能出现正数,则表示投资亏损。
  • 资产账户:可正可负。正数表示有钱存入,余额增加;负数表示有钱转出,余额减少。
  • 负债账户:可正可负。正数表示还款,负债减少;负数表示借款,负债增加。

支出为正,收入为负,有点反直觉,是会计恒等式逻辑所致。会计恒等式具体表述如下:

(Assets + Expenses) + (Liabilities + Income) + Equity = 0

一笔交易记录可能涉及 2 个以上的账户,比如例子中的「工资收入」,这时,多个账户的金额也满足「有正有负,正负相等」。实际记录时,Beancount 允许一个账户的金额为空,它会根据正负相等的原则自动计算。

看到这里,已经可以开始动手记账了。开设好账户,然后初始化,最后每天记录交易。

三、Beancount 实践

下面是些个人使用经验,供参考。

编辑器

我使用的是 VSCode 编辑器,配合 Beancount 插件(by Lencerf),能够实现语法着色、账户自动补全、数字按小数点对齐、错误提示等,大大提高记账效率。

拆分账本

如果按前面的方法记账,一段时间后会发现:随着交易增加,账本文件越来越大,维护不方便

Beancount 允许将账本拆分,然后通过 include 语法将账本进行关联起来。比如,我的账本结目录构如下:

beancount
├── 2018
│   ├── 2018.bean
│   └── yuegangzhoudian.bean
├── 2019
│   ├── 2019.bean
│   ├── 0-default
│   │   ├── 00.bean
│   │   ├── 07-expenses.bean
│   │   ├── 08-expenses.bean
│   │   ├── 09-expenses.bean
│   │   ├── 10-expenses.bean
│   │   ├── event.bean
│   │   ├── income.bean
│   │   └── transfer.bean
│   ├── 1-securities
│   │   ├── 00.bean
│   ├── 2-trip
│   │   ├── 00.bean
│   │   ├── 20190708-beijing.bean
│   │   ├── 20190720-yiwu.bean
│   ├── 3-cycle
│   │   ├── 00.bean
│   │   ├── bankcard.bean
│   │   ├── creditcard.bean
│   │   ├── cycle-expenses.bean
│   │   └── loans.bean
│   ├── 4-project
│   │   ├── 00.bean
│   └── 5-doc
│       ├── 2019-note.md
│       ├── creditcard-bill
│       └── note.xlsx
├── accounts
│   ├── assets.bean
│   ├── equity.bean
│   ├── expenses.bean
│   ├── income.bean
│   └── liabilities.bean
|── main.bean

最底下的 main.bean 是我的主账本(查账执行 fava main.bean)。该文件的内容如下:

;==main文件==
;【一、账本设置】
option "title" "我的账本"
option "operating_currency" "CNY" 
1990-01-01 custom "fava-option" "language" "zh" 

;【二、账户设置】
include "accounts/assets.bean"  ;资产账户设置及初始化
include "accounts/liabilities.bean"  ;负债账户设置及初始化
include "accounts/expenses.bean"  ;支出账户设置
include "accounts/income.bean"  ;收入账户设置
include "accounts/equity.bean"  ;权益账户设置

;【三、交易记录】
include "2018/2018.bean" ;历史账本合集
include "2019/2019.bean" ;2019年账本合集

我使用 include 导入了 7 个文件,其中 5 个账户设置文件,2 个年度账本。

我 2019 年年度账本如下:

;==2019.bean文件==
;2019年每个账本文件的描述
include "0-default/00.bean" ;默认目录,每月日常支出,收入,转账等
include "1-securities/00.bean" ;证券投资目录
include "2-trip/00.bean" ;旅行&出差目录
include "3-cycle/00.bean" ;周期性费用/交易目录
include "4-project/00.bean" ;项目目录

2019.bean 文件仍然没有交易记录,而是继续导入其它账本。

概括起来,我的账本结构分三层:

最 1 层:main.bean 作为主账本,include 各个账户文件及每年账本文件。
第 2 层:每年有个目录,下设年份.bean 的文件,include 各个子目录下 00.bean 文件。
第 3 层:每个子目录下 00.bean 文件 include 该目录下所有正式的记账文件。

年份目录下各子目录功能如下:

  • 默认目录(0-default):日常支出,每月一个文件,也包括当年的收入、转账、event 事件。
  • 证券投资(1-securities):股票和基金买卖记录。
  • 旅行出差(2-trip):旅行出差的账本,命名日期-地点.bean
  • 周期性账(3-cycle):包括每月信用卡还款、水电费、车贷房贷等。
  • 项目目录(4-project):比如装修房子

这样设计优点:

  • 按年组织,往年数据直接存档,不会被破坏
  • 年份目录下进行分类记录,避免放在一个文件不好维护

定期断言

前面介绍复式记账优点时提到:

这种方式能够保证记账准确无误。

在 Beancount 中通过 balance 实现账户核对。

假设在 10 月 17 日 24 点,尾号 1234 的银行卡余额为 5000 元,记录如下:

2019-10-18 balance Assets:Card:1234   5000.00 CNY

Beancount 会自动汇总 10 月 17 日(含)以前尾号 1234 的银行卡所有收支,如果历史交易记录无误,那么计算出来也应该是 5000 元。若不是 5000 元,则 Beancount 报错,表示历史交易记录有误。

细心的朋友会发现,10 月 17 日 24 点余额,balance 时使用 2019-10-18。

我一般每月对所有账户进行一次断言,频繁使用的账户月中会视情况穿插几次断言,避免错误。

错误 / 误差处理

如果断言报错,一般我会回溯,因为每月有断言的习惯,所以最多回溯一月数据即可。若无法回溯,则使用 Euqity:UFO 记录。

假如断言时发现 Assets:Card:1234 少了 200 元,则在断言前增加:

2019-10-18 * "" ""
    Assets:Card:1234    -200.00 CNY
    Equity:UFO

另一个错误场景是误差。假如存在四舍五入的误差,我用 Equity:Round 处理,比如:

2019-10-09 * "支付宝基金" "购买基金110011易方达中小盘混合"
    Assets:Bank:CMB:XXXX           -450.00 CNY
    Assets:Alipay:Fund               85.05 FD_110011 {5.2830 CNY}
    Expenses:Commission:AlipayFund    0.67 CNY
    Equity:Round

Equity 账户是个很特别的存在,可以处理边缘情况。我设置了四个 Equity 账户如下:

  • Equity:OpenBalance 用于初始化
  • Equity:HistoryIncome 开始复式记账(2019-07-01)前的部分收入
  • Equity:Round 四舍五入操作
  • Equity:UFO 无法追溯的差额

多币种及货币转化

账户可以根据实际情况设置多个货币。比如 Visa 信用卡美元消费以美元入账,则需要支持美元。

对于美元消费以人民币入账,可以使用 @@进行货币转化。比如,本博客的服务器账单如下,3.71 美元以 26.57 入账。

2019-10-04 * "Amazon" "付lightsail服务器月账单"
    Liabilities:CreditCard:SPDB:XXXX          -26.57 CNY
    Expenses:Digital:Software                   3.71 USD @@ 26.57 CNY

在 option 中设置账本货币时,通常只需要一种主货币,Fava 显示时,非主货币都会放进「其它」那一列,如图 1。

标签

一个问题:约会时的用餐,该记在哪个账户?

可以记在 Expenses:Food 饮食支出账户,也可以记在 Expenses:Date:Food 约会分类下饮食。如果使用后者,Food 类账户越来越多,不便于维护。

此时,我一般使用前者,并借助 Beancount 提供的标签功能。如下:

2019-10-18 * "" "" #Date
    Assets:Card:1234        -200.00 CNY
    Expenses:Food

其中#Date 是标签,在 Fava 上可以筛选标签,查看该标签下各个账户的收支。

再比如,每次旅游,我会给旅游中所有的花费打上类似#20191001-hangzhou 的标签,带有日期和目的地。通过筛选标签,轻易的查看旅游中的吃、住、行、玩等等花销。

旅行的交易通常很多,一条条添加标签比较繁琐,Beancount 支持使用 pushtag 和 poptag 给多笔交易加上标签。

pushtag #20191001-hangzhou

2019-10-18 * "" "" 
    Assets:Card:1234        -200.00 CNY
    Expenses:Food:Dinner
    
2019-10-18 * "" "" 
    Assets:Card:1234         -20.00 CNY
    Expenses:Traffic:Taxi

poptag #20191001-hangzhou

这样,在 pushtag 和 poptag 之间的所有交易,都会带上#20191001-hangzhou 标签.

事件

生活中可能有些事件希望被记录,这已经和记账没多大关系了,不过 Beancount 也支持,格式:

日期 event "事件分类" "事件详情"

比如我的事件举例:

;beancount事件
2019-06-30 event "beancount" "启用beancount"

;工作事件
2019-08-30 event "work" "神马lastday"
2019-09-02 event "work" "今天开始在AE上班"

;旅行或出差记录location事件
2019-09-13 event "location" "杭州->苏州:去程"
2019-09-15 event "location" "苏州->杭州:回程"

然后 Fava 界面有事件查看界面。

四、结语

没想到写了这么多,断断续续也写了多天,有点意犹未尽。

学习过程中,除了 byvoid 的文章,还参考了 wzyboy 和 Beancount —— 命令行复式簿记官方文档

能用这种方式去记账,很大程度上利益于移动支付的普及。熟练之后,每天晚上对照支付宝和微信账单,3 分钟就能记完。

当然,这仍然不是最好的方式,wzyboy 已经实现了 import,每月花一两个小时处理一次即可。我觉得 import 的方式得到的信息不够丰富和详细,所以没有深入研究。

Beancount 还有些高级应用,比如记录证券投资,使用 BQL 语句统计分析。各种满足工具控的折腾欲。

https://www.liaoxuefeng.com/wiki/1016959663602400

廖雪峰的官方网站

这是小白的Python新手教程,具有如下特点:

中文,免费,零起点,完整示例,基于最新的Python 3版本。

Python是一种计算机程序设计语言。你可能已经听说过很多种流行的编程语言,比如非常难学的C语言,非常流行的Java语言,适合初学者的Basic语言,适合网页编程的JavaScript语言等等。

那Python是一种什么语言?

首先,我们普及一下编程语言的基础知识。用任何编程语言来开发程序,都是为了让计算机干活,比如下载一个MP3,编写一个文档等等,而计算机干活的CPU只认识机器指令,所以,尽管不同的编程语言差异极大,最后都得“翻译”成CPU可以执行的机器指令。而不同的编程语言,干同一个活,编写的代码量,差距也很大。

比如,完成同一个任务,C语言要写1000行代码,Java只需要写100行,而Python可能只要20行。

所以Python是一种相当高级的语言。

你也许会问,代码少还不好?代码少的代价是运行速度慢,C程序运行1秒钟,Java程序可能需要2秒,而Python程序可能就需要10秒。

那是不是越低级的程序越难学,越高级的程序越简单?表面上来说,是的,但是,在非常高的抽象计算中,高级的Python程序设计也是非常难学的,所以,高级程序语言不等于简单。

但是,对于初学者和完成普通任务,Python语言是非常简单易用的。连Google都在大规模使用Python,你就不用担心学了会没用。

用Python可以做什么?可以做日常任务,比如自动备份你的MP3;可以做网站,很多著名的网站包括YouTube就是Python写的;可以做网络游戏的后台,很多在线游戏的后台都是Python开发的。总之就是能干很多很多事啦。

Python当然也有不能干的事情,比如写操作系统,这个只能用C语言写;写手机应用,只能用Swift/Objective-C(针对iPhone)和Java(针对Android);写3D游戏,最好用C或C++。

如果你是小白用户,满足以下条件:

  • 会使用电脑,但从来没写过程序;
  • 还记得初中数学学的方程式和一点点代数知识;
  • 想从编程小白变成专业的软件架构师;
  • 每天能抽出半个小时学习。

https://byvoid.com/zhs/series/beancount%E5%A4%8D%E5%BC%8F%E8%AE%B0%E8%B4%A6/

Beancount复式记账(一):为什么

本文简化字版由OpenCC转换

我在在Google的这四年(三)这篇文章中提到了一个「秘密武器」Beancount以及「复式记账」的概念。我说过我要专门写一篇文章来分享我的经验,这篇文章就是我承诺的兑现了。

如何实现财务自由

这个章节标题有点吸引人眼球,像是成功学教程。但是我保证我下面说的内容都是严肃,而且有明确定义和方法的。

为什么要记账,我来用一句话总结就是为了提升对自我的认识。记账是个人理财的基础,更是通往财务自由的必经之路。财务自由这件事情固然是根本需要赚钱来实现,但并不是只有一夜暴富才能实现的。财务自由其实并不是一件虚无缥缈的事情,而是每个人都可以努力达到的一种状态,换句话说就是「退休」,只不是有人能在三十多岁退休,有人要六十岁,有人则要到八十岁。

财务自由的一般定义是由资产产生的收入不少于生活开销。如果不知道自己有多少开销,甚至不知道自己有多少资产、收入,即便是一夜暴富,财务自由是一件不可能的事情。

接下来我来解释为什么财务自由是可以实现的,而且应该是每个人的目标。首先需要强调的概念是**资产(Assets)净资产(Net Assets)**的区别,虽然我之后还会详细解释,但是这里需要记住,财务自由并非对个人净资产的要求,这也就是为什么不需要暴富就能实现。像所有投资都有期限一样,个人的寿命也是有期限的。每当我想到我总有一天会死,都会觉得有些忧伤,但这是无法改变的自然规律。只要在预期的寿命之内资产(而不是净资产)产生现金流能够满足生活所需的开销,这就是财务自由。我对资产产生现金流的定义较为宽泛,不仅包括利息、分红、租金、版税这类收入,还包括了直接通过资产折现的收入,即净资产减少。

要想达到财务自由,需要三点要求:对支出的预期、对资产和收入的了解、对寿命的期望。这三点都是说起来容易,做起来难的,但是记账可以帮助你很大程度上解决至少前两个问题。至于第三个问题,才是财务自由的根本困难所在,不过这个问题的解决方案已经超出了本文探讨的内容了。

为什么要复式记账

记账这件事是那种容易让人因为一时冲动开始,但是很快就放弃的事情。每个记过账人都有不同的原因和契机,但能坚持下来的则凤毛麟角。记账的好处是提升对自己的了解,解决那种不知道自己赚的钱都到哪里去了的问题。但记账的困难也显而易见,那就是麻烦,还容易遗漏、错误。一般来说普通人对记账的理解就是每笔消费都干什么了,所以就是每一笔消费都有一个金额和类别就可以了。这种记账方式一开始简单,但能带来的价值有限,只是开支记录而已,长期看来难以说服自己为了这些价值而忍受麻烦的记账过程。

相比普通的流水账,复式记账的核心理念是账户之间的进出关系,要求所有的记录全部入账,它可以保证账目的完整性和一致性。复式记账可以提供除了开支记录之外的损益表、资产负债表、现金流量表、试算平衡表等报表。复式记账还可以把投资和消费轻易区分,譬如购入电脑、手机,可以作为资产项目入账并定期折旧。同理对各种代金券、点数积分的购入一样要算入资产而不是消费。

虽然有各种各样的手机应用号称可以简化记账,它们把用户交互做得更加简单友善,有的还可以从银行账户、信用卡公司直接抓取数据,降低心理障碍,但与此同时它们却带来了另一个问题,就是数据所有权、安全性和持久性的疑虑。这些工具大多都是把数据存储在云端的,泄漏隐私的可能性不言而喻。更麻烦的是,它们几乎都是自己的专有格式,无法导出保存,或者哪怕可以导出也难以使用。这也意味着你不得不一直用一个产品,直到有一天倒闭或者服务关闭为止。这对我来说是不可接受的,因为在互联网时代能活过十年的产品非常稀少,无论是像Google这样随意关闭服务的大公司,还是随时有可能倒闭的小公司。复式记账的价值在于数据的完整性,我对其数据寿命的要求是二十年以上。

复式记账是一种划时代的发明,这个发明被认为是源于中世纪的地中海城邦(意大利或者埃及犹太人)。复式记账技术成为中世纪至大航海时代复杂贸易的支柱性工具,使得复杂的合约、信贷成为可能。后世会计学、金融学的许多概念都来自于复式记账,可以说体系化的资本主义是从复式记账的实践中诞生的。

如何复式记账

网上对于复式记账的文章也是汗牛充栋了,但各种晦涩难懂的名词可能会吓跑人。我不打算在这里介绍什么是复式记账,因为很难一句话说清楚,说得复杂了又没有意义。其实你并不需要懂很多才能开始复式记账,只需要一些最基本的概念就可以了。这个概念就是会计恒等式。

会计恒等式

作为会计学的核心,复式记账是一种实践中诞生的技术。除了最基本的会计恒等式之外,复式记账只有规范,没有对错。那么什么是会计恒等式呢?最基本的形式就是:

资产 = 负债 + 权益(净资产)

这个等式对于没有接触过会计学的人来说可能不太容易理解,但是记账实践过马上会明白的。理解的难点在于,会计学上的「资产(Assets)」和一般人对一个人富有程度的理解不太一样,因为资产是负债和权益的总和,负债(Liabilities)也是资产的一部分。

更容易理解的部分其实是权益(Equity),在个人理财的上下文中,又和净资产(Net Assets)是等价的,也就是经常说的「个人净值」。净资产需要由总资产排除负债,一个人到底是否富有,看的是净资产,而不是资产

举例说明,一个人首付20万美元,贷款80万美元,买了价格为100万美元的房产。假设这个人没有别的资产和负债,那么他的资产就是100万美元,负债是80万美元,而净资产是20万美元。在房价没有变化的情况下,买房对一个人的净资产没有什么影响(忽略手续费的话),而他的资产和负债都暴增,也就是所谓的「扩大资产负债表」。

如果你理解了会计恒等式,那么复式记账的一切知识障碍已经扫除了。

使用Beancount

再说一遍,复式记账在于实践,在开始实践之前学很多艰深的会计学概念没有任何意义。要开始实践,就要有工具上手了。在众多工具中我推荐使用Beancount,原因如下:

  1. Beancount是一个开源工具,用Python实现的,可以本地运行。
  2. 账本是一套基于文本的语法,方便存储和管理,个人拥有全部的数据,还可以使用Git管理。
  3. 账本的语法很规范,也具备灵活度,像编程语言一样可以嵌套引入,也有语法高亮和代码检查工具。
  4. 有完整的命令行工具链和可视化工具fava,还有基于SQL的查询和报表生成。
  5. 没有预先定义的类别、货币等现实世界概念,可以轻松实现多币种记账,包括各种点数、虚拟货币。

除了Beancount,Plain Text Accounting网站还列出了其他的开源工具比较,类似的具有竞争力的开源工具还有Ledger和hledger,其中Ledger是这类工具的开创者。无论使用里面介绍的哪个工具,其基本理念都差不多,即记录账户之间的资金流动。最重要的是,你是账本的所有者,如果不喜欢一个工具了,可以轻易转换到另一个工具。虽然它们语法有些许区别,但写一个脚本来转换不难。

使用Beancount之前首先需要Python 3运行环境。Beancount可以轻易从PyPI获得,使用以下命令:

pip install beancount fava

上面的命令其中beancount是核心包,包括了命令行工具,fava是网页可视化工具。这里是一个示例账本:链接。示例文件可以在Beancount的Bitbucket上下载

下载之后可以在命令行中运行:

fava example.beancount

即可看到:

Running Fava on http://localhost:5000

Beancount

至此为止Beancount环境就已经设置好了。

这篇文章到此为止,下一篇我会介绍Beancount具体的使用方法和操作经验。同时欢迎加入Beancount中文讨论:t.me/beancount_zh