ごごちと申します。
仮想通貨投資を1年半ほどやっています。
今回は、Pythonを使った仮想通貨のログを記録するプログラムを作成したので紹介したいと思います。
(2023年5月6日追記)
プログラムの定期実行はこちら
スリープ中でもPythonスクレイピングプログラムを定期実行する方法【タスクスケジューラ】【Windows10】
(2023年5月7日追記)
環境構築について記事を書きました。
DebankのPythonスクレイピングプログラムの環境構築!chromedriverまで解説!
きっかけ
仮想通貨投資は知人に教えられて始めました。
私は仮想通貨投資の中でも特に、分散型金融(Decentralized Finance : Defi)に興味を持ちました。
分散型取引所(Decentralized Exchance:DEX)に仮想通貨をプールし、取引手数料を報酬として受け取ることができます。
これは分散型金融における流動性の提供(Liquidity Provider : LP)による報酬といいます。
私はDefiを活用し、LP報酬によって長期的に利益が得られるかを観察しようと思いました。
Defiの資産額はDebankというページで確認することができますので、
2022年1月ごろから現在に至るまで、毎日手作業で分散型取引所に置いた資産や取引手数料の記録を行ってきました。
しかし、手作業で記録する方式だと、以下のデメリットがあります。
- ログを手作業で記録するのが面倒
- プールする資産が増えるほど手間もかかる
- 記録する時間もまちまち
そこで、Debankに対してPythonスクレイピングすることで記録を自動取得することにしました。
最近は仕事でもPythonを使う様になっていますが、スクレイピングをしたことはなかったので、
GW中のまとまった時間で、勉強も兼ねて仮想通貨記録プログラムを作成しました。
記録内容
Debankのページから記録したい情報は以下の通りです。
①全体 : 総資産額
②Wallet : 総資産額、および各Tokenの価格と数量と総額
③Defi : 分散型取引所毎の総資産額、Token Pair毎のpool数量と金額、LP報酬量と金額
私はWalletにはあまり資産を置いておかず、
DefiのLP poolに置いておく方針としておりますので、Defiの記録にこだわりました。
Defiにもさまざまな分散型取引所があります。
CoinMarketCapにさまざまな分散型取引所がランキングでまとめてあります。
私は、9位のUniswap V3(Polygon)をメイン、6位のPancakeswap V3(BSC)をサブとして利用しています。
比較のため、これら2つの分散型取引所ごとに成績をまとめることを重視しました。
スクレイピングの方針
Pythonを用いたスクレイピングには大きく分けて2つの方法があります。
書籍「Pythonスクレイピング&クローリング データ収集マスタリングハンドブック」を参考にしました。
requestsライブラリによる静的HTMLの取得
静的HTMLはWebページが最初に読み込まれた際にサーバーから送られてくるHTMLです。
右クリック > ページのソースを検証で静的HTMLを表示できます。
この静的HTMLに所望の情報があることが分かった場合は、requestsライブラリを用います。
seleniumライブラリによる動的HTMLの取得
動的HTMLはWebページが表示されてからJavaScriptなどのプログラムによって動的に生成されるHTMLです。
Google Chromeの場合、「Ctrl + Shift + C」でインスペクター機能を起動し、
オブジェクトの上に置くと対応する動的HTMLの要素を特定することができます。
この動的HTMLで表示される情報を扱いたい場合は、seleniumライブラリを用います。
Debankに対するseleniumによる動的HTMLの操作
図にGoogle ChromeでDebankのprofileのページを開いた様子を示します。
右クリック > ページのソースを検証で静的HTMLを表示できますが、
静的HTMLからは資産額等の情報を取得することができないことが確認できました。
一方、「Ctrl + Shift + C」でChromeのインスペクター機能を起動し、オブジェクトの上に置くと
対応する動的HTML要素を特定することができます。
今回取得したい情報はすべて動的HTMLで生成され表示されていることが確認できたので、
seleniumライブラリを使ってGoogle Chromeを操作することにしました。
実行環境について
簡単ではありますが実行手順を順番にまとめました。
詳細についてGW中にできれば作成したいと思います。
OSはwindowsです。
- Anacondaでpythonをインストール(versionは最新のもの)
- Anaconda Powershell で「py_debank」というconda仮想環境を作成
- Google Chromeのversionを確認し、対応する「chromedriver.exe」をフォルダに設置
- portfolio.pyを作成
- Anaconda Powershell のpy_debank仮想環境にて、python portfolio.pyを実行
- 「record_YYYYMMDD_HHMMSS.xlsx」が作成される
seleniumを用いる際には環境構築でつまづくことが多いといわれています。
特に、3. のchromedriver.exe をフォルダに設置するところで私も戸惑いました。
苦労話で恐縮ですが、私自身も試行錯誤の末、chromeが立ち上がって喜びました。
プログラム実行結果
プログラム「portfolio.py」の実行結果は以下のようになります。
コードの中身は後ほど紹介します。
プログラム「portfolio.py」を実行すると、
「portfolio.py」と同じフォルダ内に、「record_(実行時点の日時).xlsx」が作成されます。
フォルダ内に既に「record_YYYYMMDD_HHMMSS.xlsx」がある場合、
最新のファイルを読み込み、対応するシートの一番下の行に記録を追加します。
xlsxファイルにはsummary、Uniswap V3、Pancakeswap V3、wallet、 Price という5種類のシートがあります。
すべてのシートで、インデックス(A列)は日時です。
summary シート
「summary」シートには全体成績をまとめています。
総資産額、Defi全体の資産額、Defiで得られたLP報酬額およびLP報酬額の資産額に対する割合(%)などをまとめています。
ここには入金した金額などは含まれていないので、別のファイルに入金した金額をまとめておき、
将来的には入金した金額に対するReturnを計算するようにしたいです。
Uniswap V3, Pancakeswap V3 シート
「Uniswap V3」シートおよび「Pancakeswap V3」シートにはプールしたTokenのペア毎に成績をまとめています。
後でどの取引所のどのペアが良い成績だったかを辿れるようにするために、シートを分けて保存しています。
wallet シート
「wallet」シートにはwallet内のTokenをまとめています。
今回、私はwalletの記録にはあまりこだわっていなかったため、
チェーンが異なっていても同じTokenなら数量と総額を合算しています。
チェーンごとに分けて列を作ることも可能ですが、今回は実装していません。
price シート
「price」シートにはtoken1枚あたりの価格をまとめています。
ここでもチェーンが異なると同じtokenでも価格が微妙に異なりますが、
今回の用途では問題にならないと思ったので、
debankの並び順で最後に取得した価格を記すようにしています。
実行プログラム
なんとか動作するプログラム「portfolio.py」を作ることができました。
ChatGPTを活用してなるべく簡略化しようと努力しましたが、
まだ改善の余地はありそうです。
import pandas as pd
import time
from datetime import datetime
import openpyxl
from openpyxl import load_workbook
from openpyxl.utils.dataframe import dataframe_to_rows
from openpyxl.styles import Font
import os.path
import glob
from selenium import webdriver
from selenium.webdriver.common.by import By
# wallet情報をまとめるための関数
def create_wallet(TokenWallet_elements):
# DataFrameを作成
df_wallet = pd.DataFrame(index=[now])
df_wallet["Total_$"]=0
#行の数だけ繰り返す
for i,token in enumerate(TokenWallet_elements):
text = token.text.split("\n")
asset = text[0]
price = float(text[1].replace(",", "").replace("$", ""))
amount = float(text[2].replace(",", ""))
USD_value = float(text[3].replace(",", "").replace("$", ""))
if asset not in df_wallet.columns:
df_wallet[asset]=price
df_wallet[asset +"_amount"]=amount
df_wallet[asset +"_USD_value_$"]=USD_value
#同じトークンでチェーンが違うものは足してしまう。
else:
df_wallet[asset]=price
df_wallet[asset +"_amount"]+=amount
df_wallet[asset +"_USD_value_$"]+=USD_value
df_wallet["Total_$"] += USD_value
return df_wallet
# Tokenの価格情報をまとめるための関数
def create_price(TokenWallet_elements):
# DataFrameを作成
df_price = pd.DataFrame(index=[now])
for i,token in enumerate(TokenWallet_elements):
text = token.text.split("\n")
asset = text[0]
balance = float(text[1].replace(",", "").replace("$", ""))
df_price[asset] = [balance]
return df_price
# 取引所毎に成績をまとめるための関数
def create_defi(project):
df_defi = pd.DataFrame(index=[now])
project_name = project.find_element(By.XPATH, './/*[contains(@class,"ProjectTitle_name")]').text
project_portfolio_elements = project.find_elements(By.XPATH, './/*[contains(@class,"table_contentRow")]')
df_defi[project_name+"_total_$"]=0
df_defi[project_name+"_total_Reward_$"]=0
df_defi[project_name+"_total_Reward_%"]=0
for i,defi in enumerate(project_portfolio_elements):
# 改行文字で区切ってリストに格納
text_list = defi.text.split("\n")
total_asset = float(text_list[-1].replace("$", "").replace(",", ""))
df_defi[f"{i}_"+text_list[0]+"_pool_$"] = [total_asset]
df_defi[f"{i}_"+text_list[0]+"_pool_Reward_$"] = 0
df_defi[f"{i}_"+text_list[0]+"_pool_Reward_%"] = 0
# 各poolごとのループ
for j,text in enumerate(text_list[1:5]):
col = text.split()[1]
value = float(text.replace(",", "").split()[0])
if col not in ["USDC","USDT"]:
price = value*df_price.loc[now,col]
else:
price = value
if j in [2,3]:
col += "_Reward"
df_defi[f"{i}_"+text_list[0]+"_pool_Reward_$"] += price
df_defi[f"{i}_" + col] = value
df_defi[f"{i}_" + col+"_$"] = price
df_defi[f"{i}_"+text_list[0]+"_pool_Reward_%"] = df_defi[f"{i}_"+text_list[0]+"_pool_Reward_$"] /total_asset*100
df_defi[project_name+"_total_$"] += total_asset
df_defi[project_name+"_total_Reward_$"] += df_defi[f"{i}_"+text_list[0]+"_pool_Reward_$"]
df_defi[project_name+"_total_Reward_%"]=df_defi[project_name+"_total_Reward_$"] /df_defi[project_name+"_total_$"] *100
return df_defi
# 総合成績をまとめるための関数
def create_summary(df_wallet,df_defi):
df_summary = pd.DataFrame(index=[now])
df_summary["all_asset_$"]=0
df_summary["Defi_Total_$"]=0
df_summary["Defi_Total_Reward_$"]=0
df_summary["Defi_Total_Reward_%"]=0
df_summary["Uniswap_Total_$"]=df_defi["Uniswap V3_total_$"]
df_summary["Uniswap_Reward_$"]=df_defi["Uniswap V3_total_Reward_$"]
df_summary["Uniswap_Reward_%"]=df_defi["Uniswap V3_total_Reward_%"]
df_summary["PancakeSwap_Total_$"]=df_defi["PancakeSwap V3_total_$"]
df_summary["PancakeSwap_Reward_$"]=df_defi["PancakeSwap V3_total_Reward_$"]
df_summary["PancakeSwap_Reward_%"]=df_defi["PancakeSwap V3_total_Reward_%"]
df_summary["Defi_Total_$"] = df_summary["Uniswap_Total_$"] + df_summary["PancakeSwap_Total_$"]
df_summary["Defi_Total_Reward_$"]= df_summary["Uniswap_Reward_$"] + df_summary["PancakeSwap_Reward_$"]
df_summary["Defi_Total_Reward_%"]= df_summary["Defi_Total_Reward_$"]/df_summary["Defi_Total_$"]*100
df_summary["Wallet_total_$"]=df_wallet["Total_$"]
df_summary["all_asset_$"]=df_summary["Defi_Total_$"]+df_summary["Wallet_total_$"]
return df_summary
############################################################################################################
#chromeでアクセス
options = webdriver.ChromeOptions()
# XXXにchromedriver.exeの保存場所を入力
service = webdriver.chrome.service.Service(executable_path = 'C:\\Users\\XXX\\XXX\\XXX\\chromedriver.exe')
driver = webdriver.Chrome(service=service, options=options)
# XXX...にMetamaskのアドレスを入力
driver.get('https://debank.com/profile/0xXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX')
############################################################################################################
time.sleep(5)
#ページ全体の情報がまとまったmain_containerを取得
main_container = driver.find_element(By.XPATH, '//*[@id="root"]/div/div[2]')
# 現在の日時を取得
now = datetime.now()
#Update Iconをクリック
Update_icon = main_container.find_element(By.XPATH, './/*[contains(@class,"UpdateButton_refreshIcon")]')
Update_icon.click()
time.sleep(10)
# 最新の情報を取得する
TokenWallet_elements = main_container.find_elements(By.XPATH, './/*[contains(@class,"db-table-row")]')
all_project_portfolio_elements = main_container.find_elements(By.XPATH, './/*[contains(@class,"Project_portfolio")]')
# ファイル名のパターンを指定して、最新のファイルを取得する
filename_pattern = 'record_*_.xlsx'
latest_file = max(glob.glob(filename_pattern), key=os.path.getctime, default=None)
# 既存のExcelファイルがある場合、Bookオブジェクトを作成する
book = pd.read_excel(latest_file, sheet_name=None, engine='openpyxl', index_col=0) if latest_file else {}
# 新しいExcelファイル名を作成する
strnow = datetime.now().strftime("%Y%m%d_%H%M%S")
new_filename = f"record_{strnow}_.xlsx"
# Excelファイルに保存するためのWriterオブジェクトを作成
writer = pd.ExcelWriter(new_filename, engine='xlsxwriter')
# df_Walletを作成
df_wallet = create_wallet(TokenWallet_elements)
# walletシートにデータを追加する
if "wallet" in book:
df_book = book["wallet"]
df_wallet = pd.concat([df_book, df_wallet], axis=0)
df_wallet.to_excel(writer, sheet_name="wallet", index=True)
# df_priceを作成
df_price = create_price(TokenWallet_elements)
# priceシートにデータを追加する
if "price" in book:
df_book = book["price"]
df_price = pd.concat([df_book, df_price], axis=0)
df_price.to_excel(writer, sheet_name="price", index=True)
# 分散型取引所ごとに成績を分けて取得
df_defi_summary = pd.DataFrame(index=[now])
for project in all_project_portfolio_elements[:-1]: # Quick Swapを取得しないように末尾の要素は除外
df_defi = create_defi(project)
project_name = project.find_element(By.XPATH, './/*[contains(@class,"ProjectTitle_name")]').text
# 既存のシートに対して、df_defiの内容を追加する
if project_name in book:
df_book = book[project_name]
# 新規シートを作成して、df_defiの内容を書き込む
else:
df_book = pd.DataFrame()
df_book = pd.concat([df_book, df_defi], axis=0)
df_book.to_excel(writer, sheet_name=project_name)
df_defi_summary = pd.concat([df_defi_summary, df_book], axis=1)
# Summaryデータを作成
df_summary = create_summary(df_wallet, df_defi_summary)
# Summaryシートにデータを追加する
if "summary" in book:
df_book = book["summary"]
df_summary = pd.concat([df_book, df_summary], axis=0)
df_summary.to_excel(writer, sheet_name="summary", index=True)
# ファイルを保存してWriterオブジェクトを閉じる
writer.close()
#以降はxlsxファイルを整形するための部分
# Excelファイルを開く
wb = openpyxl.load_workbook(new_filename)
# シート名のリストを取得
sheet_names = wb.sheetnames
# 新しいシート名のリストを作成 (例: A, C, B)
new_sheet_names = ['summary', 'Uniswap V3', 'PancakeSwap V3','wallet','price']
# シートを並び替え
for i, name in enumerate(new_sheet_names):
j = sheet_names.index(name) # 新しい位置を取得
wb.move_sheet(sheet_names[j], i+1) # シートを移動
# 全シートの列幅を20に指定
ws = wb[name]
for column in ws.columns:
for cell in column:
ws.column_dimensions[cell.column_letter].width = 20
# 全シートの行幅を12に指定
for row in ws.rows:
ws.row_dimensions[row[0].row].height = 12
#全シートのフォントサイズを10に設定する
fontsize = Font(size=10)
for row in ws.iter_rows():
for cell in row:
cell.font = fontsize
#全シートのA列(日付index)の幅を20に設定する
for ws in wb.worksheets:
ws.column_dimensions['A'].width = 25
#全シートの数値を少数第3位まで表示するようにする
for ws in wb.worksheets:
for row in ws.rows:
for cell in row:
if isinstance(cell.value, (int, float)):
cell.number_format = "0.000"
# 変更を保存
wb.save(new_filename)
driver.quit()
実行時の注意点
上記のコードは筆者の状況を前提にしていますので動作させる際に注意点があります。
DefiのLPペアに対応するTokenをWalletに入れておく
プログラムではDefiでpoolしてあるTokenのペアの総額を計算する際に、
walletに存在するTokenの時価を参照しています。
Defiに存在するTokenでwalletにTokenがない場合は、時価が取得できないようになっています。
例えば、MATIC+USDCのペアをpoolしてあるのに、walletにMATICが存在しないと
MATICの時価が取得できず正しく総額を計算することができません。
そのため、LPにpoolしてあるTokenは極少でもよいのでwalletにも入れておいてください。
なお、USDCとUSDTは1$とみなしているので、walletになくても問題ありません。
「1$外れる場合も考えられるだろ」というツッコミはおいておき、
今回はとりあえずこの仕様ということで…
Debankに表示されている分散型取引所を確認する
Debank上で表示されている分散型取引所にも注意してください。
コード内では、「all_project_portfolio_elements」というリスト型の変数に、分散型取引所の要素が格納されています。
筆者の場合、Debank内にUniswap V3, Pancakeswap V3, Quick Swapがありますが現在は使っていないQuick Swapはコード内で除外しています。
170行目のこの箇所で、all_project_portfolio_elements[:-1]とすることでQuick Swapの要素は取得しないようにしています。
for project in all_project_portfolio_elements[:-1]:
例えばUniswap V3、Pancakeswap V3しかDebankに表示されていない場合は、以下の様に修正します。
for project in all_project_portfolio_elements:
新たにLPペアを追加したときの挙動に注意
Uniswap V3やPancakeswap V3では、集中流動性(Concentrated Liquidity)というのがあり、
tokenの比率を自由に設定できます。
たとえば、同じペアでも以下の様に自由に比率を設定できます。
MATIC+USDCを4:6で預けるプールA
MATIC+USDCを7:3で預けるプールB
問題は、同じtokenのペアでも比率が違うプールを見分ける識別子がDebank内では見当たらなかった点です。
上の例では、プールAとプールBを区別できないということです。
今回紹介したプログラムでは、先頭に連番を追加し、
0_MATIC+USDC、1_MATIC+USDC、というようにすることで
同じtokenのペアでも比率が違うプールを区別するようにしました。
しかし、新たにpoolを追加したり、既存のpoolを引き揚げて削除したりした場合は、
ファイルを更新する際の挙動に注意する必要があります。
poolを変更した際には改めて整形する必要があるかもしれません。
追加で行いたいこと
- 投資金額に対する現在の評価額によるリターンの可視化(グラフ表示)
- プログラムの定期実行
- 手作業で記録した1年半分のログのマージ
1のグラフ化はこちらの記事にまとめました。
【仮想通貨】DebankのPythonスクレイピング結果のグラフ化!分散型取引所(DEX)の成績を比較しやすくしました!
2,3も実装はできているのでGW中にまとめたいです。
まとめ
今回はDebankのpythonスクレイピングを行いました。
普段無意識に利用しているWebページの仕組みを学び、pythonで取得する方法が学べました。
本業にも活かせそうな気がします。
仮想通貨投資の成績や筆者の投資戦略なども(結果が出たら)共有したいです😅
GWはまだ後半に入ったばかりなので、残りの休日を使って、
環境構築や追加で行いたいことなどが整ったら順次共有したいと思います。
(2023年5月6日追記)
プログラムの定期実行はこちら
スリープ中でもPythonスクレイピングプログラムを定期実行する方法【タスクスケジューラ】【Windows10】
(2023年5月7日追記)
Python環境構築について記事を書きました。
DebankのPythonスクレイピングプログラムの環境構築!chromedriverまで解説!