公平博弈必输策略及Python改进:基金补仓

  • 前言
  • 一、算术原因
  • 二、逻辑原因
  • 三、Python改进
  • 总结

前言

考虑一个场景:一名投机人每次拿出当前本金的10%进行抛硬币测试(公平博弈,输赢均50%),一共测试60次,最终输赢各30次,那么他的本金将是多少?

计算:x = 1.1^30 * 0.9^30 * 100% = 0.99^30 * 100% =73.97%

这就带来了一个问题:一个公平博弈,如果策略不妥,那么长期下来有可能是必输。

为什么公平博弈,长期下来,最后“必输”?

举个抛硬币两次的例子:一共4个结果,赢赢、赢输、输赢、输输各占25%,那么本金分别是:121%、99%、99%、81%,其平均值(期望)仍然是100%。因此,虽然是公平博弈,但是长期下来,少数人大赚,多数人亏损,彩票化了。

注:该问题,与“一支股票经历10%涨停30次,10%跌停30次,最终价格如何”,实质是一样的。

一、算术原因

从算术角度来说,胜率必须略高于52.5%,长期才可以不输。以1000次为例:
1. 1 525 ∗ 0. 9 475 = 1. 1 50 ∗ 0.9 9 475 = 0.9916 ≈ 1 1.1^{525} * 0.9^{475} = 1.1^{50} * 0.99^{475} = 0.9916 \approx 1 1.15250.9475=1.1500.99475=0.99161
但是,根据大数理论,硬币实验次数越多,概率越趋近50%。当实验次数足够多时,胜率略高于52.5%的可能性变得微乎其微,对多数人而言,就必输了。

二、逻辑原因

从逻辑角度来说,落后之时减少投入,而领先之时增大投入,是不妥的。以起始10000为例:
1)先输后赢,则10000输1000,变成9000,再赢900,变为9900
2)先赢后输,则10000赢1000,变成11000,再输1100,变为9900

只有反过来,落后时增大投入,而领先时减少投入,才是合理的。比如:
1)先输后赢,10000变9000,增加投入1100,赢后变10100
2)先赢后输,10000变11000,减少投入900,输后变10100

然而,这种策略也存在两个问题:一是有点类似倍投法,假如一开始连输几次,有输光的风险;二是领先之时,资金大了,而每次投入要变少,资金利用率降低了。

三、Python改进

事实上,“每次拿出当前本金10%”的方案,不如“固定金额”方案。套用在购买基金的场景,前者类似“固定份额”定投方案,后者是“固定金额”定投方案。

现在试一试改进方案:
1)以某天净值x0为基准
2)当净值突破至下一个基准:上涨为x0的2倍,下跌为x0的一半,切换基准
3)当上一次操作之后,净值变化超过3%,则再次操作:如下跌,则补仓(倍数见下),如上涨且持有,则减仓(倍数见下,不足则清完为止)
4)当净值在[1, 2)倍x0时,倍数为1倍;净值在[0.9, 1)倍x0时,倍数为2倍;净值在[0.8, 0.9)倍x0时,倍数为3倍;…;净值在[0.5, 0.6)倍x0时,倍数为6倍。但是,如果当前持仓过多(大于该倍数的2倍),那么补仓倍数仅为1倍(减仓倍数不影响)

以下是该方案的Python源码,在Jupyter notebook里面分三段:

import requests
import time
import execjs

fileTest = './data/accTest.csv'
jjTest = '001630'

def getUrl(fscode):
    head = 'http://fund.eastmoney.com/pingzhongdata/'
    tail = '.js?v='+ time.strftime("%Y%m%d%H%M%S",time.localtime())
    return head+fscode+tail

# 根据基金代码获取净值
def getWorth(fscode):
    content = requests.get(getUrl(fscode))
    jsContent = execjs.compile(content.text)
    name = jsContent.eval('fS_name')
    code = jsContent.eval('fS_code')
    #单位净值走势
    netWorthTrend = jsContent.eval('Data_netWorthTrend')
    #累计净值走势
    ACWorthTrend = jsContent.eval('Data_ACWorthTrend')
    netWorth = []
    ACWorth = []
    for dayWorth in netWorthTrend:
        netWorth.append(dayWorth['y'])
    for dayACWorth in ACWorthTrend:
        ACWorth.append(dayACWorth[1])
    return netWorth, ACWorth

ACWorthTestFile = open(fileTest, 'w')
_, ACWorth = getWorth(jjTest)
if len(ACWorth) > 0:
    ACWorthTestFile.write(",".join(list(map(str, ACWorth))))
    ACWorthTestFile.write("\n")
    print('{} data downloaded'.format(jjTest))
ACWorthTestFile.close()

第一段如上,在天天基金网爬基金。001630是天弘中证计算机主题ETF联接C,有两个特点:
1)在支付宝基金里面,买入0费率,持有7天卖出0费率
2)该基金2015年7月成立的,至今(2021年2月)5年多,成立以来累计涨跌幅约-10%(目前基金净值约0.9元),10多亿大小

import csv

with open(fileTest) as f:
    row = csv.reader(f, delimiter=',')
    for r in row:
        #去掉记录为None的数据(当天数据缺失)
        r = [float(x) for x in r if x != 'None']

show_days = 1500 #仅用最近的show_days天数(1年约250交易日)

if len(r) > show_days:
    r = r[len(r)-show_days:]

x0 = r[0] #当前基准
x1 = x0 #已存净值
x2 = x0 #上次操作时净值
a = 10000 #标准份额(1倍)
y = 0 #当前持有倍数
z = 0 #累计盈利(含浮盈浮亏)
s = [0] #历史盈利率(不乘以标准份额,即z/a)
t = [0] #历史倍数乘以0.1(0.1y,乘以0.1是为了图形显示)

for i in range(1,len(r)):
    x3 = r[i]

    #刷新浮盈浮亏
    if y > 0:
        z += a * y * (x3 - x1)

    s.append(z/a)

    #净值变化超过基准
    if (x3 >= x0 * 2) or (x3 < x0 / 2):
        #下跌,则买入1倍份额
        if x3 < x2:
            y += 1
        #反之,如有则卖出1倍份额
        elif y > 0:
            y -= 1

        #刷新
        x0 = x3
        x1 = x3
        x2 = x3

    #上一次操作至今,净值变化超过3%
    elif (x3 > x2 * 1.03) or (x3 < x2 * 0.97):
        if x3 >= x0:
            # [x0, 2x0)采用1倍
            i = 1
        else:
            i = 2 + int(10 * (1 - x3 / x0))
            #但如果当前持仓过多,则补仓仅采用1倍(减仓不受影响)
            if (y > 2 * i) and (x3 < x2):
                i = 1

        #下跌,则买入i倍份额
        if x3 < x2:
            y += i
        #反之,如持有则卖出i倍份额
        else:
            if y > i:
                y -= i
            else:
                y = 0

        #刷新
        x1 = x3
        x2 = x3

    else:
        x1 = x3

    t.append(y * 0.1)

print('累计盈亏为:{:.0f}'.format(z))

第二段就是之前说的“改进方案”的实现。注意:当持仓过多而补仓时,以及基准切换时,倍数都用1。
该基金成立至今5年半,近1400交易日(show_days=1500亦即全部显示)。

import numpy as np
from matplotlib import pyplot as plt

plt.rcParams['font.sans-serif']='SimHei'
plt.rcParams['axes.unicode_minus']=False

r_plot = np.array(r).reshape(-1, 1)
s_plot = np.array(s).reshape(-1, 1)
t_plot = np.array(t).reshape(-1, 1)

# 图表显示
fig=plt.figure(figsize=(15,6))
plt.plot(r_plot, color='blue', label='基金净值')
plt.plot(s_plot, color='red', label='盈亏情况')
plt.plot(t_plot, color='yellow', label='持仓情况')
plt.legend(loc='upper left')
plt.show()

第三段代码就是画图了,贴图如下:

蓝色线就是001630的净值。看似很平,1元上下,其实最低值是0.485元,最高值是1.0826元。如果只会捂着,不会波段操作,简直太惨了。
黄色线是持仓倍数情况,红色线是盈亏情况。在最惨的时候(净值0.5元左右),倍数近20倍(黄纵坐标要乘以10),浮盈浮亏约-1.5(假定1万份为1倍,即浮亏1.5万);目前净值0.9元左右,浮盈浮亏为5(同理,浮盈5万)。

如果把show_days改设为250,亦即查看近1年的情况,则图片如下所示:

近一年最高持仓倍数为6,当前浮盈7千多(1万份1倍)。

注意:因为001630持有7天卖出0费率,且基金是按照先进先出原则卖出的,因此本文忽略了持仓不足7天而卖出的惩罚费率情况。如实战,则自行注意之。

总结

本文针对特色基金001630(基本面尚可,人气较旺,表现较差),提出了一种“温和”补仓的策略——落后时略增加倍数。当然,其它基金也是可以的,试过10多个基金(都是7天0费率的)。

关于基金补仓减仓,凭感觉,两种策略都是可行的:一种即是本文的“上次操作后变化超3%”,另一种是“连涨连跌几天”。或者两者结合:5天当中4-5天涨,且涨幅超3%;5天当中4-5天跌,且跌幅超3%。采用5天是比较好的(5个交易日必满足持有7天)。这些策略经反复测试,大同小异,不另贴了。

有一种直觉,现在“韭菜”不喜欢买股票,而喜欢买基金了。尤其是女白领。于是,有些光棍跑到基金板块留言“交友启事”,蛮有趣。

本文地址:https://blog.csdn.net/fanmin2000/article/details/113392512