美文网首页玩玩量化交易
布林带——泡泡玛特

布林带——泡泡玛特

作者: 量化WK | 来源:发表于2022-04-05 09:45 被阅读0次

    原创: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年的行情下适合该策略 !

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

    相关文章

      网友评论

        本文标题:布林带——泡泡玛特

        本文链接:https://www.haomeiwen.com/subject/mafksrtx.html