头图

WonderTrader新版本v0.7.0新特性

WonderTrader
English

前言

  笔者上次发表平台文章,似乎已经过去很久了,实在有点汗颜。一方面笔者日常工作也比较繁忙,即使平台功能一直在完善,也没有太多时间去整理发文;另外一方面,之前发表的平台架构系列文章,基本上也覆盖了平台设计的要点,再要深入技术细节的话,就难免枯燥乏味了。
  这次平台发布大版本v0.7.0,改动的内容,涉及的点比较多。单纯的看更新日志,一时半会也比较难搞明白这次大版本到底做了哪些改动。于是笔者趁此机会,整理一篇文章介绍一下这个大版本的新特性,同时也刷一波存在感,希望大家不要把WonderTrader忘了。
  可能有人会问,之前也更新过很多版本了,也没看你这么正式地发个文介绍一下,那么v0.7.0这个版本有什么特别之处呢?
  v0.7.0确实是一个非常特别的版本。除了日常规的bug修复、性能优化、功能调整之外,v0.7.0最重要的修改是新增了一些组件,而这些组件正是WonderTrader发展规划所依赖的非常重要的基础组件。
  WonderTrader在架构设计阶段,就一直坚持两个设计原则:技术方面注重低耦合、功能方面注重可管理。低耦合比较容易理解,WonderTrader所有的功能组件都通过接口对接,降低了耦合性,也降低了组件开发的难度,提高了团队协作开发的效率。那么可管理性指的是什么呢?以前的平台文章,实际上或多或少都透露出了一点:那就是WonderTrader是针对量化团队设计的平台,除了覆盖个人的需求以外,同时也充分考虑量化团队的管理需求。
  在v0.7.0版本之前,WonderTrader的管理需要主要侧重于交易管理方面,亿也就是实盘交易环境的管理,而v0.7.0则把管理功能延伸到团队和投研阶段的管理需求了,而v0.7.0的主要改进,也是集中在投研管理方面。
  另外v0.7.0还有一个改动,就是将wtpy下的webui源码剥离处理,单独放到开源项目wtconsole中了。下面就分别对WonderTraderwtpywtconsole介绍一下v0.7.0版本中的新特性。

WonderTrader新特性

  WonderTrader作为C++开发的底层,承载了更多提供基础功能和运行效率的任务。v0.7.0版本中,WonderTrader也是新增了几个基础组件,并根据需求做了一些优化。

本地数据伺服模块

  本地数据伺服模块WtDtServo,是v0.7.0版本中最重要的两个新特性之一。和之前提供的数据组件导出模块WtDtPorter不同,WtDtPorter提供的是行情接入、数据落地以及行情广播等服务,是针对实盘交易的数据需求开发的服务模块,涉及到的模块的包括数据落地模块WtDataWriter和数据读取模块WtDataReader
  本地数据伺服模块WtDtServo,本质上和WtDataReader的功能是一样的,都是从WtDataWriter落地的数据文件中读取数据,但是WtDtServo作为独立的数据伺服模块,更多的是给应用层提供一个可以方便访问WonderTrader落地的数据的接口。WtDataReader是给实时交易框架提供数据访问接口。所以WtDataReader在运行过程中会根据实时行情的时间戳,同步数据读取的位置标记,而WtDtServo则是提供一个无标记的,可以根据需要随意访问数据的接口
  WtDtServo主要应用场景是在投研环节提供获取数据的接口,因为投研的数据需求是无关联的,每次拉取的数据之间是没有必然的逻辑先后顺序的。基于这样的应用场景,WtDtServo主要提供了4个接口:

  • 按照时间区间获取K线数据 get_bars_by_range
  • 按照条数和截止时间获取K线数据 get_bars_by_count
  • 按照时间区间获取tick数据 get_ticks_by_range
  • 按照条数和截止时间获取tick数据 get_ticks_by_count

  下面的代码,展示了各个接口的定义:

#pragma once
#include "PorterDefs.h"

#ifdef __cplusplus
extern "C"
{
#endif

    EXPORT_FLAG void        initialize(WtString cfgFile, bool isFile);

    EXPORT_FLAG    WtString    get_version();

    EXPORT_FLAG    WtUInt32    get_bars_by_range(const char* stdCode, const char* period, WtUInt64 beginTime, WtUInt64 endTime, FuncGetBarsCallback cb);

    EXPORT_FLAG    WtUInt32    get_ticks_by_range(const char* stdCode, WtUInt64 beginTime, WtUInt64 endTime, FuncGetTicksCallback cb);

    EXPORT_FLAG    WtUInt32    get_bars_by_count(const char* stdCode, const char* period, WtUInt32 count, WtUInt64 endTime, FuncGetBarsCallback cb);

    EXPORT_FLAG    WtUInt32    get_ticks_by_count(const char* stdCode, WtUInt32 count, WtUInt64 endTime, FuncGetTicksCallback cb);

#ifdef __cplusplus
}
#endif  

Pub/Sub消息队列模块

  消息队列模块WtMsgQuev0.7.0两个最重要的新特性中的另外一个。消息队列模块,是WonderTrader可管理性特点的最基础的组件。有了消息队列模块,就可以将很多组件进行更加合理的解耦,达到既不影响运行效率,又能更好的实现管理目标。
  考虑到WonderTrader实盘框架和回测框架的应用场景并不复杂,消息队列模块基于开源消息队列库nanomsg进行封装,仅采用了pub/sub协议。即实盘框架或者回测框架会发布端,而在应用层实现一个接收端,从而达到消息推送的目的。
  为了方便使用,WtMsgQue做了非常彻底的封装,提供C接口,应用层可以单独调用该模块。模块接口的定义如下:

#pragma once
#include "PorterDefs.h"

#ifdef __cplusplus
extern "C"
{
#endif
    EXPORT_FLAG void        regiter_callbacks(FuncLogCallback cbLog);

    EXPORT_FLAG WtUInt32    create_server(const char* url, bool confirm);
    EXPORT_FLAG void        destroy_server(WtUInt32 id);
    EXPORT_FLAG void        publish_message(WtUInt32 id, const char* topic, const char* data, WtUInt32 dataLen);

    EXPORT_FLAG WtUInt32    create_client(const char* url, FuncMQCallback cb);
    EXPORT_FLAG void        destroy_client(WtUInt32 id);
    EXPORT_FLAG void        subscribe_topic(WtUInt32 id, const char* topic);
    EXPORT_FLAG void        start_client(WtUInt32 id);
#ifdef __cplusplus
}
#endif

最小冲击执行单元

  WonderTrader提供了一个内置的执行单元工厂模块WtExecFact,该工厂模块提供了一个最简单的执行单元WtSimpleExecUnit。这个执行单元之所以命名为Simple,就是因为下单逻辑十分简单:通过参数可以选择最优价、最新价或者对手价作为基础价格,然后通过一个价格偏移参数控制基础价格偏移的跳数,从而得到委托价格,然后一次性将限价单丢出去。如果超过预设的时间,订单没有完全成交,则撤销原有订单,再重新执行一遍挂单逻辑。如此反复,直到实际持仓和目标仓位匹配为止。
  简单执行单元在下单数量很少的时候,基本还是能够胜任的,根据实盘的统计,撤单概率大概在5%以内,还是比较小的。但是一旦交易数量上升以后,单次下单数量太多,会造成很大的市场冲击。如果采用主动单(对手价挂单)很容易就击穿了对手价,促进价格往不利方向移动。如果采用被动单(最优化挂单),则会在最优价上形成较大的挂单,从而被其他市场参与者反向选择
  为了满足更大数量的交易需求,减少市场冲击,降低交易成本,于是便有了算法交易。比较流行的有TWAPVWAP等算法,本次更新,也新增了一个简化的TWAP执行单元,叫做最小冲击执行单元WtMinImpactExecUnit。最小冲击执行单元的主要逻辑简单执行单元基本一致的,还是以基础价格±偏移跳数,超时就撤单重新挂单。主要的区别在于,最小冲击执行单元每次下单的数量可以设置为固定数量,或者用对手盘的挂单量按照一定比例计算一个数量。另外,由于每次下单都是很小的数量,所以交易数量较多的时候,一定会分成很多次下单,这个时候可以通过设置两次下单的时间间隔来控制频率
  下面的代码就是最小冲击执行单元的核心计算逻辑:

void WtMinImpactExeUnit::do_calc()
{
    if (_cancel_cnt != 0)
        return;

    //这里加一个锁,主要原因是实盘过程中发现
    //在修改目标仓位的时候,会触发一次do_calc
    //而ontick也会触发一次do_calc,两次调用是从两个线程分别触发的,所以会出现同时触发的情况
    //如果不加锁,就会引起问题
    //这种情况在原来的SimpleExecUnit没有出现,因为SimpleExecUnit只在set_position的时候触发
    StdUniqueLock lock(_mtx_calc);

    double newVol = get_real_target(_target_pos);
    const char* stdCode = _code.c_str();

    double undone = _ctx->getUndoneQty(stdCode);
    double realPos = _ctx->getPosition(stdCode);
    double diffPos = newVol - realPos;

    //有未完成订单,与实际仓位变动方向相反
    //则需要撤销现有订单
    if (decimal::lt(diffPos * undone, 0))
    {
        bool isBuy = decimal::gt(undone, 0);
        OrderIDs ids = _ctx->cancel(stdCode, isBuy);
        if(!ids.empty())
        {
            _orders_mon.push_order(ids.data(), ids.size(), _ctx->getCurTime());
            _cancel_cnt += ids.size();
            _ctx->writeLog("[%s@%d] live opposite order of %s canceled, cancelcnt -> %u", __FILE__, __LINE__, _code.c_str(), _cancel_cnt);
        }
        return;
    }

    //因为是逐笔发单,所以如果有不需要撤销的未完成单,则暂不发单
    if (!decimal::eq(undone, 0))
        return;

    double curPos = realPos;

    //检查下单时间间隔
    uint64_t now = TimeUtils::getLocalTimeNow();
    if (now - _last_place_time < _entrust_span)
        return;

    if (_last_tick == NULL)
        _last_tick = _ctx->grabLastTick(stdCode);

    if (_last_tick == NULL)
    {
        _ctx->writeLog("No lastest tick data of %s, execute later", _code.c_str());
        return;
    }

    if (decimal::eq(curPos, newVol))
    {
        //当前仓位和最新仓位匹配时,如果不是全部清仓的需求,则直接退出计算了
        if (!is_clear(_target_pos))
            return;

        //如果是清仓的需求,还要再进行对比
        //如果多头为0,说明已经全部清理掉了,则直接退出
        double lPos = _ctx->getPosition(stdCode, 1);
        if (decimal::eq(lPos, 0))
            return;

        //如果还有都头仓位,则将目标仓位设置为非0,强制触发
        newVol = -min(lPos, _order_lots);
        _ctx->writeLog("Clearing process triggered, target position of %s has been set to %f", _code.c_str(), newVol);
    }

    bool bForceClose = is_clear(_target_pos);

    bool isBuy = decimal::gt(newVol, curPos);

    //如果相比上次没有更新的tick进来,则先不下单,防止开盘前集中下单导致通道被封
    uint64_t curTickTime = (uint64_t)_last_tick->actiondate() * 1000000000 + _last_tick->actiontime();
    if (curTickTime <= _last_tick_time)
    {
        _ctx->writeLog("No tick of %s updated, execute later", _code.c_str());
        return;
    }

    _last_tick_time = curTickTime;

    double this_qty = _order_lots;
    if (_by_rate)
    {
        this_qty = isBuy ? _last_tick->askqty(0) : _last_tick->bidqty(0);
        this_qty = round(this_qty*_qty_rate);
        if (decimal::lt(this_qty, 1))
            this_qty = 1;

        this_qty = min(this_qty, abs(newVol - curPos));
    }

    double buyPx, sellPx;
    if(_price_mode == -1)
    {
        buyPx = _last_tick->bidprice(0) + _comm_info->getPriceTick() * _price_offset;
        sellPx = _last_tick->askprice(0) - _comm_info->getPriceTick() * _price_offset;
    }
    else if(_price_mode == 0)
    {
        buyPx = _last_tick->price() + _comm_info->getPriceTick() * _price_offset;
        sellPx = _last_tick->price() - _comm_info->getPriceTick() * _price_offset;
    }
    else if(_price_mode == 1)
    {
        buyPx = _last_tick->askprice(0) + _comm_info->getPriceTick() * _price_offset;
        sellPx = _last_tick->bidprice(0) - _comm_info->getPriceTick() * _price_offset;
    }
    else if(_price_mode == 2)
    {
        double mp = (_last_tick->bidqty(0) - _last_tick->askqty(0))*1.0 / (_last_tick->bidqty(0) + _last_tick->askqty(0));
        bool isUp = (mp > 0);
        if(isUp)
        {
            buyPx = _last_tick->askprice(0) + _comm_info->getPriceTick() * _cancel_times;
            sellPx = _last_tick->askprice(0) - _comm_info->getPriceTick() * _cancel_times;
        }
        else
        {
            buyPx = _last_tick->bidprice(0) + _comm_info->getPriceTick() * _cancel_times;
            sellPx = _last_tick->bidprice(0) - _comm_info->getPriceTick() * _cancel_times;
        }
    }

    //检查涨跌停价
    bool isCanCancel = true;
    if (!decimal::eq(_last_tick->upperlimit(), 0) && decimal::gt(buyPx, _last_tick->upperlimit()))
    {
        _ctx->writeLog("Buy price %f of %s modified to upper limit price", buyPx, _code.c_str(), _last_tick->upperlimit());
        buyPx = _last_tick->upperlimit();
        isCanCancel = false;    //如果价格被修正为涨跌停价,订单不可撤销
    }
    
    if (!decimal::eq(_last_tick->lowerlimit(), 0) && decimal::lt(sellPx, _last_tick->lowerlimit()))
    {
        _ctx->writeLog("Sell price %f of %s modified to lower limit price", buyPx, _code.c_str(), _last_tick->upperlimit());
        sellPx = _last_tick->lowerlimit();
        isCanCancel = false;    //如果价格被修正为涨跌停价,订单不可撤销
    }

    if (isBuy)
    {
        OrderIDs ids = _ctx->buy(stdCode, buyPx, this_qty, bForceClose);
        _orders_mon.push_order(ids.data(), ids.size(), _ctx->getCurTime(), isCanCancel);
    }
    else
    {
        OrderIDs ids  = _ctx->sell(stdCode, sellPx, this_qty, bForceClose);
        _orders_mon.push_order(ids.data(), ids.size(), _ctx->getCurTime(), isCanCancel);
    }

    _last_place_time = now;
}

流程调整和性能优化

  除了前面提到的比较大的新特性以外,v0.7.0也做了很多小的修改,主要集中在一些流程的调整和性能的优化方面。

  • 完善了主力合约换月以后自动清理历史主力合约持仓的机制。主力合约换月以后,上一期的主力合约的持仓净部位会清零,并在新的主力合约上建立头寸。但是由于WonderTrader是净部位管理的,所以如果多空的持仓相等,净部位也为0,在老版本中,WonderTrader就不会自动清理剩余的头寸,需要人工介入处理。而v0.7.0对这块进行了优化,在净部位为0以后,还会继续判断是否有单边持仓,如果有单边持仓,则继续平仓,直到多空都没有持仓,真实部位也为0为止。
  • 股票实现了对后复权数据的支持。WonderTrader在开源之初就已经支持了股票前复权数据的处理了,但是当时老版本做得比较简单,在回测的时候也没有对复权处理进行还原处理,所以回测的成交价格都是复权价格。v0.7.0不仅新增了对后复权数据的支持,同时在回测的时候对复权数据也进行了还原处理,这样回测的绩效,除了没有考虑分红派息等带来的收益,已经和真实成交相差无几了。
  • 对数据获取接口进行了性能优化。WtBtPorterWtPorter是整个底层和应用层交互的粘合模块,全部C接口进行交互。在应用层获取历史行情数据的时候,老版本采用逐条数据回调的方式向应用层传递数据,而新版本则采用数据块回调的方式传递数据。这里最大的好处就是,减少了回调函数调用的次数,从而可以提升数据读取的效率10%左右
//老版本获取K线的接口
WtUInt32 cta_get_bars(CtxHandler cHandle, const char* stdCode, const char* period, WtUInt32 barCnt, bool isMain, FuncGetBarsCallback cb)
{
    CtaMocker* ctx = getRunner().cta_mocker();
    if (ctx == NULL)
        return 0;
    try
    {
        WTSKlineSlice* kData = ctx->stra_get_bars(stdCode, period, barCnt, isMain);
        if (kData)
        {
            WtUInt32 left = barCnt + 1;
            WtUInt32 reaCnt = 0;
            WtUInt32 kcnt = kData->size();
            for (uint32_t idx = 0; idx < kcnt && left > 0; idx++, left--)
            {
                WTSBarStruct* curBar = kData->at(idx);
                //每一根bar都要进行回调传回给应用层
                bool isLast = (idx == kcnt - 1) || (left == 1);
                cb(cHandle, stdCode, period, curBar, isLast);
                reaCnt += 1;
            }

            kData->release();
            return reaCnt;
        }
        else
        {
            return 0;
        }
    }
    catch(...)
    {
        return 0;
    }
}
//v0.7.0版本获取K线的接口
WtUInt32 cta_get_bars(CtxHandler cHandle, const char* stdCode, const char* period, WtUInt32 barCnt, bool isMain, FuncGetBarsCallback cb)
{
    CtaMocker* ctx = getRunner().cta_mocker();
    if (ctx == NULL)
        return 0;
    try
    {
        WTSKlineSlice* kData = ctx->stra_get_bars(stdCode, period, barCnt, isMain);
        if (kData)
        {
            uint32_t left = barCnt;
            uint32_t reaCnt = min(barCnt, (uint32_t)kData->size());
            //将历史数据块和实时数据块分两次回调传回给应用层
            if (kData->get_his_count() > 0)
            {
                uint32_t thisCnt = min(left, (uint32_t)kData->get_his_count());
                left -= thisCnt;
                reaCnt += thisCnt;
                cb(cHandle, stdCode, period, kData->get_his_addr(), thisCnt, left == 0);
            }

            if (left > 0 && kData->get_rt_count() > 0)
            {
                uint32_t thisCnt = min(left, (uint32_t)kData->get_rt_count());
                left -= thisCnt;
                reaCnt += thisCnt;
                cb(cHandle, stdCode, period, kData->get_rt_addr(), thisCnt, true);
            }

            kData->release();
            return reaCnt;
        }
        else
        {
            return 0;
        }
    }
    catch(...)
    {
        return 0;
    }
}

wtpy新特性

  wtpy除了是对WonderTraderpython封装,也提供了很多简单易用的实用工具。wtpy除了对底层模块的适配之外,最重要的功能就要算监控服务组件WtMonSvr以及绩效分析组件WtBtAnalyst。这两个组件都不依赖于底层,是纯应用层的功能组件。v0.7.0版本中,监控服务组件WtMonSvr也做了较大的调整。

底层对接优化

  前面提到WonderTrader底层的Porter接口,把数据回调从单条回调改成数据块回调,所以wtpy中的wrapper也要做相应的优化。wrapper中利用ctypesaddressoffromaddress两个方法,直接解析底层传回的内存地址,从而提高转换效率,提升了性能。
  同时,去掉了全局变量global修饰符,全部采用局部变量,提升python内的执行效率。

def on_stra_get_bar(self, id:int, stdCode:str, period:str, curBar:POINTER(WTSBarStruct), count:int, isLast:bool):
    '''
    获取K线回调,该回调函数因为是python主动发起的,需要同步执行
    @id     策略id
    @stdCode   合约代码
    @period K线周期
    @curBar 最新一条K线
    @isLast 是否是最后一条
    '''
    engine = self._engine
    ctx = engine.get_context(id)
    period = bytes.decode(period)

    bsSize = sizeof(WTSBarStruct)
    addr = addressof(curBar.contents) # 获取内存地址
    bars = [None]*count # 预先分配list的长度
    for i in range(count):
        realBar = WTSBarStruct.from_address(addr)   # 从内存中直接解析成WTSBarStruct
        bar = dict()
        if period[0] == 'd':
            bar["time"] = realBar.date
        else:
            bar["time"] = 1990*100000000 + realBar.time
        bar["bartime"] = bar["time"]
        bar["open"] = realBar.open
        bar["high"] = realBar.high
        bar["low"] = realBar.low
        bar["close"] = realBar.close
        bar["volume"] = realBar.vol
        bars[i] = bar
        addr += bsSize

    if ctx is not None:
        ctx.on_getbars(bytes.decode(stdCode), period, bars, isLast)
    return

底层消息队列模块应用

  C++底层封装了一个基于nanomsgpub/sub消息队列的实现模块WtMsgQue,这个模块采用C接口封装,可以独立使用。所以wtpy也相应的实现了一个python版本的WtMsgQue模块,直接调用底层的接口。

class WtMQServer:

    def __init__(self):
        self.id = None

    def init(self, wrapper:WtMQWrapper, id:int):
        self.id = id
        self.wrapper = wrapper

    def publish_message(self, topic:str, message:str):
        if self.id is None:
            raise Exception("MQServer not initialzied")

        self.wrapper.publish_message(self.id, topic, message)

class WtMQClient:

    def __init__(self):
        return

    def init(self, wrapper:WtMQWrapper, id:int):
        self.id = id
        self.wrapper = wrapper

    def start(self):
        if self.id is None:
            raise Exception("MQClient not initialzied")

        self.wrapper.start_client(self.id)

    def subscribe(self, topic:str):
        if self.id is None:
            raise Exception("MQClient not initialzied")
        self.wrapper.subcribe_topic(self.id, topic)

    def on_mq_message(self, topic:str, message:str, dataLen:int):
        pass

@singleton
class WtMsgQue:

    def __init__(self) -> None:
        self._servers = dict()
        self._clients = dict()
        self._wrapper = WtMQWrapper(self)

        self._cb_msg = CB_ON_MSG(self.on_mq_message)

    def get_client(self, client_id:int) -> WtMQClient:
        if client_id not in self._clients:
            return None
        
        return self._clients[client_id]

    def on_mq_message(self, client_id:int, topic:str, message:str, dataLen:int):
        client = self.get_client(client_id)
        if client is None:
            return

        client.on_mq_message(topic, message, dataLen)

    def add_mq_server(self, url:str, server:WtMQServer = None) -> WtMQServer:
        id = self._wrapper.create_server(url)
        if server is None:
            server = WtMQServer()

        server.init(self._wrapper, id)
        self._servers[id] = server
        return server

    def destroy_mq_server(self, server:WtMQServer):
        id = server.id
        if id not in self._servers:
            return
        
        self._wrapper.destroy_server(id)
        self._servers.pop(id)

    def add_mq_client(self, url:str, client:WtMQClient = None) -> WtMQClient:
        id = self._wrapper.create_client(url, self._cb_msg)
        if client is None:
            client = WtMQClient()
        client.init(self._wrapper, id)
        self._clients[id] = client
        return client

    def destroy_mq_client(self, client:WtMQClient):
        id = client.id
        if id not in self._clients:
            return
        
        self._wrapper.destroy_client(id)
        self._clients.pop(id)
        

监控服务组件完善

  v0.7.0版本中,监控服务组件WtMonSvr,主要完善了以下几类接口:

  • 用户管理接口,如修改密码、权限控制、角色分配等
  • 组合管理接口,如查看信号过滤规则、设置信号过滤规则等
  • 配置管理接口,如查看配置文件、修改配置文件等

任务调度组件优化

  这个主要是任务调度组件WatchDog的优化。
  老版本在独立线程中启动subprocess,并实时监控子进程是否停止,同时重定向子进程的控制台,并将控制台输出的日志推送到监控页面。这种方式的缺点在于,监控服务不能重启,因为一旦重启,就无法重新挂载已经启动的进程了。而在生产环境下,所有交易的进程都是不能随便重启的。但是如果不能重新挂载老的进程,则意味着要监控的策略组合的任何推送信息也无法收到了。
  针对上述缺点,v0.7.0彻底抛弃了原有的进程调度方式。还是采用subprocess启动进程,但是利用psutil库去检查进程的状态。这样如果遇到监控服务重启,只需要利用psutil获取现有进程列表,对比命令行,就能够实现重新挂载的目的。同时基于WtMsgQue实现了一个EventReceiver,用于接收子进程通过WtMsgQue组件发布的推送消息,并推送到监控页面。

回测管理组件实现

  回测管理组件WtBtMon也是v0.7.0版本中非常重要的一个组件。主要提供了以下服务:

  • 在线策略管理。针对不同的用户建立不同的管理目录,用户可以通过wtconsole发布的webui进行远程的策略管理操作,如添加、删除、修改等。
  • 在线策略回测。主要提供回测任务的调度和管理,本质上也是一个任务调度组件的实现,不过针对的是回测任务。
  • 回测结果管理。主要包括回测生成的各种交易数据、绩效分析数据、以及历史K线数据。其中历史K线数据,则是基于WtDtServo数据伺服组件进行读取,然后通过WtMonSvr的接口向webui提供服务。

主力合约工具

  为了方便用户使用,v0.7.0也分享一个实用的工具,即主力合约工具WtHotPicker。该工具可以从交易所拉取历史行情数据,也可以从WonderTrader收盘作业生成的快照文件读取历史行情数据,从而根据读取总持最大的合约作为主力合约,总持次大的合约作为次主力合约,进而生成一个主力合约切换规则hots.json以及次主力合约切换规则seconds.json
  如果配置了通知邮件,那么主力合约工具还会通过邮件通知收件人每天的主力合约和次主力合约切换的情况。
主力合约切换通知

{
    "CFFEX": {
        "IC": [
            {
                "date": 20210715,
                "from": "IC2107",
                "newclose": 6862.4,
                "oldclose": 6909.2,
                "to": "IC2108"
            },
            {
                "date": 20210819,
                "from": "IC2108",
                "newclose": 6874.4,
                "oldclose": 6916.6,
                "to": "IC2109"
            }
        ],
        "IF": [
            {
                "date": 20210715,
                "from": "IF2107",
                "newclose": 5052.4,
                "oldclose": 5075.0,
                "to": "IF2108"
            },
            {
                "date": 20210819,
                "from": "IF2108",
                "newclose": 4850.8,
                "oldclose": 4879.0,
                "to": "IF2109"
            }
        ],
        "IH": [
            {
                "date": 20210715,
                "from": "IH2107",
                "newclose": 3328.6,
                "oldclose": 3337.4,
                "to": "IH2108"
            },
            {
                "date": 20210819,
                "from": "IH2108",
                "newclose": 3173.4,
                "oldclose": 3184.4,
                "to": "IH2109"
            }
        ]
    }
}

wtconsole新特性

  wtconsole刚刚从wtpy剥离出来作为一个单独的仓库。由于是webui,所以交互的细节较多,本文就不一一列举了,主要展示一下本次更新的新功能的界面。

组合管理模块

组合管理之组合持仓
组合管理之组合风控
组合管理之组合绩效

组合配置管理

配置管理

查看通知列表

通知列表

在线回测

策略回测管理
回测绩效概览
回测信号分析
回测成交明细
回测信号明细
回测回合明细
回测每日绩效

结束语

  本文对WonderTrader此次发布的v0.7.0版本的新特性介绍就到此结束了。实际上新版本的细节改动还有很多,本文就不再一一罗列了。尤其一旦涉及到webui交互,细节就更多了。更多功能,就请各位感兴趣的读者在使用中去发现了。希望读者通过本文能够对这次的大版本有一个比较全面的认识,这样以后在运用的时候就能够更快的上手了。
  v0.7.0版本发布以后,暂时就以小功能迭代为主了。笔者很高兴有一位大神要基于wtpy搭建强化学习框架,下一个阶段准备先适配起来,协助大神尽快弄好。其次就是完善在线回测部分,这样笔者才能够借此去做做推广,毕竟有了可以用的交互界面,就不用空谈了,可以结合图表做一些分析了。其三就是准备适配更多的品种,包括期权、基金、数币等。最后还想再完善一下文档,毕竟文档已经很久没更新过了。
  WonderTrader旨在给各位量化从业人员提供更好的轮子,将技术相关的东西都封装在平台中,力求给策略研发带来更好的策略开发体验。笔者的水平有限,也希望有更多人关注到这个项目,参与到这个项目中来,这样才能让WonderTrader更加完善,更加高效。

最后再安利一下WonderTrader

WonderTradergithub地址:https://github.com/wondertrad...

WonderTrader官方文档地址:https://wondertrader.github.io

wtpygithub地址:https://github.com/wondertrad...

wtconsolegithub地址:https://github.com/wondertrad...


市场有风险,投资需谨慎。以上陈述仅作为对于历史事件的回顾,不代表对未来的观点,同时不作为任何投资建议。

阅读 272

WonderTrader专栏
主要发布WonderTrader相关的各种官方资讯
19 声望
8 粉丝
0 条评论
你知道吗?

19 声望
8 粉丝
宣传栏