步骤/目录:
1.背景介绍
2.docker安装ghostfolio
3.ghostfolio的基本使用与配置
4.总结

本文首发于个人博客https://lisper517.top/index.php/archives/4667/,转载请注明出处。
本文的目的是使用ghostfolio搭建财富管理网页,直观展示个人或者家庭名下的存款、ETF、股票、期货、加密货币等资产。特别注意:不好用、没有什么实用价值,只是弄着玩。建议自行搭建前先去 官方站 建个账号试一下。
本文写作日期为2026年10月2日,在Ubuntu 22.04.5LTS系统演示。特别注意:笔者在1核/2核、2GB内存的云服务器上运行ghostfolio还是比较吃力的,死机了三四次也无法部署,有条件的建议用更好的服务器配置。
本文使用到的参考网页有:
(1)github-ghostfolio
(2)开源财富管理软件Ghostfolio的安装

1.背景介绍

笔者之前一直使用一张excel表格记录每一笔买入、卖出、分红(见 投资策略-定投 ),最近突发奇想要搭建一个网页仪表盘展示名下资产,问了AI得到ghostfolio这个工具。除了ghostfolio,还可选择
(1)Wealthfolio,可以搜到国内股票与场内基金,但搜不到场外基金,有桌面版。
(2)Capitrack,需自己搭建,未尝试。
(3)基金记账簿(fund-ledger),一款国内app。
(4)Fund Helper,国内开源,可自己部署,有 演示网页 。
但是要注意可能有泄露风险,不论开源/闭源、免费/收费,但资金少应该问题不大。

2.docker安装ghostfolio

如果无法安装docker或者使用docker官方仓库,可能是被封禁了,解决方法请自行搜索,或者参考 自建网盘-nextcloud 。

首先生成3个密码、1个密钥并保存,等下要用:

(运行3次)openssl rand -hex 16
openssl rand -base64 32

然后安装ghostfolio

mkdir -p /docker/ghostfolio/data/ghostfolio
mkdir -p /docker/ghostfolio/data/postgres
mkdir -p /docker/ghostfolio/data/redis
cd /docker/ghostfolio
vim .env

写入如下内容( 官方参考 ):

COMPOSE_PROJECT_NAME=ghostfolio

# CACHE
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=redis密码 # 改成刚才生成的密码之一

# POSTGRES
POSTGRES_DB=ghostfolio-db
POSTGRES_USER=user
POSTGRES_PASSWORD=postgres密码 # 改成刚才生成的密码之二

# VARIOUS
ACCESS_TOKEN_SALT=密码三 # 改成刚才生成的密码之三
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@gf-postgres:5432/${POSTGRES_DB}?connect_timeout=300
JWT_SECRET_KEY=密钥 # 改成刚才生成的密钥

注意:

DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@gf-postgres:5432/${POSTGRES_DB}?

其中的 gf-postgres 要写成下面yml文件中的postgres容器名。

然后写ghostfolio的yml文件:

(查询当前用户的uid、gid)id $user
vim docker-compose.yml

写入如下内容( 官方参考 )。由于笔者使用了NPM(nginx proxy manager),这里的yml稍有不同、改动处已做了注释,详见 可视化管理Nginx-Nginx Proxy Manager

name: ghostfolio
services:
  ghostfolio:
    image: docker.io/ghostfolio/ghostfolio:latest
    container_name: ghostfolio
    restart: unless-stopped
    init: true
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true
    env_file:
      - ./.env
    #ports: # 若未使用NPM则解除注释
      #- 3333:3333
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ['CMD-SHELL', 'curl -f http://localhost:3333/api/v1/health']
      interval: 10s
      timeout: 5s
      retries: 5

  postgres:
    image: docker.io/library/postgres:15-alpine
    container_name: gf-postgres
    restart: unless-stopped
    cap_drop:
      - ALL
    cap_add:
      - CHOWN
      - DAC_READ_SEARCH
      - FOWNER
      - SETGID
      - SETUID
    security_opt:
      - no-new-privileges:true
    env_file:
      - ./.env
    healthcheck:
      test:
        ['CMD-SHELL', 'pg_isready -d "$${POSTGRES_DB}" -U $${POSTGRES_USER}']
      interval: 10s
      timeout: 5s
      retries: 5
    volumes:
      - ./data/postgres:/var/lib/postgresql/data

  redis:
    image: docker.io/library/redis:alpine
    container_name: gf-redis
    restart: unless-stopped
    user: '0:0' # 改成id $user查到的uid、gid
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true
    env_file:
      - ./.env
    command:
      - /bin/sh
      - -c
      - redis-server --requirepass "$${REDIS_PASSWORD:?REDIS_PASSWORD variable is not set}"
    healthcheck:
      test:
        ['CMD-SHELL', 'redis-cli --pass "$${REDIS_PASSWORD}" ping | grep PONG']
      interval: 10s
      timeout: 5s
      retries: 5
    volumes:
      - ./data/redis:/data

# 没使用NPM则删去以下4行
networks:
  default:
    external: true
    name: NPM_group

最后运行即可:

cd /docker/ghostfolio
docker-compose config
docker-compose up

由于笔者使用了NPM(nginx proxy manager),还需要在NPM后台设置一下映射。如果没用NPM,就从 http://localhost:3333 访问即可,如果有防火墙的记得打开:

ufw allow 3333 comment "ghostfolio"

3.ghostfolio的基本使用与配置

(1)注册admin账号

登录网页后创建一个账号,并保存好令牌。第一个账号是admin管理员账号,然后可以在后台关闭注册( 管理-概述-用户注册 ,关闭)。

(2)添加货币、平台与资产

参考 官方网页 。
在 管理-市场数据 中,点击右下角的加号,添加货币CNY,添加自定义的资产。但是笔者遇到一些网络问题,yahoo不对大陆提供服务。
在 管理-设置 中,可以添加平台。

(3)数据导入

参考了 github-ghostfolio 。
ghostfolio可以导入交易数据,格式为:

{
  "activities": [
    {
      "currency": "USD",
      "dataSource": "YAHOO",
      "date": "2021-09-15T00:00:00.000Z",
      "fee": 19,
      "quantity": 5,
      "symbol": "MSFT",
      "type": "BUY",
      "unitPrice": 298.58
    }
  ]
}

每一项的解释为:

首先使用如下python代码将excel表格记录的所有交易转换为可以导入的形式(以下代码使用AI完成):

import pandas as pd
import json
from datetime import datetime

# ==================== 配置区 ====================
INPUT_FILE = "交易记录.xlsx"  # 你的WPS表格文件
OUTPUT_FILE = "ghostfolio_import.json"  # 输出的JSON文件
CURRENCY = "CNY"  # 货币类型
DATA_SOURCE = "MANUAL"  # 数据源,可选 YAHOO, COINGECKO 等

# =================================================


def parse_date(date_str):
    """将日期字符串转换为ISO-8601格式"""
    # 处理 pandas 可能读取为 Timestamp 的情况
    if isinstance(date_str, pd.Timestamp):
        dt = date_str.to_pydatetime()
    else:
        # 尝试解析常见格式
        for fmt in ("%Y-%m-%d %H:%M:%S", "%Y/%m/%d", "%Y-%m-%d"):
            try:
                dt = datetime.strptime(str(date_str).strip(), fmt)
                break
            except ValueError:
                continue
        else:
            raise ValueError(f"无法解析日期: {date_str}")
    return dt.strftime("%Y-%m-%dT00:00:00.000Z")


def extract_symbol(code_name):
    """从'代码/名称'中提取代码"""
    if pd.isna(code_name):
        return None
    # 按 '/' 分割,取第一部分
    parts = str(code_name).split("/")
    return parts[0].strip()


def map_activity_type(op):
    """将操作类型映射为 Ghostfolio 的类型"""
    op = str(op).strip()
    if op == "买入":
        return "BUY"
    elif op == "卖出":
        return "SELL"
    elif op == "分红":
        return "DIVIDEND"
    else:
        # 对于其他类型(如分红再投、期货转入等),暂时跳过
        return None


def convert():
    # 读取 Excel 文件,指定表头行
    # 根据你提供的表格,第1行是表头(A1: 日期, B1: 品类, ...)
    df = pd.read_excel(INPUT_FILE, header=0)

    # 清理列名(去除首尾空格)
    df.columns = df.columns.str.strip()
    # 重置索引,避免 idx 为 Timestamp 等类型
    df = df.reset_index(drop=True)

    activities = []
    skipped = 0

    for i, (idx, row) in enumerate(df.iterrows(), start=2):
        try:
            # ---- 1. 日期 ----
            date_val = row.get("日期")
            if pd.isna(date_val):
                skipped += 1
                continue
            date_iso = parse_date(date_val)

            # ---- 2. 操作类型 ----
            op = row.get("操作")
            activity_type = map_activity_type(op)
            if activity_type is None:
                # 跳过不支持的类型(如分红再投、期货转入等)
                skipped += 1
                continue

            # ---- 3. 代码 ----
            symbol = extract_symbol(row.get("代码/名称"))
            if not symbol:
                skipped += 1
                continue

            # ---- 4. 单价 ----
            unit_price = row.get("单价", 0)
            if pd.isna(unit_price):
                unit_price = 0.0
            else:
                unit_price = float(unit_price)

            # ---- 5. 数量 ----
            quantity = row.get("份额/股数", 0)
            if pd.isna(quantity):
                quantity = 0.0
            else:
                quantity = float(quantity)

            # ---- 6. 手续费 ----
            fee = row.get("手续费", 0)
            if pd.isna(fee):
                fee = 0.0
            else:
                fee = float(fee)

            # ---- 7. 备注(可选) ----
            comment = row.get("备注", "")
            if pd.isna(comment):
                comment = ""
            else:
                comment = str(comment).strip()

            # ---- 构建 activity 对象 ----
            activity = {
                "currency": CURRENCY,
                "dataSource": DATA_SOURCE,
                "date": date_iso,
                "fee": fee,
                "quantity": quantity,
                "symbol": symbol,
                "type": activity_type,
                "unitPrice": unit_price,
            }

            # 如果有备注,加入 comment 字段
            if comment:
                activity["comment"] = comment

            activities.append(activity)

        except Exception as e:
            print(f"⚠️ 第 {i} 行处理失败: {e}")
            skipped += 1

    # 构建最终输出
    output = {"activities": activities}

    with open(OUTPUT_FILE, "w", encoding="utf-8") as f:
        json.dump(output, f, ensure_ascii=False, indent=2)

    print(f"✅ 转换完成!共生成 {len(activities)} 条活动记录。")
    print(f"⏭️ 跳过 {skipped} 条(不支持的类型或数据异常)。")
    print(f"📄 输出文件: {OUTPUT_FILE}")


if __name__ == "__main__":
    convert()

然后导入即可,但是要注意ghostfolio一次只能导入25条记录,还需要分割一下,这个就留给读者自行完成。

4.总结

缺点主要有:
(1)为美股设计,墙+没有股票代码,大陆无法自动获取数据,手动自定义资产太累,除非一开始就用这个。
(2)一次只能导入25条交易记录,整笑了。
(3)一笔交易要填数量、单价、手续费,对于股票来说可以,但是基金的计算是 (总价-手续费)/净值 ,它的计算方法是 总价=数量*单价+手续费 ,导致基金买卖有误差。
(4)后续不更新,以及数据泄露的风险。应该是21年推出的。
总的来说,还是excel表格稳定,不担心跑路。

标签: none

添加新评论