【實戰應用】TQuant Lab F-score 策略,找出被低估的優質股票

TEJ 台灣經濟新報
TEJ-API 金融資料分析
16 min readJun 21, 2024
F-score 策略
Photo by engin akyurt on Unsplash

本文重點摘要

  • 文章難度:★★☆☆☆
  • 簡介 F-score 策略的九項選股條件。
  • 利用 Pipeline 篩選符合 F-score 策略九項指標的股票,並形成買進訊號。
  • 透過 TQuant Lab 的簡化版回測引擎 TargetPercentPipeAlgo 回測 F-score 策略每季再平衡的績效表現。

前言

在當今的股市中,投資人面臨著選擇眾多股票的挑戰,許多投資人希望能夠找到被低估但基本面良好的股票。然而,市場的波動和信息的不對稱性往往使得這個目標變得困難重重。在這種情境下,Piotroski F-score,亦稱為皮爾托斯基分數或 F-score,成為了投資者的重要工具。

由前芝加哥大學教授 Joseph Piotroski 提出的 F-score 策略,透過 9 項財報條件的評估,幫助投資人深入了解公司的財務健康狀況,F-score 涵蓋公司的獲利性、安全性及成長性,提供了多維度的分析框架,讓投資人能夠更全面地評估公司的潛在價值。為了探討 F-score 在台股市場的表現,本文使用 TQuant Lab 建構 F-score 策略,幫助投資人深入了解 F-score 所能帶來的投資效益。

F-score 策略

根據 Joseph Piotroski 教授所著的論文,為了找出被低估且在獲利性、安全性及成長性都相對穩健的股票,建構 F-score 策略時,我們需要透過以下兩個步驟來生成交易訊號:

  1. 找出市場中淨值市價比 ( Book-to-Market Ratio, BM Ratio ) 前 20% 的股票形成股票池。
    p.s. BM Ratio 越高,代表該股票價值越被低估,因為該股票的交易價格低於其資產的價值。
  2. 根據 F-score 的 9 項基本面條件計算分數,符合一項得 1 分。我們將買入 8 分和 9 分的標的,並在每季進行一次再平衡。

F-score 策略的 9 項基本面條件如下:

  • 獲利性
  1. 資產報酬率 ( ROA ) > 0
  2. 今年 ROA > 去年 ROA
  3. 營業現金流 > 0
  4. 營業現金流 > 稅後淨利
  • 安全性
  1. 今年度的長期負債金額 < 上一年度
  2. 今年度的流動比率 > 上一年度
  3. 上一年度沒有發行新股
  • 成長性
  1. 今年度的毛利率 > 上一年度
  2. 今年度的資產週轉率 > 上一年度

以下,我們將透過 TQuant Lab 來生成 F-score 策略所需的交易訊號。

編輯環境與模組需求

本文使用 Windows 11 並以 Jupyter Lab 作為編輯器。

載入套件與環境設定

import os
import numpy as np
import pandas as pd

# tej_key
tej_key = 'your key'
api_base = 'https://api.tej.com.tw'

os.environ['TEJAPI_KEY'] = tej_key
os.environ['TEJAPI_BASE'] = api_base

取得股票池

我們透過以下兩個步驟取得被低估的股票:

  1. 利用 get_universe 函式取得 2019-05-06 所有上市櫃普通股的股票代碼。
  2. 利用 TEJ Tool API 取得上述股票的股價淨值比 ( Price-Book Ratio, PB ratio ),藉此計算 BM Ratio,進而取得 BM Ratio 前 20 % 的股票。
    p.s. PB Ratio 和我們需要的 BM Ratio 互為倒數
from zipline.sources.TEJ_Api_Data import get_universe

pool = get_universe(start = '2019-05-06',
end = '2019-05-06',
mkt = ['TWSE', 'OTC'], # 上市櫃公司
stktp_e = ['Common Stock', 'Common Stock-Foreign'] # 普通股 & KY股
)
import TejToolAPI

data = TejToolAPI.get_history_data(start = '2019-05-06',
end = '2019-05-06',
ticker = pool,
columns = ['PBR_TEJ'],
transfer_to_chinese = False
)

# 計算 BM Ratio = 1/PB Ratio
data['BM_ratio'] = 1/data['PBR_TEJ']

# 計算 BM Ratio 的 80 分位數
quantile_80 = data['BM_ratio'].quantile(0.80)

# 選取 BM Ratio 前 20% 的資料
top_20_percert_data = data[data['BM_ratio'] >= quantile_80]

# 取得 BM Ratio 在市場前 20% 的股票 -> pool
pool = top_20_percert_data['coid'].tolist()
F-score 策略
淨值市價比前 20% 的股票資料

取得 F-score 策略需要的財務資料

根據計算 F-score 所需的財務條件,我們使用 Tej Tool API 抓取以下 9 項財務資料:

  1. ROA: Return_on_Total_Assets_A_percent
  2. 營業現金流:Cash_Flow_from_Operating_Activities
  3. 每股稅後淨利:Net_Income_Per_Share
  4. 在外流通股數_仟股:Outstanding_Shares_1000_Shares
  5. 長期負債:Total_Non_current_Liabilities
  6. 流動比率:Current_Ratio
  7. 現金增資_仟股:Cash_Capital_Increase_Thousand_Shares
  8. 營業毛利率:Gross_Margin_Rate_percent
  9. 資產週轉率:Total_Assets_Turnover
start = '2019-05-06'
end = '2023-12-31'

columns = ['Return_on_Total_Assets_A_percent',
'Cash_Flow_from_Operating_Activities',
'Net_Income_Per_Share',
'Outstanding_Shares_1000_Shares',
'Total_Non_current_Liabilities',
'Current_Ratio',
'Cash_Capital_Increase_Thousand_Shares',
'Gross_Margin_Rate_percent',
'Total_Assets_Turnover']

fin_data = TejToolAPI.get_history_data(start = start,
end = end,
ticker = pool,
columns = columns,
transfer_to_chinese = False
)

fin_data = fin_data.sort_values(['coid','mdate'])
fin_data = fin_data.filter(regex='(_TTM$|^(?!.*(_A$|_Q$|_TTM$)).*$)') # 僅保留移動四季 (TTM) 的資料
fin_data['Net_Income'] = fin_data['Outstanding_Shares_1000_Shares'] * fin_data['Net_Income_Per_Share_TTM']
fin_data
F-score 策略
使用 TEJ Tool API 取得之部分原始資料

導入股票池價量資料

資料期間從 2019–05–06 至 2023–12–31,並導入上述 340 檔股票的價量資料與加權股價報酬指數 ( IR0001 ) 作為績效比較基準。

os.environ['mdate'] = start + ' ' + end
os.environ['ticker'] = ' '.join(pool) + ' ' + 'IR0001'

!zipline ingest -b tquant

將財務資料導入 Pipeline

CustomDataset 可以將資料庫中的內容導入 Pipeline 中,方便後續回測使用。於本範例我們用以將計算 F-score 所需的財務資料導入 Pipeline。擷取部分程式碼如下:

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

class CustomDataset(DataSet):

Return_on_Total_Assets_A_percent_TTM = Column(dtype=float)
Cash_Flow_from_Operating_Activities_TTM = Column(dtype=float)
Net_Income = Column(dtype=float)
Total_Non_current_Liabilities_TTM = Column(dtype=float)
Current_Ratio_TTM = Column(dtype=float)
Cash_Capital_Increase_Thousand_Shares = Column(dtype=float)
Gross_Margin_Rate_percent_TTM = Column(dtype=float)
Total_Assets_Turnover_TTM = Column(dtype=float)
domain = TW_EQUITIES

建立 CustomFactor 函式

在計算 F-score 前,我們先使用 CustomFactor 函式自訂以下兩個因子:

  1. 計算財務數據 YOY 變化。
  2. 將通過 F-score 的資料轉為 1 ,沒通過的轉為 0,用以計算 F-score 分數。

建立 Pipeline 函式

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

  • 導入股價與財務資料。
  • 計算財務數據 YOY 變化、年度平均現金增資 ( 用以判斷是否發行新股 ) 。
  • 計算 F-score,並在 longs 欄位中生成交易訊號:F-score 8 分為 True,其餘則為 False。

擷取部分 Pipeline 內容如下:

from zipline.pipeline import Pipeline
from zipline.pipeline.factors import Factor, SimpleMovingAverage
from zipline.pipeline.filters import StaticAssets

benchmark_asset = bundle.asset_finder.lookup_symbol('IR0001',as_of_date = None)

def make_pipeline():
return Pipeline(
columns = {
'price': price,
'f_score_total': f_score_total,
'longs': f_score_total >= 8
},
screen = ~StaticAssets([benchmark_asset]) # 排除大盤的數據
)

start_dt = pd.Timestamp(start, tz = 'UTC')
end_dt = pd.Timestamp(end, tz = 'UTC')

pipeline_result = engine.run_pipeline(make_pipeline(), start_dt, end_dt)
pipeline_result
F-score 策略
Pipeline 部分資訊

回測 F-score 策略

使用 TEJ 自製的簡化版 Zipline 回測引擎:TargetPercentPipeAlgo,輕鬆在一行內設定所有回測參數,最少只需輸入策略 pipeline 即可進行回測。
p.s. TargetPercentPipeAlgo 的詳細使用說明可參考 TQuant Lab GitHub: Simple Algorithm-TargetPercentPipeAlgo

本次策略修改的相關參數:

  • 回測起迄日 ( start_session, end_session ):2020 / 05 / 30 ~ 2023 / 12 / 31
  • 初始本金 ( capital_base ):1 千萬
  • 交易日 ( 再平衡日 ):3, 6, 9, 12 月的十五號
  • 最大槓桿:90%
  • 策略 pipeline:將讀取上述 Pipeline 中的 longs 欄位判斷再平衡日是否交易
  • 滑價模型:VolumeShareSlippage
  • 手續費模型:Custom_TW_Commission
from zipline.algo.pipeline_algo import *

start_dt = pd.Timestamp(start, tz = 'UTC')
end_dt = pd.Timestamp(end, tz = 'UTC')
algo = TargetPercentPipeAlgo(
start_session=start_dt,
end_session=end_dt,
capital_base=1e7,
tradeday=tradeday,
max_leverage=0.9,
pipeline=make_pipeline,
slippage_model=slippage.VolumeShareSlippage(volume_limit=0.15, price_impact=0.01),
commission_model = commission.Custom_TW_Commission(min_trade_cost = 20, discount = 1.0, tax = 0.003),
custom_loader=custom_loader,
analyze=analyze
)
results = algo.run()
F-score 策略
交易明細表

利用 Pyfolio 進行績效評估

import pyfolio as pf 

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
)
F-score 策略
回測表現與大盤比較圖

藉由上表我們可以看到 F-score 策略的年化報酬率達到 27.25%,年化波動度約為 18.6%,另外夏普比率為 1.39,且 α 值為 0.11,顯示 F-score 策略在相對可控的風險之下,亦能為投資人賺取不俗的超額報酬。觀察 β 值,0.77 代表 F-score 策略的績效與大盤走勢的相關性不算高,表示 F-score 的九項選股條件確實能為投資人規避部分系統風險。在績效比較圖中,可以發現 F-score 策略於 2022 年的回檔下跌幅度較大盤小,且脫離熊市後,回升的績效也較大盤為佳,再次說明本策略優良的獲利能力。

F-score 策略
年化報酬率圖

在年化報酬率圖中,可以發現除了 2022 年經歷較大的回檔之外,其餘三年的年化報酬率都能達到超過 30% 的表現。

F-score 策略
長短部位曝險圖

藉由長短部位曝險圖,我們可以看到因為先前在 TargetPercentPipeAlgo 設定最大槓桿為 90%,所以曝險部位穩定在 0.9 左右,使 F-score 策略保有部分餘裕不受大波動的影響。

結論

本次策略由 Joseph Piotroski ( 2002 ) 的論文作為出發點,欲探討 F-score 策略於台股市場能否為投資人穩定獲利。策略建構上,我們透過淨值市價比找尋被低估的股票,並使用 Pipeline 計算 F-score 及生成交易訊號,最後應用簡化版的回測引擎 TargetPercentPipeAlgo 進行策略回測。

因為 F-score 策略每季再平衡的特性,使投資人在每季有新的財報數據發佈後,能及時更新投資組合,而 Pyfolio 的績效分析,也進一步驗證了 F-score 策略優於大盤的獲利能力,顯示即使此策略已推出逾 20 年,在公司獲利性、安全性及成長性的評估上依然擁有不俗的能力,且在台股市場中也有應用的空間。

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

Github 原始碼

延伸閱讀

相關連結

--

--

TEJ 台灣經濟新報
TEJ-API 金融資料分析

TEJ 為台灣本土第一大財經資訊公司,成立於 1990 年,提供金融市場基本分析所需資訊,以及信用風險、法遵科技、資產評價、量化分析及 ESG 等解決方案及顧問服務。鑒於財務金融領域日趨多元與複雜,TEJ 結合實務與學術界的精英人才,致力於開發機器學習、人工智慧 AI 及自然語言處理 NLP 等新技術,持續提供創新服務