[開源程式碼分享]用Python寫台股交易策略|股票量化交易從零開始(五)

用 Python 撰寫量化交易策略

在接下來的文章,我們會開始撰寫我們的第一支策略,並且在頁面上顯示你的回測結果。那麼在開始撰寫策略之前,我們會有一些事前準備,因為我們需要準備歷史的股票數據才能分析回測。

立即訂閱電子報,掌握最新資訊!

    電子郵件

    有興趣的主題
    量化交易金融知識台灣股市國內期貨海外期貨虛擬貨幣

    有興趣的量化交易軟體/平台
    不清楚MultiChartsTradingViewPythonXQMT4MT5

    還有什麼詢問的?

    好富投 1920x400
    好富投 978x258

    點我了解更多資訊


    因此,請先回到我們上一篇下載歷史資料的地方,將開源程式碼修改為以下範例:

    from tech_analysis_api_v2.api import TechAnalysis
    from tech_analysis_api_v2.model import *
    
    
    import threading
    import pandas as pd
    from datetime import datetime, timedelta
    import mplfinance as mpf
    
    
    def OnDigitalSSOEvent(aIsOK, aMsg):
        print(f'OnDigitalSSOEvent: {aIsOK} {aMsg}')
    
    
    def OnTAConnStuEvent(aIsOK):
        print(f'OnTAConnStuEvent: {aIsOK}')
        if aIsOK:
            event.set()
    
    
    def OnUpdate(ta_Type: eTA_Type, aResultPre, aResultLast):
       
        if aResultPre != None:
            if ta_Type == eTA_Type.SMA:
                print(f'前K {str(aResultPre)}')
            if ta_Type == eTA_Type.EMA:
                print(f'前K {str(aResultPre)}')
            if ta_Type == eTA_Type.WMA:
                print(f'前K {str(aResultPre)}')
            if ta_Type == eTA_Type.SAR:
                print(f'前K {str(aResultPre)}')
            if ta_Type == eTA_Type.RSI:
                print(f'前K {str(aResultPre)}')
            if ta_Type == eTA_Type.MACD:
                print(f'前K {str(aResultPre)}')
            if ta_Type == eTA_Type.KD:
                print(f'前K {str(aResultPre)}')
            if ta_Type == eTA_Type.CDP:
                print(f'前K {str(aResultPre)}')
        if aResultLast != None:
            if ta_Type == eTA_Type.SMA:
                print(f'最新 Time:{aResultLast.KBar.TimeSn_Dply}, SMA:{aResultLast.Value}')
            if ta_Type == eTA_Type.EMA:
                print(f'最新 Time:{aResultLast.KBar.TimeSn_Dply}, EMA:{aResultLast.Value}')
            if ta_Type == eTA_Type.WMA:
                print(f'最新 Time:{aResultLast.KBar.TimeSn_Dply}, EMA:{aResultLast.Value}')
            if ta_Type == eTA_Type.SAR:
                print(f'最新 Time:{aResultLast.KBar.TimeSn_Dply}, SAR:{aResultLast.SAR}, EPh:{aResultLast.EPh}, EPl:{aResultLast.EPl}, AF:{aResultLast.AF}, RaiseFall:{aResultLast.RaiseFall}')
            if ta_Type == eTA_Type.RSI:
                print(f'最新 Time:{aResultLast.KBar.TimeSn_Dply}, RSI:{aResultLast.RSI}, UpDn:{aResultLast.UpDn}, UpAvg:{aResultLast.UpAvg}, DnAvg:{aResultLast.DnAvg}')
            if ta_Type == eTA_Type.MACD:
                print(f'最新 Time:{aResultLast.KBar.TimeSn_Dply}, DIF:{aResultLast.DIF}, OSC:{aResultLast.OSC}')
            if ta_Type == eTA_Type.KD:
                print(f'最新 Time:{aResultLast.KBar.TimeSn_Dply}, K:{aResultLast.K}, D:{aResultLast.D}')
            if ta_Type == eTA_Type.CDP:
                print(f'最新 Time:{aResultLast.KBar.TimeSn_Dply}, CDP:{aResultLast.CDP}, AH:{aResultLast.AH}, NH:{aResultLast.NH}, AL:{aResultLast.AL}, NL:{aResultLast.NL}')
           
    def OnRcvDone(ta_Type: eTA_Type, aResult):
        if ta_Type == eTA_Type.SMA:
            for x in aResult:
                print(f'回補 {x}')
        if ta_Type == eTA_Type.EMA:
            for x in aResult:
                print(f'回補 {x}')
        if ta_Type == eTA_Type.WMA:
            for x in aResult:
                print(f'回補 {x}')
        if ta_Type == eTA_Type.SAR:
            for x in aResult:
                print(f'回補 {x}')
        if ta_Type == eTA_Type.RSI:
            for x in aResult:
                print(f'回補 {x}')
        if ta_Type == eTA_Type.MACD:
            for x in aResult:
                print(f'回補 {x}')
        if ta_Type == eTA_Type.KD:
            for x in aResult:
                print(f'回補 {x}')
        if ta_Type == eTA_Type.CDP:
            for x in aResult:
                print(f'回補 {x}')
           
    def option():
        ProdID = input("商品代號: ")
        SNK = input("分K(1/3/5): ")
        STA_Type = input("指標(SMA/EMA/WMA/SAR/RSI/MACD/KD/CDP): ")
        DateBegin = input("日期(ex: 20230619): ")
    
    
        NK = eNK_Kind.K_1m
        if SNK == '1':
            NK = eNK_Kind.K_1m
        elif SNK == '3':
            NK = eNK_Kind.K_3m
        elif SNK == '5':
            NK = eNK_Kind.K_5m
    
    
        TA_Type = eTA_Type.SMA
        if STA_Type == 'SMA':
            TA_Type = eTA_Type.SMA
        elif STA_Type == 'EMA':
            TA_Type = eTA_Type.EMA
        elif STA_Type == 'WMA':
            TA_Type = eTA_Type.WMA
        elif STA_Type == 'SAR':
            TA_Type = eTA_Type.SAR
        elif STA_Type == 'RSI':
            TA_Type = eTA_Type.RSI
        elif STA_Type == 'MACD':
            TA_Type = eTA_Type.MACD
        elif STA_Type == 'KD':
            TA_Type = eTA_Type.KD
        elif STA_Type == 'CDP':
            TA_Type = eTA_Type.CDP
    
    
        return TechAnalysis.get_k_setting(ProdID, TA_Type, NK, DateBegin)
    
    event = threading.Event()
    
    def fetch_historical_data(prod_id, start_date, end_date):
        """
        根據提供的日期範圍抓取歷史成交資料。
        """
        ta = TechAnalysis(OnDigitalSSOEvent, OnTAConnStuEvent, OnUpdate, OnRcvDone)
        ta.Login('H124418422', 'steven123')  # Replace with your credentials
        event.wait()
    
    
        all_data = []
       
        # 生成日期範圍
        for single_date in pd.date_range(start=start_date, end=end_date):
            formatted_date = single_date.strftime("%Y%m%d")
            lsBS, sErrMsg = ta.GetHisBS_Stock(prod_id, formatted_date)
            if sErrMsg:
                print(f"Error on {formatted_date}: {sErrMsg}")
            else:
                for x in lsBS:
                    data_point = {
                        'ProdID': x.Prod,
                        'Match_Time': x.Match_Time,
                        'Match_Price': x.Match_Price,
                        'Match_Quantity': x.Match_Quantity,
                        'Is_TryMatch': x.Is_TryMatch,
                        'BS': x.BS,
                        'Date': single_date  # 在每條資料中新增日期
                    }
                    all_data.append(data_point)
       
        df = pd.DataFrame(all_data)
        df['Formatted_Time'] = df.apply(lambda row: convert_time(row['Match_Time'], row['Date']), axis=1)
        return df
    
    
    
    
    
    
    def convert_time(time_val, date_val):
        # 解析日期
        year = date_val.year
        month = date_val.month
        day = date_val.day
        # 轉成STR比較好處理
        time_str = str(time_val)
         
        time_str = time_str[:6]  
        if '.' in time_str:
            time_str = time_str.replace('.', '')
            time_str = '0' + time_str
    
    
        # 解析時間 小時、分鐘、秒
        hours = int(time_str[:2])
        minutes = int(time_str[2:4])
        seconds = int(time_str[4:6])
        # 返回datetime物件
        return datetime(year, month, day, hours, minutes, seconds)
    
    
    def aggregate_to_ohlc(df, time_frame='5T'):
        """
        將給定的DataFrame轉換為指定時間幀的OHLC數據,並使用最後一個有效時間點的數據填充空白時間段。
    
    
        參數:
        df -- 原始的DataFrame。
        time_frame -- 要聚合的時間幀,默認為'5T'(五分鐘)。
    
    
        返回:
        轉換後的DataFrame,包含OHLC和總交易量。
        """
        df['Formatted_Time'] = pd.to_datetime(df['Formatted_Time'])
        df.set_index('Formatted_Time', inplace=True)
    
    
        # 定義聚合成OHLC的規則
        ohlc_dict = {
            'Match_Price': 'ohlc',
            'Match_Quantity': 'sum'
        }
    
    
        # 聚合資料
        df_ohlc = df.resample(time_frame).apply(ohlc_dict)
        df_ohlc.columns = df_ohlc.columns.droplevel(0)  # 移除多级列名
    
    
        # 13:25 並不會有交易,所以使用前一筆的資料填補
        df_ohlc.fillna(method='pad', inplace=True)
       
        # Remove data not between 9:00-13:30
        df_ohlc = df_ohlc.between_time('09:00', '13:30')
        # remove data from holidays
        df_ohlc = df_ohlc[df_ohlc.index.dayofweek < 5]
    
    
        return df_ohlc
    
    
    
    
    def main():
        end_date = pd.to_datetime("today")
        start_date = end_date - pd.DateOffset(months=1)
    
    
        df = fetch_historical_data(prod_id='2454',start_date=start_date, end_date=end_date)
        if df is not None:
            df_ohlc = aggregate_to_ohlc(df, time_frame='5T')
            print("---------印出OHLC資料---------")
            print(df_ohlc)
        if df_ohlc is not None:
            # 確保你的 DataFrame 索引是日期時間型別(datetime)
            df_ohlc.index = pd.to_datetime(df_ohlc.index)
    
    
            # 交易量欄位重新命名為 volume (mplfinance 預設欄位名稱)
            df_ohlc.rename(columns={'Match_Quantity': 'volume'}, inplace=True)
            # 儲存為CSV檔案
            # df_ohlc.to_csv("2330_ohlc.csv")
            df_ohlc.to_csv("2454_ohlc.csv")
            print("OHLC資料已儲存為CSV檔案")
            mpf.plot(df_ohlc, type='candle', style='charles',
                    title='245', volume=True)
    main()
    

    步驟一:資料抓取調整

    關於上述的程式碼改動,基本上就是抓取比更多天的資料,並且去除掉不必要的資料,以及將資料輸出成 CSV 檔案。

    所以開始運行後,我們會有一個「2454_ohlc.csv」檔案。

    元富5 1

    這個檔案是過去一個月,聯發科每五分鐘開高收低(ohlc)的價格,整理在一起的資料。

    Py 101209161710
    Py 101209161711

    步驟二:策略撰寫前準備

    接下我們回到第一篇第二篇文章所建立的環境底下,建立一個名為「first_strategy」的資料夾,並且在底下新增一個「data」供我們放置資料,以及新建一個「strategy.py」的檔案。

    新增完成後,待會我們會在這裡撰寫我們的第一支策略。

    元富5 2

    步驟三:策略撰寫

    首先,我們先安裝「backtesting library」。

    pip install backtesting

    接下來我們打開「strategy.py」貼入以下程式碼,寫一隻交叉均線的策略。

    import pandas as pd
    from backtesting import Backtest, Strategy
    from backtesting.lib import crossover
    from backtesting.test import SMA
    
    
    class SmaCross(Strategy):
        n1 = 2 # 短期均線天數
        n2 = 10 # 長期均線天數
        stop_loss_percentage = 0.02  # 停損點設為入場價格的 2%
        take_profit_percentage = 0.04  # 利潤目標設為入場價格的 4%
        entry_price = 0  # 用於紀錄進場價格
    
    
        def init(self):
            close = self.data.Close
            self.sma1 = self.I(SMA, close, self.n1)
            self.sma2 = self.I(SMA, close, self.n2)
    
    
        def next(self):
            if not self.position:
                if crossover(self.sma1, self.sma2):
                    self.buy()
                    self.entry_price = self.data.Close[-1]  # 更新進場價格
                elif crossover(self.sma2, self.sma1):
                    self.sell()
                    self.entry_price = self.data.Close[-1]  # 更新進場價格
            else:
                stop_loss = self.entry_price * (1 - self.stop_loss_percentage)
                take_profit = self.entry_price * (1 + self.take_profit_percentage)
    
    
                # 檢查並執行停損或達到利潤目標
                if self.data.Close[-1] <= stop_loss or self.data.Close[-1] >= take_profit:
                    self.position.close()
                    self.entry_price = 0  # 重設進場價格
    
    
    # 載入資料
    df = pd.read_csv('data/2454_ohlc.csv', index_col='Formatted_Time', parse_dates=True)
    df.columns = [col.capitalize() for col in df.columns]
    
    
    # 設定 Backtest df是資料,SmaCross是策略, cash是初始資金, commission是手續費
    bt = Backtest(df, SmaCross, cash=1000000, commission=.002)
    
    
    # 執行你的策略
    output = bt.run()
    print(output)
    
    
    # 繪圖展示執行結果
    bt.plot()

    關於上述的程式碼,它是一個基於簡單移動平均線(SMA)交叉的交易策略。

    移動平均線是一種常用的技術分析指標,用於平滑價格數據並識別趨勢的方向。而在這個策略中,使用了兩條不同時間範圍的移動平均線,具體進出場邏輯如下:

    移動平均線的設定:

    • n1 和 n2 分別是兩條移動平均線的時間範圍。n1 為 2 期,n2 為 5 期。
    • self.sma1 是短期移動平均線(2 期 SMA),self.sma2 是長期移動平均線(5 期 SMA)。

    進場邏輯:

    • 當短期移動平均線(self.sma1)從下方穿越長期移動平均線(self.sma2)時,表示短期趨勢向上,策略會執行買入操作(self.buy())。

    出場邏輯:

    • 當短期移動平均線(self.sma1)從上方穿越長期移動平均線(self.sma2)時,表示短期趨勢向下,策略會執行賣出操作(self.sell())。

    這種基於移動平均線交叉的策略,是假設市場的趨勢將會延續,並試圖通過捕捉這些趨勢變化來賺錢。

    一般來說,短期移動平均線相對於長期移動平均線更敏感,能夠更快地反映價格變動。所以當短期線穿越長期線時,它可能表明市場趨勢的轉變,從而觸發買入或賣出信號。

    元富5 3

    同時,在「terminal」 也可以看到這隻策略相關的交易報告:

    Start                     2023-10-24 09:00:00
    End                       2023-11-23 13:30:00
    Duration                     30 days 04:30:00
    Exposure Time [%]                   93.517787
    Equity Final [$]                  1113824.842
    Equity Peak [$]                   1118620.842
    Return [%]                          11.382484
    Buy & Hold Return [%]               13.970588
    Return (Ann.) [%]                  207.144368
    Volatility (Ann.) [%]               94.571445
    Sharpe Ratio                         2.190348
    Sortino Ratio                       13.515254
    Calmar Ratio                        36.809133
    Max. Drawdown [%]                   -5.627526
    Avg. Drawdown [%]                   -0.780897
    Max. Drawdown Duration       17 days 23:50:00
    Avg. Drawdown Duration        0 days 22:37:00
    
    Trades 7 
    Win Rate [%]                        71.428571
    Best Trade [%]                       3.989552
    Worst Trade [%]                     -4.341864
    Avg. Trade [%]                       1.552014
    Max. Trade Duration          10 days 23:20:00
    Avg. Trade Duration           4 days 06:28:00
    Profit Factor                        3.511277
    Expectancy [%]                       1.590944
    SQN                                  1.335834
    _strategy                            SmaCross
    _equity_curve                             ...
    _trades                      Size  EntryBa...
    

    關於交易報告提供相關解釋如下:

    1. Start/End/Duration: 這顯示了策略測試的時間範圍。從2023年10月24日開始,到2023年11月23日結束,持續了30天4小時30分鐘。
    2. Exposure Time [%]: 這表示在整個測試期間中,您的資金有多少時間是處於市場中的(即開倉狀態)。在這個例子中,大約93.52%的時間您的資金是處於投資狀態的。
    3. Equity Final [$]: 在測試期結束時,您的帳戶價值是1,113,824.84。
    4. Equity Peak [$]: 在測試期間內,帳戶價值達到的最高點是1,118,620.84。
    5. Return [%]: 您的總回報率是11.38%。
    6. Buy & Hold Return [%]: 如果您在測試期間開始時買入並持有到結束,您的回報率會是13.97%。
    7. Return (Ann.) [%]: 這是年化回報率,如果這種表現持續一年,預計回報率會是207.14%。
    8. Volatility (Ann.) [%]: 年化波動率是94.57%,表明策略的收益波動相對較大。
    9. Sharpe Ratio: 夏普比率是2.19,這是衡量每單位風險所獲得超額回報的指標。大於1通常被認為是良好的。
    10. Sortino Ratio, Calmar Ratio: 這些都是風險調整後的回報率指標,數值越高表示策略風險越低。
    11. Max./Avg. Drawdown [%]: 最大回撤是-5.63%,平均回撤是-0.78%。這表示策略在達到峰值後可能會虧損的最大比例。
    12. Max./Avg. Drawdown Duration: 最大回撤持續時間約18天,平均回撤持續時間約1天。
    13. Trades: 總共進行了7次交易。
    14. Win Rate [%]: 勝率為71.43%,表示大約71%的交易是盈利的。
    15. Best/Worst Trade [%]: 最好的一筆交易盈利約3.99%,而最差的一筆虧損約4.34%。
    16. Avg. Trade [%], Max./Avg. Trade Duration: 平均每筆交易盈利1.55%,最長的一筆交易持續了接近11天,平均每筆交易持續約4天。
    17. Profit Factor: 利潤因子是3.51,表示獲利交易與虧損交易的比例。一般大於1表示策略是盈利的。
    18. Expectancy [%]: 預期每筆交易平均可獲得1.59%的回報。
    19. SQN: 系統品質數字(System Quality Number)是1.34,這是一個衡量交易系統性能的指標,通常大於1.5被認為是好的。

    總結來說,以過去一個月的表現來看,這可以算是一隻不錯的策略。

    透過上述的步驟,這樣我們就完成交易策略的撰寫與回測了!但請注意,如果要將此策略套用在實戰交易中,建議還是要把回測區間拉大以及嘗試各種參數!

      電子郵件

      有興趣的主題
      量化交易金融知識台灣股市國內期貨海外期貨虛擬貨幣

      有興趣的量化交易軟體/平台
      不清楚MultiChartsTradingViewPythonXQMT4MT5

      還有什麼詢問的?


      量化通粉絲社群,一起討論程式交易!

      加入LINE社群量化交易討論群」無壓力討論與分享!

      加入臉書社團「程式交易 Taiwan」即時獲取實用的資源!

      量化通
      量化通

      量化通是個致力於全民量化金融教育的社群,我們希望透過由淺入深的內容,帶領大家以正確觀念來實踐自動化的金融投資研究分析。

      文章: 176

      發佈留言

      發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *