Skip to main content

【程式交易實戰 05】讓 API 幫你定期定額存好股

· 13 min read

歡迎來到 程式交易實戰 的第五堂課,還記得上堂課實作的網格交易策略嗎?主要是以「定價」的角度建構買賣策略,而定期定額則是以「定時」的角度買進股票,不論股價的漲跌,每隔一段固定的時間就買進相同金額的股數。今天將介紹原來透過程式交易也可以讓你定期定額存好股!

讀完本篇文,您將學會...

  • 熟悉經典策略:平均成本法 (Dollar Cost Averaging, DCA)
  • 了解定期定額的實際回測績效
  • 定期定額實單演練

上堂課實作了「定價」的網格交易策略,當股價漲到一定價格時就賣出,跌到一定價格時就買進,藉此在震盪的盤勢中獲取利潤。而平均成本法,也就是大家常聽到的定期定額則是「定時」買進某檔股票,若看好一家公司的長期趨勢,定期定額的分批操作方式可以有效降低平均持有成本,待股價上漲後會因平均成本已經降低且持有的股數較多而有更可觀的獲利。

定期定額優勢

  • 上班族非常適合的投資方法,薪水是每個月撥款,持續穩定買進可以確保長期的持有成本達到相對低。
  • 可以把定期定額想像成儲蓄的概念,長期來看與銀行利率或其他儲蓄型保單相比報酬率有機會高出許多,若有資金上的需求,也更方便買賣換成現金!
  • 不需要盯盤交易,克服人性的弱點。下圖為定期定額的加碼方法,做第一次買進後股價馬上就開始下跌,或許就會讓主觀交易者思考是否該停損或是不動作,但如果是定期定額就會非常有紀律的買進以降低成本,以此案例來看定期定額是有優勢的。

week5_01

定期定額劣勢

  • 前提假設是該標的長期趨勢向上才能有這樣的效果。若標的持續下跌,會使得平均成本反而較高,需要更多時間才有機會重新獲利。
  • 獲利金額受限,若一開始就擁有一筆資金,那在等待時間買進的過程中,這些閒置資金的機會成本也是必須計算的,因為若將資金投入更多則有機會獲得更高的報酬。
  • 通常定期定額的出場點是有資金需求時,若有資金需求時恰巧為股市低迷期,可能無法獲得統計上的平均報酬。

標的選擇

上述提及定期定額必須是長期趨勢向上的標的較能夠獲利,因此選擇標的就相當重要了。單一標的較容易因為公司政策以及獲利狀況等影響變數較大,較難預測單一個股的走勢,建議可考慮涵蓋多產業或是挑選指數型 ETF 來分散風險。

定期定額實作

看到這裡,相信大家都能理解定期定額的優劣了,接下來我們就直接透過富果 API 了解實驗結果及實際下單給大家看吧!

首先先取得 0050 近 10 年的歷史資料,並且不考慮除息狀況,程式碼如下:

# 載入相關套件
import matplotlib.pyplot as plt
from datetime import timedelta
import pandas as pd
import numpy as np
import requests
import datetime
import json
import time

key = "YOUR_TOKEN" # 您的行情 API Token
symbol = "0050" # 股票代碼

candles_list = []

for y in ['2012','2013','2014','2015','2016','2017','2018','2019','2020','2021','2022']:
start_date = y+"-01-01" # 選擇資料起始日期
end_date = y+"-12-31" # 選擇資料結束日期
# API URL
url = f"https://api.fugle.tw/marketdata/v0.3/candles?symbolId={str(symbol)}&apiToken={key}&from={start_date}&to={end_date}"

# 取得歷史資料
historical_data = requests.get(url).json()
# 將資料轉為 dataframe 方便回測
candles_list = candles_list + historical_data['data']

df = pd.DataFrame(candles_list).sort_values('date').set_index('date')

取得歷史資料後,我們將進行以下實驗!

開收盤分批買進更能分散風險

相信各券商已推出不少方便好用的定期定額或定期存股功能,但除了使用自動扣款的定期存股外,我們也可以善用 API 來幫我們更自由地擬定定期存股策略,例如前面是以收盤價作為買進的價格,那萬一交易的標的剛好有開低走高的特性豈不是買進成本相對高一些了呢?如果能夠開盤時買進 50%、收盤時也買進 50% 能夠將風險更分散,以下我們就來實驗看看這樣的買進方法結果是如何吧!

def run_backtest(buy_date, invest_amount):
# 紀錄買進紀錄
buy_record_list = []

date_list = []
for y in range(2012,2023,1):
for m in range(1,13,1):
date_list.append(str(y)+"-"+str(m).zfill(2)+"-"+str(buy_date).zfill(2))

for date in date_list:
target_date = date
if target_date in df.index:
# 開盤使用一半的資金買進的股數
buy_qty = (invest_amount/2)/df.loc[target_date,'open']
# 收盤使用剩於一半的資金買進的股數
buy_qty += (invest_amount/2)/df.loc[target_date,'close']
# 計算平均成本
avg_price = invest_amount / buy_qty

buy_record_list.append({"date":target_date, "buy_qty":buy_qty, 'price': avg_price })
else:
# 若為休假日,就延後買進
count = 0
while target_date not in df.index and count <30:
target_date = str(datetime.datetime.strptime(target_date, "%Y-%m-%d").date() + timedelta(days=1))
# 如果連續找了30天沒找到帶表示資料已經到底了
count = count + 1

# 如果順利找到
if count < 30:
# 開盤使用一半的資金買進的股數
buy_qty = (invest_amount/2)/df.loc[target_date,'open']
# 收盤使用剩於一半的資金買進的股數
buy_qty += (invest_amount/2)/df.loc[target_date,'close']
# 計算平均成本
avg_price = invest_amount / buy_qty

buy_record_list.append({"date":target_date, "buy_qty":buy_qty, 'price':avg_price })

buy_record_df = pd.DataFrame(buy_record_list).set_index('date')

# 計算累積持有股數
buy_record_df['cumsum'] = buy_record_df['buy_qty'].cumsum()
# 計算該筆成本
buy_record_df['cost'] =buy_record_df['buy_qty'] * buy_record_df['price']
# 計算累積成本
buy_record_df['cost_cusum'] =buy_record_df['cost'].cumsum()
# 計算持有股數市值
buy_record_df['market_value'] = buy_record_df['cumsum'] * buy_record_df['price']
# 計算報酬率
buy_record_df['return_rate'] = buy_record_df['market_value'] / buy_record_df['cost_cusum'] *100

buy_record_df['return_rate'].plot(label=buy_date, figsize=(20,8))
return round(buy_record_df.iloc[-1]['return_rate'],2)
# 查看每月 d 日的報酬率
for d in range(1,29):
rate = run_backtest(d, 5000)
print("買進日:"+str(d) +" 報酬:"+str(rate)+"%")

透過實驗結果發現,以 0050 為例月中的表現較佳,可能是因部分投資人或某些基金在月初或月底較常進行一些交易行為,因而造成股價有較大波動。讀者可以自行嘗試其他標的或也可以嘗試將資金分散到不同的扣款日看看是不是會有更好的效果哦!根據上述實驗結果,我們選定每月 5 日作為買進的時間來實作自動下單程式。

透過富果 API 進行定期定額自動下單

首先請將設定檔及憑證檔移到相對應的路徑並輸入登入密碼及憑證密碼!

from configparser import ConfigParser
from fugle_trade.sdk import SDK
from fugle_trade.order import OrderObject
from fugle_trade.constant import (APCode, Trade, PriceFlag, BSFlag, Action)
import time

# 讀取設定檔
config = ConfigParser()
config.read('./config.simulation.ini') # 使用模擬金鑰設定檔
key = 'YOUR_TOKEN'
# 密碼輸入及登入
sdk = SDK(config)
sdk.login()

接下來為交易邏輯的部分,將開始計算若每月本金 5000 元,在每個時段可以買進的股數是多少

# 取得目前即時股價 並推算可以買進的股數
def cal_buy_qty(symbol_id, invest_amount):

data = requests.get(f"https://api.fugle.tw/realtime/v0.3/intraday/quote?symbolId={symbol_id}&apiToken={key}").text
last_price = json.loads(data)['data']['quote']['trade']['price']

return int(invest_amount / last_price)

每日執行程式,如果今日的日期為 5 號,則在 9:00 和 13:25 各買進價值 2500 元左右的零股。在執行之前,須先安裝台股市場交易日的套件來確認預計定期定額買進的日期,如下:

# 取得台股市場交易日套件,若尚未安裝請先執行以下註解
#!pip install pandas-market-calendars
import pandas_market_calendars as mcal

# 使用台股市場,並預計執行到明年底
tsx = mcal.get_calendar('TSX')
trading_list = tsx.schedule(start_date='2022-10-01', end_date='2023-12-31')['market_open'].index.tolist()

# 取得每月 5 號或 5號後的第一個交易日期
specific_list = list(filter(lambda x:x.day >= 5, trading_list))

data_dict = {"date":specific_list,"year":list(map(lambda x:x.year, specific_list)), "month":list(map(lambda x:x.month, specific_list))}

# 取得定期定額預計買進日
buy_date_list = pd.DataFrame(data_dict).drop_duplicates(subset=['year','month'])['date'].tolist()
# 如果今天並非定期定額預計買進日就會跳出提醒
if pd.Timestamp(datetime.datetime.today().date()) not in buy_date_list:
raise Exception("今天並非您設定的定期定額交易日!")

symbol_id = '0050'
# 首先我們必須要隨時檢查目前的時間是否是這兩個時間點
position = 0
while True:

time.sleep(0.5)
# 時間是開盤
if datetime.time(9, 1) >= datetime.datetime.now().time() >= datetime.time(9, 0) and position == 0:

# 計算要買的股數
qty = cal_buy_qty(symbol_id,2500)

# 因交易規則中盤中零股無法掛市價單,因此這裡掛漲停買進
order = OrderObject(
buy_sell=Action.Buy,
price_flag=PriceFlag.LimitUp, # 漲停買進
price = '',
stock_no=symbol_id,
quantity=qty,
ap_code=APCode.IntradayOdd,
trade=Trade.Cash,
)
sdk.place_order(order)
# 記錄買進部位
position = position + 1

# 時間是收盤 -> 零股交易最後一筆交易時間為 13:28
if datetime.time(13, 29) >= datetime.datetime.now().time() >= datetime.time(13, 28) and position == 1:
# 計算要買的股數
qty = cal_buy_qty(symbol_id,2500)

order = OrderObject(
buy_sell=Action.Buy,
price_flag=PriceFlag.LimitUp, # 漲停買進
price = '',
stock_no=symbol_id,
quantity=qty,
ap_code=APCode.IntradayOdd,
trade=Trade.Cash,
)
sdk.place_order(order)
break
caution

請注意!! 若您使用 Colab 進行實作,因 Colab server 的時間與本機端時間可能不一致,因此您須自行調整開收盤時間!

根據上述程式碼,因交易規則中市價單的價格旗標只有盤中整股可以掛市價單,若不了解交易規則也可參考 SDK 盤別說明!本篇文章透過定期定額實作案例讓大家理解, API 能使用的場景比大家想像的更多。若您身為一位開發者,發現了券商某些功能無法滿足自己,不妨趕快動手試試吧!

結語

本週我們介紹並實作了定期定額策略,透過將資金分批定時投入的方式解決了「怕買貴」的風險,只要標的長期是上漲趨勢就高機率能夠帶來獲利。我們也嘗試將策略變換不同的扣款日期觀察它們的差異,策略開發的過程也類似如此,透過一些變化來發現新的資訊,並透過實驗結果來進行實際交易。

下堂課,我們將進入更進階的策略建構課程,也將前幾週的策略加入更多的變化來討論其中的差異!