[Python筆記]何時買賣(2):回溯

這篇的回溯方法目前有一點問題,我似乎沒有考慮到最後要平倉的時候,賣出還要再扣一筆手續費的情況啊QQ

在上一篇的動態規劃裡,我們已經得到最大的獲利了,但我們只知道最大的獲利,什麼時候要買、什麼時候要賣,什麼時候該都不做,也就是每天的行動我們還不知道,我們還要在做一個回溯的方法。

回溯的方法有百百種,每個人寫出來的方法都不一樣,我的寫法似乎不是最佳解ㄏ,感覺浪費的不少空間去存不會再用到的資料就是了。

變數定義

priceVec
  • 是一個矩陣。
  • 裡面會是一筆價格的歷史資料(等一下會用的是 SPY ETF 每日 Adj Close 的價格)。
transFeeRate
  • 是一個數字。
  • 每次交易所需的手續費,測試用的時候常用 0.01 ,代表每次「買、賣」都要多付出 1% 的手續費給第三方(如政府)。
actionVec
  • 是一個矩陣,裡面放數字。
  • 裡面寫的是每一天的行動,總共有三種可能的情況:買(1)、賣(-1)、什麼都不做(0)。

思緒

把整個交易過程拆成兩個軌道,如上篇所述,我們會有兩個變數cashstock來記載每天持有的變化量。

對於cash的部份,我稱他為現金路線,這在這裡面紀錄「每天結束時,持有現金狀態」之下,我們「是如何持有現金」的行動,寫入cash_index

cash_index裡,只可能出現 0-1

  • 什麼都不做,一樣持有現金,寫入 0
  • 透過賣出證券得到現金,寫入 -1

對於stock的部份,我稱他為證券路線,這在這裡面紀錄「每天結束時,持有證券狀態」之下,我們「是如何持有證券」的行動,寫入stock_index

stock_index裡,只可能出現 01

  • 什麼都不做,一樣持有證券,寫入 0
  • 透過花現金來買入證券,寫入 1

因為每次都要算手續費很麻煩會有小數點ㄚ,所以下面舉的例子都是在沒有手續費的狀況。最後真的要考慮的情況下只要多除少乘就好。

每一條路線的變化

舉例而言,如果第 i 天結束時,證券價值 $3,第 i+1 天結束時,證券價值 $ 2 。

在第 i 天「結束」的時候,會有兩種假定狀態:

  • 持有 $ 1 的現金
  • 持有 13 張證券(因我們可以用一塊錢買三塊的證券,得到 13 張證券)

在第 i+1 天時,我們對於上面兩種初始假定狀態會有四種可能:

  • 在第一天持有 $1 的現金:

    • 在第二天什麼事情都不做,在結束時還是持有現金 -> 寫入現金路線0
    • 在第二天買入證券,在結束時持有證券 -> 證券路線寫入1
  • 在第一天持有 13 張證券:

    • 在第二天什麼事情都不做,在結束時還是持有證券 -> 寫入證券路線0
    • 在第二天賣出證券,在結束時持有現金 -> 現金路線寫入-1

但是這樣兩個路線在一天之內會有兩個值啊,所以我們才要在兩者所得的利潤中取最大值(max()),代表這是最好的行動。

在第 i+1 天結束時,證券價值 $2 而言:

  • 在第一天持有一元的現金

    • 在第二天什麼事情都不做,在結束時還是持有現金 -> 此時擁有現金 $1
    • 在第二天買入證券,在結束時持有證券 -> 此時擁有證券 1/$2 = 12
  • 在第一天持有 13 張證券

    • 在第二天什麼事情都不做,在結束時還是持有證券 -> 此時擁有證券 13
    • 在第二天賣出證券,在結束時持有現金 -> 此時擁有現金 13 * $2 = $23

很明顯的,對於「結束時擁有現金」的「現金路線」而言,我們「在第一天持有現金且第二天也持有現金,即什麼都做」擁有的現金($1)會比「在第一天持有證券而在第二天賣出換現」還要多($2/3),所以對「最後持有現金」這件事而言,最佳的行動選擇應該是「什麼都不做」,所以在這天的 cash_index 寫入 0

if cash > stock*(priceVec[i]*(1-transFeeRate)):
	cash_index[i] = 0 # 什麼都不做,一樣持有現金,寫入 0 
elif cash < stock*(priceVec[i]*(1-transFeeRate)):
	cash_index[i] = -1 # 賣出證券,寫入 -1

而對於「結束時擁有證券」的「證券路線」而言,我們「在第一天持有現金而第二天買入證券」擁有的證券張數(1/2 張)會比「在第一天持有證券且第二天也持有證券,即什麼都不做」還要多(1/3 張),所以對「最後持有證券」這件事而言,最佳的行動選擇應該是「買入」,所以在這天的 stock_index 寫入 1

if stock > cash/(priceVec[i]*(1+transFeeRate)):
	stock_index[i] = 0 # 什麼都不做,一樣持有證券,寫入 0 
elif stock < cash/(priceVec[i]*(1+transFeeRate)):
	stock_index[i] = 1 # 買入證券,寫入 1

我們可以用同樣的方法產生「現金路線」和「證券路線」的全部行動。

接下來要怎麼用現金路線和證券路線來產出「最終解決路線(actionVec)」呢?

最終解決路線

在寫入所有的cash_indexstock_index後,我們同樣能夠得到最大化的利潤。

我們首先要,也只需決定這個「最後得到最大化利潤」是怎麼來的,是從持有現金,還是持有證券而造成的?

我們在這裡引進一個flag來記載回溯回去時,我們當今所在的路線(即當天結束時,我們所持有的是什麼)。

flag = 0 # 代表現在的路線,1表示現金路線,-1表示證券路線

if cash > stock*priceVec[-1]*(1-transFeeRate): # 當持有的現金比較多
	actionVec[-1] = cash_index[-1] # 代表從現金路線而來
	flag = 1 # cash route 設為現金路線
elif cash < stock*priceVec[-1]*(1-transFeeRate): # 當持有的證券比較多
	actionVec[-1] = stock_index[-1] # 代表從證券路線而來
	flag = -1 # stock route 設為證券路線

接著回溯,當我們現在處於現金路線時,我們要做的actionVec就是重複一次上次cash_index所做的行動。但是如果我們上次所做的行動是 -1 ,也就是「透過上一步賣出證券,才使這一步持有現金」時,代表我們上一步是在證券路線,此時我們要把flag轉為 -1 ,代表我們走回證券路線。

if  flag == 1: # when on cash route
	actionVec[i] = cash_index[i]
	if actionVec[i] == -1: # due to sell
		flag = -1 # change to stock route

同樣的,當我們現在處於證券路線時,我們要做的actionVec也是重複一次上次stock_index所做的行動。但是如果我們上次所做的行動是 1 ,也就是「透過上一步花現金買入,才使這一步持有證券」時,代表我們上一步是在現金路線,此時我們要把flag轉為 1 ,代表我們走回現金路線。

elif flag == -1: #stock route
	actionVec[i] = stock_index[i]
	if actionVec[i] == 1: # due to buy
		flag = 1 # change to cash route

實作

'''
By ChingRu @ 2018-11-18
'''

def myOptimAction(priceVec, transFeeRate):
	import numpy as np

	# 先檢查共有幾筆資料
	dataLen = len(priceVec)
	
	# 預設的 actionVec 都是零(就是什麼都不做),大小等長於datalen
	actionVec = np.zeros(dataLen)
	
	# set as default
	cash = 1
	stock = 0
	
	# for backtracking
	cash_index = np.zeros(dataLen)
	stock_index = np.zeros(dataLen)
	
	# 計算極值
	for i in range(0, dataLen):

		# 這幾個 if 是為了回溯而寫入的index,分別記載不同路線的行動
		if cash > stock*(priceVec[i]*(1-transFeeRate)):
            cash_index[i] = 0 # 什麼都不做,一樣持有現金,寫入 0 
        elif cash < stock*(priceVec[i]*(1-transFeeRate)):
            cash_index[i] = -1 # 賣出證券,寫入 -1

		if stock > cash/(priceVec[i]*(1+transFeeRate)):
            stock_index[i] = 0 # 什麼都不做,一樣持有證券,寫入 0 
        elif stock < cash/(priceVec[i]*(1+transFeeRate)):
            stock_index[i] = 1 # 買入證券,寫入 1
		
		# maximized two situation,最主要進行利潤最大化的地方
		cash = max(cash , stock*(priceVec[i]*(1-transFeeRate)))
		stock = max(stock , cash/(priceVec[i]*(1+transFeeRate)))		

	# 選擇性印出最大利潤
	# maxvalue = max(cash,stock)
	# print(maxvalue)

	# 以下開始回溯
	# 1 for cash route ,and -1 for stock route
	flag = 0
	
	# 考慮最後一筆是持有cash比較好,還是持有股票的現值比較好
	stockvalue = stock*priceVec[-1]*(1-transFeeRate)
	
	if cash > stockvalue:
		actionVec[-1] = cash_index[-1]
		flag = 1 # cash route
	elif cash < stockvalue:
		actionVec[-1] = stock_index[-1]
		flag = -1 # stock route

	# 逆著回去做回溯
	for i in range(dataLen-1 ,-1, -1):
		if  flag == 1: # when on cash route
			actionVec[i] = cash_index[i]
			if actionVec[i] == -1: # due to sell
				flag = -1 # change to stock route

		elif flag == -1: #stock route
			actionVec[i] = stock_index[i]
			if actionVec[i] == 1: # due to buy
				flag = 1 # change to cash route
	
    # 最後輸出每天的行動
	return actionVec

結尾

整體看起來很長,但其實有幾行是可以不用寫的,像是在計算極值的if迴圈裡,讓cash_index[i] = 0stock_index[i] = 0都是可以省略的,因為在一開始就在np.zeros(dataLen)cash_indexstock_index設為零了。

Reference