====== moomoo証券: 取引履歴から利益を計算する ======
moomoo証券のUIは一見モダンだが、実績の確認が死ぬほどしづらく、資産残高が正しいかどうかの検証が非常に面倒くさい。
そのため、pythonを用いて、注文履歴のCSVから実現損益を計算するスクリプトを作った。
===== 使い方 =====
- 適当な場所から、usdjpyの日足終値のデータを見つけて、''usdjpy.csv''として保存
- moomoo証券の口座→CashまたはMargin→History→右上のハンバーガーメニュー→Exportから、CSVを保存
- 下のpythonコードにcsvを食わせる
===== 実行例 =====
左から順に、日付、費用(手数料+損失)、収益、利益の順に表示される。
$ python3 analyze.py cash cash.csv | column -t
12/30/2024 0 9952 9952
12/31/2024 0 7226 7226
01/06/2025 0 37993 37993
01/07/2025 0 23524 23524
01/10/2025 5478 0 -5478
01/14/2025 24807 0 -24807
01/30/2025 0 10974 10974
01/31/2025 0 12509 12509
$ python3 analyze.py margin margin.csv | column -t
01/06/2025 1598 0 -1598
01/15/2025 1458 41836 40378
01/16/2025 41590 10245 -31345
01/17/2025 36996 26189 -10807
01/21/2025 11257 32657 21400
01/23/2025 28821 12016 -16805
01/24/2025 10897 34714 23817
01/27/2025 35918 21044 -14874
01/28/2025 16843 23690 6847
01/29/2025 111954 25349 -86605
01/30/2025 30661 25189 -5472
01/31/2025 20618 47454 26836
02/02/2025 751 0 -751
02/03/2025 3884 50231 46347
===== スクリプト =====
import csv
import sys
from datetime import datetime
mode = sys.argv[1]
file = sys.argv[2]
# ---- usdjpyデータ読み込み
usdjpy = dict()
with open("usdjpy.csv", "r", encoding="utf-8") as f:
for idx, row in enumerate(csv.reader(f)):
if idx == 0: continue
usdjpy[row[0]] = float(row[1])
expense = dict()
revenue = dict()
days = list()
# ---- modeに応じてCSVのカラムインデックスを導く
if mode == "margin":
side_idx = 0
sym_idx = 1
qty_idx = 20
amount_idx = 22
dt_idx = 23
fee_idx = 33
fee_ratio = 0.00132
is_open = lambda x: x.endswith("Open")
is_close = lambda x: x.endswith("Close")
is_long = lambda x: x.startswith("Buy") == x.endswith("Open")
is_short = lambda x: x.startswith("Sell") == x.endswith("Open")
elif mode == "cash":
side_idx = 0
sym_idx = 1
qty_idx = 18
amount_idx = 20
dt_idx = 21
fee_idx = 30
fee_ratio = 0
is_open = lambda x: x == "Buy"
is_close = lambda x: x == "Sell"
is_long = lambda x: True
is_short = lambda x: False
else:
assert False
# ---- CSV 読み込み
with open(file, "r", encoding="utf-8") as f:
rows = []
for idx, row in enumerate(csv.reader(f)):
if idx == 0: continue
if row[qty_idx] == "": continue
side = row[side_idx]
sym = row[sym_idx]
qty = float(row[qty_idx])
amount = float(row[amount_idx].replace(",", ""))
dt = datetime.strptime(row[dt_idx], "%b %d, %Y %H:%M:%S ET")
if len(row) > fee_idx:
fee = float(row[fee_idx].replace(",", "")) if row[fee_idx] != "" else 0
else:
fee = min(fee_ratio * amount, 22)
if side == "":
pr = rows[-1]
rows[-1] = (pr[0], pr[1], pr[2], pr[3]+qty, pr[4]+amount, pr[5], pr[6]+fee)
else:
if is_long(side):
type_ = "Long"
elif is_short(side):
type_ = "Short"
else:
assert False
rows.append((side, type_, sym, qty, amount, dt, fee))
x = 0
positions = {}
for row in rows[::-1]:
side, type_, sym, qty, amount, dt, fee = row
dstr = dt.date().strftime("%m/%d/%Y")
if dstr not in days:
days.append(dstr)
p = f"{sym}/{type_}"
s = positions.get(p, (0, 0))
if is_open(side):
positions[p] = (s[0]+qty, s[1]+amount)
expense[dstr] = expense.get(dstr, 0) + fee
elif is_close(side):
avg = s[1]/s[0]
positions[p] = (s[0]-qty, s[1]-avg*qty)
if type_ == "Long":
earn = amount - avg*qty
elif type_ == "Short":
earn = avg*qty - amount
else:
assert False
expense[dstr] = expense.get(dstr, 0) + fee
if earn > 0:
revenue[dstr] = revenue.get(dstr, 0) + earn
else:
expense[dstr] = expense.get(dstr, 0) - earn
for day in days:
d = usdjpy[day]
e = int(expense.get(day, 0)*d)
r = int(revenue.get(day, 0)*d)
print(day, e, r, r-e)