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

凯利公式指导投资示例

引言

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

凯利公式的套用

其实换一种思路,我们完全自己在策略中进行运算,笔者这里简单起见,对沪深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')

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

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

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

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

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

讨论

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

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

固定分数法

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

固定分数法介绍

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

假设我们可以承受的损失为可用资金的 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.4的时候,回测结果甚至已经为负,说明,一味放大风险并不能带来收益率的提高,甚至会导致不必要的亏损。

写在最后

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

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

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

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

来源:掘金量化      作者:胡琛 (南京师范大学理论物理博士) 

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容