TQuant Lab 營收數字不撒謊:如何從季報選股中洞察股價漲跌玄機

季報選股
Created By Midjourney

本文重點摘要

  • 文章難度:★☆☆☆☆
  • 取得上市櫃股票的每季季報選股
  • 透過 TQuant Lab 回測,觀察此選股策略與股價之關係。

前言

你有曾經想過營收會是一種訊號嗎?財報中的營收數字,不僅是公司營運狀況的直接反映,更是預測股價波動的潛在關鍵。本篇文章將利用季報選股,篩選台股上市櫃公司每季營收的前 20%,並進行多頭操作。透過這種基於營收數據的回測方法,我們可以捕捉市場中潛在的強勢股,並透過實際數據驗證此策略的有效性。這不僅簡單,而且能夠為投資人提供一種穩定且具系統性的選股依據,從中挖掘股價漲跌的玄機。

編輯環境及模組需求

本文使用 Mac OS 及 VS Code 作為編輯器

資料導入

我們的策略是選擇台股上市櫃的所有股票。由於我們的指標是每季營收,一年最多只能取得四次資料。為了確保有足夠的樣本數來評估每季營收作為指標的季報選股的可行性,我們將回測期間設定為 2010 年至 2023 年,這樣能夠涵蓋足夠的時間範圍,提供更具代表性的數據進行分析。

# Import package
import os
import pandas as pd
import TejToolAPI
from zipline.sources.TEJ_Api_Data import get_universe

# Input your key
os.environ['TEJAPI_KEY'] = 'your key'
os.environ['TEJAPI_BASE'] = 'https://api.tej.com.tw'

# Get all stock of TWSE & OTC 
pool = get_universe(start = '2010-01-01', 
                    end = '2023-12-29',
                    mkt = ['TWSE', 'OTC'],
                    stktp_e=['Common Stock-Foreign', 'Common Stock']
                    )

# "ip12" is the column of monthly revenue
columns = ["ip12"]
data = TejToolAPI.get_history_data(
    ticker = pool,
    columns = columns,
    fin_type = 'Q',
    transfer_to_chinese = True,
    start = pd.Timestamp("2010-01-01"),
    end = pd.Timestamp("2023-12-29"),
)

# Set the interval of backtesting
start = '2010-01-01'
end = '2023-12-29'
pool.append('IR0001')
os.environ['mdate'] = start + ' ' + end
os.environ['ticker'] = ' '.join(pool)

# Ingest data to tquant
!zipline ingest -b tquant
季報選股
Tej Tool Api 取得每季營收資料

建立季報選股回測函式

將每季營收訊息導入 Pipeline

CustomDataset 可以將資料庫中的內容導入 Pipeline 中,方便後續回測使用。於本範例我們用以將上述 transfer 欄位紀錄的每日轉讓資訊導入 Pipeline。擷取部分程式碼如下:

from zipline.pipeline.data.dataset import Column, DataSet
from zipline.pipeline.domain import TW_EQUITIES

class CustomDataset(DataSet):
    # Define a floating point number field to store quarterly revenue
    revenue = Column(float)
    domain = TW_EQUITIES

建立 Pipeline 函式

Pipeline() 提供使用者快速處理多檔標的的量化指標與價量資料的功能,於本次案例我們用以處理:

  • 導入每日收盤價
  • 導入轉換完成的季營收資料,並提取每季前後 20%
  • 前 20% 在 longs 欄位顯示 True,後 20% 在 shorts 欄位顯示 True
from zipline.pipeline.filters import StaticAssets

# Importing the StaticAssets filter to create a custom asset mask that excludes a specific benchmark asset.
assets_ex_IR0001 = [i for i in assets if i != bundle.asset_finder.lookup_symbol('IR0001', as_of_date=None)]
benchmark_asset = bundle.asset_finder.lookup_symbol('IR0001',as_of_date = None)

def make_pipeline():
    
    # Get the season revenue column from the CustomDataset.
    seanson_revenue = CustomDataset.revenue.latest

    # To filter out stocks with revenue in the top and bottom 20% while excluding the specific asset 'IR0001'.
    top_20_decile = seanson_revenue.percentile_between(80, 100, mask=StaticAssets(assets_ex_IR0001))
    bottom_20_decile = seanson_revenue.percentile_between(0, 20, mask=StaticAssets(assets_ex_IR0001))

    # Return value of pipeline column
    my_pipeline = Pipeline(
        columns = {
            'curr_price': TWEquityPricing.close.latest,
            'season_revenue': seanson_revenue,
            'longs': top_20_decile,
            'shorts': bottom_20_decile
        },
        screen = ~StaticAssets([benchmark_asset])
    )
    return my_pipeline

# Redefine the start and end dates for the backtesting period.
start_dt = pd.Timestamp(start, tz = 'UTC')
end_dt = pd.Timestamp(end, tz = 'UTC')
my_pipeline = engine.run_pipeline(make_pipeline(), start_dt, end_dt)
my_pipeline 
季報選股
Pipeline 輸出結果

建立交易日期

根據會計師簽證跟公司自結數的報告,以及交易所規定,加上各產業季報公告時間不同,所以把平衡日設置為每年的 3, 6, 9, 12月底。

from zipline.utils.calendar_utils import get_calendar

# Convert the dates into a DataFrame and calculate the difference between each date and the midpoint of the season.
cal = pd.DataFrame(cal).rename(columns={0: 'date'})

def get_quarter_end(x):
    month = (x.month - 1) // 3 * 3 + 3  # 將月份設置為 3、6、9、12
    return pd.Timestamp(year=x.year, month=month, day=1, tz='UTC') + pd.offsets.MonthEnd(0)

cal['date'] = cal['date'].apply(get_quarter_end)

# Change the grouping to be based on year and quarter.
tradeday = cal.groupby([cal['date'].dt.year, cal['date'].dt.quarter]).apply(lambda x: x.head(1)).date.tolist()

# Convert the trading days to string format.
tradeday = [str(i.date()) for i in tradeday]

tradeday[:12]

進行季報選股回測

TargetPercentPipeAlgo 會根據 longs 欄位為 True 的股票進行多頭操作,並且可以設定相關回測需要的參數,與此例中我們設置:

  • 起迄日期
  • 初始資金
  • 多頭操作交易日 (前面我們設定為每季季中)
  • 最大槓桿 (即最多使用多少的借貸資金)
  • 將上述計算的 Pipeline 導入交易流程中
  • 流動性滑價
  • 交易手續費
  • 因為本文有使用 customdataset 導入從 TEJ Tool API 取得的資料,因此我們需要設定 custom_loader 參數使程式順利執行
from zipline.algo.pipeline_algo import *

# Setting the start and end dates for the backtest
start_dt = pd.Timestamp(start, tz = 'UTC')
end_dt = pd.Timestamp(end, tz = 'UTC')

# Creating the Target Percent Pipeline Algorithm
algo = TargetPercentPipeAlgo(
                        start_session=start_dt, # Start time of the backtest
                        end_session=end_dt, # End time of the backtest
                        capital_base=1e7, # Initial capital set to 10 million        
                        tradeday=tradeday, # Trading day parameter
                        max_leverage=0.9, # Maximum leverage ratio of 0.9
                        pipeline=make_pipeline, # Use the data of pipeline 
                        slippage_model=slippage.VolumeShareSlippage(volume_limit=0.15, price_impact=0.01), # Slippage model
                        commission_model = commission.Custom_TW_Commission(min_trade_cost = 20, discount = 1.0, tax = 0.003), # Commission model for the Taiwan market
                        custom_loader=custom_loader, # Custom data loader for loading specific data
)

# Run backtesting
results = algo.run()
季報選股
部分交易明細表

利用 Pyfolio 進行績效評估

# Extract from the backtest results of Zipline and generate a comprehensive analysis of the portfolio.
returns, positions, transactions = pf.utils.extract_rets_pos_txn_from_zipline(results)
benchmark_rets = results['benchmark_return']

pf.tears.create_full_tear_sheet(returns=returns,
                                positions=positions,
                                transactions=transactions,
                                benchmark_rets=benchmark_rets
                                )
季報選股
回測表現與大盤比較圖

從上圖數據中可以看出,季報選股回測的累積報酬率達到 260.087%,而年化回報率為 9.843%,以 13 年回測期間來看的話並不是特別突出。儘管最大回撤達到了 -28.299%,但以 13 年回測期間來看的話是可以接受的,且年化波動率為 13.817%,顯示此為相對穩定的策略。

季報選股
長短部位持有檔數變化圖

那從這個圖可以看到因為我們是取台股上市櫃每季的前 20%,但實際上一個人不會操作那麼多支股票,筆者這邊建議若要實際操作,可以使用其他基本面資料例如 PE、BE 再篩選一次股票池,大約 10 支股票就差不多了。

做多與做空策略比較

季報選股
做多與做空策略比較圖

上圖可以看到季報選股僅作多策略在長期回測中表現顯著優於作多加作空策略。前者能夠在牛市環境中穩定增長,創造出超額回報,而後者季報選股則在震盪市場中雖有短期優勢,但未能在長期內有效跑贏基準。筆者推測是因為在大盤很強勢成長的情況下,作空的成效不好,才導致最後幾年的表現相對大盤略為遜色。

結論

本次季報選股策略是一個特別的回測,因為是季營收資訊,所以需要相對長的時間才能有比較顯著的成果。而在股市貧弱的熊市,作空+作多的策略可以獲得較高的獲利;但是在股市強勁的牛市,作多策略更具有優勢,實現較穩定的報酬,但是從 alpha 值也可以看到不會有太多的超額報酬。整體來說雖然在市場波動的情況下持續上升,顯示出穩健的上行趨勢,且風險較低,但需要較長時間才能收到成效的策略。

溫馨提醒,本次策略與標的僅供參考,不代表任何商品或投資上的建議。之後也會持續使用 TEJ 資料庫及 TQuant Lab 來建構各式指標,並回測指標績效,所以歡迎對各種交易回測有興趣的讀者,選購 TQuant Lab 的相關方案,用高品質的資料庫,建構出適合自己的交易策略。

【TQuant Lab 回測系統】解決你的量化金融痛點

全方位提供交易回測所需工具

Github 原始碼

點此前往 Github

延伸閱讀

相關連結

返回總覽頁
Procesing