从零开始量化交易 - 工具选择与第一个策略

不盗墓的三爷 / 2023-04-28 / 原文

在学习了一定的理论知识之后,需要对其进行模拟练习以加深理解。

量化交易的核心在于如何构建自己的策略,以及对于构建的策略进行验证。就好像写代码一样,首先要配置代码的编写缓解,以及代码的运行测试环境。

因此,首先进行工具或平台的选择,以及编写策略的必要配置。

平台的选择

对于一窍不通的我来说,这一步首先还是先用万能的Google进行资料的查询。根据前人整理和总结的资料,经过初步的信息阅读,根据我的理解对其进行归类,如下所示:

名称 涵盖金融产品 是否付费 回测与交易 特点 策略编写语言 国内/国外 机构或个人
BigQuant 股票、基金、期货、期权、可转债、数字货币 未知 均支持 AI,低代码 可视化 未知 均可
聚宽 股票(A股,非科创板)、场内基金 均支持 优秀社区、教程丰富 Python 未知 均可
DigQuant / / 不支持 教育学院、卖课程 / / /
老虎量化 / / / 美股交易、接口 / / /
TradingView 能源、贵金属、股票期指合约、外汇 未知 图表交易 国外 均可
发明者 商品期货CTP/易盛API/中泰证券XTP/腾讯富途证券(美股港股) 支持 策略超市 支持Python 国内 均可
VNPY 股票、期货、期权、外汇、数字货币 实盘交易 私人部署、开发框架 Python 未知 均可

结合自身的实际情况,最适合的方式是从聚宽这个平台开始,先忽略底层的实现,专注在策略本身,同时在社区中学习经验,让自己对于量化交易了解的更为深刻,其目标就在于:

  1. 了解什么是策略,其如何运行。
  2. 了解支撑策略的运行机制是什么。其所需要的数据有哪些?
  3. 了解怎么去评判一个策略,具体指标有哪些?

当有了一定的了解之后,可以从VNPY这个框架入手,按自己的习惯搭建一个最符合自己的量化交易系统。

使用聚宽

跟随着聚宽官网上的新手教程:https://www.joinquant.com/view/community/detail/8ec7aaaa899cf928550f89a104637f22。初步熟悉了聚宽工具的使用,还是相当简单和友好的。新手教程中对于之前没有从事过编程的人员也非常友好,给出了很清晰的编程入门信息,并结合策略例子执行。

其界面如下图所示:

对于结果的验证也比较清晰,分为编译运行结果和回测结果。编译运行结果如下图所示:

在回测结果中可以查看策略的收益概览,以及实际的交易情况(包括买入卖出股票,以及交易后的实际金额),如下图所示:

并且聚宽还提供了统计分析工具,用于更好的了解策略回测情况。

美中不足

美中不足的地方在于JQData SDK的本地化测试需要申请,并且试用期只有三个月。而在平台上编码确实不是很顺手。

另外,限制了免费的回测时间(60分钟)。

当然天下没有免费的午餐,提供这些能力去支付费用是应该的。所以,美中不足的不是平台,而是我们自己。

聚宽上的第一个策略

聚宽的新手教程中,给出了一个策略编写自测的题目,正好拿来当作自己的第一个策略。

其描述如下:

1. 每天找出市值排名最小的 N 只股票作为买入股票
2. 若持仓股票不在买入股票列表中,则清仓
3. 买入N只股票,买入资金为 总资金 / N
4. 设定止盈止损条件
5. 排除ST股票、停牌股票、涨停股票、跌停股票
6. 控制交易周期:即不再是每日交易,而是每 T 日交易

实现与结果

对于有一定编程经验的我来说,这块内容的实现并不复杂,参考了聚宽 Context中的变量含义,实现代码如下所示:

from jqdata import *

def initialize(context):
    """
    初始化
    """
    # 设置股票数量
    g.stock_number = 10
    g.loss_limit = -0.01 # 跌5个点就卖出
    g.win_limit = 0.02 # 涨5个点就卖出
    # 周期运行. 开盘前运行. 停牌数据最好是在开盘后获取。
    run_daily(period, time='09:10')
    
def period(context):
    """
    周期运行函数
    """
    # 找出要交易的股票
    find_stocks(context)
    
    # 卖出持仓的不在g.security列表中的股票
    for stock in context.portfolio.positions:
        if stock not in g.security:
            # 清仓
            order_target(stock, 0)
    
    # 买入股票
    for stock in g.security:
        stk_cnt = context.portfolio.cash / g.stock_number
        order_value(stock, stk_cnt)
        # 止损与止盈
        sellLoss(context, stock)
        sellWin(context, stock)
    
def find_stocks(context):
    """
    找寻市值最小的N个股票
    """
    ## step1: 获取当天存在的股票
    stocks = get_all_securities(date=context.current_dt).index.tolist()
    ## step2: 过滤stocks, 去除st股票
    ### Tips: 通过sum计算,若第二列>0则表示至少出现过一次True。则为ST
    ### Tips: 只要出现过ST,都不包含在内。保守策略
    st_stocks = get_extras("is_st", stocks, context.run_params.start_date, context.run_params.end_date, df=True).T
    stocks = st_stocks[~st_stocks.iloc[:,0]].index.tolist()
    ## step3: 过滤stocks, 去除当天停牌的股票
    ### Tips: 根据价格表中过滤
    ### Tips: 过滤今日停牌股票
    stopped_stocks = get_price(stocks, end_date=context.current_dt, count=1, fields='paused').paused.sum()
    stocks = stopped_stocks[stopped_stocks < 1].index.tolist()
    ## step4: 过滤stocks, 去除当前涨停或跌停的股票。 这里待优化
    
    ## step2: 按市值排序,获取最小的N个
    ## 这里是SQL Alchemy的语法。从市值表里查询
    condition = query( valuation.code
                  ).filter(
                      valuation.code.in_(stocks)
                  ).order_by(
                      valuation.market_cap.asc()
                  ).limit(g.stock_number)
    df = get_fundamentals(condition)
    # 选取股票代码并转为list
    g.security = list(df['code'])
    print(g.security)
    
def sellLoss(context, stock):
    """
    止损卖出
    """
    cost = context.portfolio.positions[stock].avg_cost
    current_price = context.portfolio.positions[stock].price
    if cost <= 0:
        return
    ret = current_price / cost - 1
    if ret < g.loss_limit:
        order_target(stock, 0)
        
def sellWin(context, stock):
    """
    止盈卖出
    """
    cost = context.portfolio.positions[stock].avg_cost
    current_price = context.portfolio.positions[stock].price
    if cost <= 0:
        return
    ret = current_price / cost - 1
    if ret > g.win_limit:
        order_target(stock, 0)

其回测结果如下图所示:

可以看到结果并不是很好。这时候想起了一句话“让盈利奔跑吧”,所以我去掉止盈和止损策略之后,再看看结果:

虽然跑赢了“大盘”,但结果并不是很理想。因此可以断定,小市值的股票在选定的时间范围内表现不理想。或许也是近期大盘萎靡不振的原因。如果放在2015年的牛市跑这个策略,可以看到如下的结果:

可以看到这个策略的收益明显非常夸张。结合

问题与思考

  1. 当 总资金/N 不足以买一手(即100股,根据A股的交易逻辑,股票交易需要按手买入卖出),如何处理?

    在实际的测试中,看到聚宽平台有如下日志输出:

    下单检查标的数量:StockOrder(entrust_id=1682588131 security=300478.XSHE mode=OrderValue: _value=10000.0 style=MarketOrderStyle: _limit_price=0.0 side=long action=open margin=False entrust_time=2023-01-03 09:10:00 error=开仓数量必须是 100 的整数倍,调整为 1000)
    

    可以看到,平台在此处做了整数保护,当然,在后续的过程中也可以自己做处理。或向上调整,或向下调整。

  2. 在每日开盘前运行这个策略,获取当天的涨停和跌停股票是存在使用未来数据的问题。如何修改?

    在此处,设定了在"09:10"这个时间点运行。以当前时间点计算涨停和跌停。当然,个人觉得还不是很保险。最好是使用昨天的数据判断昨日是否涨停或跌停

  3. 如何修改不同的初始化参数,针对不同的N进行遍历,取最优?是否需要使用JQData SDK进行本地回测

    JQData SDK需要申请使用,暂时没有处理。目前是手动的进行参数调整多次回测。并在平台的回测列表中查看不同参数的运行结果。也不失为一个方法。

  4. 当前没有过滤科创板。需要进行过滤。

  5. 在止盈止损过程中,设定了清仓。但是在买入之后。按照A股T+1的原则,应该是无法清仓的。这里还需要处理