掘金社区

示例策略——小市值策略(股票)——错误分析1Pinned highlighted

jqz1226 发表在问题反馈 2021-07-20 01:42:49

问题反馈
382
4
0

示例策略—小市值策略(股票)—错误分析(一)

说实话,我为找到一个适合自己的量化平台,踏遍了“千山万水”。

现在的主流平台,大致可以分为三类:1)基于Web网页类平台,例如聚宽(Joinquant),实盘交易的最后一公里没有打通;聚宽衍生出来的,比如一创聚宽,打通了实盘交易的最后一公里,但有诸多限制,比如不能访问外网,这样策略需要的一些第三方数据就无法取得;2)网络终端类平台,例如恒生Pro-Trade(又称PTrade),跟交易客户端是天然一体,委托和成交回报不是问题,但同样存在着系统不够开放的问题,一些第三方Python库文件不能安装,也影响了数据的获取;3)本地终端类平台,掘金(GoldMiner)是其中的优秀代表,既解决了实盘交易的问题,又有足够的开放性,不论是安装第三方库还是访问第三方数据,都不是问题,也让宽客们不再担心策略保密的问题,非常棒的思路。

所以,从本质上来说,我非常希望掘金好,能给我们用户一个完美的平台。但我在试第一个例子“示例策略—小市值策略(股票)”,就发现错误,掘金也让我失望!下面我就来剖析一下这个示例策略存在的第一个错误(是的,错误不止一个)。

一、错误所在

错误出现在如下这段代码里:

def algo(context):
    # 获取筛选时间:date1表示当前日期之前的100天,date2表示当前时间
    date1 = (context.now - timedelta(days=100)).strftime("%Y-%m-%d %H:%M:%S")
    date2 = context.now.strftime("%Y-%m-%d %H:%M:%S")

    # 通过get_instruments获取所有的上市股票代码
    all_stock = get_instruments(exchanges='SHSE, SZSE', sec_types=[1], 
                                fields='symbol, listed_date, delisted_date', df=True)

    # 剔除停牌和st股和上市不足100日的新股和退市股和B股
    code = all_stock[(all_stock['listed_date'] < date1) & 
                     (all_stock['delisted_date'] > date2) &
                     (all_stock['symbol'].str[5] != '9') & 
                     (all_stock['symbol'].str[5] != '2')]

或者说,其错误在于使用了get_instruments函数。

二、基本常识

在一般量化软件里,**数据获取函数**会依据数据与时间点的关系,将函数分为四类:

1)研究类,只能在Jupyter Notebook中使用,这种函数比较少见,比如PTrade的get_market_list函数等,缘于数据简单且固定,与时间没有关系;

2)回测类,只能在回测中使用,因为依赖于回测的上下文context。

比如Joinquant的函数:

history(count, unit='1d', field='close', security_list=None, df=True, skip_paused=False, fq='pre')

再比如PTrade中的函数:

get_history(count, frequency='1d', field='low', security_list=None, fq=None, include=False)

与掘金的history_n函数相比较,可以看出以上两个函数是没有end_date参数,这样设计函数的好处是可以避免未来,相当于系统强制性赋值end_date为context.now的前一个交易日。

再比如跟持仓Positiond的数据只保存在context中,相关函数就无法离开context而独立存在。

3)实盘类,只能在实盘交易中使用而不能在回测中使用,因为返回的永远是最新的数据(即时间点永远是datetime.datetime.now()),而不是历史当时的数据。

掘金的get_instruments函数、PTrade的get_individual_transaction(获取逐笔成交行情数据)就属于这一类。无论回测还是交易都获取的是当前实盘最新的数据,没有历史数据。

4)通用类,研究、回测、实盘中都能使用,因为函数包括了时间点。例如功能相似的两个函数:

Joinquant的函数:

get_all_securities(types=[], date=None)  # 获取指定日期、指定类别的标的代码列表

PTrade的函数:

get_Ashares(date=None)  # 获取指定日期沪深市场的所有A股代码列表

可以看到这两个函数都包含date参数,可以通过date参数指定查询日期,也可以不指定查询日期(即默认的date=None)。当不指定查询日期, 研究和回测中date取值有所不同:在研究中,date取的是当前日期(即:datetime.datetime.now());在回测中,date取当前回测周期所属历史日期(即:context.now)。而且这类函数还具有侦测未来的功能,即若回测中指定的date参数大于context.now的时候,系统报错并终止策略的运行。

三、掘金数据的分析

使用实盘类函数get_instruments来获取回测日的数据,将不能得到历史时点的准确数据。今天是2021-7-19日,假设回测时间范围:

backtest_start_time='2018-05-01 08:00:00',
backtest_end_time='2018-05-31 16:00:00',

现在回测已经进行到了2018-05-28日。

分析阶段一

根据示例策略:

all_stock = get_instruments(exchanges='SHSE, SZSE', sec_types=[1], 
                                fields='symbol, listed_date, delisted_date', df=True)

2018-05-28日:len(all_stock)是4317

根据示例程序,把2018-05-28日之后上市的股票去掉:

date2 = datetime.date(2018,5,28).strftime("%Y-%m-%d %H:%M:%S")
code = all_stock[(all_stock['listed_date'] <= date2) & (all_stock['delisted_date'] > date2) &
                     (all_stock['symbol'].str[5] != '9') & (all_stock['symbol'].str[5] != '2')]

留下2018-05-28日之前上市且2018-05-28日还没有退市的,再去掉上证B股和深证B股,只保留A股。现在len(code)是33132018-05-28日是3313只A股股票,这么?

对照一下其它软件查询的结果,其它软件显示2018-5-28全市场A股是3523只,掘金少了200多只股票啊!居然丢了这么多股票,赶紧查查,看把谁丢弃了。查询结果显示,掘金2018-5-28日不见了的股票有:

{'000005', '000007', '000022', '000410', '000418', '000502',....'603779', '603996'}

长长的210只不见了的股票名单,就不一一列举了。不过,从列举出来的这个名单,可以发现一些问题,比如:

000005.SZ,2018-5-28日的时候是存在的,当时的名称是“世纪星源”,现在的名称叫“*ST星源”,

000007.SZ,2018-5-28日的时候是存在的,当时的名称是“全新好”,现在的名称叫“*ST全新”

......。噢,如今他们沦落为ST了,就可以把他们从2018-5-28的历史上抹掉?显然不行!抹掉了就不是2018-5-28日全A了!而且你也没有“透视眼”、“后视镜”,不能像神仙一样预测未来他们会被ST,当时他们是存在的,是正常的股票。

分析阶段二

追溯原因,这些今天的ST之所以会被从2018-5-28的历史上抹掉,是源于函数get_instruments的两个默认参数:skip_suspended=True, skip_st=True,改过来,再看看:

all_stock = get_instruments(exchanges='SHSE, SZSE', sec_types=[1],
                            skip_suspended=False, skip_st=False,
                            fields='symbol, listed_date, delisted_date', df=True)
print(len(all_stock))

现在all_stock是4537只股票。然后继续:

date2 = datetime.date(2018,5,28).strftime("%Y-%m-%d %H:%M:%S")
code = all_stock[(all_stock['listed_date'] <= date2) & (all_stock['delisted_date'] > date2) &
                     (all_stock['symbol'].str[5] != '9') & (all_stock['symbol'].str[5] != '2')]
print(len(code))

现在code是3518只股票,比其它软件显示的3523,仍然少了5只,分析后发现少了如下5只:

{'000022', '000511', '300427', '600432', '600806'}

000022, 深赤湾A 2018-12-25退市

000511, 烯碳新材 2018-07-17退市

300427, 红相股份

600432, 吉恩镍业 2018-07-13退市

600806, 昆明机床 2018-07-11退市

这5只股票里面,有4只已经退市了,今天2021-7-19日他们是已经不见了,但历史上的2018-5-28日他们是存在的啊,而且当天连ST都不是,凭什么把他们从2018-5-28日的A股历史上抹掉?

更不可思议的是,300427, 红相股份,历史上都没有被ST过,正正常常,浓眉大眼的,居然也被从历史和今天抹掉了!

四、错误后果

从前述的分析中可以看出,示例策略的错误:

一是源于函数get_instruments的两个默认参数:skip_suspended=True, skip_st=True,从而把今天(2021-7-19日)是ST的股票,从历史上(2018-5-28日)抹掉了。

二是get_instruments是实盘类函数,获取的是当前实盘最新的数据,所以被退市从而如今不存在的、但历史上是存在的股票,也被顺手从历史上抹掉了。

这种错误的是幸存者偏差,是严重的未来,是“透视眼”、“后视镜”,这些股票后来被ST了,被退市了,不行了,就让他们不在历史回测的候选股票里出现,这样就不会踩大雷、跳大坑,回测的策略业绩就有可能好一些甚至好很多!

我自己在编写策略的过程中就遇到过,在600734实达集团被ST的前一天买入,然后被坑了N个跌停板。

这些被从历史上抹掉的股票可能是不服的:别看我今天不行了,我今天不见了,但历史上我也是辉煌过的,也是每天被N多人抢筹的。

五、有替代方案么——没有!

通过上述分析,知道了错误在哪,错有多大,错有多严重,那就不用函数get_instruments,换个别的函数呗,比如:get_history_instruments函数,不就行了么?

答案是不行,因为

get_history_instruments(symbols, fields=None, start_date=None, end_date=None, df=False)	

第一个参数symbols 是必填参数,不能忽略!那这个symbols参数值从哪里来?你不会告诉我从get_instruments函数来吧!且不说这样写起来很滑稽,而且Garbage In,Garbage Out,get_instruments函数结果就不正确了,再被get_history_instruments函数过滤一遍,仍旧是不正确的。

所以,结论是:没有替代方案

六、未来的可能的解决方案

get_instruments从实盘类改为通用类函数,即增加一个参数date。

这与get_history_instruments函数并不矛盾。

评论: 4
  • 您好,谢谢您对掘金的支持
    (1)关于示例策略中使用函数get_instruments,直接将当前最新的停牌股和ST股剔除,在回测中是错误的,我们将提出以下修改

    now = datetime.date(2018,5,28)
    # 通过get_instruments获取所有的上市股票代码
    all_stock = get_instruments(exchanges='SHSE, SZSE',
                                sec_types=[1],
                                skip_suspended=False,
                                skip_st=False,
                                fields='symbol, listed_date, delisted_date',
                                df=True)
    # 获取筛选时间:date1表示当前日期之前的100天,date2表示当前时间
    date1 = (now - timedelta(days=0)).strftime("%Y-%m-%d %H:%M:%S")
    date2 = now.strftime("%Y-%m-%d %H:%M:%S")
    # 上市不足100日的新股和退市股和B股
    code = all_stock[(all_stock['listed_date'] < date1)
                     & (all_stock['delisted_date'] > date2) &
                     (all_stock['symbol'].str[5] != '9') &
                     (all_stock['symbol'].str[5] != '2')]
    

    (2)关于如何剔除历史上停牌股和st股,提出以下修改:

    # 剔除停牌和st股
    df_code = get_history_instruments(symbols=code['symbol'].to_list(),
                                      start_date=date2,
                                      end_date=date2,
                                      df=True)
    df_code = df_code[(df_code['is_suspended'] == 0) & (df_code['sec_level'] == 1)]`
    

    (3)关于缺失{'000022', '000511', '300427', '600432', '600806'}数据问题:
    3.1 其中000022深赤湾A股票自2018年12月26日开市起以变更后的证券简称及证券代码复牌并交易,变更后的证券简称为“招商港口”,证券代码为“001872”。
    3.2 其它四支票,我们将尽快补齐

    (4)关于示例策略,请勿用于实盘,只是提供学习参考的意见,如果仍有问题,可以继续提出探讨

    2021-07-20 12:16:57
  • 聚宽Joingquant的数据:
    0_1626829789811_聚宽20180528全A.png

    2021-07-21 09:10:12
  • 同花顺MindGo的数据:
    0_1626829849973_20180528全A股票数量.png

    2021-07-21 09:10:53
  • @testing 你好:优化改善更新后的这个策略在哪里可以克隆研究?烦劳告知链接,谢谢!同时非常感谢楼主技术牛!!!

    2021-07-21 19:27:05

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