最近股市空頭陰霾壟罩, 幣圈也是掉入熊市, 比特幣等虛擬幣也是跌很慘, 我在雜誌上看到一篇文章, 作者認為市場反轉時機可以看兩個指標, 一是要看通膨有否有好轉跡象; 另外則是看比特幣價格是否回升. 雖然我對虛擬幣還是敬而遠之 (了解不夠), 但透過比特幣價格起伏來觀察它與股市的關係卻有點興趣.
我在下面這本書找到現成範例, 今天就來測試看看唄!
# 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()
結果如下 :
沒有留言 :
張貼留言