iT邦幫忙

4

演算法交易(Algorithmic Trading) 實作

前言

現在全球股市大熱,很多人都忍不住誘惑,跳進去買賣,如何『選股』、『擇時』,賺到錢就是一門重要而困難的課題,本文以最簡單的『移動平均法』(Moving Average)為例,說明如何以此操作策略,『擇時』買賣,並以回測(Back Testing)計算損益,驗證策略是否可行。這種以策略進行交易的模式,稱為『演算法交易』(Algorithmic Trading),當然,也意謂我們可以導入機器學習、深度學習或強化學習等人工智慧的各式演算法,進行更智慧型的投資,例如多家金控推出的機器人理財。

實作

太多的文章介紹如何使用爬蟲,抓取股票歷史資料,筆者就不贅述了。直接抓取5年資料,進行回測(Back Testing)。
首先讀進最夯的台積電(2330)股票5年日股價資料,如下:

# 載入套件
import pandas as pd

# 讀日股價資料
df = pd.read_csv('2330.csv')
df['date'] = pd.to_datetime(df['date'])
df.head(10)

執行結果如下:
https://ithelp.ithome.com.tw/upload/images/20210114/2000197611takwryGt.png

注意,我們會使用調整後股價(adjusted close),欄位名稱為adjclose,而不是使用收盤價,因為,股票會經過除權、除息或分割,調整後股價會反映真實的淨股價漲跌。

計算移動平均線,非常簡單,Pandas 直接提供 Rolling 函數實踐『移動視窗』,另 mean 函數計算平均數,組合起來就是移動平均,月線(約22個營業日) 程式如下:

# 載入套件
sma_short = pd.DataFrame()
sma_short['date'] = df['date']
sma_short['adjclose'] = df['adjclose'].rolling(window=22).mean()
sma_short.loc[15:30]

執行結果如下,前21筆為Null:
https://ithelp.ithome.com.tw/upload/images/20210114/200019763grhmU8C43.png

計算季線(約22個營業日)程式如下:

sma_long = pd.DataFrame()
sma_long['date'] = df['date']
sma_long['adjclose'] = df['adjclose'].rolling(window=66).mean()
sma_long.loc[90:110]

我們以短期移動平均線與長期移動平均線交叉點作為買賣點,以月線與季線為例:

  1. 買點:月線由下往上穿過季線,即黃金交叉,買進股票。
  2. 賣點:月線由上往下穿過季線,即死亡交叉,賣出股票。

先畫出兩條線

import seaborn as sns
import matplotlib.pyplot as plt

plt.figure(figsize=(10,6))
sns.lineplot(x='date', y='adjclose', data=sma_short)
sns.lineplot(x='date', y='adjclose', data=sma_long)

執行結果如下:
https://ithelp.ithome.com.tw/upload/images/20210114/20001976i4SXXDyL8X.png

回測(Back Testing)

寫一個函數如下,找出買賣點,請仔細看程式註解:

import numpy as np

def buy_sell(df):
    signal_buy = []  # 買點價格
    signal_sell = [] # 賣點價格
    
    flag=-1          # 買賣點旗標,短期超過長期為1,反之為0
    
    # 掃描每一筆資料
    for index, row in df.iterrows():
        # 短期超過長期
        if row[df.columns[1]] > row[df.columns[2]]:
            if flag!=1: # 之前的短期未超過長期,即黃金交叉
                signal_buy.append(row[df.columns[3]])
                signal_sell.append(np.nan)
                flag=1
            else:
                signal_buy.append(np.nan)
                signal_sell.append(np.nan)
        elif row[df.columns[1]] < row[df.columns[2]]:
            if flag!=0: # 之前的長期未超過短期,即死亡交叉
                signal_buy.append(np.nan)
                signal_sell.append(row[df.columns[3]])
                flag=0
            else:
                signal_buy.append(np.nan)
                signal_sell.append(np.nan)
        else:
            signal_buy.append(np.nan)
            signal_sell.append(np.nan)
    return (signal_buy, signal_sell)

呼叫上述函數,得到買賣點:

signal_buy, signal_sell = buy_sell(df_new)
# 買點
df_buy = pd.DataFrame({'date': df['date'], 'signal_buy':signal_buy})
df_buy = df_buy[~np.isnan(signal_buy)]
df_buy

買點:
https://ithelp.ithome.com.tw/upload/images/20210114/20001976RVzCko5IBl.png
賣點:

# 賣點
df_sell = pd.DataFrame({'date': df['date'], 'signal_sell':signal_sell})
df_sell = df_sell[~np.isnan(signal_sell)]
df_sell

https://ithelp.ithome.com.tw/upload/images/20210114/20001976OeDpc3cjPp.png

繪製所有資訊:

# 合併短期與長期移動平均線
df_new = sma_short.copy()
df_new = df_new.rename({'adjclose':'sma_short'}, axis=1)
df_new.insert(2, 'sma_long', sma_long['adjclose'])
df_new.insert(3, 'adjclose', df['adjclose'])
signal_buy, signal_sell = buy_sell(df_new)

# 繪圖
import seaborn as sns
import matplotlib.pyplot as plt

plt.figure(figsize=(10,6))
sns.lineplot(x='date', y='adjclose', data=sma_short, color='g', label='短期趨勢')
sns.lineplot(x='date', y='adjclose', data=sma_long, color='b', label='長期趨勢')

plt.plot(df['date'], df['adjclose'], color='r', alpha=0.5, label='日線')
plt.scatter(df['date'], signal_buy, c='r', marker='^', s=150)
plt.scatter(df['date'], signal_sell, c='g', marker='^', s=150)

plt.legend()

執行結果如下:
https://ithelp.ithome.com.tw/upload/images/20210114/20001976HW75A8P50W.png

損益計算

這部份比較複雜,基本上在賣出時計算損益,但是,額外有兩點要考慮:

  1. 無庫存時先賣出:兩種處理方法,放棄此賣點,另一方法,先賣出,庫存為負,本文採後者。
  2. 最後要平倉:將庫存以最後一天的收盤價賣出。

寫個函數計算損益:

# 計算損益(profit/loss)
def calc_profit(df_buy, df_sell, df):
    df_profit = df_buy.merge(df_sell, on='date', how='outer') 
    df_profit.sort_values(by='date', inplace=True)

    df_date = df.set_index('date')

    balance=0
    profit=0
    cost=0
    for index, row in df_profit.iterrows():
        if not row['signal_buy'] is None:
            balance+=1
            cost+=df_date.loc[row['date'], 'adjclose']
        elif not row['signal_sell'] is None:
            if balance>0:
                avg_cost = cost / balance
                profit += df_date.loc[row['date'], 'adjclose'] - avg_cost
                cost -= avg_cost
            else:
                profit += df_date.loc[row['date'], 'adjclose']

            balance-=1

    if balance>0:
        profit += df_date.loc[row['date'], 'adjclose'] * balance - cost
    elif balance<0:
        profit += df_date.loc[row['date'], 'adjclose'] * balance
    
    return profit
    
calc_profit(df_buy, df_sell, df)    

** 以上假設一次只買賣一張,大賺 97 萬(972 x 1000)。 **

其他測試

移動平均法其實有一個缺點,因為採用平均數,買點會比起漲點晚入場,賣點會比起跌點晚出場,如遇大跌,恐怕來不及出清,因此,加上停損點/停利點會更好,譬如,賺20%就賣,不等死亡交叉點才賣,避免大賠,停利點操作也是類似作法。

除了上述實驗外,還可以作其他測試:

  1. 以其他股票測試,例如台泥(1101),只要改讀1101.csv即可,損益 = 11.9 萬。
  2. 改變移動平均的週期,以週/月線、台泥(1101)測試,損益 = 21.6 萬,以較短期的操作,會有更多的買賣點。
  3. 訂定買進/賣出策略,例如金字塔交易策略,隨著走勢逐步加碼/出清,降低風險。
  4. 搭配期權資料,訂定更全面性的操作策略。

結論

10年前筆者任職財經資料庫公司時,曾設計一個『策略交易系統』,可提供使用者撰寫簡單程式碼,自訂策略,進行回測,並列印投資報告,只可惜當時未獲公司高層抬愛,無法順利上市銷售,10年後,該公司才推出相關的API,功能不及當初系統的一半,銷售狀況想當然耳,令人不勝噓唏。

除了移動平均法,也可使用其他技術指標,進行回測,甚至基本面也可以,包括月營收、盈餘、財務比率均可,或者搭配量能(Volume),都是常用的策略,每一個策略並不會一體適用於所有公司,因此,如何找到適合的策略套用在特定的公司,就夠大家研究一陣子了。如果再將AI導入,那就夠精彩了。

本篇程式及測試資料收錄在『這裡』。


1 則留言

0
draguitar
iT邦新手 5 級 ‧ 2021-01-14 14:34:19

感謝無私分享!!

分享是進步的動力,謝謝鼓勵!

我要留言

立即登入留言