布林带——泡泡玛特

原创:WK
关注我的知识星球

本文重在策略学习探讨,不构成任何投资建议!

作为股民,相信不少人都听说一个名词——量化交易。
量化交易并不神秘,它的核心是策略。策略的制定者还是人,只不过策略的执行者由人变成了程序。一方面,它不会有人类的恐惧和贪婪,不会因情绪而导致动作变形;另外一方面,它又显得死板,在某些特殊情况下反而愚蠢!耳边可能听说过很多技术指标、量化策略。比如双均线策略、布林带、网格交易、右侧追击……
那么,它们在股市中的真实表现究竟如何?

本篇属小试牛刀,以布林带指标为策略指引,手办龙头泡泡玛特历史数据为基石,看看它们能碰撞出怎样的火花!

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中运行以上代码,运行结果如下图(后文都会以截图的形式贴出代码运行后的结果):


image.png

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()
ppmt

K线图画起来代码太繁琐。这里简单画出布林带走势及收盘价曲线,可以发现其股价从年初的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()
image.png

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 # 观察账户收益
image.png
# 观察订单记录
print(ORDER.shape)
ORDER.head()
image.png
POSITION.tail() # 观察持仓情况
image.png

观察本金使用情况,在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()
image.png

再看收益率,最高收益率达到了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()
image.png

结果似乎还行,但是根本不能说明策略优秀,如果你被这次单次成绩欺骗到了,那么市场先生会好好的给你上一课。

它仅仅只能代表在泡泡玛特2021年的行情下适合该策略 !

策略需要不断组合和优化,可以牺牲掉高期望收益,但一定要泛化。在更多的时间里,更多不同的行情下跑赢市场!

你可能感兴趣的:(布林带——泡泡玛特)