掘金社区

仓位管理(2): 凯利公式指导投资与多种资金管理方式Pinned highlighted

掘金小Q 【 论坛管理员 】 发表在策略研究 2018-07-24 09:34:19

策略研究
2550
1
0

凯利公式指导投资示例

引言

在上一次我们提到了凯利公式,很多人可能会想去套用凯利公式到我们的投资策略里面去,但是实际操作中,我们会发现,很多量化平台,回测数据并不够,譬如说,掘金量化平台,它提供了胜率,收益情况等,但是并没有赔率的结果,那么,我们是不是就不能去进行相应的凯利公式计算了呢?

凯利公式的套用

其实换一种思路,我们完全自己在策略中进行运算,笔者这里简单起见,对沪深300指数进行买入和平仓操作,交易策略用的是双均线策略,为了看出策略在交易标的上的胜率与赔率表现,笔者每次都是满仓买入满仓卖出,同时,简单起见同时为了看到足够多的交易,这里用了 1分钟数据择时,不再加入 T+1 限制。 策略代码如下所示:

# coding=utf-8

"""
双均线策略
"""
from gm.api import *
from datetime import datetime
from datetime import date
from datetime import timedelta
from collections import deque
import numpy as np
import talib

# 常用常量设置
DATE_STR = "%Y-%m-%d"
TIME_STR = "%Y-%m-%d %H:%M:%S"
HIST_WINDOW = 50
LIMIT_LISTED_MONTH = 2
SHORT_PERIOD = 5
LONG_PERIOD = 10

def init(context):
    stock = 'SHSE.000300'
    context.stock_prices = deque(maxlen=HIST_WINDOW)
    context.risk_ratio = 1.0
    context.entry_price = 0.
    context.profit = 0.
    context.loss = 0.
    context.win_counter = 0
    context.loss_counter = 0
    start_today = datetime.strftime(date.today()-timedelta(days=1), DATE_STR)

    history_bars = history_n(symbol=stock,
                            frequency='60s',
                            count=HIST_WINDOW,
                            end_time=start_today,
                            adjust=ADJUST_PREV,
                            adjust_end_time=context.backtest_end_time)
    for bar in history_bars:
        context.stock_prices.append(bar.close)

    subscribe(stock, '60s')

def on_bar(context, bars):

    # ----------------------- 策略执行 -----------------------------------------
    # 数据不足数据滑窗要求,返回
    if len(context.stock_prices) < HIST_WINDOW:
        return

    pos = context.account().position(symbol=bars[0].symbol, side=PositionSide_Long)
    open_vol = int(context.account().cash.available * context.risk_ratio /bars[0].close/100)*100
    # 指标计算
    closes = np.array(context.stock_prices)

    ma5 = talib.SMA(closes, SHORT_PERIOD)
    ma10 = talib.SMA(closes, LONG_PERIOD)

    if ma5[-2] < ma10[-2] and ma5[-1] >= ma10[-1]:
        flag_golden_cross = True
        flag_dead_cross = False
    elif ma5[-2] > ma10[-2] and ma5[-1] <= ma10[-1]:
        flag_dead_cross = True
        flag_golden_cross = False
    else:
        flag_dead_cross = False
        flag_golden_cross = False

    if pos is None and flag_golden_cross:
        order_volume(symbol=bars[0].symbol,
                     volume=open_vol,
                     side=OrderSide_Buy,
                     order_type=OrderType_Limit,
                     position_effect=PositionEffect_Open,
                     price=bars[0].open)
        context.entry_price = bars[0].open
    if pos is not None and flag_dead_cross:
        order_volume(symbol=bars[0].symbol,
                     volume=pos.volume,
                     side=OrderSide_Sell,
                     order_type=OrderType_Limit,
                     position_effect=PositionEffect_Close,
                     price=bars[0].open)
        if bars[0].open > context.entry_price:
            context.profit = context.profit + (bars[0].open - context.entry_price)*pos.volume
            context.win_counter += 1
        else:
            context.loss = context.loss + (context.entry_price - bars[0].open)*pos.volume
            context.loss_counter += 1

    # ----------------------- 数据填充 -----------------------------------------
    context.stock_prices.append(bars[0].close)

    if datetime.strftime(bars[0].eob, TIME_STR) == context.backtest_end_time:
        print("总盈利为: ", context.profit, "\n"
            "总亏损为: ", context.loss, "\n",
            "实际盈利为: ", context.profit-context.loss)
        print("盈利次数为: ", context.win_counter, "\n", 
            "亏损次数为: ", context.loss_counter, 
            "胜率为: ", context.win_counter/(context.win_counter+context.loss_counter))
        print("平均每次盈利为: ", context.profit/context.win_counter, "\n", 
            "平均每次亏损为: ", context.loss/context.loss_counter, "\n",
            "盈亏比为: ", context.profit*context.loss_counter/context.loss/context.win_counter)

if __name__ == '__main__':
    run(strategy_id='5127bcbb-8da3-11e8-9ce5-f48c50eb367a',
        filename='main.py',
        backtest_initial_cash=10000000,
        mode=2,
        token='64c33fc82f334e11e1138eefea8ffc241db4a2a0',
        backtest_start_time='2015-02-01 09:30:00',
        backtest_end_time='2017-04-05 15:00:00')

运行结果,得到结果如下:

0_1532395824013_1.png

可能有读者会奇怪,为何我们自己计算出来的盈亏结果和掘金量化终端显示有差异,其实这里的差异来自于最后一笔交易,可能策略仅仅做了买入的操作,而没有卖出,导致的浮动盈亏,实际如果读者去下载回测详细记录,可以看到,如果将总盈利减去浮动盈亏,得到的结果和我们计算的结果一致。

代入凯利公式计算最优投资比例,这里用凯利公式的变形K = W - {1-W}/R, 这里, W, R分别为胜率和赔率,最优投资比例大约为 96.93%.

将策略中的 context.risk_ratio 改为 0.96, 可以看到,回测情况如下所示:

0_1532395920965_2.png

盈利确实有增加,那么,是不是我们就可以按照凯利公式去进行投资了呢?

讨论

实际情况下,一般不会有人去按照凯利公式进行仓位配置的,凯利公式又被称为破产公式,因为回测行情仅仅针对于回测区间的行情对策略有效性的判断,当行情切换,或者哪怕行情不变,但是不同时间区间内,行情也不会完全一样,回测时得到的胜率,盈亏比,仅供参考,给出一个大概区间范围,如果真的去按照凯利公式配置自己的仓位,因为风险放的很大,往往一个极端行情过来,本金就会亏掉大半。

因此,在我们评估回测绩效的时候, 最大回撤往往是评估策略风险的极重要指标,因为这表明了在历史行情中,我们可能承受的最大风险,根据这个风险,我们可以以此来评估配置一个合理的仓位。海龟策略作为一个公开几十年的策略依然被很多人青睐和使用,就在于其对于仓位的管理,充分考虑了风险在仓位管理中的重要性。

固定分数法

这里,笔者根据《资金管理方法及其应用》这本书介绍固定分数法的资金配置方案,并通过一个简单的示例,试图给读者一个比较直观的感受。

固定分数法介绍

固定分数法是股市交易中最常用的一种方法,它有多种表现形式,但他们都建立在同意原则的基础上。实际使用这种方法的目的是,让交易系统在每次交易中确定好投注的资金比例。这种交易方式不同于凯利公式,其并不涉及到交易系统的各项参数,但却要考虑投资者的心理素质和可承受的损失数额。

假设我们可以承受的损失为可用资金的 f%,用这个可承受损失金额除以我们设定的止损值,就可以得到我们可以买进的合约数。譬如,我们有 100 万的资金,我们可承受的损失为 10万,买入的股票我们能承受 50% 的亏损,假如股票初始价格为 10元,那么,我们可以买入股票数目为 100000/(10*0.5)=200000 股。

示例

不同的可承受风险,对应的收益曲线可能差异巨大,这充分反映出盈亏同源的理念,可承受风险足够大,那么获取超额收益的可能性也就相应变大;但是,当风险达到一定程度的时候,当交易数目足够多的时候,我们很可能出现本金都被亏损完的窘境,因此,确定好一个合理的,可控的风险度对于资金管理至关重要。

这里,笔者利用双均线,在沪深 300的成分股上进行简单的交易,代码如下,其中 context.risk_ratio对应的是可承受的风险,为了避免出现买入股票太多,导致资金不够用的情况,笔者这里简单起见,设定了持仓股票最多 20支的限制,同时,由于持仓有不同股票,笔者这里简单采用可承受风险资金除以满足条件股票价格作为买入股票数目

from gm.api import *
import numpy as np
from collections import deque
import datetime
import talib


DATE_STR = '%Y-%m-%d'
TIME_STR = '%Y-%m-%d %H:%M:%S'


def init(context):
    print('starting initialization')
    # 股票池
    context.stock_pool = get_constituents('SHSE.000300')
    # 订阅行情
    subscribe(context.stock_pool)
    # 量价信息存储
    context.dict_vol_price = {}
    # 开平仓信息
    context.dict_open_close_signal = {}
    # 时间窗
    context.hist_size = 20
    # 风险暴露
    context.risk_ratio = 0.1
    # 技术指标参数
    context.short_period = 5
    context.long_period = 10
    # 持仓股票种类
    context.set_hold_stock = set()
    context.hold_stock_limit = 20
    # 日期设置
    context.curr_bar_time = context.backtest_start_time
    curr_bar_time = datetime.datetime.strptime(context.curr_bar_time, TIME_STR)
    shift_bar_time = datetime.timedelta(days=1)
    pre_bar_time = datetime.datetime.strftime(curr_bar_time-shift_bar_time, TIME_STR)
    # 开平仓信息
    for stock in context.stock_pool:
        # 初始化开平仓信息
        context.dict_open_close_signal.setdefault(stock, False)
        # 定义存储时间窗内历史信息
        deque_open = deque(maxlen=context.hist_size)
        deque_high = deque(maxlen=context.hist_size)
        deque_low = deque(maxlen=context.hist_size)
        deque_close = deque(maxlen=context.hist_size)
        history_bars = history_n(
            symbol=stock,
            frequency='1d',
            count=context.hist_size,
            end_time=pre_bar_time,
            adjust=ADJUST_PREV,
            adjust_end_time=context.backtest_end_time
        )
        for bar in history_bars:
            deque_open.append(bar.open)
            deque_high.append(bar.high)
            deque_low.append(bar.low)
            deque_close.append(bar.close)
        context.dict_vol_price[stock] = [deque_open, deque_high, deque_low, deque_close]
    print('end initialization')


def on_bar(context, bars):
    # 判断是否换日
    if datetime.datetime.strftime(context.now, DATE_STR) != context.curr_bar_time[0:10]:
        context.curr_bar_time = datetime.datetime.strftime(context.now, DATE_STR) + ' 09:30:00'
        # 重置开平仓信号
        for key in context.dict_open_close_signal:
            context.dict_open_close_signal[key] = False

    for bar in bars:
        context.dict_vol_price[bar.symbol][0].append(bar.open)
        context.dict_vol_price[bar.symbol][1].append(bar.high)
        context.dict_vol_price[bar.symbol][2].append(bar.low)
        context.dict_vol_price[bar.symbol][3].append(bar.close)
        # 如果用于计算的量不够
        if len(context.dict_vol_price[bar.symbol][0]) < context.hist_size:
            continue
        else:
            algo(context, bar)


def algo(context, bar):
    # 获取仓位
    pos = context.account().position(symbol=bar.symbol, side=PositionSide_Long)
    open_vol = int(context.account().cash.available*context.risk_ratio/bar.close/100)*100

    # 如果没钱开仓,跳过该股票
    if open_vol <= 0:
        return

    # 计算指标
    closes = np.asarray(context.dict_vol_price[bar.symbol][3])
    ma5 = talib.SMA(closes, context.short_period)
    ma10 = talib.SMA(closes, context.long_period)

    if ma5[-2] <= ma10[-2] and ma5[-1] > ma10[-1]:
        flag_golden_cross = True
        flag_dead_cross = False
    elif ma5[-2] >= ma10[-2] and ma5[-1] < ma10[-1]:
        flag_dead_cross = True
        flag_golden_cross = False
    else:
        flag_dead_cross = False
        flag_golden_cross = False

    if pos is None and flag_golden_cross and len(context.set_hold_stock) < context.hold_stock_limit:
        if context.dict_open_close_signal[bar.symbol] is False:
            order_volume(symbol=bar.symbol,
                         volume=open_vol,
                         side=OrderSide_Buy,
                         order_type=OrderType_Market,
                         position_effect=PositionEffect_Open,
                         price=0,
                         )
            context.dict_open_close_signal[bar.symbol] = True
            context.set_hold_stock.add(bar.symbol)
    if pos is not None:
        if flag_dead_cross or pos.fpnl/pos.vwap <= -1.1 or pos.fpnl/pos.vwap > 1.3:
            if context.dict_open_close_signal[bar.symbol] is False:
                # print('stock %s at date %s has fpnl %f' %(bar.symbol, context.now, pos.fpnl))
                order_volume(symbol=bar.symbol,
                             volume=pos.volume,
                             side=OrderSide_Sell,
                             order_type=OrderType_Market,
                             position_effect=PositionEffect_Close,
                             price=0,
                             )
                context.dict_open_close_signal[bar.symbol] = True
                context.set_hold_stock.remove(bar.symbol)

if __name__ == '__main__':
    run(strategy_id='e4fa3c86-8cb9-11e8-b011-f48c50eb367a',
        filename='main.py',
        mode=2,
        backtest_initial_cash=10000000,
        token='64c33fc82f334e11e1138eefea8ffc241db4a2a0',
        backtest_start_time='2016-06-17 13:00:00',
        backtest_end_time='2017-08-21 15:00:00')

当设置 context.risk_ratio 为 0.1 和 0.2的时候,回测绩效差异已然很大,具体见下图:

0_1532397073086_3.png

而当风险设置到 0.4的时候,回测结果甚至已经为负,说明,一味放大风险并不能带来收益率的提高,甚至会导致不必要的亏损。

写在最后

篇幅所限,笔者仅介绍了固定分数法的仓位管理方法,其实,还有包括最优 f值法,安全 f 值法,固定比例法,变动比例法,不同仓位管理方法对应了不同的风险偏好,但是有一点是共通的,对于风险的控制,在仓位管理中是非常非常重要,无论如何强调都不会过分。

很多读者在炒股的时候,经常都会有满仓去操作的情况,往往持仓的股票一个细微的波动就给自己的心理带来非常大的影响,于是,要不就是早早地亏损出场,要不就是明明有一波大行情,但是又会在行情到来之前就出场了,如何去选择一个让自己心理安定,不至于让短期的行情波动去影响自己的心理,一个合理的仓位管理方法尤为重要。

祝各位读者早日找到一个符合自己的交易逻辑与仓位管理方法出来。

作者:胡琛 (南京师范大学理论物理博士) 编辑:掘金小Q

相关阅读:
《仓位管理(1): 鞅与反鞅策略,凯利公司及其局限 》

评论: 1

Looks like your connection to 掘金量化社区 - 量化交易者的策略交流学习社区 was lost, please wait while we try to reconnect.