教學
寫選股條件
本頁內容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 收盤價 | 2330 | 2317 | 2454 | |
|---|---|---|---|
| 10/27 | 98 | 51 | 120 |
| 10/28 | 101 | 50 | 118 |
| 10/29 | 104 | 49 | 121 |
| 10/30 | 103 | 52 | 117 |
close.average(60) 60 日均線 | 2330 | 2317 | 2454 | |
|---|---|---|---|
| 10/27 | 100 | 52 | 115 |
| 10/28 | 100 | 51 | 116 |
| 10/29 | 101 | 51 | 117 |
| 10/30 | 101 | 51 | 118 |
position 要持有的股票 | 2330 | 2317 | 2454 | |
|---|---|---|---|
| 10/27 | False | False | True |
| 10/28 | True | False | True |
| 10/29 | True | False | True |
| 10/30 | True | True | False |
可以用的運算:
| 類型 | 寫法 |
|---|---|
| 算術 | + - * / |
| 比較 | > >= == < <= |
| 邏輯 | &(且)、|(或)、~(非) |
日資料和季資料直接一起算
股價每天更新、財報每季更新,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。
排名挑前幾名
is_largest(n) 每天挑出數值最大的 n 檔,is_smallest(n) 挑最小的 n 檔:
roe.is_largest(3) 某一天的六檔股票- 2330 18.2% True
- 2317 6.1% False
- 2454 24.5% True
- 2308 11.8% False
- 2603 3.4% False
- 2881 15.0% True
from finlab import data
roa = data.get('fundamental_features:ROA稅後息前')
good_stocks = roa.is_largest(10) # 每期 ROA 前 10 名| date | 000116 | 000538 | 000616 | 000700 | 000779 |
|---|---|---|---|---|---|
| 2013-Q1 | False | False | True | False | False |
| 2013-Q2 | False | False | False | False | True |
| 2013-Q3 | False | True | False | False | False |
| 2013-Q4 | False | False | False | True | False |
| 2014-Q1 | True | False | False | False | False |
from finlab import data
pb = data.get('price_earning_ratio:股價淨值比')
cheap_stocks = pb.is_smallest(10) # 股價淨值比最低的 10 檔想看每天的門檻值,例如前 10% 的收盤價,用 quantile_row:
data.get('price:收盤價').quantile_row(0.9) # 每天收盤價的 90 百分位數設定進場與出場
entries.hold_until(exits):進場訊號是 True 就買進,一直持有到出場訊號是 True 才賣出。
buy進場訊號sell出場訊號buy.hold_until(sell)持有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'])
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 檔,股價淨值比低的優先。
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 日上漲
移除市值或產業的影響
多因子選股時,因子之間常常有關聯。例如小型股的本益比通常比較低,「低本益比」可能只是在挑小型股。中性化用橫截面迴歸移除指定因子的影響,留下因子本身的訊號。
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,
})neutralize_industry() 以產業別做迴歸,回傳殘差,預設使用 security_categories 的產業分類:
from finlab import data
factor = data.get('price_earning_ratio:本益比')
neutralized = factor.neutralize_industry()也可以自訂分類(stock_id 與 category 兩欄):
import pandas as pd
custom_cats = pd.DataFrame(
{'stock_id': ['2330', '2317', '1101'], 'category': ['半導體', '電子', '水泥']}
)
neutralized = factor.neutralize_industry(categories=custom_cats)中性化後的因子可以直接接 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。
下一步
這一頁有幫到你嗎?