在上一篇 mplfinance 文章中已經測試過 K 線圖的疊圖與副圖作法, 雖然程式碼並不長, 但若每次畫 K 線圖都要重新寫一次也很麻煩, 所以我參考 "Python 量化交易 Ta-Lib 技術指標 139 個活用技巧 (博碩, 劉承彥, 2022)" 這本書第三章中的下載盤後資料與繪製 K 線圖的模組 Function.py 加以改寫成 kbar.py 模組, 可大幅簡化之後測試技術指標與進行策略回測的程式碼.
本系列之前的文章參考 :
另外 kbar.py 模組也將另一個盤後資料來源 FinMind 納進來, 且 OHLCV 欄位名稱已配合 mplfinance 套件要求調整為全小寫字母 (若要用 backtesting 套件做策略回測, 則需將 OHLCV 欄位的首字母改成大寫). 關於 FinMind 參考 :
K 線圖模組 kbar.py 程式碼如下 :
# kbar.py
import yfinance as yf
from FinMind.data import DataLoader
import pandas as pd
import mplfinance as mpf
def get_yf(symbol, start, end, interval='1d'):
df=yf.download(symbol, start=start, end=end, interval=interval)
df.columns=[column.lower() for column in df.columns]
df.open=[round(i, 2) for i in df.open]
df.high=[round(i, 2) for i in df.high]
df.low=[round(i, 2) for i in df.low]
df.close=[round(i, 2) for i in df.close]
return df
def get_fm(symbol, start, end, token):
data_loader=DataLoader()
data_loader.login_by_token(api_token=token)
df=data_loader.taiwan_stock_daily(stock_id=symbol, start_date=start, end_date=end)
df['date']=pd.to_datetime(df['date'])
df=df.set_index(df['date'])
columns={'open': 'Open', 'max': 'High', 'min': 'Low', 'close': 'Close', 'Trading_Volume': 'Volume'}
df=df.rename(columns=columns)
return df
class KBar():
def __init__(self, df):
self.df=df
self.addplots=[]
def addplot(self, data, panel=0, type='line', marker='.', color='blue', scatter=False, ylabel=''):
plot=mpf.make_addplot(data, panel=panel, type=type, marker=marker, color=color, scatter=scatter, ylabel=ylabel, secondary_y=False)
self.addplots.append(plot)
def plot(self, title='K 線圖'):
color=mpf.make_marketcolors(up='red', down='green', inherit=True)
font={'font.family': 'Microsoft JhengHei'}
style=mpf.make_mpf_style(base_mpf_style='default', marketcolors=color, rc=font)
mpf.plot(self.df, type='candle', title=title, style=style, volume=True, addplot=self.addplots)
因為 yfinance 傳回的 DataFrame 價格精度都是到小數點後六位, 由於 Python 有浮點數精確度問題, 因此 get_yf() 函式中使用 round() 將 OHLC 價都取四捨五入到小數點後第二位 (FinMind 傳回的 OHLC 價格本來就是到小數點後第二位, 無須調整).
使用此模組只要 4 個指令就可以畫出 K 線圖了, 首先匯入 kbar 模組 :
>>> import kbar
當然也可以用 from 匯入所有成員, 這樣不需要參考 kbar 模組名稱 :
>>> from kbar import *
然後呼叫 get_yf() 函式從 yfinance 下載盤後資料 :
>>> df=kbar.get_yf('0050.tw', start='2024-07-01', end='2024-08-21')
接著呼叫 KBar 類別的建構式 KBar() 並傳入 DataFrame 建立 KBar 物件 :
>>> kb=kbar.KBar(df)
>>> type(kb)
<class 'kbar.KBar'>
最後只要呼叫 KBar 物件的 plot() 方法就會繪製 K 線圖了 :
>>> kb.plot()
結果如下 :
可見 plot() 方法預設會繪製成交量子圖, 且預設的標題是 "K 線圖", 這可以在呼叫 plot() 方法時傳入標題字串來覆蓋它 :
>>> kb.plot('台灣五十')
如果要繪製副圖就要呼叫 KBar 物件的 addplot() 方法並傳入圖形的 Series 物件, 例如要繪製技術指標 RSI 可利用 Ta-Lib 套件來計算圖形資料 :
>>> from talib.abstract import RSI
>>> rsi=RSI(df)
>>> type(rsi)
<class 'pandas.core.series.Series'>
然後就可以將此 Series 物件傳給 KBar 物件的 addplot() 方法並指定 panel=2 (因為 1 是內建成交量副圖使用的) :
>>> kb.addplot(rsi, panel=2, ylabel='RSI')
>>> kb.plot('台灣五十')
如果還要再增加副圖就再次呼叫 addplot() 方法畫在下一個 panel 即可, 例如 MFI 指標 :
>>> from talib.abstract import MFI
>>> mfi=MFI(df, timeperiod=14)
>>> kb.addplot(mfi, panel=3, ylabel='MFI')
>>> kb.plot('台灣五十')
from kbar import *
from talib.abstract import RSI, MFI
df=get_yf('0050.tw', start='2024-07-01', end='2024-08-21')
kb=KBar(df)
rsi=RSI(df)
mfi=MFI(df, timeperiod=14)
kb.addplot(rsi, panel=2, ylabel='RSI')
kb.addplot(mfi, panel=3, ylabel='MFI')
kb.plot()
如果要從 FinMind 下載則須呼叫 get_fm() 函式, 我已經先將 FinMind 的存取權杖存入目前工作目錄下的 .env 檔內的 FinMind_TOKEN 變數, 可用 dotcnv 模組先將其讀出來 :
>>> import os
>>> from dotenv import load_dotenv
>>> load_dotenv()
True
>>> token=os.environ.get('FinMind_TOKEN')
參考 :
然後用四個指令就可以畫出 K 線圖了 :
>>> import kbar
>>> df=kbar.get_fm('0050', '2024-07-01', '2024-08-21', token)
2024-08-23 22:33:54.720 | INFO | FinMind.data.finmind_api:get_data:125 - download TaiwanStockPrice, data_id: 0050
>>> kb=kbar.KBar(df)
>>> kb.plot('台灣五十 (data : FinMind)')
結果與上面使用 yfinance 資料畫的圖相同 :
2024-08-24 補充 :
由於 addplot() 的關鍵字參數因需求而異 (例如要不要傳 ylim), 今天將其改為可變數量的關鍵字參數 **kwargs, 這樣彈性較大, 常用的參數如下表 :
圖形字典屬性 | 說明 |
panel | 繪圖框編號 (0~9, 0 為疊圖, 1~9 為副圖, 但 1 為內建成交量副圖) |
color | 線條顏色 (預設為暗藍色) |
marker | 資料點標記符號, 例如 'o' 為圓點, 's' 為方點 |
ylabel | Y 軸標籤 (字串) |
ylim | Y 軸座標範圍 (串列), [ymin, ymax] |
width | 線條寬度 (px, 預設=3) |
更新後的模組如下 :
# kbar.py
import yfinance as yf
from FinMind.data import DataLoader
import pandas as pd
import mplfinance as mpf
def get_yf(symbol, start, end, interval='1d'):
df=yf.download(symbol, start=start, end=end, interval=interval)
df.columns=[column.lower() for column in df.columns]
df.open=[round(i, 2) for i in df.open]
df.high=[round(i, 2) for i in df.high]
df.low=[round(i, 2) for i in df.low]
df.close=[round(i, 2) for i in df.close]
return df
def get_fm(symbol, start, end, token):
data_loader=DataLoader()
data_loader.login_by_token(api_token=token)
df=data_loader.taiwan_stock_daily(stock_id=symbol, start_date=start, end_date=end)
df['date']=pd.to_datetime(df['date'])
df=df.set_index(df['date'])
columns={'open': 'Open', 'max': 'High', 'min': 'Low', 'close': 'Close', 'Trading_Volume': 'Volume'}
df=df.rename(columns=columns)
return df
class KBar():
def __init__(self, df):
self.df=df
self.addplots=[]
def addplot(self, data, **kwargs):
plot=mpf.make_addplot(data, **kwargs)
self.addplots.append(plot)
def plot(self, title='K 線圖', mav=[]):
color=mpf.make_marketcolors(up='red', down='green', inherit=True)
font={'font.family': 'Microsoft JhengHei'}
style=mpf.make_mpf_style(base_mpf_style='default', marketcolors=color, rc=font)
mpf.plot(self.df, type='candle', title=title, style=style, volume=True, mav=mav, addplot=self.addplots)
例如 :
>>> kb.addplot(rsi, panel=2, ylabel='RSI', ylim=[0, 100], width=1)
>>> kb.addplot(mfi, panel=3, ylabel='MFI', ylim=[0, 100], width=1)
>>> kb.plot(mav=[3, 5, 7])
2024-08-25 補充 :
想依樣畫葫蘆利用可變數量的關鍵字參數 **kwargs 幫 mpf.plot() 維持參數彈性 :
def plot(self, **kwargs):
color=mpf.make_marketcolors(up='red', down='green', inherit=True)
font={'font.family': 'Microsoft JhengHei'}
style=mpf.make_mpf_style(base_mpf_style='default', marketcolors=color, rc=font)
kwargs['type']='candle'
kwargs['style']=style
kwargs['addplot']=self.addplots
mpf.plot(self.df, **kwargs)
但是用如下程式去測試卻出現錯誤 :
from kbar import *
from talib.abstract import RSI, MFI
df=get_yf('0050.tw', start='2024-07-01', end='2024-08-24')
kb=KBar(df)
rsi=RSI(df)
mfi=MFI(df, timeperiod=14)
kb.addplot(rsi, panel=2, ylabel='RSI', ylim=[0, 100], color='r')
kb.addplot(mfi, panel=3, ylabel='MFI', ylim=[0, 100], color='g')
kb.plot(mav=[3, 5, 7], tight_layout = True)
錯誤訊息如下 :
>>> %Run kbar-test-1.py
[*********************100%%**********************] 1 of 1 completed
Traceback (most recent call last):
File "D:\python\test\kbar-test-1.py", line 10, in <module>
kb.plot(mav=[3, 5, 7], tight_layout = True)
File "D:\python\test\kbar.py", line 39, in plot
mpf.plot(self.df, **kwargs)
File "C:\Users\tony1\AppData\Roaming\Python\Python310\site-packages\mplfinance\plotting.py", line 494, in plot
panels = _build_panels(fig, config)
File "C:\Users\tony1\AppData\Roaming\Python\Python310\site-packages\mplfinance\_panels.py", line 101, in _build_panels
raise ValueError('inferred panel list is missing panels: '+str(missing))
ValueError: inferred panel list is missing panels: [1]
看起來像是無法讀取到 addplot 中的 panel 參數, 在原因未排除前還是用列舉參數的做法吧.
2024-08-25 再補充 :
早上參加完觀音廟普渡回到家才 10:30, 我把上面的錯誤訊息丟給 ChatGPT 去查, 回覆如下 :
"這個錯誤訊息 ValueError: inferred panel list is missing panels: [1] 表示 mplfinance 在處理圖表面板時發現有一個面板缺失,具體是缺少了第 1 個面板。這通常是因為在你設定 addplot 時,面板的順序或配置不正確導致的。
在 mplfinance 中,面板是從 0 開始計數的,所以當你在第 2 和第 3 面板(即 panel=2 和 panel=3)添加圖形時,如果沒有設置第 1 面板,這會導致 mplfinance 出現問題,因為它期待所有面板都是連續的。"
雖然 ChatGPT 建議的解決方案是錯的 (所以有人說 AI 將取代軟體工程師我看目前是未必), 但它的錯誤分析倒是一個重要線索, 檢視上面的程式碼. 發現我把 RS1 副圖放在 panel=2, 把 MFI 副圖放在 panel=3, 卻沒有設定 volume=True, 這導致 panel=1 是空的, 所以 mplfinance 在處理 addplot 副圖時無法找到連續的 panel 編號而發生錯誤.
所以並非上面使用可變數量關鍵字參數 **kwargs 的 mpf.plot() 函式有問題, 而是呼叫的方式有問題, 即在使用 addplot 製作疊圖或副圖時務必理解, mplfinance 把 panel=0 固定給了疊圖; panel=1~9 給了副圖, 但要注意這些副圖的順序要連續不能跳空, 例如若傳入 volume=True 繪製成交量副圖, 則 panel=1 已被其使用, 其他副圖的 panel 要從 2 開始往下排; 若沒有開啟成交量副圖, 則 panel=1 未被占用, 則其他副圖的 panel 就可以從 1 開始往下排.
修改後全部 API 使用可變數量關鍵字參數 **kwargs 的 kbar.py 如下 :
# kbar.py
import yfinance as yf
from FinMind.data import DataLoader
import pandas as pd
import mplfinance as mpf
def get_yf(symbol, start, end, interval='1d'):
df=yf.download(symbol, start=start, end=end, interval=interval)
df.columns=[column.lower() for column in df.columns]
df.open=[round(i, 2) for i in df.open]
df.high=[round(i, 2) for i in df.high]
df.low=[round(i, 2) for i in df.low]
df.close=[round(i, 2) for i in df.close]
return df
def get_fm(symbol, start, end, token):
data_loader=DataLoader()
data_loader.login_by_token(api_token=token)
df=data_loader.taiwan_stock_daily(stock_id=symbol, start_date=start, end_date=end)
df['date']=pd.to_datetime(df['date'])
df=df.set_index(df['date'])
columns={'open': 'Open', 'max': 'High', 'min': 'Low', 'close': 'Close', 'Trading_Volume': 'Volume'}
df=df.rename(columns=columns)
return df
class KBar():
def __init__(self, df):
self.df=df
self.addplots=[]
def addplot(self, data, **kwargs):
plot=mpf.make_addplot(data, **kwargs)
self.addplots.append(plot)
def plot(self, **kwargs):
color=mpf.make_marketcolors(up='red', down='green', inherit=True)
font={'font.family': 'Microsoft JhengHei'}
style=mpf.make_mpf_style(base_mpf_style='default',
marketcolors=color,
rc=font)
kwargs['type']='candle'
kwargs['style']=style
kwargs['addplot']=self.addplots
mpf.plot(self.df, **kwargs)
第一個測試程式為不開啟成交量副圖 (預設), 其他副圖的 panel 可以從 1 開始往下排 :
from kbar import *
from talib.abstract import RSI, MFI
df=get_yf('0050.tw', start='2024-07-01', end='2024-08-24')
kb=KBar(df)
rsi=RSI(df)
mfi=MFI(df, timeperiod=14)
kb.addplot(rsi, panel=1, ylabel='RSI')
kb.addplot(mfi, panel=2, ylabel='MFI')
kb.plot(mav=[3, 5, 7])
此處 RSI 與 MFI 的 panel 要從 1 開始連續往下排, 如果排 1, 3 或 2, 3 都會出現上面的 panel 錯誤. 結果如下 :
下面是開啟成交量副圖的測試程式 :
from kbar import *
from talib.abstract import RSI, MFI
df=get_yf('0050.tw', start='2024-07-01', end='2024-08-24')
kb=KBar(df)
rsi=RSI(df)
mfi=MFI(df, timeperiod=14)
kb.addplot(rsi, panel=2, ylabel='RSI')
kb.addplot(mfi, panel=3, ylabel='MFI')
kb.plot(volume=True, mav=[3, 5, 7])
注意, 此處因為 plot() 中傳入 volume=True 開啟了成交量副圖, 它固定會占用 panel=1, 所以其他副圖必須從 panel=2 開始連續往下排, 結果如下 :
哈哈, 終於搞定了 kbar.py, 這樣就可以傳遞任何 mpf.plot() 的參數了.
2024-09-11 補充 :
若要更改線條顏色, 參考 :
沒有留言:
張貼留言