Featured image of post 用 Obsidian 制作个人资产趋势分析看板

用 Obsidian 制作个人资产趋势分析看板

使用 Markdown、DataviewJS 和 Charts 插件,在 Obsidian 中搭建一个本地化、可自动更新的个人资产趋势分析看板。

-

不知道有没有人想持续观察自己的资产增长趋势,本文介绍一种基于 Obsidian 适合个人长期使用的方案:

用 Markdown 保存资产数据,用 DataviewJS 读取和汇总,用 Charts 绘制可交互图表。

这套方案运行在 Obsidian Vault 中,不依赖外部数据库,也不需要把资产数据上传到第三方服务。新增一条资产记录后,看板中的总资产趋势、分类资产趋势、资产分布图和账户余额表都会自动更新。而且,不止电脑上,手机上也支持。

最终效果

完成后,你会得到一个资产分析看板,用来展示:

  • 最新总资产
  • 相比上一次盘点的资产变化额
  • 总资产增长曲线
  • 银行卡、支付账户、股票、基金、理财等分类趋势
  • 最新资产分类及占比
  • 每个账户的最新余额或市值

Obsidian 个人资产趋势看板效果

这个看板不追求复杂的财务模型,而是解决一个最常见的需求:用尽可能低的维护成本,长期跟踪自己的资产变化。

实现思路

本文方案依赖两个 Obsidian 社区插件:

  1. Dataview:读取 Markdown 数据,并通过 DataviewJS 完成聚合计算。
  2. Charts:把 DataviewJS 计算出来的数据渲染成折线图、环形图等 Chart.js 图表。

二者的分工很清晰:

  • Markdown 表格负责保存原始资产记录。
  • DataviewJS 负责读取表格、补齐账户余额、计算总资产和分类资产。
  • Charts 负责把计算结果渲染成图表。

在 Obsidian 的 设置 → 社区插件 中分别安装并启用 Dataview 和 Charts。启用后,还需要确认 Dataview 的 JavaScript 查询能力可用,否则 dataviewjs 代码块无法执行。

文件结构

建议在 Vault 中新建一个 property 目录,结构如下:

1
2
3
4
5
property/
├── README.md              # 录入规则和字段说明,可选
├── 资产趋势.md            # DataviewJS + Charts 自动看板
└── data/
    └── asset_snapshots.md # 资产数据源

其中:

  • asset_snapshots.md 只负责保存资产数据。
  • 资产趋势.md 负责读取数据、计算结果并展示图表。

这样做的好处是数据和展示逻辑分离:以后想调整图表样式时,不需要修改原始数据;想新增资产记录时,也不需要碰看板代码。

资产数据格式

data/asset_snapshots.md 使用 Markdown 表格保存数据:

1
2
3
4
5
| date | category | account | amount | note |
| --- | --- | --- | ---: | --- |
| 2026-09-01 | 银行卡 | 工商银行储蓄卡 | 50000 | 初始值 |
| 2026-09-01 | 股票 | 某券商账户 | 120000 | 市值+现金 |
| 2026-09-01 | 基金 | 某基金账户 | 80000 | 市值 |

字段含义如下:

字段含义
date记录日期,建议使用 YYYY-MM-DD
category资产类别,例如银行卡、支付账户、股票、基金、债券/理财、其他
account稳定的账户名称,例如“工商银行储蓄卡”“某券商账户”
amount该账户当前总余额或总净值,单位为人民币元
note可选备注,例如“初始值”“市值变化”“消费后余额”

这里有一个关键设计:amount 记录的是账户当时的总额,不是当天变化额。

例如股票账户建议记录“持仓市值 + 账户现金”,基金账户记录当前总市值,银行卡记录当前余额。这样每个账户在任意日期都有一个明确的资产值,后续计算会更简单。

为什么采用“变动记录制”

如果每次盘点都把所有账户重新填写一遍,时间久了会非常繁琐,也容易因为重复录入导致错误。更推荐使用“变动记录制”:

  1. 第一次使用时,为每个账户录入初始金额。
  2. 后续只有某个账户的余额或市值发生变化时,才新增一行。
  3. 没有变化的账户不需要重复填写。
  4. 看板会自动沿用该账户最近一次已知余额。

例如:

1
2
3
4
5
6
| date | category | account | amount | note |
| --- | --- | --- | ---: | --- |
| 2026-09-01 | 银行卡 | 工商银行 | 50000 | 初始值 |
| 2026-09-01 | 股票 | 某券商账户 | 120000 | 初始值 |
| 2026-09-03 | 股票 | 某券商账户 | 122000 | 市值变化 |
| 2026-09-05 | 银行卡 | 工商银行 | 48000 | 消费后余额 |

在这个例子中:

  • 9 月 3 日只更新股票账户,不需要重新填写银行卡。
  • 9 月 5 日只更新银行卡,不需要重新填写股票账户。
  • 计算 9 月 5 日总资产时,看板会使用银行卡的 48000 和股票账户最近一次记录的 122000。

这种方式牺牲了一点点计算逻辑的复杂度,但大幅降低了日常维护成本。

自动看板代码

下面是 资产趋势.md 的核心实现。把以下代码放到一个 dataviewjs 代码块中即可:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
const raw = await dv.io.load("data/asset_snapshots.md");
if (!raw) {
  dv.paragraph("cant find data/asset_snapshots.md");
  return;
}
const lines = raw.trim().split(/\r?\n/).filter(x => x.trim().startsWith("|"));
const parse = x => x.trim().replace(/^\||\|$/g, "").split("|").map(v => v.trim());
const h = parse(lines.shift());
lines.shift();
const data = lines.map(l => {
  const c = parse(l),
    o = {};
  h.forEach((k, i) => o[k] = c[i] ?? "");
  o.amount = Number(o.amount) || 0;
  return o;
});
if (!data.length) {
  dv.paragraph("暂无数据");
  return;
}
const dates = [...new Set(data.map(x => x.date))].sort();
const accounts = [...new Set(data.map(x => x.account))];
const cats = [...new Set(data.map(x => x.category))];
const latestAt = (a, d) => data
  .filter(x => x.account === a && x.date <= d)
  .sort((x, y) => x.date.localeCompare(y.date)).at(-1);
const balancesAt = d => accounts.map(a => latestAt(a, d)).filter(Boolean);
const total = d => balancesAt(d).reduce((s, x) => s + x.amount, 0);
const latest = dates.at(-1),
  previous = dates.at(-2),
  now = total(latest);
const money = n => "¥" + Math.round(n).toLocaleString("zh-CN");
dv.table(["指标", "数值"], [
  ["最新日期", latest],
  ["总资产", money(now)],
  ["较上次", previous ? money(now - total(previous)) : "—"],
  ["账户数", accounts.length]
]);

const render = async (title, config) => {
  dv.header(3, title);
  if (window.renderChart) {
    const host = dv.el("div", "", {
      cls: "asset-chart-host"
    });
    window.renderChart(config, host);
  } else dv.paragraph("⚠️ Charts 插件未启用");
};
const colors = [
  '#5B7FFF',
  '#FF9F7B',
  '#7BC8A4',
  '#F4A7B9',
  '#B8A9D4',
  '#79C9D1',
  '#E8B87C'
];
const lineOptions = {
  responsive: true,
  plugins: {
    legend: {
      position: "bottom"
    }
  },
  scales: {
    y: {
      beginAtZero: true
    }
  }
};

// ---- 总资产趋势 ----
render("总资产趋势", {
  type: "line",
  data: {
    labels: dates,
    datasets: [{
      label: "总资产",
      data: dates.map(total),
      borderColor: colors[0],
      borderWidth: 3,
      tension: 0.25,
      fill: false
    }]
  },
  options: lineOptions
});

// ---- 分类资产趋势 ----
const catDatasets = cats.map((c, i) => ({
  label: c,
  data: dates.map(d => balancesAt(d).filter(x => x.category === c).reduce((s, x) => s + x.amount, 0)),
  borderColor: colors[i % colors.length],
  borderWidth: 2,
  tension: 0.25,
  fill: false
}));
render("分类资产趋势", {
  type: "line",
  data: {
    labels: dates,
    datasets: catDatasets
  },
  options: lineOptions
});

// ---- 资产分布圆环图 ----
const latestData = cats.map(c =>
  balancesAt(latest).filter(x => x.category === c).reduce((s, x) => s + x.amount, 0)
);
render("资产分布", {
  type: "doughnut",
  data: {
    labels: cats,
    datasets: [{
      label: latest,
      data: latestData,
      backgroundColor: colors
    }]
  },
  options: {
    responsive: true,
    plugins: {
      legend: {
        position: "bottom"
      }
    }
  }
});

dv.header(3, "各账户余额");
dv.table(
  ["类别", "账户", "最新日期", "金额"],
  balancesAt(latest).sort((a, b) => b.amount - a.amount).map(x => [
    x.category,
    x.account,
    x.date,
    money(x.amount)
  ]));

这段代码主要做了几件事:

  1. 读取 data/asset_snapshots.md 中的 Markdown 表格。
  2. 把每一行记录转换成 JavaScript 对象。
  3. 找出所有日期、账户和资产类别。
  4. 对每个日期计算“当时每个账户的最近一次余额”。
  5. 汇总总资产、分类资产和账户余额。
  6. 调用 Charts 插件渲染折线图和环形图。

最核心的是 latestAtbalancesAt

  • latestAt(account, date):找到某个账户在某个日期之前的最近一次记录。
  • balancesAt(date):计算某个日期所有账户的有效余额。

有了这两个函数,就可以自然地支持“只记录发生变化的账户”。

日常使用流程

日常维护时,只需要操作数据文件:

  1. 打开 data/asset_snapshots.md
  2. 复制一行已有记录。
  3. 修改日期、分类、账户和金额。
  4. 可在备注中写明“消费”“转入”“市值变化”等原因。
  5. 打开 资产趋势.md 查看最新图表结果。

例如股票账户市值从 120000 变为 122000,只需新增:

1
| 2026-09-03 | 股票 | 某券商账户 | 122000 | 市值变化 |

不需要重新填写其他没有变化的账户。

使用建议

为了让数据长期可用,建议遵守几条简单规则:

  • 账户名称保持稳定:不要今天写“招行”,明天写“招商银行”,否则会被识别成两个账户。
  • 日期格式保持统一:统一使用 YYYY-MM-DD,便于按时间排序。
  • 金额只填数字:不要在 amount 里写 ¥、逗号或中文单位。
  • 投资账户记录总资产:股票、基金、理财账户建议记录“市值 + 现金”。
  • 定期备份 Vault:资产数据虽然只保存在本地,但仍然需要备份,避免误删或设备损坏。

如果数据比较敏感,也可以单独给这个目录配置同步规则,或者只在本地设备中维护,不参与云同步。

可扩展方向

这个基础看板还可以继续扩展,例如:

  • 增加负债数据,计算净资产。
  • 增加月度收益率和年度收益率。
  • 为账户增加多币种账户分析。
  • 增加柱状图,对比不同月份的资产规模。
  • 增加目标资产线,观察距离阶段目标还有多远。
  • 增加现金、权益、固收等大类资产占比。

如果后续数据量变大,也可以把不同年份的数据拆成多个 Markdown 文件,再由 DataviewJS 统一读取。

总结

这套方案的核心价值不是“做一个复杂的财务系统”,而是用 Obsidian 把个人资产数据沉淀下来,并自动生成可持续观察的趋势看板。

Markdown 负责可读和可备份,DataviewJS 负责计算,Charts 负责展示。只要定期维护少量资产记录,就能长期看到自己的总资产变化、分类结构和账户分布,对个人财务状况形成更直观的判断。