文件
開啟 Studio

教學

寫選股條件

本頁內容8

選股條件就是一張 True/False 的表:每一列是一天,每一欄是一檔股票,True 代表那天要持有。data.get() 回傳的表是 FinlabDataFrame,Pandas 的寫法都能用,另外多了選股專用的函式,並且會自動對齊不同頻率的資料。

不熟悉 Pandas 可以先看策略常用的 Pandas 語法。

一行比較整張表

比較運算會一次算完整張表的每一格:

from finlab import data

close = data.get('price:收盤價')

# 收盤價在 10 到 100 元之間
between_10_100 = (close > 10) & (close < 100)
close 收盤價
233023172454
10/279851120
10/2810150118
10/2910449121
10/3010352117
close.average(60) 60 日均線
233023172454
10/2710052115
10/2810051116
10/2910151117
10/3010151118
position 要持有的股票
233023172454
10/27FalseFalseTrue
10/28TrueFalseTrue
10/29TrueFalseTrue
10/30TrueTrueFalse
每一格是一檔股票在一天的值。比較運算一次算完整張表,日期與股票會自動對齊;True 就是那天要持有的股票。數字為示意。

可以用的運算:

類型寫法
算術+ - * /
比較> >= == < <=
邏輯&(且)、|(或)、~(非)

日資料和季資料直接一起算

股價每天更新、財報每季更新,FinlabDataFrame 會在運算前自動對齊,不用自己 reindex:

對齊什麼怎麼對齊
日期(index)取聯集,缺的日期沿用最近一筆數值(forward fill)
股票(column)取交集,只有一邊有資料的股票會被剔除
from finlab import data

close = data.get('price:收盤價')                     # 日資料
roa = data.get('fundamental_features:ROA稅後息前')    # 季資料

cond1 = close > 37
cond2 = roa > 0
cond_1_2 = cond1 & cond2

下圖是 1101 台泥的訊號:cond1 每天變、cond2 一季才變一次,cond_1_2 兩者都滿足時才是 True。

1101 台泥的 cond1(日頻)、cond2(季頻)與兩者交集的訊號

排名挑前幾名

is_largest(n) 每天挑出數值最大的 n 檔,is_smallest(n) 挑最小的 n 檔:

roe.is_largest(3) 某一天的六檔股票
  1. 2330 18.2% True
  2. 2317 6.1% False
  3. 2454 24.5% True
  4. 2308 11.8% False
  5. 2603 3.4% False
  6. 2881 15.0% True
每一天都依當天的數值排名,最大的 3 檔是 True;隔天重新排名,互不影響。數字為示意。
from finlab import data

roa = data.get('fundamental_features:ROA稅後息前')
good_stocks = roa.is_largest(10)   # 每期 ROA 前 10 名
date000116000538000616000700000779
2013-Q1FalseFalseTrueFalseFalse
2013-Q2FalseFalseFalseFalseTrue
2013-Q3FalseTrueFalseFalseFalse
2013-Q4FalseFalseFalseTrueFalse
2014-Q1TrueFalseFalseFalseFalse

想看每天的門檻值,例如前 10% 的收盤價,用 quantile_row:

data.get('price:收盤價').quantile_row(0.9)   # 每天收盤價的 90 百分位數

設定進場與出場

entries.hold_until(exits):進場訊號是 True 就買進,一直持有到出場訊號是 True 才賣出。

buy進場訊號
sell出場訊號
buy.hold_until(sell)持有
1234567891011121314
出現進場訊號就買進,一直持有到出場訊號出現;持有期間再出現進場訊號不會有影響。下方數字是第幾個交易日。
from finlab import data

close = data.get('price:收盤價')
buy = close > close.average(5)     # 站上 5 日均線就進場
sell = close < close.average(20)   # 跌破 20 日均線就出場

position = buy.hold_until(sell)

close.average(n) 是 n 日移動平均;視窗內可用的值不到一半時,那一格是 NaN。

畫圖檢查條件對不對

挑一檔股票,把收盤價、均線和條件畫在一起:

import matplotlib.pyplot as plt

sma = close.average(10)
cond = close > sma

close.loc['2021', '2330'].plot()
sma.loc['2021', '2330'].plot()
cond.loc['2021', '2330'].mul(20).add(500).plot()
plt.legend(['close', 'sma', 'cond'])
2330 的收盤價、10 日均線與條件訊號

hold_until 常用的參數:

參數作用範例
nstocks_limit最多同時持有幾檔,進場訊號太多時輪動buy.hold_until(sell, nstocks_limit=5)
rank進場訊號超過上限時,先挑誰(數值大的優先)buy.hold_until(sell, rank=-pb)
stop_loss停損比例buy.hold_until(sell, stop_loss=0.1)
take_profit停利比例buy.hold_until(sell, take_profit=0.2)

完整策略:站上 20 日均線進場、跌破 60 日均線出場,最多持有 10 檔,股價淨值比低的優先。

strategy.py
from finlab import data
from finlab.backtest import sim

close = data.get('price:收盤價')
pb = data.get('price_earning_ratio:股價淨值比')

entries = close > close.average(20)
exits = close < close.average(60)

position = entries.hold_until(exits, nstocks_limit=10, rank=-pb)
sim(position)
出場訊號的日期比較稀疏時(exit_mode)

進場訊號一律是事件,只在自己有的日期生效。出場訊號來自月營收、財報、週/月頻條件這類日期比較稀疏的資料時,exit 沒有的日期怎麼處理由 exit_mode 決定:

  • 'state'(預設):出場是持續的狀態,沒有的日期沿用前一個日期的值。所以月營收條件在下次公布前一直有效,期間即使進場訊號是 True 也不會買進。exit 的日期必須由舊到新排序,否則會出現 ValueError,請先 exit.sort_index()。
  • 'event':出場是事件,只在 exit 自己有的日期生效,其他日期視為不出場。

兩種模式下 NaN 都視為不出場。exit 在每個進場日都有資料時,兩種模式的結果相同。

想知道哪天進場、哪天出場,用 is_entry() 和 is_exit():

position = data.get('price:收盤價').is_largest(10)
entry_signals = position.is_entry()   # 新進場的那一天是 True
exit_signals = position.is_exit()     # 出場的那一天是 True

看趨勢:上升、下降、連續幾天

寫法意思
close.rise(10)比 10 期前高
close.fall(10)比 10 期前低
cond.sustain(window, count)最近 window 期中至少有 count 期為 True(省略 count 就是每期都要)
from finlab import data

close = data.get('price:收盤價')

close.rise(10)               # 是否高於 10 日前
close.rise().sustain(2)      # 連續 2 日上漲
close.rise().sustain(3, 2)   # 3 日中有 2 日上漲
rise(10) 的訊號示意

移除市值或產業的影響

多因子選股時,因子之間常常有關聯。例如小型股的本益比通常比較低,「低本益比」可能只是在挑小型股。中性化用橫截面迴歸移除指定因子的影響,留下因子本身的訊號。

neutralize(factors) 對指定因子做迴歸,回傳殘差:

from finlab import data

factor = data.get('price_earning_ratio:本益比')
size = data.get('etl:market_value')

neutralized = factor.neutralize(size)   # 去除市值影響

可以一次放多個因子,用 list 或命名的 dict(方便除錯):

close = data.get('price:收盤價')
volatility = (close / close.shift(1) - 1).rolling(60).std()   # 60 日波動率

neutralized = factor.neutralize([size, volatility])
neutralized = factor.neutralize({
    'size': size,
    'size2': size**2,
    'volatility': volatility,
})

中性化後的因子可以直接接 rank()、is_largest(),選出的股票更能反映因子本身,而不是被市值或產業主導。

其他好用的函式

依產業分組:groupby_category()

類似 pd.DataFrame.groupby(),依產業分組計算:

from finlab import data

pb = data.get('price_earning_ratio:股價淨值比')
pb.groupby_category().mean()['半導體業'].plot()   # 半導體業的平均股價淨值比
半導體業平均股價淨值比走勢
月、季資料的索引換成日期:index_str_to_date()

把月營收、財報的字串索引換成公告截止日:

轉換前轉換後
2022-M1(月營收)2022-01-10
2022-Q1(財務季報)2022-05-15
from finlab import data

data.get('monthly_revenue:當月營收').index_str_to_date()
data.get('financial_statement:現金及約當現金').index_str_to_date()
把一般的 Pandas 表轉成 FinlabDataFrame

data.get() 回傳的已經是 FinlabDataFrame。自己做的 DataFrame 可以這樣轉:

import pandas as pd
from finlab.dataframe import FinlabDataFrame

df = pd.DataFrame()
df_finlab = FinlabDataFrame(df)

所有函式與參數請看 FinlabDataFrame API。

下一步

  • 歷史回測 把條件交給 sim():換股頻率、停損停利與交易紀錄。
  • 技術指標 用 data.indicator() 算 RSI、KD、MACD。
  • 下載資料 找到條件要用的財報、月營收與籌碼。
  • 策略範例 看完整策略怎麼組合這些條件。
↑↓ 選擇 Enter 開啟 Esc 關閉