Beancount 技术交流贴

有个问题咨询一下各位大神 :joy:
该怎么记录预付机票/酒店呢?

对于其它的预付我都有一个assets,例如assets:prepaid:gas

但我不知道要不要把已经定好的里程票记录在assets里面,这样会让balance sheet看起来有点怪怪,毕竟这部分点已经“不可用”了

遇事不决Equity

近日又尝试开始折腾。

第一步,用 marker 把 statement PDF 转为 Markdown。这个marker连US Bank那种transaction table乱七八糟的都能整理得干干净净,非常amazing!

from marker.converters.pdf import PdfConverter
from marker.models import create_model_dict
from marker.output import text_from_rendered

converter = PdfConverter(
    artifact_dict=create_model_dict(),
)
rendered = converter(file_path)
text, _, images = text_from_rendered(rendered)

print(text)

第二步,用 Instructor 把非结构化的 Markdown 转为结构化的 JSON。

Statement class 定义如下。银行相关的 account name 会从文件里面读出,动态生成一个Enum作为validator。Closing date,account和balance可以直接生成balance directive。Transactions需要再做处理生成transaction directives。这一步现在调用OpenAI的API,尝试过用local llm做,不给OpenAI喂个人数据,但是速度和结果还是差太多了。

def get_accounts() -> list[str]:
    beancount_file_path = "/path/to/my/accounts.beancount"
    accounts_file = open(beancount_file_path, "r", encoding="utf-8")
    accounts_lines = accounts_file.readlines()
    accounts = []
    for line in accounts_lines:
        parts = line.strip().split(" ")
        if (
            len(parts) >= 4
            and (
                parts[2].startswith("Assets:Bank:")
                or parts[2].startswith("Liabilities:CreditCard:")
                or parts[2].startswith("Liabilities:Loan:")
            )
        ):
            accounts.append(parts[2])

    return accounts

accounts = get_accounts()
Account = Enum("Account", ((a, a) for a in accounts), type=str)

class Transaction(BaseModel):
    date: datetime = Field(description="Post date")
    description: str = Field(description="Human readable transaction description")
    amount: float = Field(description="Transaction amount")
    is_credit: bool = Field(description="Is this a credit, e.g., payment or refund")

class BankStatement(BaseModel):
    closing_date: datetime = Field(description="Statement closing date")
    account: Account = Field(
        description="Account name determined by bank, card and owner"
    )
    balance: float = Field(description="New balance after this statement")
    is_credit: bool = Field(description="Is balance a credit, e.g., negative balance")
    transactions: List[Transaction] = Field(
        description="List of transactions in this statement"
    )

openai_api_key = "sk-xxxxx"
client = instructor.from_openai(OpenAI(api_key=openai_api_key))

def extract(content: str) -> BankStatement:
    statement = client.chat.completions.create(
        model="gpt-4o-mini",
        response_model=BankStatement,
        messages=[
            {
                "role": "user",
                "content": content,
            }
        ],
    )
    return statement

现在就做到这一步为止,整体结果可以说非常的好!但是,这里的transaction想要生成Beancount的transaction,还缺乏payee和posting account。后面的想法是对以前的transactions做embedding,然后新的transaction去模糊查询以前的transaction,匹配出payee和posting account。一个transaction拥有多个posting的情况估计还是需要手动处理。

最后一步是要把生成的directives放在合适的文件。每个人都有自己的beancount文件划分方式,这里估计会重复利用上一步的embedding数据库,猜出应该存放的文件路径。最后需要工具放在文件的合适位置(例如文件内部使用时间排序)。

大伙有什么想法?

6 个赞

:mobaidalao:
太牛了

我最近也在思考这个事情,我认为bean importer其实是一个高度定制化的东西
每个人对导入器的需求都不一样
就像我可能希望导入器可以自动生成对应的返点
然后我可能比较喜欢ofx导入,去重很方便

我还没有试过OFX哦。提供这个文件的银行多吗?它本身就是结构化数据吧?

这个目测很复杂。Groceries在不同银行还有不同定义,像UAR这种Apple Pay好像在 statement 在看不到具体信息吧。 :thinking:

只有大银行提供

是的,主要是会有一个ofx 交易 uuid,唯一的,查重很方便

我目前的想法是根据不同的银行/Account写一张不同的lookup table
但我还真不知道Groceries在不同银行还有不同定义?不都是一样的吗?

这些可能就要手动adjust一下了,或者默认UAR的消费都是Apple Pay,自动apply apple pay的points,然后少数非apple pay手动调整一下

其实我个人的想法是大部分的日常消费商户其实是比较固定的,如果能总结出lookup table,对于这部分经常重复交易的商户就很省事了。然后每次一旦有新的商户交易,再添加一条到DB就行了,不太需要用到LLM的

关键词->payee->Category
For each Credit Card account:
Category->CashBackRate->自动生成完整的一条记录

1 个赞

最典型的是Walmart吧。而Walmart又分Walmart Neighborhood Market和Walmart Supercemter。Neighborhood 那个有的时候有的地区会被算 groceries store的。

Wow,我在看chroma,他在添加记录的时候需要一个id。我才想起来beancount directive全程没有id这个概念,只能用index弱化表示。

你在导入之后,OFX的uuid会作为tag或者note挂在transaction下面吗?

Lookup table或者正则的最大的问题是,corner case处理不完,大量重复的人工工作,整体又不值得写code处理每一个小的corner case。我感觉积分这个也可以尝试用embedding+query history data的方式去先生成一遍,然后再修正。毕竟同一个店,同一种MS都是重复发生的,人工处理了第一遍,让LLM处理第二第三遍。

最后,积分记录会和 statement 上的数字 cross validate 一下吗?我的 transaction 上的信用卡的支出会最后匹配上 statement balance,以确保没有漏了transaction。积分记录多了少了好像也很难查出来?

:yaoming: bos没有Walmart,烦恼少一半

是的,会作为meta data

嗯嗯,也是个方案,可以试试

差不多每个季度或者半年check一次吧,然后稍微抹平一下diff。 :yaoming: 有些statement上会有点数summary
而且amex是有api可以导出点数记录的

我最近用llm导入 手动纠错一张卡的话15-30min 12个月的

1 个赞

这个速度很快了,基于ofx还是pdf?

直接pdf给llm 输出后自己人眼校对 最终跟我银行自己的差3刀多

其实人眼校对看数字和加tag都挺必要的… 我手动加了些重要交易或者旅游之类的tag

问下 beancount 的前端 fava 有办法自定义 column 的顺序吗?
之前是按照 currency 的定义顺序来的,后来某个版本更新了以后变成了按首字母……
USD 就排到很后面,不是很好找,谢谢。

有点东西,用的哪个llm?反正我试过让chatgpt OCR 不好用

就chatgpt的4.5啊

可能我那个时候还是3的时代吧

大佬们,你们是支出的时候手动记账,然后账单出来把记录的支出和账单对账吗?

我的方案是依赖账单,然后用备忘录记录一下现金交易

我最近看到一个介绍备忘录+AI的视频,感觉可以结合一下。

1 个赞

依赖账单真的不会拖一年没整理吗…比如我

会 ,我现在也是 :melting_face:

下个月应该总算忙完一段时间,再搞搞上面的方案 :face_with_peeking_eye:

具体怎么搞啊

我发现 https://global.americanexpress.com/rewards/summary 可以导出CSV,但是每次只让选30天,感觉太少了

edit:上面有

自己发个request可以下载任意时间区间的(最早是两年前而不是Amex网页显示的一年前),赞!

(不过联名卡好像不行?)

1 个赞