2024年8月23日 星期五

Python 學習筆記 : 用 mplfinance 套件繪製金融圖表 (三) K 線圖模組

在上一篇 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 如下 :

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 補充 :

若要更改線條顏色, 參考 : 


沒有留言 :