跟著外資進出,賺的到錢嗎? [附程式碼]


金融爬蟲系列文(一 ~ 三)已經詳細介紹,如何抓取證交所報酬指數及三大法人數據。

有了這些數據,我們試著利用三大法人數據建立交易策略。

三大法人當中,本篇要研究對台股影響力最大的「外資」。

首先,由於外資每天在台股的成交量變化很大,直接觀察「外資買賣超」容易失真。

於是,量化操盤手建構「外資多空比」指標,用來衡量「外資買賣超」佔外資成交量的比例。

外資多空比 = 外資買賣超 ÷ 外資買進金額 (若外資買超)
                     = 外資買賣超 ÷ 外資賣出金額 (若外資賣超)

接著,繪製「報酬指數」及「外資多空比」:


乍看之下,訊號似乎有點雜亂,稍微仔細看一下,應該可以發現「報酬指數」與「外資多空比」時常呈現同向走勢,也就是說當外資買超,大盤傾向上漲;反之,當外資賣超,大盤傾向下跌。

以上現象,其實再自然不過了,股價本來就是由市場供需決定,而外資持股約佔台股40%,這麼大量的買賣,當然足以牽動股市。

所以,光是知道同向變動,尚不足以獲利,讓我們繼續看下去。


如果我們把任意一段「外資多空比」放大,會發現外資常常連續買超或賣超,也就是「外資買賣超」存在叢聚現象(Clustering),買賣方向具有慣性。

結合「與大盤同向變動」及「叢聚現象」,一個可能的交易策略就浮現了。

我們可以嘗試跟單(持有與外資同向部位),由於外資買賣超具有慣性,很可能會帶動一波上漲或下跌,我們就能夠藉此獲利。


上圖是2015年以來,跟單策略的損益績效,雖然是正報酬,但波動很大,表現不甚理想。

切記,研發交易策略,千萬不要輕易放棄,往往再多深入研究一下,就會有嶄新的收穫。

量化操盤手常常會做一個動作,將多單及空單損益分開來看。


將多空損益分別畫出來,可以看到「多單損益」比「空單損益」要好上很多,從2016年到目前為止,報酬率為35%,而且績效曲線還算穩定。

總結下來,「只跟外資多單」是一個值得進一步探索的研究方向。

最後,在看程式碼之前,簡單說明兩點:

1. 第一張圖的X座標軸採用「序號」而非「日期」(df.index),是為了避免繪圖時,出現假日(非交易日)沒有數據的問題。

2. 回測採用「報酬指數」,實際操作時,最接近回測的做法是在證交所公布「三大法人買賣金額統計表」後,依「外資買賣超」方向建立期貨多單或空單(夜盤),因期貨數據處理較為複雜,這裡僅用「報酬指數」做為回測數據。

程式碼:
# 載入需要用到的函式庫
import IPython # 互動模組
import sqlalchemy # SQL資料庫ORM函式庫
import numpy as np # 科學計算函式庫
import pandas as pd # 數據分析函式庫
import matplotlib.pyplot as plt # 繪圖函式庫

# 連結SQLite資料庫
engine = sqlalchemy.create_engine('sqlite:///data.db')

# 讀取資料
df_fini = pd.read_sql('fini', engine, index_col='table_index', parse_dates=['table_index']) # 讀取外資數據
df_total = pd.read_sql('total_return_index', engine, index_col='table_index', parse_dates=['table_index']) # 讀取報酬指數資料

# 建立資料集
df = pd.DataFrame(index=df_total.index)
df.insert(len(df.columns), '序號', range(len(df.index)))
df.insert(len(df.columns), '報酬指數', df_total['發行量加權股價報酬指數'])
df.insert(len(df.columns), '報酬率', df['報酬指數'].pct_change() * 100)
df.insert(len(df.columns), '外資多空比', df_fini['買賣差額'] / df_fini[['買進金額', '賣出金額']].max(axis=1))

# 設定圖片輸出位置
IPython.get_ipython().enable_matplotlib(gui='qt') # 圖片輸出於新視窗

# 繪圖(1):觀察外資多空比
fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True) # 開啟新圖(2列1行、X軸相同)
ax1.plot(df['序號'], df['報酬指數'], color='b')
ax2.bar(df['序號'][df['外資多空比'] > 0], df['外資多空比'][df['外資多空比'] > 0], color='r', width=1)
ax2.bar(df['序號'][df['外資多空比'] < 0], df['外資多空比'][df['外資多空比'] < 0], color='g', width=1)
ax2.axhline(y=0, color='k')
plt.show()

# 回測
df.insert(len(df.columns), '外資方向', np.sign(df['外資多空比']))
df.insert(len(df.columns), '跟單部位', df['外資方向'].shift(1))
df.drop(df.index[0], inplace=True) # 第一天沒有部位,將其刪去,方便計算

df.insert(len(df.columns), '跟單損益', df['跟單部位'] * df['報酬率'])
df.insert(len(df.columns), '跟單損益累計', df['跟單損益'].cumsum())

# 繪圖(2):跟單損益累計
plt.figure()
plt.plot(df.index, df['跟單損益累計'], color='b')
plt.show()

# 分別觀察多單與空單損益
df.insert(len(df.columns), '只跟多單', df['跟單部位'].clip(0, None))
df.insert(len(df.columns), '只跟空單', df['跟單部位'].clip(None, 0))

df.insert(len(df.columns), '只跟多單損益', df['只跟多單'] * df['報酬率'])
df.insert(len(df.columns), '只跟多單損益累計', df['只跟多單損益'].cumsum())

df.insert(len(df.columns), '只跟空單損益', df['只跟空單'] * df['報酬率'])
df.insert(len(df.columns), '只跟空單損益累計', df['只跟空單損益'].cumsum())

# 繪圖(3):多單/空單損益累計
plt.figure()
plt.plot(df.index, df['只跟多單損益累計'], color='r', label='Long Only')
plt.plot(df.index, df['只跟空單損益累計'], color='g', label='Short Only')
plt.axhline(y=0, color='k')
plt.legend(fontsize=20)
plt.show()

參考資料:
1. 金融爬蟲(一)
2. 金融爬蟲(二)
3. 金融爬蟲(三)