2022年7月13日 星期三

Python 學習筆記 : 比特幣行情追蹤

最近股市空頭陰霾壟罩, 幣圈也是掉入熊市, 比特幣等虛擬幣也是跌很慘, 我在雜誌上看到一篇文章, 作者認為市場反轉時機可以看兩個指標, 一是要看通膨有否有好轉跡象; 另外則是看比特幣價格是否回升. 雖然我對虛擬幣還是敬而遠之 (了解不夠), 但透過比特幣價格起伏來觀察它與股市的關係卻有點興趣. 

我在下面這本書找到現成範例, 今天就來測試看看唄! 

# Python 技術者們-實踐!  (旗標, 2018) 第六章

書中利用爬蟲程式到比特幣網站 CoinGeko 擷取近 90 天 (三個月) 的價量資訊, 然後用 Pandas 將其轉成 DataFrame 並計算 100 小時移動平均價, 再用 Matplotlib 繪製價格與均線趨勢圖. 


1. 從比特幣網站 CoinGecko 取得資料來源之 URL :    

首先連到 CoinGecko 網站, 首頁會列出前 100 種虛擬幣 :





其中排第一的就是比特幣, 點比特幣進去會顯示價格趨勢變化圖 : 




按滑鼠右鍵點選 "檢視網頁原始碼", 然後在原始碼頁面按 Ctrl-F 搜尋 "TradingView", 上面這個圖形的資料源網址就在它底下的 a 標籤內 : 




將此頁往右邊拉, 就可以看到 a 標籤的 href 屬性值, 我們的目標是 90d 的 json 檔 : 




將此 90_days.json 檔之網址複製下來給爬蟲使用 : 


點上面超連結可知比特幣過去 90 天每小時的價格資料都被放在 stats 鍵裡面 (成交量則是放在 'total_volumes' 鍵), 其值為以二維陣列表示的時間序列, 第一個元素是 1970/01/01 以來的毫秒時戳, 第二個元素為台幣計價的 BTC 價格 : 




接下來就可以撰寫爬蟲程式來取得這些價格資料. 


2. 以爬蟲程式擷取資料 :

首先匯入 requests 與 pandas : 

>>> import requests    
>>> import pandas as pd      

然後將上面取得的 90d_days.json 檔網址傳入 requests.get() 擷取資料, 它會傳回一個 Response 物件, 呼叫 Response 物件的 json() 方法可將回應內容轉成 Python 的字典, 價格資料會被轉成二維串列, 放在其 stats 鍵的值裡面 (原始 json 資料中還有一個 'total_volumes' 鍵用來存放成交量), 然後傳給 pd.DataFrame() 轉成資料框 : 

>>> url='https://www.coingecko.com/price_charts/1/twd/90_days.json'    
>>> res=requests.get(url)   
>>> type(res)   
<class 'requests.models.Response'>     
>>> res_json=res.json()                  # 將 json 格式資料轉成 Python 字典
>>> type(res_json)       
<class 'dict'>   
>>> prices=res_json['stats']           # 由 stats 鍵取得價格的時間序列資料
>>> type(prices)   
<class 'list'>   
>>> df=pd.DataFrame(prices)      # 將串列轉成資料框
>>> df   
                  0             1
0     1649919786292  1.194869e+06
1     1649923299298  1.198578e+06
2     1649926928481  1.197027e+06
3     1649930479641  1.195884e+06
4     1649934193291  1.192530e+06
...             ...           ...
2151  1657681238819  5.831092e+05
2152  1657684945130  5.827014e+05
2153  1657688435442  5.825076e+05
2154  1657692023661  5.845832e+05
2155  1657694917114  5.841831e+05

[2156 rows x 2 columns]

這裡僅顯示資料框 df 的前五筆與末五筆資料, 可見比特幣在過去三個月內價格已腰斬. 


3. 用 Pandas 清理資料 :

為了後續處理方便還需要進行資料清理作業, 首先是要將欄索引從預設的 0 與 1 改為較易理解的 'datetime' 與 'TWD', 然後呼叫 pd.to_datetime() 將毫秒時戳欄位資料型態改為 datatime, 並以此欄作為列索引, 例如 : 

>>> df.columns=['datetime', 'TWD']      # 設定欄索引
>>> df['datetime']=pd.to_datetime(df['datetime'], unit='ms')     # 轉成 datetime 型態
>>> df.index=df['datetime']       # 設定列索引
>>> df   
                                       datetime           TWD
datetime                                                     
2022-04-14 07:03:06.292 2022-04-14 07:03:06.292  1.194869e+06
2022-04-14 08:01:39.298 2022-04-14 08:01:39.298  1.198578e+06
2022-04-14 09:02:08.481 2022-04-14 09:02:08.481  1.197027e+06
2022-04-14 10:01:19.641 2022-04-14 10:01:19.641  1.195884e+06
2022-04-14 11:03:13.291 2022-04-14 11:03:13.291  1.192530e+06
...                                         ...           ...
2022-07-13 03:00:38.819 2022-07-13 03:00:38.819  5.831092e+05
2022-07-13 04:02:25.130 2022-07-13 04:02:25.130  5.827014e+05
2022-07-13 05:00:35.442 2022-07-13 05:00:35.442  5.825076e+05
2022-07-13 06:00:23.661 2022-07-13 06:00:23.661  5.845832e+05
2022-07-13 06:48:37.114 2022-07-13 06:48:37.114  5.841831e+05

[2156 rows x 2 columns]

可見這些價格資料的時間間隔是以小時為單位, 注意, 這個時間是 UTC 時間, 如果要轉成台灣時間需加 8 小時, 不過為了避免複雜這裡暫時不處理. 

雖然這樣就可以進行價格趨勢的繪圖, 但如果能加上移動平均線會更容易看出趨勢, 移動平均值可以用 Series 物件的 rolling(window).mean() 方法來計算, 其中傳入參數 window 為移動窗的大小, 單位為小時, 以 window=100 (約 4 天) 為例 : 

>>> df['100MA']=df['TWD'].rolling(window=100).mean()   # 計算 100 小時移動平均
>>> df   
                                       datetime           TWD          100MA
datetime                                                                    
2022-04-14 07:03:06.292 2022-04-14 07:03:06.292  1.194869e+06            NaN
2022-04-14 08:01:39.298 2022-04-14 08:01:39.298  1.198578e+06            NaN
2022-04-14 09:02:08.481 2022-04-14 09:02:08.481  1.197027e+06            NaN
2022-04-14 10:01:19.641 2022-04-14 10:01:19.641  1.195884e+06            NaN
2022-04-14 11:03:13.291 2022-04-14 11:03:13.291  1.192530e+06            NaN
...                                         ...           ...            ...
2022-07-13 03:00:38.819 2022-07-13 03:00:38.819  5.831092e+05  618021.586268
2022-07-13 04:02:25.130 2022-07-13 04:02:25.130  5.827014e+05  617369.868976
2022-07-13 05:00:35.442 2022-07-13 05:00:35.442  5.825076e+05  616759.612546
2022-07-13 06:00:23.661 2022-07-13 06:00:23.661  5.845832e+05  616202.822676
2022-07-13 06:48:37.114 2022-07-13 06:48:37.114  5.841831e+05  615632.029145

[2156 rows x 3 columns]

注意, 因為要滿 100 小時才能計算移動平均值, 所以前 100 筆的 '100MA' 欄位為 NaN.


4. 用 Matplotlib 繪製價格趨勢圖 :

接下來就可以用列索引 'datetime' 欄位當 X 軸, 用 'TWD' 與 '100MA' 欄位值當 Y 軸繪製折線圖, 這時需先匯入 Matplotlib (因為 Pandas 是依賴 Matplotlib 繪圖的) :

>>> import matplotlib.pyplot as plt   
>>> df[['TWD', '100MA']].plot(kind='line')       
<matplotlib.axes._subplots.AxesSubplot object at 0x000002AE63545588>
>>> plt.show()    

結果如下 : 




此圖還有改進空間, 首先是預設的 X 軸標籤 datetime 不需要; Y 軸標籤可設為 Prices (TWD); 以及可在標題列顯示最近一筆之日期時間與價格資訊, 取得資料框最後一筆資料可用 iloc[-1:], 它會傳回一個只有一列資料的 DataFrame 物件 :  

>>> df.iloc[-1:]    
                                       datetime            TWD          100MA
datetime                                                                     
2022-07-13 06:48:37.114 2022-07-13 06:48:37.114  584183.095196  615632.029145
>>> type(df.iloc[-1:])      
<class 'pandas.core.frame.DataFrame'>  

所以只要用 index 屬性即可取得列索引 (日期時間), 它會傳回一個 DatetimeIndex 物件, 其 values 屬性值為一個單一元素 (時間字串) 的 ndarray 陣列, 因此用 [0] 索引即可取得此元素, 然後用 str() 將其轉成字串後以切片取前 19 個字元即可得到 '年-月-日T時-分-秒' 的日期時間字串  : 

>>> df.iloc[-1:].index    
DatetimeIndex(['2022-07-13 06:48:37.114000'], dtype='datetime64[ns]', name='datetime', freq=None)
>>> df.iloc[-1:].index.values    
array(['2022-07-13T06:48:37.114000000'], dtype='datetime64[ns]')
>>> df.iloc[-1:].index.values[0]   
numpy.datetime64('2022-07-13T06:48:37.114000000')  
>>> str(df.iloc[-1:].index.values[0])      
'2022-07-13T06:48:37.114000000'
>>> str(df.iloc[-1:].index.values[0])[0:19]    
'2022-07-13T06:48:37'

價格部分則是要先取 ['TWD'] 欄位, 它會傳回一個 Series 物件, 其 values 屬性會傳回單一元素的陣列, 因此用 [0] 即可取得其值, 然後呼叫 int() 來略掉小數部分 :

>>> df.iloc[-1:]["TWD"]    
datetime
2022-07-13 06:48:37.114    584183.095196
Name: TWD, dtype: float64
>>> type(df.iloc[-1:]["TWD"])     
<class 'pandas.core.series.Series'>
>>> df.iloc[-1:]["TWD"].values    
array([584183.09519612])    
>>> df.iloc[-1:]["TWD"].values[0]    
584183.0951961185
>>> int(df.iloc[-1:]["TWD"].values[0])    
584183

總結以上測試, 可以用下列程式碼來設定圖形標題 : 

>>> last_time=str(df.iloc[-1:].index.values[0])[0:19]    
>>> last_time     
'2022-07-13T06:48:37'  
>>> last_price=int(df.iloc[-1:]["TWD"].values[0])      
>>> last_price      
584183   
>>> df[['TWD', '100MA']].plot(kind='line')                    # 繪製 DataFrame 圖形
<matplotlib.axes._subplots.AxesSubplot object at 0x000002AE6388EB70>
>>> plt.title(f'Bit Coin {last_time} NT${last_price}')      # 設定標題
Text(0.5, 1.0, 'Bit Coin 2022-07-13T06:48:37 NT$584183')
>>> plt.xlabel('')                          # 設定 X 軸標籤 (不顯示)
Text(0.5, 0, '')
>>> plt.ylabel('Price(TWD)')     # 設定 Y 軸標籤
Text(0, 0.5, 'Price(TWD)')
>>> plt.grid(True)                       # 顯示隔線
>>> plt.show()   

結果如下 :




OK, 這樣就算是完成價格趨勢繪圖啦! 

完整程式如下 : 

測試 1 : 擷取 CoinGecko 網站資料繪製比特幣價格變化圖形 [看原始碼]

#btc_prices.py
import requests
import pandas as pd
import matplotlib.pyplot as plt

def coingecko_btc_crawler(url):
    res=requests.get(url)
    prices=res.json()['stats']
    df=pd.DataFrame(prices)
    df.columns=['datetime', 'TWD']
    df['datetime']=pd.to_datetime(df['datetime'], unit='ms')
    df.index=df['datetime']
    return df

url='https://www.coingecko.com/price_charts/1/twd/90_days.json'
btc=coingecko_btc_crawler(url)
btc['100MA']=btc['TWD'].rolling(window=100).mean()
btc[['TWD', '100MA']].plot(kind='line')
last_time=str(btc.iloc[-1:].index.values[0])[0:19]
last_price=int(btc.iloc[-1:]["TWD"].values[0])
plt.title(f'Bit Coin {last_time} NT${last_price}')
plt.xlabel('')
plt.ylabel('Price(TWD)')
plt.grid(True)
plt.savefig('btc_prices.jpg')
plt.show()

此處是將爬蟲部分寫成一個函式. 


5. 用 Line Notify 推播價格趨勢圖 :

上面所繪製的比特幣價格曲線圖片可用 Line Notify 傳送, 方法參考 : 


上面所繪製的曲線圖其實外圍有白邊, 這除了增加圖檔大小外, 在 Line 上也會看起來較小, 可以用 Pillow 套件的 Image 模組將外圍的白邊切除. 先用小畫家開啟 btc_prices.jpg, 確定要裁切的左上角與右下角座標 :




然後將此座標傳入 Image.crop() 進行裁切後再存回 btc_prices.jpg 即可, 例如 : 

>>> from PIL import Image
>>> img=Image.open('btc_prices.jpg')   
>>> img=img.crop((18, 26, 590, 453))      
>>> img.save('btc_prices.jpg')   

參考 :


完整程式碼如下 :
 

測試 2 : 用 Line Notify 推播比特幣價格趨勢圖 [看原始碼]

import requests
import pandas as pd
import matplotlib.pyplot as plt
from PIL import Image

def coingecko_btc_crawler(url):
    res=requests.get(url)
    prices=res.json()['stats']
    df=pd.DataFrame(prices)
    df.columns=['datetime', 'TWD']
    df['datetime']=pd.to_datetime(df['datetime'], unit='ms')
    df.index=df['datetime']
    return df

def notify_image(msg, token, image):
    url='https://notify-api.line.me/api/notify'
    headers={'Authorization': 'Bearer ' + token}
    data={'message': msg}
    image=open(image, 'rb')
    imageFile={'imageFile': image}
    r=requests.post(url, headers=headers, data=data, files=imageFile)
    if r.status_code==requests.codes.ok:
        return '圖片發送成功!'
    else:
        return f'圖片發送失敗: {r.status_code}'

url='https://www.coingecko.com/price_charts/1/twd/90_days.json'
btc=coingecko_btc_crawler(url)
btc['100MA']=btc['TWD'].rolling(window=100).mean()
btc[['TWD', '100MA']].plot(kind='line')
last_time=str(btc.iloc[-1:].index.values[0])[0:19]
last_price=int(btc.iloc[-1:]["TWD"].values[0])
plt.title(f'Bit Coin {last_time} NT${last_price}')
plt.xlabel('')
plt.ylabel('Price(TWD)')
plt.grid(True)
plt.savefig('btc_prices.jpg')
img=Image.open('btc_prices.jpg')       # 開啟檔案   
img=img.crop((18, 26, 590, 453))       # 裁切圖片去除外圍白邊  
img.save('btc_prices.jpg')                    # 回存檔案   
token='ud7PaDL45fz849A0e1f5oaMCbRIkxMXapQCt7PfNkzz'
msg='Bit Coin'
notify_image(msg, token, 'btc_prices.jpg')
plt.show()

結果如下 : 




若將上面裁切圖片的三列程式碼去掉, 推播原始圖片的結果如下 :




雖然看起來有較小, 但其實差別不大, 因為點擊圖片瀏覽原圖時幾乎沒差. 

2022-07-19 補充 :

今天抽空解決了上面的時差問題, CoinGeko 網站上的時間是 UTC, 如果要轉成台灣時間需加上 8 小時, 作法是呼叫 pd.DataOffset() 並傳入 hours=8 參數更改 datatime 欄位值即可 : 

df['datetime'] += pd.DateOffset(hours=8)

參考 : 


另外也把 Matplotlib 繪圖中文化, 完整程式碼如下 : 


測試 3 : 用 Line Notify 推播比特幣價格趨勢圖 (台灣時間+中文化) [看原始碼]

import requests
import pandas as pd
import matplotlib.pyplot as plt
from PIL import Image

def coingecko_btc_crawler(url):
    res=requests.get(url)
    prices=res.json()['stats']
    df=pd.DataFrame(prices)
    df.columns=['datetime', 'TWD']
    df['datetime']=pd.to_datetime(df['datetime'], unit='ms')
    df['datetime'] += pd.DateOffset(hours=8)
    df.index=df['datetime']
    return df

def notify_image(msg, token, image):
    url='https://notify-api.line.me/api/notify'
    headers={'Authorization': 'Bearer ' + token}
    data={'message': msg}
    image=open(image, 'rb')
    imageFile={'imageFile': image}
    r=requests.post(url, headers=headers, data=data, files=imageFile)
    if r.status_code==requests.codes.ok:
        return '圖片發送成功!'
    else:
        return f'圖片發送失敗: {r.status_code}'

plt.rcParams["font.family"]=["Microsoft JhengHei"]
url='https://www.coingecko.com/price_charts/1/twd/90_days.json'
btc=coingecko_btc_crawler(url)
btc['100MA']=btc['TWD'].rolling(window=100).mean()
btc[['TWD', '100MA']].plot(kind='line')
last_time=str(btc.iloc[-1:].index.values[0])[0:19]
last_price=int(btc.iloc[-1:]["TWD"].values[0])
plt.title(f'比特幣 {last_time} 台幣 {last_price} 元')
plt.xlabel('')
plt.ylabel('價格(台幣)')
plt.grid(True)
plt.legend(['價格', '100小時均價'], loc='best') 
plt.savefig('btc_prices.jpg')
img=Image.open('btc_prices.jpg')       # 開啟檔案   
img=img.crop((18, 26, 590, 453))       # 裁切圖片去除外圍白邊  
img.save('btc_prices.jpg')                    # 回存檔案   
token='ud7PaDL45fz849A0e1f5oaMCbRIkxMXapQCt7PfNkzz'
msg='比特幣價格變化'
notify_image(msg, token, 'btc_prices.jpg')
plt.show()

結果如下 : 




沒有留言 :