初识个人股票量化交易策略

2,086 阅读7分钟

最近通过对元宵大师的小册Python股票数据分析:手把手构建量化交易系统的学习,对股票的量化交易有了新的认识,并且自己根据大师教学写出了自己的首个策略,在此跟大家分享,还望大家多多指导:

炒股谨记

时刻铭记“韭菜”的特征,反省自身的问题,避免加入“韭菜”队伍中。

“韭菜”普遍喜欢追求胜率,希望自己所有的交易都能以盈利收尾。直接表现为盈利的单子稍有一点利润就忙于兑现,亏损的交易却不卖出,等待持有到可以再次盈利为止。 “韭菜”交易的主要目的是寻找交易的刺激,企图抓住市场中每一次价格的波动,贪婪地不想错过任何一次机会,甚至认为要像上班一样每天进行交易。 “韭菜”由于捡便宜的心理,喜欢在股票下跌的时候买入,殊不知此时很有可能是逆势操作。 “韭菜”喜欢在交易中时刻盯着行情报价系统,无法豁然面对价格的波动,幻想在低点买入,在高点卖出,抓住市场中的每一次波动。 “韭菜”往往不能坚持自己的策略决策,往往因为临时的一点点小的变动,将自己的交易搁置甚至反转。

股票量化交易策略

股票数据获取

获取股票数据的方法有多种,包括pandas、tushare等很多,个人偏好于tushare获取,相比较来说速度比较快点:

import tushare as ts
# '600410', '2018-10-01', '2019-04-01'
def get_stock_hist_data(stock_code, start_date, end_date):
    df_sh = ts.get_hist_data(stock_code, start_date, end_date)
    df_sh.index = pd.to_datetime(df_sh.index)
    df_sh.sort_index(inplace=True)
    return df_sh

得到股票历史数据如:

择时策略

择时策略采用大师提供的N日突破择时策略,其中参数N1、N2参数配置15、5,两参数可以控制买入卖出的频次,参数配置越小买入卖出的频率就越高 对获取到的股票历史数据df_sh进行处理,新增了cloumn买入卖出列signal=0 代表卖出 signal=1 代表买入

def deal_stock_data(stock_code=None, stock_s_date=None, stock_e_date=None, N1=None, N2=None):
    df_sh = get_stock_hist_data(stock_code, stock_s_date, stock_e_date)
    df_sh['N1_high'] = df_sh.high.rolling(window=N1).max()
    df_sh['N1_high'] = df_sh.N1_high.shift(1)
    fill_max = df_sh.close.expanding().max()
    df_sh['N1_high'].fillna(value=fill_max, inplace=True)

    df_sh['N2_low'] = df_sh.low.rolling(window=N2).min()
    df_sh['N2_low'] = df_sh.N2_low.shift(1)
    fill_min = df_sh.close.expanding().min()
    df_sh['N2_low'].fillna(value=fill_min, inplace=True)
    for kindex, ktoday in df_sh.iterrows():
        if ktoday.close > ktoday.N1_high:
            print("N-day_buy_signal:", kindex, ktoday.close)
            buy_price = ktoday.close
            df_sh.loc[kindex, 'signal'] = 1
       
        elif ktoday.close < ktoday.N2_low:
            print('sell:', kindex, ktoday.close)
            df_sh.loc[kindex, 'signal'] = 0
            buy_price = 0
        else:
            pass
    df_sh['signal'].fillna(method='ffill', inplace=True)
    df_sh['signal'] = df_sh.signal.shift(1)
    df_sh['signal'].fillna(method='bfill', inplace=True)
    return df_sh

之后根据上述方法获取到股票的历史信息及买入卖出点数据后,再计算出股票的基准首义率及执行策略后的收益率:

# 计算基准收益率
df_sh['benchmark_profit'] = np.log(df_sh.close/df_sh.close.shift(1))
# 计算执行策略后对应收益率
df_sh['trend_profit'] = df_sh.signal * df_sh.benchmark_profit

对股票数据进行可视化,转换成图表:

plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
fig = plt.figure(figsize=(8, 6), dpi=100, facecolor='white')    gs = matgrid.GridSpec(3, 1, left=0.10, bottom=0.15, right=0.96, top=0.96, wspace=None,
                          hspace=0, height_ratios=[4, 2, 2])
graph_buyorsell = fig.add_subplot(gs[0, :]) # 股票收盘价及买卖点指示图
graph_profit = fig.add_subplot(gs[1, :]) # 股票基准收益率及策略收益率
graph_money = fig.add_subplot(gs[2, :]) # 资金曲线图

根据买入卖出数据点标识出买入卖出区间,并计算账户余额:

    for index, today in df_sh.iterrows():
        if today.signal == 1 and skip_today == 0:
            skip_today = -1
            stock_hold = int(cash_total / today.close)
            stock_hold_money = 0
            cash_total = 0
            start = df_sh.index.get_loc(index)
            graph_buyorsell.annotate('买入', xy=(index, df_sh.close.asof(index)),
                                     xytext=(index, df_sh.close.asof(index) + 2),
                                     arrowprops=dict(facecolor='r', shrink=0.1),
                                     horizontalalignment='left', verticalalignment='top')
            # print("buy: ", index)
        elif today.signal == 0 and skip_today == -1:
            skip_today = 0
            end = df_sh.index.get_loc(index)
            cash_total = int(stock_hold * today.close)
            stock_hold_money = 0
            if df_sh.close[end] < df_sh.close[start]:
                graph_buyorsell.fill_between(df_sh.index[start:end], 0, df_sh.close[start:end], color='green',
                                             alpha=0.38)
            else:
                graph_buyorsell.fill_between(df_sh.index[start:end], 0, df_sh.close[start:end], color='red', alpha=0.38)
            graph_buyorsell.annotate('卖出', xy=(index, df_sh.close.asof(index)), xytext=(index,
                                                                                        df_sh.close.asof(index) + 2),
                                     arrowprops=dict(facecolor='g', shrink=0.1), horizontalalignment='left',
                                     verticalalignment='top')
            # print("sell : ", (df_sh.close[end] - df_sh.close[start]))
            profit_total[index] = df_sh.close[end] - df_sh.close[start]
        if skip_today == -1:
            stock_hold_money = int(stock_hold * today.close)
            df_sh.loc[index, 'total'] = stock_hold_money
        else:
            df_sh.loc[index, 'total'] = cash_total

根据账户金额及股票市值,计算资金最大滚动曲线:

df_sh['max_total'] = df_sh['total'].expanding().max()
    df_sh[['max_total', 'total']].plot(grid=True, ax=graph_money)

    df_sh['per_total'] = df_sh['total'] / df_sh['max_total']
    min_point_total = df_sh.sort_values(by=['per_total']).iloc[[0], df_sh.columns.get_loc('per_total')]
    max_point_total = df_sh[df_sh.index <= min_point_total.index[0]].sort_values(by=['total'],
                                                                                 ascending=False).iloc[
        [0], df_sh.columns.get_loc('total')]
    graph_money.annotate('滚动最大点',
                         xy=(max_point_total.index[0], df_sh.total.asof(max_point_total.index[0])),
                         xytext=(max_point_total.index[0], df_sh.total.asof(max_point_total.index[0]) + 4),
                         arrowprops=dict(facecolor='yellow', shrink=0.1), horizontalalignment='left',
                         verticalalignment='top')
    graph_money.annotate('最大回撤点',
                         xy=(min_point_total.index[0], df_sh.total.asof(min_point_total.index[0])),
                         xytext=(min_point_total.index[0], df_sh.total.asof(min_point_total.index[0]) + 4),
                         arrowprops=dict(facecolor='yellow', shrink=0.1), horizontalalignment='left',
                         verticalalignment='top')

最后去掉第一二个图标的x标签:

    for label in graph_buyorsell.xaxis.get_ticklabels():
        label.set_visible(False)
    for label in graph_profit.xaxis.get_ticklabels():
        label.set_visible(False)
    plt.legend(loc='best')
    plt.title(u'招商银行')
    plt.show()

执行代码后:

show_pic_stock(cash_total=10000, stock_code='000002', st='2019-01-01', et='2020-01-01',
                                                      N1=15, N2=5)

结果如下图:

但是我们可以从图看出我们策略的收益率一直低于基准收益率,为解决这问题我们可以引入风险管理策略

风险管理策略

我们根据股票的ATR(Average True Range)指标作为止盈止损的基数, 止盈值设置为n_win倍的ATR值,止损值设置为n_loss倍的ATR值,n_win和n_loss分别为最大止盈系数和最大止损系数,此处设置最大止盈系数为2,最大止损系数为0.9,倾向于盈利值要大于亏损值。触发止盈止损条件为:

 当n_win*ATR值 > (今日收盘价格 - 买入价格),触发止盈信号,卖出股票
 当n_loss*ATR值 > (买入价格 - 今日收盘价格),触发止损信号,卖出股票

修改方法:deal_stock_data(stock_code=None, stock_s_date=None, stock_e_date=None, N1=None, N2=None, n_loss=None, n_win=None) 增加 n_loss=None, n_win=None两个参数,得到如下结果:

对比前面结果可以看出,增加止盈止损风险管理后,多了几次止损卖出的交易

增加仓位管理

仓位管理

首先创建账户类,来操作买入股票卖出股票

class ST_account:
    def __init__(
            self,
            init_hold={},
            init_cash=10000):
        self.hold = init_hold
        self.cash = init_cash

    # 查询账户持股数量
    def hold_available(self, code=None):
        if code in self.hold:
            return self.hold[code]

    # 查询账户可用资金
    def cash_available(self):
        return self.cash

    # 更新账户资产
    def latest_assets(self, price):
        assets = 0
        for code in self.hold.values():
            assets += code * price
        assets += self.cash
        return assets

    # 账户买入卖出股票
    def send_order(self, code=None, amount=None, price=None, order_type=None):
        if order_type == 'buy':
            self.cash = self.cash - amount*price
            self.hold[code] = amount
        else:
            self.cash = self.cash + amount*price
            self.hold[code] = self.hold[code] - amount
            if self.hold[code] == 0:
                del self.hold[code]

修改show_pic_stock(account, stock_code=None, st=None, et=None, N1=None, N2=None, n_loss=None, n_win=None)方法,传入account对象,再修改买入卖出操作时对account对象操作的代码:

# 买入
account.send_order(code=stock_code, amount=stock_hold, price=today.close, order_type='buy')
# 卖出
account.send_order(code=stock_code, amount=stock_hold, price=today.close,order_type='sell')

动态仓位管理

《海龟交易法则》建议第一笔仓位的一个ATR波动与总资金1%波动相对应。即:买入股票数量 * ATR = 资金 * 1% 假如1月1日股票A向上突破4.12元出现买入点,手上有10万元资金,那么10万资金的1%波动就是1000元。截止1月1日,股票A的14日ATR为0.15元,1000元÷0.15元=6666股。 我们股票买入数量更改为:

stock_hold = int(account.cash_available() * 0.01 / today.atr14)

买入股票后我们可以根据ATR值的变化来控制我们的持仓数量,在根据股票买卖信号买入或卖出后做如下处理:

        if skip_today == -1 and not np.isnan(today.atr14):
            posit_num_wave = int(account.total_cash * 0.01 / today.atr14)  # 动态计算持仓的股票数量
            am = int(posit_num_wave - account.hold_available(code=stock_code))
            print("postice_num:{} ava_num{}, am bigger than 0:{}".format(posit_num_wave,
                                                                         account.hold_available(code=stock_code),
                                                                         am > 0))
            if am > 0:  # 波动后加仓
                print("adjust buy{} minux{}".format(posit_num_wave, am))
                account.send_order(code=stock_code,
                                   amount=int(posit_num_wave - account.hold_available(code=stock_code)),
                                   price=today.close, order_type='buy')
            else:
                print("adjust buy{} minux{}".format(posit_num_wave, am))
                account.send_order(code=stock_code,
                                   amount=int(account.hold_available(code=stock_code) - posit_num_wave),
                                   price=today.close, order_type='sell')

展示持股数量如下图所示,我们股票的持股数量随着ATR的变化而变化:

蒙特卡洛算法优化

我们的方法:def show_pic_stock(account, stock_code=None, st=None, et=None, N1=None, N2=None, n_loss=None, n_win=None):中的 参数N1,N2,n_loss,n_win前面是按照经验随机取值,我们可以通过蒙特卡洛算法对4个参数优化后选出最优的参数组合。 在N1取10-30,N2取5-15,n_loss取0.5-1.5,n_win取1.5,2.5范围内取200组随机组合,进行计算:


def cal_navagel(n=200):
    n1_min, n1_max = 10, 30
    n2_min, n2_max = 5, 15
    win_min, win_max = 1.5, 2.5
    loss_min, loss_max = 0.5, 1.5
    ma_list = []
    profit_list = []

    for i in range(0, n + 1):
        n1 = int(random.uniform(n1_min, n1_max))
        n2 = int(random.uniform(n2_min, n2_max))
        win = round(random.uniform(win_min, win_max), 1)
        loss = round((random.uniform(loss_min, loss_max)), 1)
        ma_list.append([n1, n2, win, loss])
        profit_list.append(run_for_para(stock_code='000002', st='2019-01-01', et='2020-01-01',
                                        N1=n1, N2=n2, n_loss=loss, n_win=win))
    profit_max = max(profit_list)
    ma_max = ma_list[profit_list.index(max(profit_list))]
    plt.bar(np.arange(0, len(ma_list), 1), profit_list)
    plt.annotate(str(ma_max) + '\n' + str(profit_max),
                 xy=(ma_list.index(ma_max), profit_max), xytext=(ma_list.index(ma_max) - 10, profit_max - 10),
                 arrowprops=dict(facecolor='yellow', shrink=0.1),
                 horizontalalignment='left', verticalalignment='top')
    # 设置坐标标签字体大小
    plt.xlabel('均线参数')
    plt.ylabel('资金收益')
    # 设置坐标轴的取值范围
    plt.ylim(min(profit_list) * 0.99, max(profit_list) * 1.01)
    # 设置图例字体大小
    plt.legend(['profit_list'], loc='best')
    plt.title("均线最优参数")
    plt.show()

得出如图所示:

可以看出最优参数N1=24,N2=14,n_win=1.6,n_loss=0.7,根据最优参数得出:

对于参数N1=15,N2=5,n_win=2,n_loss=0.9可得出:

对比两图可知收益明显增加了

个人学习心得

这是我个人自学python后首次接触股票量化交易,通过对小册的学习,元宵大师老师的疑问解答觉得自己学到不少东西,提升自己python编程的技术外,也对股票量化交易有了一个初步的认识。 股票量化交易对股票历史数据的分析,各种图形化展示确实方便直观很多,但是量化交易也只是也只能够根据历史数据来进行预测的工具,策略方面本人也只是刚入门以后还有很多学习的地方。

感谢您的阅读!