布林带——泡泡玛特
作為股民,相信不少人都聽說一個名詞——量化交易。量化交易并不神秘,它的核心是策略。策略的制定者還是人,只不過策略的執行者由人變成了程序。一方面,它不會有人類的恐懼和貪婪,不會因情緒而導致動作變形;另外一方面,它又顯得死板,在某些特殊情況下反而愚蠢!耳邊可能聽說過很多技術指標、量化策略。比如雙均線策略、布林帶、網格交易、右側追擊…… 那么,它們在股市中的真實表現究竟如何?
本篇屬小試牛刀,以布林帶指標為策略指引,手辦龍頭泡泡瑪特歷史數據為基石,看看它們能碰撞出怎樣的火花!
1.數據整理
1.1 定義全局變量
如果需要回測策略對其它股票的表現,只需修改全局變量SYMBOL和ONE_AMOUNT 。
其中日期區間以及賬戶原始本金也可靈活調整。
# 導入相關模塊 import akshare as ak import pandas as pd import numpy as np import matplotlib.pyplot as plt# 定義全局變量 SYMBOL = '09992' # 股票代碼 ONE_AMOUNT = 200 # 一手股數 CAPTIAL = 100000 # 賬戶本金 STRAT_DATE = '2021-01-01' # 開始日期 END_DATE = '2021-12-31' # 結束日期
1.2 獲取股票數據
獲取股票歷史數據需要用到一個庫:akshare 。本文選擇的是港股,數據接口是 akshare.stock_hk_hist 。需獲取其它市場數據參考官網https://www.akshare.xyz/
# 泡泡瑪特歷史數據 ppmt = ak.stock_hk_hist(symbol=SYMBOL, period="daily", start_date=STRAT_DATE, end_date=END_DATE, adjust="") # 日期這一列為字符串格式,將其轉換為日期格式 ppmt['日期'] = pd.to_datetime(ppmt['日期']) ppmt.head()在jupyter notebook中運行以上代碼,運行結果如下圖(后文都會以截圖的形式貼出代碼運行后的結果):
1.3 數據處理
既然是布林帶交易策略,需要計算出其中軌、上下軌。下面是它的計算公式:
中軌線 = N日的移動平均線
上軌線 = 中軌線 + K倍的標準差
下軌線 = 中軌線 - K倍的標準差
一般而言,N取值為20,K取值為2。將其算出后直接保存到變量ppmt中。可以發現,因為以20日移動均線為基準,所以前20個交易日是沒有布林帶數據的。
# 先創建3列數據,分別存放布林帶上、中、下軌。并設定默認值為NaN ppmt['ma20'] = np.nan ppmt['upper'] = np.nan ppmt['lower'] = np.nan# 計算布林帶,并將其保存到變量ppmt for i in range(20,ppmt.shape[0]):ppmt['ma20'][i] = ppmt['收盤'][i-20:i].mean() for i in range(20,ppmt.shape[0]):ppmt['upper'][i] = ppmt['ma20'][i] + 2*ppmt['收盤'][i-20:i].std()ppmt['lower'][i] = ppmt['ma20'][i] - 2*ppmt['收盤'][i-20:i].std() ppmtK線圖畫起來代碼太繁瑣。這里簡單畫出布林帶走勢及收盤價曲線,可以發現其股價從年初的90元左右,一路趨勢向下,到年底不足45元。腰斬之下,不知道布林帶指標會有怎樣的表現。
# 收盤價及布林帶預覽 plt.figure('ppmt') plt.title('ppmt',fontsize = 18) plt.xlabel('date',fontsize = 14) plt.ylabel('price', fontsize = 14) plt.grid(linestyle = ':') plt.plot(ppmt['日期'], ppmt['收盤'], label = 'close_price') plt.plot(ppmt['日期'], ppmt['ma20'], label = 'ma20') plt.plot(ppmt['日期'], ppmt['upper'], label = 'upper') plt.plot(ppmt['日期'], ppmt['lower'], label = 'lower') plt.legend() plt.show()2. 賬戶初始化
用三個DataFrame數據來描述一個賬戶:
PROPERTY代表資產詳情,記錄每一天的資產變化。其中的數據有日期、總資產、現金、股票資產、收益、收益率
POSITION代表持倉詳情,當持倉發生變動時,添加其最新持倉。其中數據有日期、股票代碼、持倉數量、成本價、持倉收益、持倉收益率
ORDER代表訂單記錄,發生交易時,記錄交易信息。其中數據有日期、交易類型(B/S :買入/賣出)、股票代碼、交易數量、交易價格
#資產詳情 PROPERTY = pd.DataFrame(columns = ['date','total', 'cash', 'stock', 'profit', 'profit_rate']) # 持倉 POSITION = pd.DataFrame(columns = ['date','symbol', 'amount', 'buy_price', 'profit', 'profit_rate']) # 訂單記錄 ORDER = pd.DataFrame(columns = ['date', 'trade_type', 'symbol', 'amount', 'trade_price'])3. 交易
布林帶有很多用法,本文直接大道至簡,采取最簡單的買賣邏輯:
當日收盤價跌破布林帶下軌時,買入。如果收盤價持續在下軌下方游蕩,則持續買入,直至賬戶本金見底;
當日收盤價突破布林帶上軌時,清倉。一次性賣出該股票的所有持倉。
遍歷所有數據,其中資產詳情PROPERTY需每個交易日更新,持倉POSITION和訂單記錄ORDER在發生交易后需更新。
for i in range(0, ppmt.shape[0]):# i<20時,無買賣操作。記錄賬戶每日數據if i < 20 :PROPERTY = PROPERTY.append({'date': ppmt['日期'][i],'total': CAPTIAL,'cash' : CAPTIAL,'stock' : 0,'profit' : 0,'profit_rate' : 0}, ignore_index = True)else :# 當日收盤價下穿lower時,并且賬戶余額足夠,以收盤價買入if ppmt.iloc[i].收盤 < ppmt.iloc[i].lower and \(PROPERTY.iloc[i-1].cash >= ppmt.iloc[i].收盤 * ONE_AMOUNT):### 訂單記錄ORDER = ORDER.append({'date' : ppmt.iloc[i].日期,'trade_type':'B','symbol': SYMBOL,'amount': ONE_AMOUNT,'trade_price': ppmt.iloc[i].收盤 },ignore_index=True)### 持倉# 第一次持倉if SYMBOL not in POSITION['symbol'].values :POSITION = POSITION.append({'date': ppmt.iloc[i].日期,'symbol': SYMBOL, # 代碼'amount': ONE_AMOUNT, # 數量'buy_price': ppmt.iloc[i].收盤,# 買入價'profit' : 0, #持倉收益'profit_rate' : 0 #收益率}, ignore_index = True)# 已有持倉else : POSITION = POSITION.append({'date':ppmt.iloc[i].日期,'symbol':SYMBOL, # 代碼'amount' : POSITION.iloc[-1].amount + ONE_AMOUNT, # 數量'buy_price' : (POSITION.iloc[-1].amount * POSITION.iloc[-1].buy_price + ppmt.iloc[i].收盤 * ONE_AMOUNT) / (POSITION.iloc[-1].amount + ONE_AMOUNT), # 買入價'profit' : (POSITION.iloc[-1].amount + ONE_AMOUNT) * ppmt.iloc[i].收盤 - (POSITION.iloc[-1].amount * POSITION.iloc[-1].buy_price + ppmt.iloc[i].收盤 * ONE_AMOUNT), #持倉收益'profit_rate' :((POSITION.iloc[-1].amount + ONE_AMOUNT) * ppmt.iloc[i].收盤 - (POSITION.iloc[-1].amount * POSITION.iloc[-1].buy_price + ppmt.iloc[i].收盤 * ONE_AMOUNT))/(POSITION.iloc[-1].amount * POSITION.iloc[-1].buy_price + ppmt.iloc[i].收盤 * ONE_AMOUNT) # 收益率} , ignore_index = True)### 賬戶總覽PROPERTY = PROPERTY.append({'date': ppmt.iloc[i].日期, 'total': (PROPERTY.iloc[i-1].cash - ppmt.iloc[i].收盤 * ONE_AMOUNT) + ppmt.iloc[i].收盤 * POSITION[POSITION['symbol'] == SYMBOL].iloc[-1].amount, # 總資產'cash' : PROPERTY.iloc[i-1].cash - ppmt.iloc[i].收盤 * ONE_AMOUNT, # 現金'stock' : ppmt.iloc[i].收盤 * POSITION[POSITION['symbol'] == SYMBOL].iloc[-1].amount, # 股票'profit' : (PROPERTY.iloc[i-1].cash - ppmt.iloc[i].收盤 * ONE_AMOUNT) + ppmt.iloc[i].收盤 * POSITION[POSITION['symbol'] == SYMBOL].iloc[-1].amount - 100000, # 利潤'profit_rate' : ((PROPERTY.iloc[i-1].cash - ppmt.iloc[i].收盤 * ONE_AMOUNT) + ppmt.iloc[i].收盤 * POSITION[POSITION['symbol'] == SYMBOL].iloc[-1].amount)/100000 -1# 利潤率}, ignore_index = True)continue### 當日收盤價上穿upper,且有持倉時,以收盤價清倉else:if ppmt.iloc[i].收盤 > ppmt.iloc[i].upper and \SYMBOL in POSITION['symbol'].values and POSITION.iloc[-1].amount > 0:### 訂單記錄ORDER = ORDER.append({'date' : ppmt.iloc[i].日期,'trade_type':'S','symbol': SYMBOL,'amount': POSITION.iloc[-1].amount,'trade_price': ppmt.iloc[i].收盤 },ignore_index=True)# 持倉更新POSITION = POSITION.append({'date': ppmt.iloc[i].日期,'symbol': SYMBOL, # 代碼'amount': 0, # 數量'buy_price': 0,# 買入價'profit' : 0, #持倉收益'profit_rate' : 0 #收益率}, ignore_index = True)### 賬戶總覽PROPERTY = PROPERTY.append({'date': ppmt.iloc[i].日期, 'total': PROPERTY.iloc[i-1].cash + ppmt.iloc[i].收盤 * POSITION.iloc[-2].amount, # 總資產'cash' : PROPERTY.iloc[i-1].cash + ppmt.iloc[i].收盤 * POSITION.iloc[-2].amount, # 現金'stock' : 0, # 股票'profit' : (PROPERTY.iloc[i-1].cash + ppmt.iloc[i].收盤 * POSITION.iloc[-2].amount) - 100000, # 利潤'profit_rate' : (PROPERTY.iloc[i-1].cash + ppmt.iloc[i].收盤 * POSITION.iloc[-2].amount)/100000 - 1# 利潤率}, ignore_index = True)continue### 沒有買賣操作時,也需要更新賬戶總覽if SYMBOL not in POSITION['symbol'].values :PROPERTY = PROPERTY.append({'date' : ppmt.iloc[i].日期,'total': PROPERTY.iloc[-1].total,'cash' : PROPERTY.iloc[-1].cash,'stock': PROPERTY.iloc[-1].stock,'profit' : PROPERTY.iloc[-1].profit,'profit_rate' : PROPERTY.iloc[-1].profit_rate,}, ignore_index=True)else : PROPERTY = PROPERTY.append({'date' : ppmt.iloc[i].日期,'total': PROPERTY.iloc[-1].cash + ppmt.iloc[i].收盤 * POSITION.iloc[-1].amount,'cash' : PROPERTY.iloc[-1].cash,'stock': ppmt.iloc[i].收盤 * POSITION.iloc[-1].amount,'profit' : (PROPERTY.iloc[-1].cash + ppmt.iloc[i].收盤 *POSITION.iloc[-1].amount) - 100000,'profit_rate' : (PROPERTY.iloc[-1].cash + ppmt.iloc[i].收盤 * POSITION.iloc[-1].amount) / 100000 -1,}, ignore_index=True)4. 回測結果
觀察描述賬戶的3個數據,發現一年期間一共發生了30次交易,年底賬戶仍舊有6手持倉,持倉盈虧為 -12.9%。
最終取得31.73%的年化回報。
PROPERTY # 觀察賬戶收益 # 觀察訂單記錄 print(ORDER.shape) ORDER.head() POSITION.tail() # 觀察持倉情況觀察本金使用情況,在5月份本金使用率達到了最高,差不多是84%。此刻手上資金還有16160港幣,
仍舊有余力補倉,只不過接下來出現了賣出機會,現金全部回籠。可謂是幾乎充分利用了本金,又沒有出現需要補倉時,本金不足的情況。
# 本金使用情況 plt.figure('cash') plt.title('cash',fontsize = 18) plt.xlabel('date',fontsize = 14) plt.ylabel('cash', fontsize = 14) plt.grid(linestyle = ':') plt.plot(PROPERTY['date'], PROPERTY['cash'] * 100) plt.show()再看收益率,最高收益率達到了35.8% ,而最大回撤發生在3月份,虧損不到8%。一年期間,在股價腰斬的情況下最終取得年化超過30%的回報,策略表現算是相當不錯了!
# 收益率曲線 plt.figure('ppmt_profit_rate') plt.title('ppmt_profit_·rate',fontsize = 18) plt.xlabel('date',fontsize = 14) plt.ylabel('rate', fontsize = 14) plt.grid(linestyle = ':') plt.plot(PROPERTY['date'], PROPERTY['profit_rate'] * 100) plt.show()結果似乎還行,但是根本不能說明策略優秀,如果你被這次單次成績欺騙到了,那么市場先生會好好的給你上一課。
它僅僅只能代表在泡泡瑪特2021年的行情下適合該策略 !
策略需要不斷組合和優化,可以犧牲掉高期望收益,但一定要泛化。在更多的時間里,更多不同的行情下跑贏市場!
總結
- 上一篇: jquery 下拉列表框 改变 动态 改
- 下一篇: “喜茶Go”微信小程序新零售商业实战案例