├── .github └── screenshot │ └── report-demo.png ├── .gitignore ├── .python-version ├── Makefile ├── README.md ├── importers └── spdccc_importer.py ├── ledger ├── account.beancount ├── commodity.beancount ├── cryptocoin.beancount ├── daily │ ├── 2019 │ │ ├── 2019-06-03-other.beancount │ │ ├── 2019-06-03-settle.beancount │ │ └── 2019-06-03-spdbcc.beancount │ └── 2019.beancount ├── init.beancount ├── invest.beancount ├── latest-prices.beancount ├── main.beancount ├── prices.beancount └── salary.beancount ├── raw-data └── spdbcc │ └── 2019-05-spdbcc.csv ├── requirements.in ├── requirements.txt ├── scripts ├── README.md ├── generate-networth-report.py ├── generate-portfolio.py └── update-prices.py └── sources ├── eastmoney.py ├── exchangeratesapi.py └── xueqiu.py /.github/screenshot/report-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mckelvin/beancount-boilerplate-cn/f250b25c2a05b6cf922b59a598ffb4fda4dead8d/.github/screenshot/report-demo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | .py[oc] 3 | ledger/latest-prices.beancount 4 | local/ 5 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | beancount-env 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GDOCIDFILE=local/gdocid 2 | GDOCID=`cat $(GDOCIDFILE)` 3 | TODAY=`date "+%Y-%m-%d"` 4 | MONTH=`date "+%Y-%m"` 5 | DIARY_MD_PATH=`date "+raw-data/.notable/notes/diary/%Y/%m/%Y-%m-%d.md"` 6 | PFCSV="local/portfolio-$(TODAY).csv" 7 | NWCSV="local/net-worth-$(TODAY).csv" 8 | 9 | 10 | all: prices 11 | 12 | prices-today: 13 | ./scripts/update-prices.py --today-only | sh 14 | 15 | prices: 16 | ./scripts/update-prices.py | sh 17 | 18 | fava: 19 | fava ledger/main.beancount 20 | 21 | %: 22 | bean-$@ ledger/main.beancount 23 | 24 | portfolio: 25 | @./scripts/generate-portfolio.py 26 | 27 | networth: 28 | @./scripts/generate-networth-report.py 29 | 30 | gdocid: 31 | @test -f $(GDOCIDFILE) && true || (echo "Please fill gdoc id in $(GDOCIDFILE)"; exit 1) 32 | 33 | spreadsheet: gdocid 34 | @echo "Generating $(PFCSV) ..." 35 | @./scripts/generate-portfolio.py > $(PFCSV) 36 | @echo "$(PFCSV) Done\n" 37 | @echo "Generating $(NWCSV) ..." 38 | @./scripts/generate-networth-report.py --padding --since 2019-10-01 > $(NWCSV) 39 | @echo "$(NWCSV) Done\n" 40 | @egrep "^(日期|$(MONTH))" $(NWCSV) | column -ts, 41 | @echo "Uploading to spreadsheet ..." 42 | @upload-to-sheets --docid=$(GDOCID) $(PFCSV):持仓 $(NWCSV):净值 43 | 44 | today: 45 | make prices-today 46 | make spreadsheet 47 | 48 | backup: 49 | git stash 50 | git pull --rebase origin master 51 | git stash pop 52 | git add . 53 | git commit -m "AutoBackup" 54 | git push origin master 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # beancount-boilerplate-cn 2 | 3 | 本项目是为如下人士准备的 (AND) 4 | 5 | - 生活、工作在中国 6 | - 熟练使用 \*nix 环境下终端操作 7 | - 对[文本记账](https://plaintextaccounting.org/)感兴趣 8 | - 看过了最基础的 beancount 介绍但是没有看完所有文档(太多了) 9 | - 想马上试试用 beancount 记账 10 | 11 | ### 安装 12 | 13 | 1. Python 3.6.5 # Python3 就行 14 | 如果你已经安装了 [pyenv-virtualenv](https://github.com/pyenv/pyenv-virtualenv), 那需要一个基于 Python 3.6.5 的虚拟环境,否则请自行准备 Python 3.6.5 运行环境: 15 | 16 | ```shell 17 | pyenv virtualenv 3.6.5 beancount-env 18 | ``` 19 | 20 | 2. 安装依赖的库 21 | 22 | ```shell 23 | pip install -r requirements.txt 24 | ``` 25 | 26 | ### 这份模板中的假定使用者是这样的: 27 | 28 | - 有如下银行的储蓄卡 29 | - 招商银行 (工资卡) 30 | - 招商银行香港分行 31 | - 有如下银行的全币种信用卡 32 | - 浦发银行 (日常消费主力卡, 外币交易自动购汇人民币还款) 33 | - 工商银行 (境外消费为主,外币交易不自动购汇,需要自行购汇还外币) 34 | - 有如下投资(机)账户 35 | - 富途证券港美股账户 36 | - 东方财富证券A股账户 37 | - 蚂蚁财富(支付宝)基金账户 38 | - 天天基金账户 39 | 40 | ### 如何查看报表 41 | 42 | 1. 在 https://docs.google.com/spreadsheets 新建一个表格,复制URL中的ID部分到`local/gdocid` 这个文件中。 43 | 44 | ```bash 45 | # 比如 https://docs.google.com/spreadsheets/d/1jnds3X_-RSTN4ATuOFV-v6tar0KfAla88vC43Vq6ubc/edit?usp=sharing 46 | $ cat local/gdocid 47 | 1jnds3X_-RSTN4ATuOFV-v6tar0KfAla88vC43Vq6ubc 48 | ``` 49 | 50 | 2. `make prices` 确认标的价格是最新的 51 | 3. `make spreadsheets` 生成净值和持仓csv文件,并上传到上述新建的spreadsheet. 期间可能会提示配置 Google Docs 的 API Key, 按提示进行即可。 52 | 4. `净值`和`持仓`分别会出现在2个sheet里,如果需要报表的话,可以基于这些数据新建一个sheet放报表。举个例子: 53 | 54 | [![](https://raw.githubusercontent.com/mckelvin/beancount-boilerplate-cn/dev/.github/screenshot/report-demo.png)](https://docs.google.com/spreadsheets/d/1jnds3X_-RSTN4ATuOFV-v6tar0KfAla88vC43Vq6ubc/edit#gid=0) 55 | 56 | ### 如何初始化/已有帐本迁移过来 57 | 58 | 可见 `ledger/init.beancount` 文件中初始化的过程 59 | 60 | ### Commodity 命名 61 | 62 | 每一个投资标的就是一个commodity, 比如现金 CNY, USD, 股票 SPY, AAPL ... 63 | 64 | - 各国外汇按标准名称来: 65 | - `CNY`, `USD`, `CNH`, ... 66 | - 美股代号按交易所代码来 67 | - `AAPL`, `GOOGL` 68 | - 港股代号以 `HK_` 开头按 4 位数字命名 69 | - `HK_2800`, `HK_0700` 70 | - A股(含场内基金)代号以 `CN_` 开头按 6 位数字命名 71 | - `CN_000001`, `CN_510300`, ... 72 | - 场外基金(货币基金除外)以 `CN_F` 开头按 6 位数字命名 73 | - `CN_F110011` 74 | - 货币基金直接用 CNY 表示,收益需要手工更新(原因是货币基金无净值的概念)。 75 | - CNY 76 | 77 | ### 你可能想批量替换的关键字 78 | - 帐本名称 `YourLedger` 79 | - 所在城市 `YourCity` 80 | - 供职单位 `YourEmployer` 81 | - `XXX` 82 | - `TODO` 83 | 84 | ### 文件布局 85 | 86 | - importers/ : 通过 bean-extract 自动导入帐单所需的自定义脚本 87 | - raw-data/ : csv 等格式的原始帐单存放目录 88 | - sources/ : 自定义的股票、基金行情脚本(默认只支持从雅虎财经中获取) 89 | - ledger/ : 所有的 beancount 文件都在这里面 90 | - main.beancount : 主入口 91 | - account.beancount : 所有的账户定义在这里 92 | - commodity.beancount : 如果你不需要自动获取行情价格,你甚至都不需要这个文件 93 | - prices.beancount : 这个文件由 bean-price 命令管理,一般不需要手动修改 94 | - init.beancount : 初始化帐本用 95 | - salary.beancount : 工资收入 96 | - invest.beancount : 现金之外的投资(机)相关记录(买卖、申赎) 97 | - daily/ : 日常流水 98 | - daily/2019 : 按年分目录 99 | - daily/2019/2019-07-03-spdbccc.beancount : 主力消费卡记录(自动生成+人工修改) 100 | - daily/2019/2019-07-03-other.beancount : 非主力下记录 101 | - daily/2019/2019-07-03-settle.beancount : 定期结算检查点 102 | 103 | ### 支出如何分类 104 | 105 | - 参照随手记App的支出分类法, 分为2级, 见 ledger/account.beancount 106 | 中账户的定义。 107 | 108 | ### 如何处理五险一金 109 | 110 | - 公司缴纳的公积金作为收入计算 111 | - 公司缴纳的各种险不入帐 112 | - 个人缴纳的各种税和险作为支出计算 113 | - 个人缴纳的公积金当作转帐计算 114 | - 详见 salary.beancount 115 | 116 | ### 如何处理股票、基金 117 | 118 | - 因为交易频率不高,因此数据都是手工输入 119 | - 可参见 invest.beancount 中的定义 120 | 121 | ### 如何获取外汇、股票、基金行情 122 | 123 | - ledger/commodity.beancount 中定义了某一个 commodity 从哪里获取行情 124 | - 自定义行情脚本定义在 sources/ , 目前支持从雪球获取A/港/美股股票行情,从天天基金获取基金行情 125 | - 货币基金当现金处理 126 | 127 | ### 如何获取标的的最新价格 128 | 129 | ``` 130 | make prices 131 | # 或 132 | ./scripts/update-prices.py | sh 133 | ``` 134 | 135 | 这个命令会自动拉取最后一条 price 记录的时间到昨天为止的各个持仓标的的价格。建议隔三差五就跑一次。 136 | 137 | ### 如何自动生成帐单 138 | 139 | 基于2019年5月的帐单(csv格式)生成 beancount 文件 140 | 141 | ``` 142 | bean-extract importers/spdccc_importer.py raw-data/spdbcc/2019-05-spdbcc.csv > ledger/daily/2019/2019-06-03-spdbcc.beancount 143 | ``` 144 | 145 | ### 如何加密帐单信息 146 | 147 | 当前仓库默认没有加密,如果你想加密的话,推荐使用 [git-crypt](https://github.com/AGWA/git-crypt), 它比 [git-secret](https://git-secret.io/) 使用起来更加方便。 148 | 149 | ### 如何查看当前和历史持仓 150 | 151 | ``` 152 | ./scripts/generate-portfolio.py -d 2019-12-31 # 查看某一日的持仓 153 | ./scripts/generate-portfolio.py # 查看当前持仓 154 | ``` 155 | 156 | 输出的csv格式持仓可以[导入表格软件](https://bitbucket.org/blais/beancount/src/default/beancount/tools/sheets_upload.py)进一步分析,或者使用 [tabview](https://pypi.org/project/tabview/) 直接在终端浏览。例子: 157 | 158 | |一级类别|二级类别 |标的 |代号 |持仓量 |市场价格 |报价日期 |市场价值 |货币 |人民币价值 |占比 | 159 | |----|------|----------------|----------|----------|---------|----------|------|----|---------|------| 160 | |现金 |本币 |人民币 |CNY |59287.860 |1.0000 | |59288 |CNY |59287.86 |10.68%| 161 | |现金 |外汇 |美元 |USD |19460.700 |1.0000 | |19461 |USD |138338.94|24.92%| 162 | |现金 |外汇 |港元 |HKD |134112.000|1.0000 | |134112|HKD |123000.76|22.16%| 163 | |现金 |外汇 |离岸人民币 |CNH |1000.000 |1.0000 | |1000 |CNH |999.20 |0.18% | 164 | |债权 |债券基金 |华泰柏瑞丰盛纯债债券A |CN_F000187|6000.000 |1.2902 |2020-05-15|7741 |CNY |7741.20 |1.39% | 165 | |债权 |可转换债券 |光电转债 |CN_128047 |100.000 |129.0270 |2019-10-15|12903 |CNY |12902.70 |2.32% | 166 | |股权 |指数基金 |SPDR S&P 500 ETF|SPY |15.000 |286.2800 |2020-05-15|4294 |USD |30525.88 |5.50% | 167 | |股权 |指数基金 |华泰沪深300ETF |CN_510300 |100.000 |3.9020 |2020-05-15|390 |CNY |390.20 |0.07% | 168 | |股权 |偏股混合基金|易方达中小盘混合 |CN_F110011|10000.000 |5.2343 |2020-05-15|52343 |CNY |52343.00 |9.43% | 169 | |股权 |A股 |平安银行 |CN_000001 |100.000 |13.2300 |2020-05-15|1323 |CNY |1323.00 |0.24% | 170 | |股权 |港股 |腾讯控股 |HK_0700 |50.000 |421.2000 |2020-05-15|21060 |HKD |19315.17 |3.48% | 171 | |股权 |港股 |盈富基金 |HK_2800 |100.000 |24.0000 |2020-05-15|2400 |HKD |2201.16 |0.40% | 172 | |股权 |美股 |Apple Inc. |AAPL |10.000 |307.7100 |2020-05-15|3077 |USD |21873.97 |3.94% | 173 | |另类 |加密货币 |USDT |USDT |776.340 |1.0000 | |776 |USDT|5395.56 |0.97% | 174 | |另类 |加密货币 |比特币 |BTC |1.175 |9336.9561|2020-05-15|10967 |USD |77958.38 |14.04%| 175 | |另类 |加密货币 |以太坊 |ETH |1.000 |223.8500 |2019-07-16|224 |USDT|1555.76 |0.28% | 176 | 177 | ### 如何查看投资净值 178 | 179 | ``` 180 | ./scripts/generate-networth-report.py # 查看年初至今的净值 181 | ./scripts/generate-networth-report.py -s 2019-12-01 # 查看某一日至今的净值 182 | ``` 183 | 184 | 输出的csv格式持仓可以[导入表格软件](https://bitbucket.org/blais/beancount/src/default/beancount/tools/sheets_upload.py)进一步分析,或者使用 [tabview](https://pypi.org/project/tabview/) 直接在终端浏览。例子: 185 | 186 | |日期 |净资产 |可投资金额 |非投资收入 |非投资支出 |投资盈亏 |投资盈亏% |累计净值 |累计盈亏|当年净值 |当年盈亏 |另类% |股权% |债权% |现金% | 187 | |---|------|----------------|----------|----------|---------|----------|------|----|---------|------|------|------|-----|------| 188 | |2020-01-01|559138.53|531175.73 |0.00 |0.00 |n/a |n/a |1.0000|0.00|1.0000 |0.00 |13.20%|23.54%|3.84%|59.43%| 189 | |2020-01-02|558641.52|530678.72 |0.00 |0.00 |-497.01 |-0.0936% |0.9991|-497.01|0.9991 |-497.01|12.94%|23.73%|3.84%|59.49%| 190 | |2020-01-03|560789.32|532826.52 |0.00 |0.00 |2147.79 |0.4047% |1.0031|1650.79|1.0031 |1650.79|13.37%|23.47%|3.82%|59.33%| 191 | |2020-01-06|562714.79|534751.99 |0.00 |0.00 |1925.47 |0.3614% |1.0067|3576.26|1.0067 |3576.26|13.87%|23.17%|3.81%|59.15%| 192 | |2020-01-07|564344.69|536381.89 |0.00 |0.00 |1629.91 |0.3048% |1.0098|5206.16|1.0098 |5206.16|14.30%|23.14%|3.80%|58.76%| 193 | |2020-01-08|563946.82|535984.02 |0.00 |0.00 |-397.87 |-0.0742% |1.0091|4808.29|1.0091 |4808.29|14.20%|23.19%|3.80%|58.81%| 194 | |2020-01-09|563990.03|536027.23 |0.00 |0.00 |43.20 |0.0081% |1.0091|4851.49|1.0091 |4851.49|13.92%|23.54%|3.80%|58.74%| 195 | |2020-01-10|566470.03|538507.23 |0.00 |0.00 |2480.00 |0.4627% |1.0138|7331.50|1.0138 |7331.50|14.21%|23.59%|3.79%|58.41%| 196 | |2020-01-13|566349.87|538387.07 |0.00 |0.00 |-120.16 |-0.0223% |1.0136|7211.34|1.0136 |7211.34|14.14%|23.87%|3.79%|58.20%| 197 | |2020-01-14|569554.58|541591.78 |0.00 |0.00 |3204.72 |0.5952% |1.0196|10416.05|1.0196 |10416.05|14.92%|23.52%|3.77%|57.80%| 198 | |2020-01-15|569552.86|541590.06 |0.00 |0.00 |-1.73 |-0.0003% |1.0196|10414.33|1.0196 |10414.33|14.89%|23.51%|3.77%|57.83%| 199 | |2020-01-16|569231.86|541269.06 |0.00 |0.00 |-320.99 |-0.0593% |1.0190|10093.33|1.0190 |10093.33|14.78%|23.64%|3.77%|57.80%| 200 | |2020-01-17|569877.74|541914.94 |0.00 |0.00 |645.88 |0.1193% |1.0202|10739.21|1.0202 |10739.21|14.99%|23.64%|3.77%|57.60%| 201 | |2020-01-20|568368.80|540406.00 |0.00 |0.00 |-1508.94 |-0.2784% |1.0174|9230.27|1.0174 |9230.27|14.70%|23.70%|3.78%|57.82%| 202 | |2020-01-21|569909.61|541946.81 |0.00 |0.00 |1540.81 |0.2851% |1.0203|10771.08|1.0203 |10771.08|14.83%|23.50%|3.77%|57.90%| 203 | |2020-01-22|569567.31|541604.51 |0.00 |0.00 |-342.30 |-0.0632% |1.0196|10428.78|1.0196 |10428.78|14.76%|23.55%|3.77%|57.92%| 204 | |2020-01-23|567857.58|539894.78 |0.00 |0.00 |-1709.73 |-0.3157% |1.0164|8719.05|1.0164 |8719.05|14.50%|23.40%|3.78%|58.32%| 205 | |2020-01-24|567857.58|539894.78 |0.00 |0.00 |0.00 |0.0000% |1.0164|8719.05|1.0164 |8719.05|14.50%|23.40%|3.78%|58.32%| 206 | |2020-01-27|567857.58|539894.78 |0.00 |0.00 |0.00 |0.0000% |1.0164|8719.05|1.0164 |8719.05|14.50%|23.40%|3.78%|58.32%| 207 | |2020-01-28|567857.58|539894.78 |0.00 |0.00 |0.00 |0.0000% |1.0164|8719.05|1.0164 |8719.05|14.50%|23.40%|3.78%|58.32%| 208 | |2020-01-29|567857.58|539894.78 |0.00 |0.00 |0.00 |0.0000% |1.0164|8719.05|1.0164 |8719.05|14.50%|23.40%|3.78%|58.32%| 209 | |2020-01-30|567857.58|539894.78 |0.00 |0.00 |0.00 |0.0000% |1.0164|8719.05|1.0164 |8719.05|14.50%|23.40%|3.78%|58.32%| 210 | |2020-01-31|567857.58|539894.78 |0.00 |0.00 |0.00 |0.0000% |1.0164|8719.05|1.0164 |8719.05|14.50%|23.40%|3.78%|58.32%| 211 | |2020-02-03|573898.42|545935.62 |0.00 |0.00 |6040.84 |1.1189% |1.0278|14759.89|1.0278 |14759.89|15.61%|22.36%|3.75%|58.27%| 212 | |2020-02-04|574237.05|546274.25 |0.00 |0.00 |338.63 |0.0620% |1.0284|15098.52|1.0284 |15098.52|15.42%|22.75%|3.75%|58.08%| 213 | |2020-02-05|577799.49|549836.69 |0.00 |0.00 |3562.44 |0.6521% |1.0351|18660.96|1.0351 |18660.96|15.83%|22.89%|3.73%|57.55%| 214 | |2020-02-06|580439.96|552477.16 |0.00 |0.00 |2640.47 |0.4802% |1.0401|21301.43|1.0401 |21301.43|15.89%|23.15%|3.71%|57.25%| 215 | |2020-02-07|580439.96|552477.16 |0.00 |0.00 |0.00 |0.0000% |1.0401|21301.43|1.0401 |21301.43|15.89%|23.15%|3.71%|57.25%| 216 | |2020-02-10|582205.63|554242.83 |0.00 |0.00 |1765.67 |0.3196% |1.0434|23067.10|1.0434 |23067.10|16.03%|23.12%|3.70%|57.16%| 217 | |2020-02-11|585175.01|557212.21 |0.00 |0.00 |2969.38 |0.5358% |1.0490|26036.48|1.0490 |26036.48|16.37%|23.16%|3.68%|56.79%| 218 | |2020-02-12|586760.25|558797.45 |0.00 |0.00 |1585.24 |0.2845% |1.0520|27621.72|1.0520 |27621.72|16.46%|23.27%|3.67%|56.60%| 219 | |2020-02-13|586547.84|558585.04 |0.00 |0.00 |-212.41 |-0.0380% |1.0516|27409.31|1.0516 |27409.31|16.35%|23.26%|3.67%|56.71%| 220 | |2020-02-14|587939.30|559976.50 |0.00 |0.00 |1391.45 |0.2491% |1.0542|28800.77|1.0542 |28800.77|16.44%|23.30%|3.67%|56.59%| 221 | |2020-02-17|583519.50|555556.70 |0.00 |0.00 |-4419.79 |-0.7893% |1.0459|24380.97|1.0459 |24380.97|15.78%|23.52%|3.70%|57.01%| 222 | |2020-02-18|586376.83|558414.03 |0.00 |0.00 |2857.33 |0.5143% |1.0513|27238.30|1.0513 |27238.30|16.30%|23.18%|3.68%|56.84%| 223 | |2020-02-19|586376.83|558414.03 |0.00 |0.00 |0.00 |0.0000% |1.0513|27238.30|1.0513 |27238.30|16.30%|23.18%|3.68%|56.84%| 224 | |2020-02-20|585097.66|557134.86 |0.00 |0.00 |-1279.16 |-0.2291% |1.0489|25959.13|1.0489 |25959.13|15.70%|23.53%|3.69%|57.08%| 225 | |2020-02-21|584653.91|556691.11 |0.00 |0.00 |-443.75 |-0.0796% |1.0480|25515.38|1.0480 |25515.38|15.83%|23.32%|3.69%|57.16%| 226 | |2020-02-24|581216.57|553253.77 |0.00 |0.00 |-3437.34 |-0.6175% |1.0416|22078.04|1.0416 |22078.04|15.89%|22.88%|3.72%|57.52%| 227 | |2020-02-25|576706.02|548743.22 |0.00 |0.00 |-4510.55 |-0.8153% |1.0331|17567.49|1.0331 |17567.49|15.59%|22.79%|3.75%|57.87%| 228 | |2020-02-26|572481.47|544518.67 |0.00 |0.00 |-4224.55 |-0.7699% |1.0251|13342.94|1.0251 |13342.94|15.04%|22.84%|3.78%|58.34%| 229 | |2020-02-27|569084.25|541121.45 |0.00 |0.00 |-3397.22 |-0.6239% |1.0187|9945.72|1.0187 |9945.72|15.07%|22.52%|3.80%|58.61%| 230 | |2020-02-28|564817.25|536854.45 |0.00 |0.00 |-4267.00 |-0.7885% |1.0107|5678.72|1.0107 |5678.72|15.01%|22.23%|3.83%|58.93%| 231 | |2020-03-02|569944.19|541981.39 |0.00 |0.00 |5126.95 |0.9550% |1.0203|10805.66|1.0203 |10805.66|15.09%|22.83%|3.80%|58.29%| 232 | |2020-03-03|569387.90|541425.10 |0.00 |0.00 |-556.29 |-0.1026% |1.0193|10249.37|1.0193 |10249.37|15.02%|22.74%|3.80%|58.44%| 233 | |2020-03-04|568808.58|540845.78 |0.00 |0.00 |-579.32 |-0.1070% |1.0182|9670.05|1.0182 |9670.05|14.90%|23.14%|3.81%|58.15%| 234 | |2020-03-05|572248.46|544285.66 |0.00 |0.00 |3439.87 |0.6360% |1.0247|13109.93|1.0247 |13109.93|15.24%|23.12%|3.78%|57.86%| 235 | |2020-03-06|569850.80|541888.00 |0.00 |0.00 |-2397.66 |-0.4405% |1.0202|10712.26|1.0202 |10712.26|15.35%|22.79%|3.80%|58.06%| 236 | |2020-03-09|556295.99|528333.19 |0.00 |0.00 |-13554.81|-2.5014% |0.9946|-2842.54|0.9946 |-2842.54|14.21%|22.20%|3.90%|59.70%| 237 | |2020-03-10|560642.04|532679.24 |0.00 |0.00 |4346.06 |0.8226% |1.0028|1503.51|1.0028 |1503.51|14.09%|22.77%|3.87%|59.28%| 238 | |2020-03-11|557566.89|529604.09 |0.00 |0.00 |-3075.15 |-0.5773% |0.9970|-1571.64|0.9970 |-1571.64|14.16%|22.37%|3.89%|59.58%| 239 | |2020-03-12|533753.00|505790.20 |0.00 |0.00 |-23813.90|-4.4965% |0.9522|-25385.54|0.9522 |-25385.54|10.85%|22.25%|4.07%|62.83%| 240 | |2020-03-13|540151.97|512189.17 |0.00 |0.00 |6398.98 |1.2651% |0.9643|-18986.56|0.9643 |-18986.56|11.50%|22.62%|4.02%|61.87%| 241 | |2020-03-16|529322.49|501359.69 |0.00 |0.00 |-10829.48|-2.1144% |0.9439|-29816.04|0.9439 |-29816.04|11.01%|21.42%|4.10%|63.46%| 242 | |2020-03-17|533129.86|505167.06 |0.00 |0.00 |3807.37 |0.7594% |0.9510|-26008.67|0.9510 |-26008.67|11.22%|21.73%|4.07%|62.98%| 243 | |2020-03-18|530493.16|502530.36 |0.00 |0.00 |-2636.70 |-0.5219% |0.9461|-28645.37|0.9461 |-28645.37|11.31%|21.18%|4.09%|63.42%| 244 | |2020-03-19|540071.63|512108.83 |0.00 |0.00 |9578.47 |1.9060% |0.9641|-19066.90|0.9641 |-19066.90|12.51%|20.67%|4.01%|62.81%| 245 | |2020-03-20|539069.72|511106.92 |0.00 |0.00 |-1001.91 |-0.1956% |0.9622|-20068.81|0.9622 |-20068.81|12.51%|20.69%|4.02%|62.78%| 246 | |2020-03-23|537334.85|509372.05 |0.00 |0.00 |-1734.87 |-0.3394% |0.9590|-21803.68|0.9590 |-21803.68|12.85%|20.17%|4.04%|62.95%| 247 | |2020-03-24|544602.16|516639.36 |0.00 |0.00 |7267.31 |1.4267% |0.9726|-14536.37|0.9726 |-14536.37|13.07%|21.05%|3.98%|61.89%| 248 | |2020-03-25|548926.96|520964.16 |0.00 |0.00 |4324.80 |0.8371% |0.9808|-10211.57|0.9808 |-10211.57|12.94%|21.46%|3.95%|61.65%| 249 | |2020-03-26|550531.93|522569.13 |0.00 |0.00 |1604.97 |0.3081% |0.9838|-8606.60|0.9838 |-8606.60|12.92%|21.85%|3.94%|61.29%| 250 | |2020-03-27|548513.85|520551.05 |0.00 |0.00 |-2018.09 |-0.3862% |0.9800|-10624.69|0.9800 |-10624.69|12.66%|21.69%|3.95%|61.69%| 251 | |2020-03-30|548359.00|520396.20 |0.00 |0.00 |-154.84 |-0.0297% |0.9797|-10779.53|0.9797 |-10779.53|12.62%|21.70%|3.96%|61.73%| 252 | |2020-03-31|549446.65|521483.85 |0.00 |0.00 |1087.65 |0.2090% |0.9818|-9691.88|0.9818 |-9691.88|12.60%|21.85%|3.95%|61.60%| 253 | |2020-04-01|548247.47|520284.67 |0.00 |0.00 |-1199.18 |-0.2300% |0.9795|-10891.06|0.9795 |-10891.06|12.86%|21.42%|3.96%|61.76%| 254 | |2020-04-02|548247.47|520284.67 |0.00 |0.00 |0.00 |0.0000% |0.9795|-10891.06|0.9795 |-10891.06|12.86%|21.42%|3.96%|61.76%| 255 | |2020-04-03|549544.74|521581.94 |0.00 |0.00 |1297.27 |0.2493% |0.9819|-9593.79|0.9819 |-9593.79|12.99%|21.53%|3.95%|61.53%| 256 | |2020-04-06|549544.74|521581.94 |0.00 |0.00 |0.00 |0.0000% |0.9819|-9593.79|0.9819 |-9593.79|12.99%|21.53%|3.95%|61.53%| 257 | |2020-04-07|555576.69|527613.89 |0.00 |0.00 |6031.95 |1.1565% |0.9933|-3561.84|0.9933 |-3561.84|13.39%|22.11%|3.91%|60.59%| 258 | |2020-04-08|558227.88|530265.08 |0.00 |0.00 |2651.19 |0.5025% |0.9983|-910.65|0.9983 |-910.65|13.55%|22.20%|3.89%|60.36%| 259 | |2020-04-09|558985.91|531023.11 |0.00 |0.00 |758.02 |0.1430% |0.9997|-152.62|0.9997 |-152.62|13.47%|22.42%|3.89%|60.21%| 260 | |2020-04-10|556044.40|528081.60 |0.00 |0.00 |-2941.50 |-0.5539% |0.9942|-3094.13|0.9942 |-3094.13|12.96%|22.57%|3.91%|60.55%| 261 | |2020-04-13|555767.48|527804.68 |0.00 |0.00 |-276.92 |-0.0524% |0.9937|-3371.05|0.9937 |-3371.05|12.94%|22.56%|3.91%|60.58%| 262 | |2020-04-14|558327.52|530364.72 |0.00 |0.00 |2560.03 |0.4850% |0.9985|-811.02|0.9985 |-811.02|12.88%|22.95%|3.89%|60.28%| 263 | |2020-04-15|556094.93|528132.13 |0.00 |0.00 |-2232.59 |-0.4210% |0.9943|-3043.61|0.9943 |-3043.61|12.67%|22.85%|3.91%|60.57%| 264 | |2020-04-16|560659.12|532696.32 |0.00 |0.00 |4564.20 |0.8642% |1.0029|1520.59|1.0029 |1520.59|13.20%|22.80%|3.88%|60.12%| 265 | |2020-04-17|561653.16|533690.36 |0.00 |0.00 |994.03 |0.1866% |1.0047|2514.63|1.0047 |2514.63|13.15%|22.95%|3.87%|60.03%| 266 | |2020-04-20|559297.52|531334.72 |0.00 |0.00 |-2355.64 |-0.4414% |1.0003|158.99|1.0003 |158.99|12.93%|22.89%|3.89%|60.29%| 267 | |2020-04-21|557604.40|529641.60 |0.00 |0.00 |-1693.12 |-0.3187% |0.9971|-1534.13|0.9971 |-1534.13|12.99%|22.47%|3.90%|60.63%| 268 | |2020-04-22|561186.28|533223.48 |0.00 |0.00 |3581.88 |0.6763% |1.0039|2047.75|1.0039 |2047.75|13.20%|22.78%|3.88%|60.14%| 269 | |2020-04-23|563852.05|535889.25 |0.00 |0.00 |2665.77 |0.4999% |1.0089|4713.52|1.0089 |4713.52|13.55%|22.78%|3.86%|59.82%| 270 | |2020-04-24|565331.28|537368.48 |0.00 |0.00 |1479.23 |0.2760% |1.0117|6192.75|1.0117 |6192.75|13.67%|22.83%|3.85%|59.65%| 271 | |2020-04-27|568194.44|540231.64 |0.00 |0.00 |2863.16 |0.5328% |1.0170|9055.91|1.0170 |9055.91|13.92%|22.89%|3.83%|59.36%| 272 | |2020-04-28|569105.92|541143.12 |0.00 |0.00 |911.48 |0.1687% |1.0188|9967.39|1.0188 |9967.39|13.91%|23.05%|3.82%|59.22%| 273 | |2020-04-29|577034.26|549071.46 |0.00 |0.00 |7928.35 |1.4651% |1.0337|17895.73|1.0337 |17895.73|14.99%|22.88%|3.77%|58.36%| 274 | |2020-04-30|574514.61|546551.81 |0.00 |0.00 |-2519.65 |-0.4589% |1.0289|15376.08|1.0289 |15376.08|14.83%|22.94%|3.79%|58.44%| 275 | |2020-05-01|574514.61|546551.81 |0.00 |0.00 |0.00 |0.0000% |1.0289|15376.08|1.0289 |15376.08|14.83%|22.94%|3.79%|58.44%| 276 | |2020-05-04|574514.61|546551.81 |0.00 |0.00 |0.00 |0.0000% |1.0289|15376.08|1.0289 |15376.08|14.83%|22.94%|3.79%|58.44%| 277 | |2020-05-05|574514.61|546551.81 |0.00 |0.00 |0.00 |0.0000% |1.0289|15376.08|1.0289 |15376.08|14.83%|22.94%|3.79%|58.44%| 278 | |2020-05-06|581227.30|553264.50 |0.00 |0.00 |6712.69 |1.2282% |1.0416|22088.77|1.0416 |22088.77|15.51%|22.69%|3.74%|58.06%| 279 | |2020-05-07|586322.79|558359.99 |0.00 |0.00 |5095.49 |0.9210% |1.0512|27184.26|1.0512 |27184.26|16.21%|22.62%|3.70%|57.46%| 280 | |2020-05-08|586633.56|558670.76 |0.00 |0.00 |310.76 |0.0557% |1.0518|27495.03|1.0518 |27495.03|16.05%|22.91%|3.70%|57.35%| 281 | |2020-05-11|579773.30|551810.50 |0.00 |0.00 |-6860.25 |-1.2280% |1.0388|20634.77|1.0388 |20634.77|14.68%|23.39%|3.74%|58.19%| 282 | |2020-05-12|580085.90|552123.10 |0.00 |0.00 |312.60 |0.0567% |1.0394|20947.37|1.0394 |20947.37|14.92%|23.24%|3.74%|58.10%| 283 | |2020-05-13|583816.75|555853.95 |0.00 |0.00 |3730.85 |0.6757% |1.0465|24678.22|1.0465 |24678.22|15.42%|23.13%|3.71%|57.74%| 284 | |2020-05-14|588018.88|560056.08 |0.00 |0.00 |4202.13 |0.7560% |1.0544|28880.35|1.0544 |28880.35|15.92%|23.01%|3.69%|57.39%| 285 | |2020-05-15|584580.07|556617.27 |0.00 |0.00 |-3438.81 |-0.6140% |1.0479|25441.54|1.0479 |25441.54|15.52%|22.99%|3.71%|57.78%| 286 | |2020-05-18|584580.07|556617.27 |0.00 |0.00 |0.00 |0.0000% |1.0479|25441.54|1.0479 |25441.54|15.52%|22.99%|3.71%|57.78%| 287 | |2020-05-19|584580.07|556617.27 |0.00 |0.00 |0.00 |0.0000% |1.0479|25441.54|1.0479 |25441.54|15.52%|22.99%|3.71%|57.78%| 288 | |2020-05-20|584580.07|556617.27 |0.00 |0.00 |0.00 |0.0000% |1.0479|25441.54|1.0479 |25441.54|15.52%|22.99%|3.71%|57.78%| 289 | |2020-05-21|584580.07|556617.27 |0.00 |0.00 |0.00 |0.0000% |1.0479|25441.54|1.0479 |25441.54|15.52%|22.99%|3.71%|57.78%| 290 | |2020-05-22|584580.07|556617.27 |0.00 |0.00 |0.00 |0.0000% |1.0479|25441.54|1.0479 |25441.54|15.52%|22.99%|3.71%|57.78%| 291 | -------------------------------------------------------------------------------- /importers/spdccc_importer.py: -------------------------------------------------------------------------------- 1 | from beancount.ingest.importers import csv 2 | from beancount.core.data import Posting 3 | 4 | 5 | 6 | COMPACT_CATE_DICT = { 7 | "滴滴,嘀嘀,天津舒行科技,CAB": "Transport:Taxi", 8 | "药房,医院,医药,DRUG": "Health:Drugs", 9 | "交通卡,地铁,摩拜": "Transport:Public", 10 | "铁路,上铁": "Leisure:Train", 11 | "联合网络,中国移动": "Comm:PhonePlan", 12 | "宽带,VPS,Hosting": "Comm:Internet", 13 | "顺丰": "Comm:Express", 14 | "全家,便利,果品,果业,生鲜,水果": "Food:FruitSnacks", 15 | "餐厅,包子铺,名吃,小吃,餐饮,面馆," 16 | "盒马,豆浆": "Food:Meals", 17 | "优衣库": "Clothes:Clothes", 18 | "发型,美发": "Health:HairCutting", 19 | "AVIATION,航空": "Leisure:Aviation", 20 | "日上": "Leisure:Souvenir", 21 | "Smart2Pay B.V.": "Leisure:Gaming", 22 | "京东,宜家": "Home:Groceries", 23 | "电力公司": "Home:Utilities", 24 | "Spotify": "Leisure:Media", 25 | } 26 | 27 | CATE_DICT = { 28 | kw: cate_vals 29 | for kws, cate_vals in COMPACT_CATE_DICT.items() 30 | for kw in kws.split(",") 31 | } 32 | 33 | 34 | def _get_category(narration, default_cate="TODO"): 35 | for kw in CATE_DICT: 36 | if kw in narration: 37 | return CATE_DICT[kw] 38 | 39 | return default_cate 40 | 41 | 42 | def categorizer(txn): 43 | assert len(txn.postings) == 1, txn 44 | post = txn.postings[0] 45 | if post.units.number < 0: 46 | # 支出 47 | category = _get_category(txn.narration) 48 | txn.postings.append(post._replace( 49 | account=f"Expenses:{category}", 50 | units=-post.units, 51 | )) 52 | elif post.units.number > 0: 53 | if "还款" in txn.narration: 54 | txn.postings.append(post._replace( 55 | account=f"Assets:CN:Saving:CMB:CNY", 56 | units=-post.units, 57 | )) 58 | else: 59 | extra_narration = " TODO" 60 | if "返现" in txn.narration: 61 | extra_narration = "" 62 | 63 | txn.postings.append(post._replace( 64 | account=post.account.replace("Liabilities", "Income"), 65 | units=-post.units, 66 | )) 67 | if extra_narration: 68 | txn = txn._replace(narration=txn.narration + extra_narration) 69 | return txn 70 | 71 | 72 | CONFIG = [ 73 | csv.Importer( 74 | { 75 | # 日期、金额等字段分别叫什么? 76 | csv.Col.DATE: '记账日期', 77 | csv.Col.AMOUNT_DEBIT: '交易金额', 78 | csv.Col.NARRATION1: '交易描述', 79 | csv.Col.LAST4: '卡号末四位' 80 | }, 81 | # CSV文件中有哪几列? 82 | regexps='卡号末四位,记账日期,交易金额,交易描述', 83 | account="Liabilities:CN:CreditCard:SPDB", 84 | currency='CNY', 85 | categorizer=categorizer, 86 | ) 87 | ] 88 | -------------------------------------------------------------------------------- /ledger/account.beancount: -------------------------------------------------------------------------------- 1 | ; 短期内不可用资产标记为 nondisposable:1 2 | ; 不可退款的预付款标记为 sunk:1 3 | 4 | * General Accounts 5 | 6 | 2000-01-01 open Equity:Openings-Balance CNY,USD,HKD,CNH,BTC,CN600519,AAPL,SPY,HK_2800,HK_0700,CN_510300,CN_000001,CN_128047,CN_F110011,CN_F000187,CN_510300 7 | 8 | 2000-01-01 open Expenses:Food:Meals 9 | name: "食品酒水/早中晚餐" 10 | 2000-01-01 open Expenses:Food:FruitSnacks 11 | name: "食品酒水/水果零食" 12 | 2000-01-01 open Expenses:Transport:Public 13 | name: "行车交通/公共交通" 14 | 2000-01-01 open Expenses:Transport:Taxi 15 | name: "休闲娱乐/打车租车" 16 | 2000-01-01 open Expenses:Leisure:Media 17 | name: "休闲娱乐/娱乐影音" 18 | 2000-01-01 open Expenses:Leisure:Study 19 | name: "休闲娱乐/学习进修" 20 | 2000-01-01 open Expenses:Leisure:Train 21 | name: "休闲娱乐/高铁旅行" 22 | 2000-01-01 open Expenses:Leisure:Aviation 23 | name: "休闲娱乐/飞机旅行" 24 | 2000-01-01 open Expenses:Leisure:Souvenir 25 | name: "休闲娱乐/旅行纪念品" 26 | 2000-01-01 open Expenses:Leisure:Gaming 27 | name: "休闲娱乐/游戏" 28 | 2000-01-01 open Expenses:Health:Drugs 29 | name: "医疗保健/药品费" 30 | 2000-01-01 open Expenses:Health:HairCutting 31 | name: "医疗保健/理发" 32 | 2000-01-01 open Expenses:Comm:PhonePlan 33 | name: "交流通讯/话费" 34 | 2000-01-01 open Expenses:Comm:Express 35 | name: "交流通讯/邮寄费" 36 | 2000-01-01 open Expenses:Comm:Internet 37 | name: "交流通讯/上网费" 38 | 2000-01-01 open Expenses:Comm:Registry 39 | name: "交流通讯/注册费" 40 | 2000-01-01 open Expenses:Clothes:Clothes 41 | name: "衣服饰品/衣服裤子" 42 | 2000-01-01 open Expenses:Home:Groceries 43 | name: "居家物业/生活用品" 44 | 2000-01-01 open Expenses:Home:Rental 45 | name: "居家物业/房租" 46 | 2000-01-01 open Assets:Receivables:HomeRental CNY 47 | name: "居家物业/房租押金" 48 | nondisposable: 1 49 | 2000-01-01 open Expenses:Home:Utilities 50 | name: "居家物业/水电煤费" 51 | 2000-01-01 open Expenses:Human:Family 52 | name: "人情往来/家人" 53 | 2000-01-01 open Expenses:Human:Friends 54 | name: "人情往来/朋友" 55 | 2000-01-01 open Expenses:Human:Donation 56 | name: "人情往来/捐赠" 57 | 58 | ; 五险一金 59 | ; 失业险、工伤险、生育险、医疗险、养老险、公积金 60 | ; (其中生育险已纳入医疗险) 61 | ; 62 | ; 63 | 1949-10-01 open Expenses:CN:YourCity:UnemploymentInsurance 64 | name: "个人缴纳的失业险" 65 | 1949-10-01 open Expenses:CN:YourCity:WorkInjuryInsurance 66 | name: "个人缴纳的工伤险" 67 | 1949-10-01 open Expenses:CN:YourCity:MedicalInsurance 68 | name: "个人缴纳的医疗保险(含生育险)" 69 | 1949-10-01 open Expenses:CN:YourCity:PensionInsurance 70 | name: "个人缴纳的养老金(只记账不坐实, 个人认为应该算支出)" 71 | 1949-10-01 open Assets:CN:YourCity:HousingProvidentFund 72 | name: "住房公积金账户(含个人和公司的基本和补充)" 73 | nondisposable: 1 74 | 1949-10-01 open Expenses:CN:YourCity:Tax 75 | name: "YourCity税" 76 | 77 | * Cash/Bank Accounts 78 | 79 | 2000-01-01 open Assets:Cash CNY,USD,JPY 80 | name: "现钞" 81 | 2000-01-01 open Assets:CN:Saving:Wechat CNY 82 | name: "微信钱包余额" 83 | 2000-01-01 open Assets:CN:Saving:Alipay CNY 84 | name: "支付宝余额" 85 | 2000-01-01 open Assets:CN:Saving:CMB CNY,USD,HKD 86 | name: "招商银行" 87 | 2000-01-01 open Income:CN:Saving:Interest CNY 88 | 89 | 2000-01-01 open Assets:CN:Saving:CCB CNY,USD,HKD 90 | name: "建设银行" 91 | 2000-01-01 open Assets:HK:Saving:CMB CNY,USD,HKD 92 | name: "招商银行香港分行" 93 | 94 | 95 | * Liabilities 96 | 97 | ; 同一家银行的多张信用卡合在一个账户结算 98 | ; 浦发银行全币种卡会在消费时候自动购汇,还款还的人民币, 99 | ; 所以本质上是人民币计算的 100 | 2000-01-01 open Liabilities:CN:CreditCard:SPDB CNY,USD 101 | name: "浦发银行信用卡" 102 | ; 工商银行全币种卡需要收购购汇还款,因此当股票账户处理, 103 | ; 消费直接记外币,还款时先购汇再还外币 104 | 2000-01-01 open Liabilities:CN:CreditCard:ICBC "FIFO" 105 | name: "工商银行信用卡" 106 | ; 信用卡返现 107 | 2000-01-01 open Income:CN:CreditCard:SPDB CNY 108 | 109 | 110 | * Company Accounts 111 | 112 | 2000-01-01 open Assets:Receivables:YourEmployer CNY 113 | name: "应收报销" 114 | 2000-01-01 open Income:YourEmployer:BaseSalary CNY 115 | name: "税前工资" 116 | 2000-01-01 open Income:YourEmployer:HousingProvidentFund CNY 117 | name: "公司缴纳的公积金" 118 | 2000-01-01 open Income:YourEmployer:AnnualBonus CNY 119 | name: "年终奖" 120 | 121 | 122 | * Investment Accounts 123 | 2017-01-01 open Assets:Fund:TT:Positions "FIFO" 124 | name: "天天基金" 125 | 2017-01-01 open Assets:Fund:AF:Positions "FIFO" 126 | name: "蚂蚁财富(含余额宝)" 127 | 2017-01-01 open Assets:Securities:FT:Cash HKD,USD,CNH 128 | name: "富途证券现金账户(其中CNH为离岸人民币, CNY为在岸人民币)" 129 | 2017-01-01 open Assets:Securities:FT:Positions "FIFO" 130 | name: "富途证券投资组合账户" 131 | 2017-01-01 open Assets:Securities:XZ:Cash CNY 132 | name: "东方财富(西藏)证券现金账户" 133 | 2017-01-01 open Assets:Securities:XZ:Positions "FIFO" 134 | name: "东方财富(西藏)证券投资组合账户" 135 | 2017-01-01 open Income:Trade:PnL CNY,USD,HKD 136 | name: "投资盈亏" 137 | 2017-01-01 open Income:Trade:Dividen CNY,USD,HKD 138 | name: "分红" 139 | 2017-01-01 open Expenses:Trade:Fee CNY,USD,HKD 140 | name: "交易税费" 141 | 142 | 2000-01-01 open Assets:CryptoCoins:Positions "FIFO" 143 | name: "加密货币持仓" 144 | 2000-01-01 open Assets:CryptoCoins:Coinbase:Positions "FIFO" 145 | name: "加密货币交易所持仓" 146 | 147 | * Other 148 | 149 | ; LiuNeng 欠你钱 150 | 2000-01-01 open Assets:Receivables:LiuNeng CNY 151 | name: "刘能应还" 152 | ; 你欠 ZhaoSi 钱 153 | 2000-01-01 open Liabilities:Payables:ZhaoSi CNY 154 | name: "赵四应还" 155 | -------------------------------------------------------------------------------- /ledger/commodity.beancount: -------------------------------------------------------------------------------- 1 | * Currency 2 | 3 | 4 | 2019-01-01 commodity CNY 5 | name: "人民币" 6 | asset-class: "现金" 7 | asset-subclass: "本币" 8 | 2019-01-01 commodity CNH 9 | name: "离岸人民币" 10 | asset-class: "现金" 11 | asset-subclass: "外汇" 12 | 2019-01-01 commodity USD 13 | name: "美元" 14 | asset-class: "现金" 15 | asset-subclass: "外汇" 16 | price: "CNY:exchangeratesapi/USDCNY,yahoo/USDCNY=X" 17 | 2019-01-01 commodity JPY 18 | name: "日元" 19 | asset-class: "现金" 20 | asset-subclass: "外汇" 21 | price: "CNY:exchangeratesapi/JPYCNY,yahoo/JPYCNY=X" 22 | 2019-01-01 commodity HKD 23 | name: "港元" 24 | asset-class: "现金" 25 | asset-subclass: "外汇" 26 | price: "CNY:exchangeratesapi/HKDCNY,yahoo/HKDCNY=X" 27 | 28 | 2019-01-01 commodity BTC 29 | name: "比特币" 30 | asset-class: "另类" 31 | asset-subclass: "加密货币" 32 | price: "USD:yahoo/BTCUSD=X" 33 | 34 | 2019-01-01 commodity ETH 35 | name: "以太坊" 36 | asset-class: "另类" 37 | asset-subclass: "加密货币" 38 | price: "USD:yahoo/ETHUSD=X" 39 | 40 | 2019-01-01 commodity USDT 41 | name: "USDT" 42 | asset-class: "另类" 43 | asset-subclass: "加密货币" 44 | price: "USDT:yahoo/USDT-USD" 45 | 46 | * Symbol 47 | 48 | 2019-06-07 commodity AAPL 49 | name: "Apple Inc." 50 | asset-class: "股权" 51 | asset-subclass: "美股" 52 | price: "USD:xueqiu/US:AAPL,yahoo/AAPL" 53 | 54 | 2019-06-07 commodity SPY 55 | name: "SPDR S&P 500 ETF" 56 | asset-class: "股权" 57 | asset-subclass: "指数基金" 58 | price: "USD:xueqiu/US:SPY,yahoo/SPY" 59 | 60 | 2019-06-07 commodity HK_0700 61 | name: "腾讯控股" 62 | asset-class: "股权" 63 | asset-subclass: "港股" 64 | price: "HKD:xueqiu/HK:00700,yahoo/0700.HK" 65 | 66 | 2019-06-07 commodity HK_2800 67 | name: "盈富基金" 68 | asset-class: "股权" 69 | asset-subclass: "港股" 70 | price: "HKD:xueqiu/HK:02800,yahoo/2800.HK" 71 | 72 | 2019-06-07 commodity CN_F110011 73 | name: "易方达中小盘混合" 74 | asset-class: "股权" 75 | asset-subclass: "偏股混合基金" 76 | price: "CNY:eastmoney/F110011" 77 | 78 | 2019-01-01 commodity CN_F000187 79 | name: "华泰柏瑞丰盛纯债债券A" 80 | asset-class: "债权" 81 | asset-subclass: "债券基金" 82 | price: "CNY:eastmoney/F000187" 83 | 84 | 2019-01-01 commodity CN_510300 85 | name: "华泰沪深300ETF" 86 | asset-class: "股权" 87 | asset-subclass: "指数基金" 88 | price: "CNY:xueqiu/CN:SH510300,yahoo/510300.SS" 89 | 90 | 2019-01-01 commodity CN_000001 91 | name: "平安银行" 92 | asset-class: "股权" 93 | asset-subclass: "A股" 94 | price: "CNY:xueqiu/CN:SZ000001,yahoo/000001.SZ" 95 | 96 | 2019-01-01 commodity CN_128047 97 | name: "光电转债" 98 | asset-class: "债权" 99 | asset-subclass: "可转换债券" 100 | price: "CNY:xueqiu/CN:SZ128047" 101 | -------------------------------------------------------------------------------- /ledger/cryptocoin.beancount: -------------------------------------------------------------------------------- 1 | 2 | 2019-07-16 * "人民币买USDT" 3 | Assets:CN:Saving:CMB -19999.95 CNY 4 | Assets:CryptoCoins:Coinbase:Positions 2877.69 USDT {6.95 CNY} 5 | 6 | 2019-07-16 * "USDT买BTC" 7 | Assets:CryptoCoins:Coinbase:Positions -1877.69 USDT 8 | Assets:CryptoCoins:Coinbase:Positions 0.17455 BTC {10757.30 USDT} 9 | 10 | 2019-07-16 * "USDT买ETH" 11 | Assets:CryptoCoins:Coinbase:Positions -223.66 USDT 12 | Assets:CryptoCoins:Coinbase:Positions 1 ETH {223.66 USDT} 13 | -------------------------------------------------------------------------------- /ledger/daily/2019.beancount: -------------------------------------------------------------------------------- 1 | include "2019/2019-06-03-spdbcc.beancount" 2 | include "2019/2019-06-03-other.beancount" 3 | include "2019/2019-06-03-settle.beancount" 4 | -------------------------------------------------------------------------------- /ledger/daily/2019/2019-06-03-other.beancount: -------------------------------------------------------------------------------- 1 | 2019-05-04 * "吃饭同事同事付的钱, 转给他" 2 | card: "9999" 3 | Expenses:Food:Meals 20.00 CNY 4 | Assets:CN:Saving:Wechat -20.00 CNY 5 | 6 | 2019-05-31 * "支付房租" 7 | Expenses:Home:Rental 5000 CNY 8 | Assets:CN:Saving:CMB 9 | 10 | 2019-05-29 * "借给LiuNeng5000元" 11 | Assets:Receivables:LiuNeng 5000 CNY 12 | Assets:CN:Saving:CMB 13 | 14 | 2019-05-31 * "LiuNeng还款5000元" 15 | Assets:CN:Saving:CMB 16 | Assets:Receivables:LiuNeng -5000 CNY 17 | 18 | 2019-05-29 * "向ZhaoSi借款2000元" 19 | Assets:CN:Saving:CMB 2000 CNY 20 | Liabilities:Payables:ZhaoSi 21 | 22 | 2019-05-29 * "还ZhaoSi 1999元" 23 | Assets:CN:Saving:CMB -1999 CNY 24 | Liabilities:Payables:ZhaoSi 1999 CNY 25 | 26 | 2019-05-20 * "出差吃了一顿饭" #meals #xx-trip-2019-may 27 | ; 出差吃饭花了 50 USD, 银行自动购汇 28 | Liabilities:CN:CreditCard:SPDB -50 USD @ 6.86 CNY 29 | Assets:Receivables:YourEmployer 30 | 31 | 2019-05-20 * "出差打车" #transport #xx-trip-2019-may 32 | Liabilities:CN:CreditCard:SPDB -30 USD @ 6.85 CNY 33 | Assets:Receivables:YourEmployer 34 | 35 | 2019-05-30 * "收到出差报销" #xx-trip-2019-may 36 | Assets:Receivables:YourEmployer 37 | Assets:CN:Saving:CMB 548.50 CNY 38 | 39 | -------------------------------------------------------------------------------- /ledger/daily/2019/2019-06-03-settle.beancount: -------------------------------------------------------------------------------- 1 | 2019-06-30 balance Assets:CN:Saving:CMB 42430.10 CNY 2 | -------------------------------------------------------------------------------- /ledger/daily/2019/2019-06-03-spdbcc.beancount: -------------------------------------------------------------------------------- 1 | ;; -*- mode: beancount -*- 2 | ; 使用如下命令 3 | ; bean-extract importers/spdccc_importer.py raw-data/spdbcc/2019-05-spdbcc.csv > ledger/daily/2019/2019-06-03-spdbcc.beancount 4 | ; 生成本文件后人工修改错误的内容 5 | 6 | 2019-05-02 * "信用卡还款" 7 | card: "9999" 8 | Liabilities:CN:CreditCard:SPDB 1230 CNY 9 | Assets:CN:Saving:CMB -1230 CNY 10 | 11 | 2019-05-03 * "支付宝 餐厅" 12 | card: "9999" 13 | Liabilities:CN:CreditCard:SPDB -44.00 CNY 14 | Expenses:Food:Meals 44.00 CNY 15 | 16 | 2019-05-03 * "京东买了个包" 17 | card: "9999" 18 | Liabilities:CN:CreditCard:SPDB -320.00 CNY 19 | Expenses:Home:Groceries 320.00 CNY 20 | 21 | 2019-05-03 * "不想要了京东退款" 22 | card: "9999" 23 | Liabilities:CN:CreditCard:SPDB 320.00 CNY 24 | Expenses:Home:Groceries -320.00 CNY 25 | 26 | 2019-05-03 * "支付宝 XX小吃" 27 | card: "9999" 28 | ; 和同事一起吃了一份人均23元的盒饭,帮同事代付, 29 | ; 同事立刻转了钱到我的微信 30 | Liabilities:CN:CreditCard:SPDB -46.00 CNY 31 | Expenses:Food:Meals 23.00 CNY 32 | Assets:CN:Saving:Wechat 23.00 CNY 33 | 34 | 2019-05-04 * "支付宝 XX餐饮" 35 | card: "9999" 36 | Liabilities:CN:CreditCard:SPDB -152.00 CNY 37 | Expenses:Food:Meals 152.00 CNY 38 | 39 | 2019-05-04 * "支付宝 刘能" 40 | card: "9999" 41 | Liabilities:CN:CreditCard:SPDB -13.00 CNY 42 | Expenses:Food:Meals 13.00 CNY 43 | 44 | 2019-05-05 * "支付宝 中国联合网络通信有限公司" 45 | card: "9999" 46 | Liabilities:CN:CreditCard:SPDB -99.50 CNY 47 | Expenses:Comm:PhonePlan 99.50 CNY 48 | 49 | 2019-05-05 * "支付宝 XX电力公司" 50 | card: "9999" 51 | Liabilities:CN:CreditCard:SPDB -56.49 CNY 52 | Expenses:Home:Utilities 56.49 CNY 53 | 54 | 2019-05-05 * "支付宝 XX果品" 55 | card: "9999" 56 | Liabilities:CN:CreditCard:SPDB -30.00 CNY 57 | Expenses:Food:FruitSnacks 18.00 CNY 58 | note: "西瓜" 59 | Expenses:Food:FruitSnacks 12.00 CNY 60 | note: "苹果" 61 | 62 | 2019-05-06 * "支付宝 上海赵四餐饮服务有限公司" 63 | card: "9999" 64 | Liabilities:CN:CreditCard:SPDB -20.00 CNY 65 | Expenses:Food:Meals 20.00 CNY 66 | 67 | 2019-05-06 * "支付宝 谢广坤" 68 | card: "9999" 69 | Liabilities:CN:CreditCard:SPDB -22.00 CNY 70 | Expenses:Food:Meals 22.00 CNY 71 | 72 | 2019-05-06 * "支付宝 上海福满家便利有限公司" 73 | card: "9999" 74 | Liabilities:CN:CreditCard:SPDB -13.80 CNY 75 | Expenses:Food:FruitSnacks 13.80 CNY 76 | 77 | 2019-05-07 * "支付宝 北京嘀嘀无限科技发展有…" 78 | card: "9999" 79 | Liabilities:CN:CreditCard:SPDB -11.00 CNY 80 | Expenses:Transport:Taxi 11.00 CNY 81 | -------------------------------------------------------------------------------- /ledger/init.beancount: -------------------------------------------------------------------------------- 1 | ; 从当日起使用 beancount 记账,为了做平帐,初始化的资金 2 | ; 从 Equity:Openings-Balance CNY,USD,HKD,BTC 中来 3 | 4 | 5 | 2019-01-01 * "Opening Balances" 6 | Equity:Openings-Balance 7 | ; 应收房租押金 8 | Assets:Receivables:HomeRental 5000 CNY 9 | ; 公积金账户余额(含补充公积金) 10 | ; 公司和个人缴纳的公积金都会进这个账户 11 | Assets:CN:YourCity:HousingProvidentFund 20000 CNY 12 | ; 现钞 13 | Assets:Cash 1000 CNY 14 | Assets:Cash 1000 USD 15 | Assets:Cash 0 JPY 16 | Assets:CN:Saving:Wechat 123.50 CNY 17 | Assets:CN:Saving:Alipay 1233.0 CNY 18 | Assets:CN:Saving:CMB 50000 CNY 19 | Assets:HK:Saving:CMB 1000 USD 20 | Assets:HK:Saving:CMB 10000 HKD 21 | Assets:CN:Saving:CCB 10000 CNY 22 | ; 应收报销 23 | Assets:Receivables:YourEmployer 0 CNY 24 | ; FT=富途证券 25 | Assets:Securities:FT:Cash 100000 HKD 26 | Assets:Securities:FT:Cash 20000 USD 27 | Assets:Securities:FT:Cash 1000 CNH 28 | ; 已有 10 股 AAPL, 持仓成本价 153.39 USD 29 | Assets:Securities:FT:Positions 10 AAPL {153.39 USD} 30 | Assets:Securities:FT:Positions 10 SPY {243.315 USD} 31 | Assets:Securities:FT:Positions 100 HK_0700 {314 HKD} 32 | Assets:Securities:FT:Positions 100 HK_2800 {25.8 HKD} 33 | ; XZ=东方财富证券(西藏证券) 34 | ; 股票账户中的现金余额 35 | Assets:Securities:XZ:Cash 10000 CNY 36 | Assets:Securities:XZ:Positions 100 CN_510300 {3.011 CNY} 37 | Assets:Securities:XZ:Positions 100 CN_000001 {9.25 CNY} 38 | Assets:Securities:XZ:Positions 100 CN_128047 {100 CNY} 39 | ; TT=天天基金 40 | Assets:Fund:TT:Positions 10000 CN_F110011 {3.2376 CNY} 41 | Assets:Fund:TT:Positions 100 CN_F000187 {1.2088 CNY} 42 | ; AF=AntFund=蚂蚁基金 43 | Assets:Fund:AF:Positions 4900 CN_F000187 {1.2088 CNY} 44 | ; 基金账户中的货币基金当现金(CNY)处理 45 | Assets:Fund:AF:Positions 5000 CNY 46 | Assets:CryptoCoins:Positions 1 BTC {4102 USD} 47 | ; 信用卡欠款 48 | Liabilities:CN:CreditCard:SPDB -1230 CNY 49 | -------------------------------------------------------------------------------- /ledger/invest.beancount: -------------------------------------------------------------------------------- 1 | ; 货币基金当现金处理,这和其他场外基金(如CN_F000187)处理方式不一样 2 | ; 原因是货币基金没有净值的概念,没办法自动计算收益 3 | 2019-05-20 * "在天天基金中购买了10000元某货币基金" #money-fund 4 | Assets:Fund:TT:Positions 10000 CNY 5 | Assets:CN:Saving:CMB 6 | 7 | 2019-05-31 * "Buy 5 SPY@291.46" 8 | Assets:Securities:FT:Cash -1459.3 USD 9 | Assets:Securities:FT:Positions 5 SPY {291.46 USD} 10 | Expenses:Trade:Fee 2 USD 11 | 12 | 2019-05-31 * "富途证券 USD换HKD, 手续费 2 USD" 13 | Assets:Securities:FT:Cash -1000 USD 14 | Assets:Securities:FT:Cash 7850 HKD @ USD 15 | Expenses:Trade:Fee 2 USD 16 | 17 | 2019-05-31 * "富途证券 卖出 50 HK_0700" 18 | ; 成本价 314 HKD 19 | ; 买入金额 314 HKD * 50 = 15700 HKD 20 | ; 卖出金额 326 HKD * 50 = 16300 HKD 21 | ; 卖出手续费 38 HKD 22 | ; 金额入帐 16300 - 38 HKD = 16262 23 | ; 投资收益(不计手续费) 16300 - 15700 = 600 HKD 24 | ; 投资收益(考虑手续费) 600 - 38 = 562 HKD 25 | Assets:Securities:FT:Cash 16262 HKD 26 | Assets:Securities:FT:Positions -50 HK_0700 {} 27 | Expenses:Trade:Fee 38 HKD 28 | Income:Trade:PnL; 此处 值位 -600 HKD 但一般不需要写会自动计算 29 | 30 | ; CN_F000187 的价格通过 bean-price 自动获取自动计算收益 31 | 2019-05-31 * "购买债券基金 CN_F000187 (手续费千分之一)" 32 | Assets:Fund:TT:Positions 1000 CN_F000187 {1.209 CNY} 33 | Assets:CN:Saving:CMB -1210 CNY 34 | Expenses:Trade:Fee 1 CNY 35 | 36 | 2019-05-30 * "货币基金自买入之日起收益" #money-fund 37 | Assets:Fund:TT:Positions 7 CNY 38 | Income:Trade:PnL 39 | -------------------------------------------------------------------------------- /ledger/latest-prices.beancount: -------------------------------------------------------------------------------- 1 | 2020-11-25 price BTC 19185.474609375 USD 2 | 2020-11-25 price CN_000001 19.059999999999998721 CNY 3 | 2020-11-25 price CN_510300 4.977999999999999758 CNY 4 | 2020-11-25 price CN_F000187 1.006100000000000000 CNY 5 | 2020-11-25 price CN_F110011 7.945300000000000000 CNY 6 | 2020-11-25 price HK_0700 573.000000000000000000 HKD 7 | 2020-11-25 price HK_2800 26.780000000000001137 HKD 8 | -------------------------------------------------------------------------------- /ledger/main.beancount: -------------------------------------------------------------------------------- 1 | option "title" "YourLedger" 2 | 3 | ; 记账时千分之一的偏差忽略不计 4 | option "inferred_tolerance_default" "*:0.001" 5 | ; 汇总时你希望以什么货币单位结算 6 | option "operating_currency" "CNY" 7 | option "operating_currency" "USD" 8 | option "operating_currency" "HKD" 9 | 10 | include "account.beancount" 11 | include "init.beancount" 12 | 13 | ;; 工资流水 14 | include "salary.beancount" 15 | 16 | ;; 工资流水 17 | include "daily/2019.beancount" 18 | 19 | ;; 标的 20 | include "commodity.beancount" 21 | 22 | include "invest.beancount" 23 | include "prices.beancount" 24 | include "cryptocoin.beancount" 25 | -------------------------------------------------------------------------------- /ledger/salary.beancount: -------------------------------------------------------------------------------- 1 | 2019-05-31 * "2019-05工资" 2 | Income:YourEmployer:BaseSalary -12345.00 CNY 3 | Income:YourEmployer:HousingProvidentFund -1481.4 CNY 4 | Expenses:CN:YourCity:PensionInsurance 987.60 CNY 5 | Expenses:CN:YourCity:MedicalInsurance 246.90 CNY 6 | Expenses:CN:YourCity:UnemploymentInsurance 61.73 CNY 7 | Expenses:CN:YourCity:Tax 246.74 CNY 8 | Assets:CN:YourCity:HousingProvidentFund 2962.8 CNY 9 | Assets:CN:Saving:CMB 9320.6 CNY 10 | -------------------------------------------------------------------------------- /raw-data/spdbcc/2019-05-spdbcc.csv: -------------------------------------------------------------------------------- 1 | 卡号末四位,记账日期,交易金额,交易描述 2 | 9999,2019-05-03,44.00,支付宝 餐厅 3 | 9999,2019-05-03,320.00,京东 4 | 9999,2019-05-03,-320.00,京东退款 5 | 9999,2019-05-03,46.00,支付宝 XX小吃 6 | 9999,2019-05-04,152.00,支付宝 XX餐饮 7 | 9999,2019-05-04,13.00,支付宝 刘能 8 | 9999,2019-05-05,99.50,支付宝 中国联合网络通信有限公司 9 | 9999,2019-05-05,56.49,支付宝 XX电力公司 10 | 9999,2019-05-05,30.00,支付宝 XX果品 11 | 9999,2019-05-06,20.00,支付宝 上海赵四餐饮服务有限公司 12 | 9999,2019-05-06,22.00,支付宝 谢广坤 13 | 9999,2019-05-06,13.80,支付宝 上海福满家便利有限公司 14 | 9999,2019-05-07,11.00,支付宝 北京嘀嘀无限科技发展有… 15 | -------------------------------------------------------------------------------- /requirements.in: -------------------------------------------------------------------------------- 1 | pip-tools 2 | beancount 3 | fava 4 | requests 5 | apiclient 6 | oauth2client 7 | httplib2 8 | pandas 9 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile 6 | # 7 | --index-url https://pypi.doubanio.com/simple 8 | 9 | apiclient==1.0.4 # via -r requirements.in 10 | attrs==20.3.0 # via pytest 11 | babel==2.9.0 # via fava, flask-babel 12 | beancount==2.3.3 # via -r requirements.in, fava 13 | beautifulsoup4==4.9.3 # via beancount 14 | bottle==0.12.19 # via beancount 15 | cachetools==4.1.1 # via google-auth 16 | certifi==2020.11.8 # via apiclient, requests 17 | chardet==3.0.4 # via beancount, requests 18 | cheroot==8.4.8 # via fava 19 | click==7.1.2 # via fava, flask, pip-tools 20 | fava==1.17 # via -r requirements.in 21 | flask-babel==2.0.0 # via fava 22 | flask==1.1.2 # via fava, flask-babel 23 | google-api-core==1.23.0 # via google-api-python-client 24 | google-api-python-client==1.12.8 # via beancount 25 | google-auth-httplib2==0.0.4 # via google-api-python-client 26 | google-auth==1.23.0 # via google-api-core, google-api-python-client, google-auth-httplib2 27 | googleapis-common-protos==1.52.0 # via google-api-core 28 | httplib2==0.18.1 # via -r requirements.in, google-api-python-client, google-auth-httplib2, oauth2client 29 | idna==2.10 # via requests 30 | iniconfig==1.1.1 # via pytest 31 | itsdangerous==1.1.0 # via flask 32 | jaraco.functools==3.0.1 # via cheroot 33 | jinja2==2.11.2 # via fava, flask, flask-babel 34 | lxml==4.6.1 # via beancount 35 | markdown2==2.3.10 # via fava 36 | markupsafe==1.1.1 # via jinja2 37 | more-itertools==8.6.0 # via cheroot, jaraco.functools 38 | numpy==1.19.4 # via pandas 39 | oauth2client==4.1.3 # via -r requirements.in 40 | packaging==20.4 # via pytest 41 | pandas==1.1.4 # via -r requirements.in 42 | pip-tools==5.4.0 # via -r requirements.in 43 | pluggy==0.13.1 # via pytest 44 | ply==3.11 # via beancount, fava 45 | protobuf==3.14.0 # via google-api-core, googleapis-common-protos 46 | py==1.9.0 # via pytest 47 | pyasn1-modules==0.2.8 # via google-auth, oauth2client 48 | pyasn1==0.4.8 # via oauth2client, pyasn1-modules, rsa 49 | pyparsing==2.4.7 # via packaging 50 | pytest==6.1.2 # via beancount 51 | python-dateutil==2.8.1 # via beancount, pandas 52 | python-magic==0.4.18 # via beancount 53 | pytz==2020.4 # via babel, flask-babel, google-api-core, pandas 54 | requests==2.25.0 # via -r requirements.in, beancount, google-api-core 55 | rsa==4.6 # via google-auth, oauth2client 56 | simplejson==3.17.2 # via fava 57 | six==1.15.0 # via cheroot, google-api-core, google-api-python-client, google-auth, google-auth-httplib2, oauth2client, packaging, pip-tools, protobuf, python-dateutil 58 | soupsieve==2.0.1 # via beautifulsoup4 59 | toml==0.10.2 # via pytest 60 | uritemplate==3.0.1 # via google-api-python-client 61 | urllib3==1.26.2 # via apiclient, requests 62 | werkzeug==1.0.1 # via fava, flask 63 | 64 | # The following packages are considered to be unsafe in a requirements file: 65 | # pip 66 | # setuptools 67 | -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | 理论净资产 2 | = 总资产 - 负债 3 | 4 | 净资产 5 | = 理论总资产 - 预付款项 6 | 7 | 沉没资产 8 | = 预付款项(房租、宽带) 9 | 10 | 可投资现金流(包含借来的钱) 11 | = 总资产 - 预付款项(房租、宽带)-不可支配款项(房租押金、住房公积金) 12 | 13 | 可投资净资产 14 | = 可投资现金流 - 负债(应还信用卡、预收款项) 15 | 16 | 杠杆率 17 | = 可投资现金流 / 可投资净资产 - 1 18 | 19 | 非投资收入 20 | = 工资收入等 21 | 22 | 非投资支出 23 | = 税、生活开销等。预付款也作为支出。比如一次性预付3个月的房租作为一笔支出。 24 | 25 | 投资盈亏 26 | = 净资产 + 非投资收入 - 非投资支出 - 昨日净资产 27 | 28 | 投资盈亏% 29 | = 投资盈亏 / 昨日可投资净资产 30 | -------------------------------------------------------------------------------- /scripts/generate-networth-report.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | 计算每日投资盈亏和投资净值曲线 4 | """ 5 | 6 | import os 7 | import csv 8 | import datetime 9 | import logging 10 | import collections 11 | import io 12 | import decimal 13 | 14 | import click 15 | import beancount.loader 16 | import beancount.core.data 17 | from beancount.core import prices 18 | from beancount.ops.holdings import get_assets_holdings 19 | 20 | 21 | logger = logging.getLogger() 22 | 23 | KNOWN_ASSET_CLASSES = { 24 | "股权", 25 | "另类", 26 | "债权", 27 | "现金", 28 | } 29 | EXPENSES_PREFIX = "Expenses:" 30 | EXPENSES_TRADE_PREFIX = "Expenses:Trade:" 31 | EXPENSES_PREPAYMENTS_PREFIX = "Assets:PrePayments" 32 | 33 | 34 | def get_maps(entries): 35 | account_map = {} 36 | commodity_map = {} 37 | for entry in entries: 38 | if isinstance(entry, beancount.core.data.Open): 39 | account_map[entry.account] = entry 40 | elif isinstance(entry, beancount.core.data.Commodity): 41 | commodity_map[entry.currency] = entry 42 | return account_map, commodity_map 43 | 44 | 45 | def get_ledger_file(): 46 | this_file = os.path.abspath(__file__) 47 | workdir = os.path.dirname(this_file) 48 | while not os.path.exists(os.path.join(workdir, "ledger")): 49 | workdir = os.path.dirname(workdir) 50 | 51 | return os.path.join(workdir, "ledger", "main.beancount") 52 | 53 | 54 | def compute_networth_series(since_date, end_date=None): 55 | if end_date is None: 56 | end_date = datetime.date.today() 57 | (entries, errors, options_map) = beancount.loader.load_file(get_ledger_file()) 58 | account_map, commodity_map = get_maps(entries) 59 | 60 | target_currency = 'CNY' 61 | curr_date = since_date 62 | 63 | result = [] 64 | prev_networth = None 65 | prev_disposable_networth = None 66 | cum_invest_nav = decimal.Decimal("1.0") 67 | cum_invest_nav_ytd = decimal.Decimal("1.0") 68 | cum_invest_pnl = decimal.Decimal(0) 69 | cum_invest_pnl_ytd = decimal.Decimal(0) 70 | 71 | while curr_date <= end_date: 72 | entries_to_date = [entry for entry in entries if entry.date <= curr_date] 73 | holdings_list, price_map_to_date = get_assets_holdings( 74 | entries_to_date, options_map, target_currency 75 | ) 76 | raw_networth_in_cny = decimal.Decimal(0) # 包含sunk资产的理论净资产 77 | networth_in_cny = decimal.Decimal(0) # 不包含sunk资产的净资产 78 | disposable_networth_in_cny = decimal.Decimal(0) 79 | dnw_by_asset_class = {} 80 | 81 | for hld in holdings_list: 82 | if hld.currency == "DAY": 83 | continue 84 | 85 | acc = account_map[hld.account] 86 | cmdt = commodity_map[hld.currency] 87 | if hld.market_value is None: 88 | raise ValueError(hld) 89 | 90 | raw_networth_in_cny += hld.market_value 91 | # 预付但大部分情况下不能兑现的沉没资产,比如预付的未来房租 92 | is_sunk = bool(int(acc.meta.get("sunk", 0))) 93 | if is_sunk: 94 | continue 95 | 96 | # 不可支配,比如房租押金 97 | nondisposable = bool(int(acc.meta.get("nondisposable", 0))) 98 | if not nondisposable: 99 | disposable_networth_in_cny += hld.market_value 100 | asset_class = cmdt.meta["asset-class"] 101 | if asset_class not in dnw_by_asset_class: 102 | dnw_by_asset_class[asset_class] = decimal.Decimal(0) 103 | dnw_by_asset_class[asset_class] += hld.market_value 104 | 105 | networth_in_cny += hld.market_value 106 | 107 | txs_of_date = [ 108 | entry for entry in entries 109 | if entry.date == curr_date and 110 | isinstance(entry, beancount.core.data.Transaction) 111 | ] 112 | 113 | non_trade_expenses = decimal.Decimal(0) 114 | non_trade_incomes = decimal.Decimal(0) 115 | for tx in txs_of_date: 116 | is_time_tx = any((posting.units.currency == "DAY" for posting in tx.postings)) 117 | if is_time_tx: 118 | continue 119 | 120 | for posting in tx.postings: 121 | acc = posting.account 122 | is_non_trade_exp = ( 123 | acc.startswith(EXPENSES_PREFIX) and not acc.startswith(EXPENSES_TRADE_PREFIX) 124 | ) or ( 125 | acc.startswith(EXPENSES_PREPAYMENTS_PREFIX) 126 | ) 127 | 128 | is_non_trade_inc = acc.startswith("Income:") and not acc.startswith("Income:Trade:") 129 | if is_non_trade_exp or is_non_trade_inc: 130 | if posting.units.currency != target_currency: 131 | base_quote = (posting.units.currency, target_currency) 132 | _, rate = prices.get_latest_price(price_map_to_date, base_quote) 133 | else: 134 | rate = decimal.Decimal(1) 135 | 136 | if is_non_trade_exp: 137 | non_trade_expenses += (posting.units.number * rate) 138 | else: 139 | non_trade_incomes -= (posting.units.number * rate) 140 | 141 | if prev_networth: 142 | pnl = networth_in_cny - non_trade_incomes + non_trade_expenses - prev_networth 143 | pnl_str = ("%.2f" % pnl) if not isinstance(pnl, str) else pnl 144 | pnl_rate_str = "%.4f%%" % (100 * pnl / prev_disposable_networth) 145 | cum_invest_nav *= (1 + pnl / prev_disposable_networth) 146 | cum_invest_nav_ytd *= (1 + pnl / prev_disposable_networth) 147 | cum_invest_pnl += pnl 148 | cum_invest_pnl_ytd += pnl 149 | else: 150 | pnl = None 151 | pnl_str = 'n/a' 152 | pnl_rate_str = 'n/a' 153 | 154 | daily_status = { 155 | "日期": curr_date, 156 | # 理论净资产=总资产 - 负债 157 | "理论净资产": "%.2f" % raw_networth_in_cny, 158 | # 净资产=总资产' - 负债(信用卡)- 沉没资产 159 | "净资产": "%.2f" % networth_in_cny, 160 | "沉没资产": "%.2f" % (raw_networth_in_cny - networth_in_cny), 161 | # 可投资金额=净资产 - 不可支配资产(公积金、预付房租、宽带) 162 | "可投资净资产": "%.2f" % disposable_networth_in_cny, 163 | # Income:Trade(已了结盈亏、分红) 以外的 Income (包含公积金收入、储蓄利息) 164 | "非投资收入": "%.2f" % non_trade_incomes, 165 | # Expenses:Trade 以外的 Expenses (包含社保等支出) 166 | "非投资支出": "%.2f" % non_trade_expenses, 167 | "投资盈亏": pnl_str, 168 | # 投资盈亏% = 当日投资盈亏/昨日可投资金额 169 | "投资盈亏%": pnl_rate_str, 170 | "累计净值": "%.4f" % cum_invest_nav, 171 | "累计盈亏": "%.2f" % cum_invest_pnl, 172 | "当年净值": "%.4f" % cum_invest_nav_ytd, 173 | "当年盈亏": "%.2f" % cum_invest_pnl_ytd, 174 | } 175 | 176 | if curr_date.weekday() >= 5: 177 | if pnl is not None: 178 | assert pnl <= 0.01, daily_status # 预期周末不应该有投资盈亏 179 | 180 | assert abs(sum(dnw_by_asset_class.values()) - disposable_networth_in_cny) < 1 181 | 182 | assert set(dnw_by_asset_class.keys()) <= KNOWN_ASSET_CLASSES, dnw_by_asset_class 183 | for asset_class in KNOWN_ASSET_CLASSES: 184 | propotion = 100 * dnw_by_asset_class.get(asset_class, 0) / disposable_networth_in_cny 185 | daily_status[f"{asset_class}%"] = f"{propotion:.2f}%" 186 | 187 | result.append(daily_status) 188 | 189 | 190 | next_date = curr_date + datetime.timedelta(days=1) 191 | if curr_date.year != next_date.year: 192 | cum_invest_nav_ytd = decimal.Decimal("1.0") 193 | cum_invest_pnl_ytd = decimal.Decimal(0) 194 | curr_date = next_date 195 | prev_networth = networth_in_cny 196 | prev_disposable_networth = disposable_networth_in_cny 197 | return result 198 | 199 | 200 | def print_portfolio_csv(rows, transpose): 201 | fhandler = io.StringIO() 202 | writer = csv.DictWriter(fhandler, fieldnames=rows[0].keys()) 203 | writer.writeheader() 204 | writer.writerows(rows) 205 | if not transpose: 206 | print(fhandler.getvalue().strip()) 207 | return 208 | 209 | transposed = list(zip(*csv.reader(io.StringIO(fhandler.getvalue())))) 210 | t_fhandler = io.StringIO() 211 | csv.writer(t_fhandler).writerows(transposed) 212 | print(t_fhandler.getvalue().strip()) 213 | 214 | def add_padding(rows): 215 | curr_date = rows[-1]["日期"] 216 | empty_row = {key: "" for key, val in rows[-1].items()} 217 | while True: 218 | curr_date += datetime.timedelta(days=1) 219 | if (curr_date.month == 1 and curr_date.day == 1): 220 | break 221 | 222 | empty_row["日期"] = "" # curr_date 223 | rows.append(empty_row.copy()) 224 | return rows 225 | 226 | 227 | @click.command() 228 | @click.option('-s', '--since', default=None) 229 | @click.option('--padding/--no-padding', default=False) 230 | @click.option('--transpose/--no-transpose', default=False) 231 | def main(since, padding, transpose): 232 | if since is None: 233 | today = datetime.date.today() 234 | since_date = datetime.date(today.year, 1, 1) 235 | else: 236 | since_date = datetime.datetime.strptime(since, "%Y-%m-%d").date() 237 | 238 | rows = compute_networth_series(since_date) 239 | if padding: 240 | rows = add_padding(rows) 241 | print_portfolio_csv(rows, transpose) 242 | 243 | 244 | if __name__ == "__main__": 245 | main() 246 | -------------------------------------------------------------------------------- /scripts/generate-portfolio.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | 本脚本用来生成持仓CSV, 后续可以把CSV导入表格软件进一步聚合分析。 4 | 和 bean-report 相比主要的优先是: 5 | 6 | - 输出带 meta 中自定义的名称,资产类别 7 | - 过滤已支出不可退款的,预付款项 8 | - 过滤不可支配资产 9 | """ 10 | import os 11 | import csv 12 | import datetime 13 | import logging 14 | from decimal import Decimal 15 | import io 16 | 17 | import click 18 | import beancount.loader 19 | import beancount.core.data 20 | from beancount.core import getters 21 | from beancount.ops.holdings import get_assets_holdings 22 | 23 | 24 | logger = logging.getLogger() 25 | 26 | 27 | def get_account_map(entries): 28 | account_map = {} 29 | for entry in entries: 30 | if isinstance(entry, beancount.core.data.Open): 31 | account_map[entry.account] = entry 32 | return account_map 33 | 34 | 35 | def sort_key(row): 36 | """ 37 | 输出大体的风险系数以供排序 38 | """ 39 | asset_class = row["一级类别"] 40 | asset_subclass = row["二级类别"] 41 | ac_list = ["现金", "债券", "债权", "基金", "股票", "股权", "另类资产", "另类"] 42 | asc_list = [ 43 | "本币", "外汇", "债券基金", "可转换债券", "指数基金", '偏债混合基金', 44 | "偏股混合基金", "A股", "中国股票", "港股", "香港股票", 45 | "美股", "美国股票", "贵金属", "加密货币", 46 | ] 47 | factor1 = ac_list.index(asset_class) 48 | factor2 = asc_list.index(asset_subclass) 49 | return (1 + factor1) * 100 + factor2 50 | 51 | 52 | def get_ledger_file(): 53 | this_file = os.path.abspath(__file__) 54 | workdir = os.path.dirname(this_file) 55 | while not os.path.exists(os.path.join(workdir, "ledger")): 56 | workdir = os.path.dirname(workdir) 57 | 58 | return os.path.join(workdir, "ledger", "main.beancount") 59 | 60 | 61 | def get_portfolio_matrix(asof_date=None): 62 | """ 63 | 打印持仓 64 | Args: 65 | asof_date: 计算该日为止的持仓, 避免未来预付款项影响。 66 | """ 67 | if asof_date is None: 68 | asof_date = datetime.date.today() 69 | 70 | (entries, errors, option_map) = beancount.loader.load_file(get_ledger_file()) 71 | entries = [entry for entry in entries if entry.date <= asof_date] 72 | 73 | assets_holdings, price_map = get_assets_holdings(entries, option_map) 74 | account_map = get_account_map(entries) 75 | commoditiy_map = getters.get_commodity_directives(entries) 76 | 77 | holding_groups = {} 78 | for holding in assets_holdings: 79 | if holding.currency == "DAY": 80 | continue 81 | 82 | account_obj = account_map[holding.account] 83 | if account_obj is None: 84 | raise ValueError(f"account is not defined for {holding}") 85 | currency_obj = commoditiy_map[holding.currency] 86 | if currency_obj is None: 87 | raise ValueError(f"commoditiy is not defined for {holding}") 88 | 89 | if bool(int(account_obj.meta.get("sunk", 0))): 90 | logger.warning(f"{account_obj.account} is an sunk. Ignored.") 91 | continue 92 | if bool(int(account_obj.meta.get("nondisposable", 0))): 93 | logger.warning(f"{account_obj.account} is nondisposable. Ignored.") 94 | continue 95 | 96 | for meta_field in ("name", "asset-class", "asset-subclass"): 97 | if meta_field not in currency_obj.meta: 98 | raise ValueError( 99 | "Commodity %s has no '%s' in meta" 100 | "" % (holding.currency, meta_field) 101 | ) 102 | 103 | account_name = account_obj.meta.get("name") 104 | if not account_name: 105 | raise ValueError("Account name not set for %s" % holding.account) 106 | account_nondisposable = bool(int(account_obj.meta.get("nondisposable", 0))) 107 | symbol_name = currency_obj.meta["name"] 108 | asset_class = currency_obj.meta["asset-class"] 109 | asset_subclass = currency_obj.meta["asset-subclass"] 110 | symbol = holding.currency 111 | currency = holding.cost_currency 112 | price = holding.price_number 113 | price_date = holding.price_date 114 | if price is None: 115 | if symbol == currency: 116 | price = 1 117 | price_date = '' 118 | qty = holding.number 119 | if currency == "CNY": 120 | cny_rate = 1 121 | else: 122 | cny_rate = price_map[(currency, "CNY")][-1][1] 123 | 124 | holding_dict = { 125 | "account": account_name, 126 | "nondisposable": account_nondisposable, 127 | "symbol": symbol, 128 | "symbol_name": symbol_name, 129 | "asset_class": asset_class, 130 | "asset_subclass": asset_subclass, 131 | "quantity": qty, 132 | "currency": currency, 133 | "price": price, 134 | "price_date": price_date, 135 | "cny_rate": cny_rate, 136 | # optional: 137 | # book_value 138 | # market_value 139 | # 140 | # chg_1 141 | # chg_5 142 | # chg_30 143 | } 144 | 145 | hld_prices = price_map.get((holding.currency, holding.cost_currency)) 146 | if hld_prices is not None and holding.cost_number is not None: 147 | holding_dict["book_value"] = holding.book_value 148 | holding_dict["market_value"] = holding.market_value 149 | 150 | for dur in (1, 2, 7, 30): 151 | if len(hld_prices) < dur: 152 | continue 153 | 154 | base_date = asof_date - datetime.timedelta(days=dur) 155 | latest_price = hld_prices[-1][1] 156 | base_pxs = [(dt, px) for dt, px in hld_prices if dt <= base_date] 157 | if base_pxs: 158 | base_price = base_pxs[-1][-1] # O(N)! 159 | holding_dict[f"chg_{dur}"] = latest_price / base_price - 1 160 | else: 161 | holding_dict[f"chg_{dur}"] = 'n/a' 162 | 163 | group_key = (symbol, account_nondisposable) 164 | holding_groups.setdefault(group_key, []).append(holding_dict) 165 | 166 | rows = [] 167 | cum_networth = 0 168 | for (symbol, account_nondisposable), holdings in holding_groups.items(): 169 | qty_by_account = {} 170 | total_book_value = Decimal(0) 171 | total_market_value = Decimal(0) 172 | for holding in holdings: 173 | if holding["account"] not in qty_by_account: 174 | qty_by_account[holding["account"]] = 0 175 | qty_by_account[holding["account"]] += holding["quantity"] 176 | if "book_value" in holding: 177 | total_book_value += holding["book_value"] 178 | total_market_value += holding["market_value"] 179 | if total_book_value == 0: 180 | pnlr = "n/a" 181 | else: 182 | pnlr = "%.4f%%" % ((total_market_value / total_book_value - 1) * 100) 183 | 184 | total_qty = sum(qty_by_account.values()) 185 | hld_px = holding["price"] 186 | if hld_px is None: 187 | raise ValueError(f"price not found for {holding}") 188 | 189 | networth = holding["cny_rate"] * hld_px * total_qty 190 | cum_networth += networth 191 | row = { 192 | "一级类别": holding["asset_class"], 193 | "二级类别": holding["asset_subclass"], 194 | "标的": holding["symbol_name"], 195 | "代号": symbol, 196 | "持仓量": "%.3f" % total_qty, 197 | "市场价格": "%.4f" % holding["price"], 198 | "报价日期": holding["price_date"], 199 | "市场价值": int(round(holding["price"] * total_qty)), 200 | "货币": holding["currency"], 201 | "人民币价值": "%.2f" % networth, 202 | "持仓盈亏%": pnlr, 203 | } 204 | for optional_col, col_name in [ 205 | ("chg_1", "1日%"), 206 | ("chg_2", "2日%"), 207 | ("chg_7", "7日%"), 208 | ("chg_30", "30日%") 209 | ]: 210 | if optional_col not in holding or holding[optional_col] == 'n/a': 211 | row[col_name] = "n/a" 212 | else: 213 | row[col_name] = "%.2f%%" % (holding[optional_col] * 100) 214 | rows.append(row) 215 | 216 | rows.sort(key=sort_key) 217 | for row in rows: 218 | pct = Decimal(row["人民币价值"]) / cum_networth 219 | row.update({ 220 | "占比": "%.2f%%" % (100 * pct) 221 | }) 222 | return rows, cum_networth 223 | 224 | 225 | def print_portfolio_csv(rows): 226 | fhandler = io.StringIO() 227 | writer = csv.DictWriter(fhandler, fieldnames=rows[0].keys()) 228 | writer.writeheader() 229 | writer.writerows(rows) 230 | print(fhandler.getvalue().strip()) 231 | 232 | 233 | @click.command() 234 | @click.option('-d', '--date', default=None) 235 | def main(date): 236 | logging.basicConfig(level=logging.INFO) 237 | if date is None: 238 | asof_date = datetime.date.today() 239 | else: 240 | asof_date = datetime.datetime.strptime(date, "%Y-%m-%d").date() 241 | 242 | rows, cum_networth = get_portfolio_matrix(asof_date) 243 | logger.info("As of %s, disposable networth=%d CNY", asof_date, cum_networth) 244 | print_portfolio_csv(rows) 245 | 246 | 247 | if __name__ == "__main__": 248 | main() 249 | -------------------------------------------------------------------------------- /scripts/update-prices.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import sys 4 | import datetime 5 | 6 | 7 | ONE_DAY = datetime.timedelta(days=1) 8 | ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 9 | PRICE_PATH = os.path.join(ROOT_DIR, "ledger", "prices.beancount") 10 | 11 | 12 | def yield_date_range(start_date, end_date): 13 | curr_date = start_date 14 | while curr_date <= end_date: 15 | if curr_date.weekday() < 5: 16 | yield curr_date 17 | curr_date += ONE_DAY 18 | 19 | 20 | def main(argv): 21 | today_only = False 22 | if len(argv) > 1 and argv[1] == "--today-only": 23 | today_only = True 24 | 25 | with open(PRICE_PATH, "rb") as fhandler: 26 | fhandler.seek(-1000, os.SEEK_END) 27 | last_line = fhandler.readlines()[-1] 28 | last_date_str = last_line.split()[0].decode() 29 | last_date = datetime.datetime.strptime(last_date_str, "%Y-%m-%d") 30 | 31 | print("echo > ledger/latest-prices.beancount") 32 | # 部分香港基金的净值更新时间比较慢,所以此处不用 last_date + 1 33 | start_date = last_date 34 | utc_now = datetime.datetime.utcnow() 35 | end_date = utc_now 36 | for curr_date in yield_date_range(start_date, end_date): 37 | if curr_date.date() == utc_now.date(): 38 | suffix = f"| grep {curr_date:%Y-%m-%d} > ledger/latest-prices.beancount" 39 | else: 40 | suffix = f">> ledger/prices.beancount" 41 | if not today_only or curr_date.date() == utc_now.date(): 42 | print( 43 | f"PYTHONPATH=`pwd`/sources bean-price --no-cache -d {curr_date:%Y-%m-%d}" 44 | f" ledger/main.beancount {suffix};" 45 | ) 46 | 47 | 48 | def get_existed_symbol_dates(target_symbol): 49 | existed_date = set() 50 | with open(PRICE_PATH) as fhandler: 51 | for line in fhandler.readlines(): 52 | dt, _, symbol, px, unit = line.split() 53 | if symbol != target_symbol: 54 | continue 55 | existed_date.add(dt) 56 | return existed_date 57 | 58 | 59 | if __name__ == "__main__": 60 | main(sys.argv) 61 | -------------------------------------------------------------------------------- /sources/eastmoney.py: -------------------------------------------------------------------------------- 1 | import time 2 | import re 3 | import logging 4 | import json 5 | import requests 6 | from dateutil import tz,utils 7 | from datetime import datetime 8 | from urllib import error 9 | from math import log10, floor 10 | 11 | from beancount.core.number import D 12 | from beancount.prices import source 13 | from beancount.utils import net_utils 14 | 15 | 16 | CN_TZ = tz.gettz("Asia/Shanghai") 17 | 18 | 19 | class Source(source.Source): 20 | """ 21 | 东方财富天天基金基金净值 22 | 注意:需要在雪球标的前加F(方便区分基金与其他标的) 23 | 24 | PYTHONPATH=`pwd`/sources bean-price --no-cache -e CNY:eastmoney/F110011 25 | """ 26 | 27 | http = requests.Session() 28 | 29 | def get_historical_price(self, ticker, date): 30 | return self._get_daily_price(ticker, date) 31 | 32 | def get_latest_price(self, ticker): 33 | return self._get_daily_price(ticker) 34 | 35 | def _get_daily_price(self, fund, date=None): 36 | assert fund[0] == "F" 37 | params = { 38 | "callback": "thecallback", 39 | "fundCode": fund[1:], 40 | "pageIndex": 1, 41 | "pageSize": 1, 42 | 43 | } 44 | if date is not None: 45 | dt_str = date.strftime("%Y-%m-%d") 46 | params.update({ 47 | "startDate": dt_str, 48 | "endDate": dt_str, 49 | }) 50 | resp = self.http.get( 51 | "https://api.fund.eastmoney.com/f10/lsjz", 52 | params=params, 53 | headers={ 54 | "Referer": "http://fundf10.eastmoney.com/jjjz_{fund}.html" 55 | } 56 | ) 57 | assert resp.status_code == 200, resp.text 58 | result_str = next(re.finditer("thecallback\((.*)\)", resp.text)).groups()[0] 59 | result = json.loads(result_str) 60 | records = result["Data"]["LSJZList"] 61 | if len(records) == 0: 62 | return 63 | 64 | trade_date = records[0]["FSRQ"] 65 | nav = D(records[0]["DWJZ"]).quantize(D('1.000000000000000000')) 66 | trade_date = datetime.strptime(trade_date, "%Y-%m-%d") 67 | trade_date = utils.default_tzinfo(trade_date, CN_TZ) 68 | 69 | return source.SourcePrice(nav, trade_date, 'CNY') 70 | -------------------------------------------------------------------------------- /sources/exchangeratesapi.py: -------------------------------------------------------------------------------- 1 | import time 2 | import json 3 | import requests 4 | from dateutil import tz,utils 5 | from datetime import datetime 6 | 7 | from beancount.core.number import D 8 | from beancount.prices import source 9 | 10 | 11 | class Source(source.Source): 12 | """ 13 | PYTHONPATH=`pwd`/sources bean-price --no-cache -e CNY:exchangeratesapi/USDCNY 14 | """ 15 | 16 | http = requests.Session() 17 | 18 | def get_historical_price(self, ticker, date=None): 19 | return self._get_daily_price(ticker, date) 20 | 21 | def get_latest_price(self, ticker): 22 | return self._get_daily_price(ticker) 23 | 24 | def _get_daily_price(self, ticker, date=None): 25 | assert len(ticker) == 6, ticker 26 | base = ticker[:3] 27 | symbol = ticker[3:] 28 | if date is None: 29 | date_str = "latest" 30 | else: 31 | date_str = date.strftime("%Y-%m-%d") 32 | 33 | resp = self.http.get( 34 | "https://api.exchangeratesapi.io/" + date_str, 35 | params={ 36 | "symbols": symbol, 37 | "base": base, 38 | } 39 | ) 40 | result = resp.json() 41 | 42 | close_price = D(result["rates"][symbol]).quantize(D('1.000000000000000000')) 43 | trade_date = utils.default_tzinfo( 44 | datetime.strptime(result["date"], "%Y-%m-%d"), 45 | tz.UTC 46 | ) 47 | currency = base 48 | return source.SourcePrice(close_price, trade_date, currency) 49 | -------------------------------------------------------------------------------- /sources/xueqiu.py: -------------------------------------------------------------------------------- 1 | import time 2 | import json 3 | import requests 4 | from dateutil import tz,utils 5 | from datetime import datetime 6 | 7 | from beancount.core.number import D 8 | from beancount.prices import source 9 | 10 | EXPECTED_COLS = [ 11 | "timestamp", 12 | "volume", 13 | "open", 14 | "high", 15 | "low", 16 | "close", 17 | "chg", 18 | "percent", 19 | "turnoverrate", 20 | "amount", 21 | "volume_post", 22 | "amount_post" 23 | ] 24 | CN_TZ = tz.gettz("Asia/Shanghai") 25 | NY_TZ = tz.gettz("America/New_York") 26 | 27 | 28 | class Source(source.Source): 29 | """ 30 | 雪球 A股、港股、美股 31 | 注意:需要在雪球标的前加国家(方便区分时区) 32 | 33 | PYTHONPATH=`pwd`/sources bean-price --no-cache -e CNY:xueqiu/CN:SH510300 34 | PYTHONPATH=`pwd`/sources bean-price --no-cache -e HKD:xueqiu/HK:02800 35 | PYTHONPATH=`pwd`/sources bean-price --no-cache -e USD:xueqiu/US:SPY 36 | """ 37 | 38 | http = requests.Session() 39 | headers = requests.utils.default_headers() 40 | headers.update( 41 | { 42 | 'User-Agent': ('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) ' 43 | 'AppleWebKit/537.36 (KHTML, like Gecko) ' 44 | 'Chrome/73.0.3683.103 Safari/537.36'), 45 | } 46 | ) 47 | http.get("https://xueqiu.com/", headers=headers) 48 | 49 | def get_historical_price(self, ticker, date): 50 | return self._get_daily_price(ticker, date) 51 | 52 | def get_latest_price(self, ticker): 53 | return self._get_daily_price(ticker) 54 | 55 | def _get_daily_price(self, ticker, date=None): 56 | region, symbol = ticker.split(":", 1) 57 | if region in {"HK", "CN"}: 58 | exchange_tz = CN_TZ 59 | if region == "HK": 60 | currency = "HKD" 61 | else: 62 | currency = "CNY" 63 | else: 64 | assert region == "US" 65 | exchange_tz = NY_TZ 66 | currency = "USD" 67 | if date is None: 68 | trade_date = utils.default_tzinfo( 69 | datetime.now(), 70 | exchange_tz, 71 | ) 72 | else: 73 | trade_date = utils.default_tzinfo( 74 | datetime.combine(date, datetime.max.time()), 75 | exchange_tz 76 | ) 77 | begin = int(time.mktime(trade_date.timetuple())) * 1000 78 | url = ( 79 | f"https://stock.xueqiu.com/v5/stock/chart/kline.json?" 80 | f"symbol={symbol}&begin={begin}&period=day&" 81 | f"type=before&count=-1&indicator=kline" 82 | ) 83 | 84 | resp = self.http.get(url, headers=self.headers) 85 | assert resp.status_code == 200, resp.text 86 | result = resp.json() 87 | assert result["error_code"] == 0, result["error_description"] 88 | bar = result["data"]["item"][0] 89 | assert result["data"]["column"] == EXPECTED_COLS 90 | returned_ts = bar[EXPECTED_COLS.index("timestamp")] 91 | close_price = D( 92 | bar[EXPECTED_COLS.index("close")] 93 | ).quantize(D('1.000000000000000000')) 94 | 95 | trade_date = datetime.fromtimestamp(returned_ts/1000, exchange_tz) 96 | return source.SourcePrice(close_price, trade_date, currency) 97 | --------------------------------------------------------------------------------