wide and deep

複数アカウントのMoneyForwardから資産情報を取得して結合する方法で連携上限4件の縛りを突破する【docker, python, selenium, gspread】

はじめに

表題のことをやりたいです

2022/12/7 から無料会員アカウントでの連携上限数が10->4になるらしいです。

自分の連携数Xが 4 < X < 10 であり、総資産推移くらいしか見ないことから、
連携を複数アカウントに分散させ収集し結合すればいいじゃんと考えました。

過去に(無料連携数が10の頃に)1つのMoneyForwardアカウントから資産推移情報を持ってきてGoogleスプレッドシートに吐き出すところまで作っていたので、複数アカウントに対応することが今回のゴールになります。

catdance124.hatenablog.jp

catdance124.hatenablog.jp

↑ の記事を書いた頃からリポジトリ内容はだいぶ変わっており、いつの間にかdockerで動かすようになっています。
(前まで仮想ディスプレイとか設定が面倒でしたが、全部docker内でやることで何も考えなくてよくなりました…)

リポジトリはこちらです。

github.com

規約確認

複数アカウント利用が利用規約に抵触しないかが心配だったので、サポートに問い合わせてみました。

  • 問い合わせ内容(記録が残っていないのでざっくり)

    • 1個人による複数アカウントの利用は容認されているか
    • 容認されない場合、どのように本人確認を実施し同一人物ではないと判定するのか
  • 回答(原文ママ

    弊社サービスは、ご登録のメールアドレスでアカウントの管理を行っております。
    このため、ご登録メールアドレス以外の「氏名、電話番号、住所」等の個人情報は
    お預かりしておりません。
    ご登録メールアドレスとパスワードにてログイン可能な仕組みのため、
    確認されるアカウントを変更する際に、都度ログアウトのうえで、再ログインしていただく
    形にはなりますが、複数アカウントをご利用いただくことは可能でございます。

個人で複数アカウントを使っていいよとの回答なので、今回の開発内容は現時点で問題なさそうです。

構成

全体こんな感じです。 簡単な図

実行後、ファイル構成はこんな感じになります。

├── README.md
├── Dockerfile
├── docker-compose.yml
├── make_env.sh
├── requirements.txt
├── download
├── log
│   └── log
├── csv
│   ├── all_history_with_profit_and_loss.csv    ...    アップロード用に過去の資産推移情報と統合したもの
│   ├── concat
│   │   ├── all_history_with_profit_and_loss.csv    ...    アカウント1~3の資産推移情報を集約したもの
│   │   ├── portfolio_det_depo.csv
│   │   ├── portfolio_det_eq.csv
│   │   ├── portfolio_det_mf.csv
│   │   └── portfolio_det_pns.csv
│   ├── <アカウント1>
│   │   ├── all_history.csv
│   │   ├── all_history_with_profit_and_loss.csv    ...    アカウント1の資産推移情報
│   │   ├── history
│   │   │   └── this_month.csv
│   │   └── portfolio
│   │       ├── portfolio_det_eq.csv
│   │       └── portfolio_det_pns.csv
│   ├── <アカウント2>
│   │   ├── all_history.csv
│   │   ├── all_history_with_profit_and_loss.csv    ...    アカウント2の資産推移情報
│   │   ├── history
│   │   │   └── this_month.csv
│   │   └── portfolio
│   │       └── portfolio_det_depo.csv
│   └── <アカウント3>
│       ├── all_history.csv
│       ├── all_history_with_profit_and_loss.csv    ...    アカウント3の資産推移情報
│       ├── history
│       │   └── this_month.csv
│       └── portfolio
│           └── portfolio_det_mf.csv
└── src
    ├── client_secret.json
    ├── config.ini
    ├── download_history.py
    ├── export_gspread.py
    ├── mf2gs.py
    └── my_logging.py

(portfolio_**.csvが分かれているのは自分の例です)
src/config.ini で↓のように書いています。

[MONEYFORWARD]
Email = [
    "example1@hoge.com",
    "example2@hoge.com",
    "example2@hoge.com"
    ]
Password = [
    "password1",
    "password2",
    "password3"
    ]

[SPREAD_SHEET]
Key = 1_*****
Worksheet_name = 資産推移データ(自動入力)

[asset_depo]
id = portfolio_det_depo
column_name =
sheet_name = _預金・現金・暗号資産

[asset_eq]
id = portfolio_det_eq
column_name = 損益_株式(現物)
sheet_name = _株式(現物)

[asset_mf]
id = portfolio_det_mf
column_name = 損益_投資信託
sheet_name = _投資信託

[asset_pns]
id = portfolio_det_pns
column_name = 損益_年金
sheet_name = _年金

前からの変更点

アカウントごとに収集してくるようにしました。
ここで、用語は下記のとおりです。

  • 各月の資産推移 ... 名前の通り、各資産カテゴリごとの日別推移です。アカウントに登録して以降の情報をMoneyForwardは保持しています。
  • 各assetの資産内訳(損益) ... 各資産ごとの詳細情報です。MoneyForwardは損益情報を当日分しか保持していません。
    • そのため、資産推移と資産内訳(損益)を結合することで日ごとの損益情報をローカルcsvに保持します。

↓でcsv/<アカウント>/*.csvを収集・作成。

    # download each files
    for email, password in zip(emails, passwords):
        mf = Moneyforward(email=email, password=password)
        try:
            mf.login()
            mf.download_history()  #    ...    各月の資産推移を取得し結合する
            for asset_id in [asset['id'] for asset in assets]:
                mf.get_valuation_profit_and_loss(asset_id)  #    ...    各assetの資産内訳(損益)を取得する
            mf.calc_profit_and_loss(assets)  #    ...    資産推移と資産内訳(損益)を結合
        finally:
            mf.close()

↓で csv/<アカウントn>/*.csvからcsv/concat/*.csv を作成します。
*[!concat]と指定しないと結合したものを再計上しちゃうので気をつけないといけません(1敗)
結合方針は下記のとおりです。

  • 資産推移 ... sumを取る
  • 資産内訳(損益) ... axis=0で結合する
def concat_files(assets: list) -> Path:
    """
    複数アカウントから取得された資産推移と資産内訳(損益)を結合する
    Parameters
    ----------
    assets : list of dict
        各assetのidを含む辞書のリスト
    Returns
    -------
    output_path : Path
        各アカウント、各月、各assetの資産内訳(損益)を結合したcsvのパス
    """
    concat_csv_dir = root_csv_dir / "concat"
    concat_csv_dir.mkdir(exist_ok=True, parents=True)
    ## asset files
    for asset in assets:
        df_list = []
        for asset_csv_path in root_csv_dir.glob(f"*/portfolio/{asset['id']}.csv"):
            df = pd.read_csv(asset_csv_path, encoding="utf-8", sep=',')
            df_list.append(df)
        df_concat = pd.concat(df_list)
        df_concat.to_csv(concat_csv_dir / f"{asset['id']}.csv", encoding="utf-8", index=False)
    ## history files
    output_path = concat_csv_dir / "all_history_with_profit_and_loss.csv"
    df_concat = None
    for csv_path in root_csv_dir.glob(f"*[!concat]/all_history_with_profit_and_loss.csv"):
        df = pd.read_csv(csv_path, encoding="utf-8", sep=',')
        df.set_index('日付', inplace=True)
        df_concat = df_concat.add(df, fill_value=0) if df_concat is not None else df
    df_concat.sort_index(inplace=True, ascending=False)
    df_concat.to_csv(output_path, encoding="utf-8")
    return output_path

↓ でアップロード用の過去の情報と統合します。
なぜこんな処理があるかというと、今回の運用の都合上です。

  • MoneyForwardはアカウントに登録して以降の資産情報しか保持しない
  • 今回複数アカウントに分けて運用するため、新規アカウント登録が発生
  • 各アカウントは過去情報を持っていないため、収集・結合とは別に統合が必要

統合処理を挟むことで、前時代(無料連携数が10の頃)に収集した情報を無駄にしなくて済みました。

pd.mergeするときhow='outer'にしていますが、同じキーのレコードを持つ場合に第1引数のdataframeが優先される仕様を今回はじめて知りました(1敗)

    # concat each files
    new_all_history_wpl_csv_path = concat_files(assets)
    new_all_history_wpl = pd.read_csv(new_all_history_wpl_csv_path, encoding="utf-8", sep=',')

    # generate result csv
    all_history_wpl_csv_path = root_csv_dir / "all_history_with_profit_and_loss.csv"
    current_all_history_wpl = pd.read_csv(all_history_wpl_csv_path, encoding="utf-8", sep=',') if all_history_wpl_csv_path.exists() else new_all_history_wpl
    df_merged = pd.merge(new_all_history_wpl, current_all_history_wpl, how='outer')
    df_merged.drop_duplicates(subset='日付', inplace=True)
    df_merged.set_index('日付', inplace=True)
    df_merged.sort_index(inplace=True, axis='columns')
    df_merged.sort_index(inplace=True, ascending=False)
    df_merged.to_csv(all_history_wpl_csv_path, encoding="utf-8")

おわりに

MoneyForwardは便利ですが、これで資産推移グラフを見られるのでMoneyForwardに課金する必要がなくなりました

月500yenというのもチリツモなので...

あと、ソースに関数レベルでコメントをつけるようにしましたが、これがあれば解説書かなくていいじゃんと気づきました

github.com