vn.py官网
开源量化社区

置顶主题

centos7.6 搭建vnpy量化交易环境

作为Python开发的开源项目,vn.py本身具有非常好的跨平台通用性,毕竟Python几乎可以在所有主流操作系统上运行。但对于Linux系统,官方团队只提供了对Ubuntu 18.04版本的支持(主要就是安装脚本)。

本人一直用的是CentOS的服务器,折腾了几天,终于在上面把vnpy跑起来了。没有记录折腾的细节,只是记录了下正常的操作步骤,欢迎大家一起交流。
以下内容全部基于CentOS 7.6版本,首先准备好一个全新安装的系统,然后跟着一步步操作即可。

安装python环境

wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
bash Miniconda3-latest-Linux-x86_64.sh
按照提示信息进行安装,当出现以下信息时,选择no,不然后面vncserver无法正常启动。还不清楚具体原因。
by running conda init? [yes|no]
[no] >>> no

安装Mate桌面

yum groups install "X Window System" -y
yum install epel-release -y
yum groups install "MATE Desktop" -y
systemctl set-default graphical.target

安装VNC Server

yum install tigervnc-server -y

# 替换User为root,增加显示分辨率参数设置
sed -r -i "s/^(ExecStart.*)<USER>(.*%i)/\1root\2 -geometry 1920x1200 -depth 16/" /lib/systemd/system/vncserver@.service
sed -r -i "s/^(PIDFile.*)home\/<USER>(.*pid)/\1root\2/" /lib/systemd/system/vncserver@.service

mv /lib/systemd/system/vncserver@.service /lib/systemd/system/vncserver@:1.service
systemctl daemon-reload
vncpasswd
systemctl start vncserver@:1.service
systemctl enable vncserver@:1.service

# 屏蔽默认桌面,启动mate桌面
sed -r -i "s%^/etc/X11/xinit/xinitrc$%# &%" /root/.vnc/xstartup
echo "/usr/bin/mate-session &" >> /root/.vnc/xstartup

# 其它操作
# 禁用selinux
sed -r -i "s/^(SELINUX=).*/\1disabled/" /etc/selinux/config
# 关闭防火墙
systemctl stop firewalld.service
systemctl disable firewalld.service

reboot

# 如果是云服务器,需要确保开放了TCP 5901端口

安装VS Code

rpm --import https://packages.microsoft.com/keys/microsoft.asc
sh -c 'echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com/yumrepos/vscode\nenabled=1\ngpgcheck=1\ngpgkey=https://packages.microsoft.com/keys/microsoft.asc" > /etc/yum.repos.d/vscode.repo'
yum check-update
yum install code -y

升级GCC版本

注意GCC必须要使用9.1.0以上的版本,否则在编译vnpy的时候,会报-std=c++17相关的错误。
GCC的编译时间很长,估计得几个小时。

yum install gcc gcc-c++ bzip2 m4  gmp-devel.x86_64  -y

wget https://mirrors.ustc.edu.cn/gnu/gcc/gcc-9.1.0/gcc-9.1.0.tar.gz
tar xvf gcc-9.1.0.tar.gz
cd gcc-9.1.0/
./contrib/download_prerequisites

cd gmp;mkdir temp;cd temp
../configure --prefix=/usr/local/gmp-6.1.0
make && make install

cd ../../mpfr;mkdir temp;cd temp
../configure --prefix=/usr/local/mpfr-3.1.4 --with-gmp=/usr/local/gmp-6.1.0
make && make install

cd ../../mpc;mkdir temp;cd temp
../configure --prefix=/usr/local/mpc-1.0.3 --with-gmp=/usr/local/gmp-6.1.0 --with-mpfr=/usr/local/mpfr-3.1.4
make && make install

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/mpc-1.0.3/lib:/usr/local/gmp-6.1.0/lib:/usr/local/mpfr-3.1.4/lib

cd ../..;mkdir temp;cd temp
../configure --disable-multilib --enable-languages=c,c++ --with-gmp=/usr/local/gmp-6.1.0 --with-mpfr=/usr/local/mpfr-3.1.4 --with-mpc=/usr/local/mpc-1.0.3
make -j4 && make install

我的服务器是4核的,就在make 后面加了-j4,大家可根据自己的情况调整,缩短编译时间。

安装vnpy

最后终于可以安装vn.py了,在此过程中会自动编译Linux上支持的交易接口,如CTP/OES等。

# 切换到python环境
. ~/miniconda3/bin/activate
yum install postgresql-devel* libxkbcommon-x11 -y
下载vnpy的最新源码包并解压。
cd vnpy
bash install.sh


在Mac上安装vnpy,保证一次成功!

一、提前下载需要的安装包:
1、Miniconda3
https://docs.conda.io/en/latest/miniconda.html#
选择MacOSX installers里的最新版本,这里是Python 3.9下载。

2、pycharm
pycharm-community-2020.3.3.dmg
从官网上下载社区版https://www.jetbrains.com/pycharm/

3、vnpy安装包(解压后,复制文件夹到自己喜欢的位置)
从vnpy在gitee的官方地址下载最新的安装包,采用zip格式下载。
https://gitee.com/vnpy/vnpy

二、安装
1、安装Miniconda,这里是Miniconda3-latest-MacOSX-x86_64.pkg
2、添加国内源:
添加国内源:在当前用户下,编辑.condarc,内容如下:
channels:

3、创建虚拟环境
conda create -n py37_vnpy python=3.7
conda activate py37_vnpy
(退出:conda deactivate)

4、安装python.app
conda install -c conda-forge python.app
可能会因为网络问题不成功,多试几次。

5、安装pycharm-community-2020.3.3.dmg
从官网上下载社区版https://www.jetbrains.com/pycharm/

7、打开vnpy所在的文件夹,进行配置
点击‘PyCharm’菜单->Preferences菜单->Project:vnpy一级菜单->Python Interpreter二级菜单->点击右上齿轮->Add菜单->Conda Environment->Existing enviroment->Interpreter:/opy/miniconda3/envs/py37_vnpy/bin/pythonw(选择前面新建的虚拟环境的pythonw)->点OK->点OK->点OK

8、(确认在PyCharm里已经打开了vnpy项目),在PyCharm的底部,找到Terminal的标签,点击,进入py37_vnpy环境的终端,并且当前路径位于vnpy项目的文件夹。
执行以下的安装语句(requirements.txt是vnpy项目文件夹下面的一个文件),这个安装时间比较长,需要较好的网络。

pip install -r requirements.txt -i http://pypi.douban.com/simple --trusted-host pypi.douban.com

9、创建run.py文件,复制以下代码,来源 README.md

因为mac上不支持ctp接口,所以要注释掉ctp接口,否则运行会报错。

from vnpy.event import EventEngine
from vnpy.trader.engine import MainEngine
from vnpy.trader.ui import MainWindow, create_qapp

# from vnpy.gateway.ctp import CtpGateway

from vnpy.app.cta_strategy import CtaStrategyApp
from vnpy.app.cta_backtester import CtaBacktesterApp

def main():
"""Start VN Trader"""
qapp = create_qapp()

event_engine = EventEngine()
main_engine = MainEngine(event_engine)

# main_engine.add_gateway(CtpGateway)
main_engine.add_app(CtaStrategyApp)
main_engine.add_app(CtaBacktesterApp)

main_window = MainWindow(main_engine, event_engine)
main_window.showMaximized()

qapp.exec()


if name == "main":
main()

10、运行 python run.py,注意环境名称是 py37_vnpy



vn.py发布v2.6.0 - 高性能金融数据库

发布于vn.py社区公众号【vnpy-community】
 
原文作者:用Python的交易员 | 发布时间:2021-09-26
 

昨天发布了vn.py的2.6.0版本,本次更新的内容主要是新增了一系列专门针对金融时序数据的高性能数据库支持,包括:DolphinDB、Arctic和LevelDB,大幅提高各类量化策略回测研究的效率。

请注意,2.6.0新版本中包含了对数据库(database)和数据服务(datafeed)全局配置字段的修改,老版本用户升级后需要重新手动配置,具体请参考后面对应的章节内容。

和之前一样,对于使用VN Studio的用户,启动VN Station后,直接点击界面右下角的【更新】按钮就能完成自动更新升级,对于没有安装的用户,请下载VN Studio-2.6.0,体验一键安装的量化交易Python发行版,下载链接:

https://download.vnpy.com/vnstudio-2.6.0.exe

 

高性能数据库支持

 

全局配置变化

在2.6.0新版本中,参考vn.py对于交易接口的标准化设计BaseGateway(位于vnpy.trader.gateway中),添加了对于数据库适配器的标准化接口BaseDatabase(位于vnpy.trader.database中),实现简洁易用的插件化数据库支持。

新版本的VN Trader全局配置中,和数据库相关的字段全部以database作为前缀,如下图所示(点击主界面顶部菜单栏的【配置】按钮打开):

description

具体字段含义如下:

  • database.timezone:数据库时区,考虑到大部分用户都在国内,一般无需修改;
  • database.name:数据库适配器接口的名称,通常采用数据库全称的小写英文字母;
  • database.database:数据库中用于保存vn.py相关数据的实例,例如SQLite的文件名、MySQL的Schema等;
  • database.host:数据库服务器程序所在的IP地址,如果安装于本地电脑则直接使用localhost;
  • database.port:数据库服务器程序监听的IP端口,不同数据库的默认端口不同;
  • database.user:数据库的登录用户名;
  • database.password:数据库的登录密码。

2.6.0版本对原有的数据库适配器(vnpy.database)进行了剥离和代码优化,放置到了对应的独立仓库中,允许用户按需安装,这下用Linux系统的同学们再也不用为psycopg2(PostgreSQL数据库驱动)的安装折腾了。

具体的数据库配置请参考下文。

 

DolphinDB

description

项目地址:vnpy_dolphindb

DolphinDB是由浙江智臾科技有限公司研发的一款高性能分布式时序数据库,特别适用于对速度要求极高的低延时或实时性任务,在国内外金融投资领域有着丰富的应用案例。

作为国产数据库的DolphinDB,在定位上对标的是世界顶级的金融时序数据库Kdb+。目前2.6.0版本支持的所有数据库中,DolphinDB的读写速度均占据No.1的位置。由于目前我们只采用了最简单的分区数据表设计,理论上还有进一步优化的空间。

description

尽管DolphinDB是商业软件,但是也提供了免费的社区版,在安装时注意要选择2.0的Beta版本,我们会在下一篇公众号文章中详细介绍DolphinDB的特点和使用方法

 

Arctic(MongoDB)

description

项目地址:vnpy_arctic

由英国量化对冲基金Man AHL基于MongoDB开发的高性能金融时序数据库,我们团队对于Arctic的了解来源于公众号【量化投资与机器学习】的这篇文章《盘点对冲基金大佬『开源』的Python工具包!》。

简单总结一下Arctic的优势:

  • 支持直接存储pandas的DataFrame和numpy的ndarray对象,在量化投研中可太实用了;
  • 允许对数据进行版本化管理(类似于数据库中的git),便于因子挖掘过程中的数据迭代管理;
  • 基于分块化存储和LZ4压缩,在网络和磁盘IO方面节省大量资源,实现每秒最高每秒百万行的数据查询。

description

Arctic底层使用的是MongoDB数据库服务器,因此安装时直接前往MongoDB官网下载最新版本安装即可。目前不支持自定义端口、用户名、密码等修改,因此请保持默认配置。

 

LevelDB

description

项目地址:vnpy_leveldb

由Google推出的高性能Key/Value数据库,基于LSM算法实现进程内存储引擎,支持数十亿级别的海量数据。LevelDB的定位是通用性数据存储方案,对于金融领域的时序数据存储没有特别大的优势,但也比一堆SQL类数据库快多了。

关于LevelDB的作者Google传奇工程师Jeff Dean,在知乎上有一个非常有趣的讨论,感兴趣的同学可以看看:有谁可以介绍一下谷歌大牛Jeff Dean以及与他相关的事迹么?

description

作为单机数据库的LevelDB,和SQLite类似只需要配置一个数据存储的路径(文件夹)即可,适合作为SQLite的轻量级高性能替代方案。

 

原有数据库

之前版本中原有的数据库适配器也都进行了剥离优化:

  • SQL类

  • NoSQL类

    • MongoDB:剥离到vnpy_mongodb,改用pymongo替代mongoengine重构实现,大幅提高了性能;
    • InfluxDB:剥离到vnpy_influxdb,改用influxdb-client支持2.0版本的InfluxDB。

 

灵活数据服务支持

 

全局配置变化

和数据库适配器类似,对于数据服务也新增了标准化接口BaseDatafeed(位于vnpy.trader.datafeed中),实现了更加灵活的数据服务支持。在全局配置中,和数据服务相关的字段全部以datafeed作为前缀。

具体字段含义如下:

  • datafeed.name:数据服务接口的名称,同样是全称的小写英文字母;
  • datafeed.username:数据服务的用户名;
  • datafeed.password:数据服务的密码。

以上字段对于所有数据服务都是必填,如果是token方式授权请填写在database.password字段中,具体每个数据服务的细节如下。

 

RQData

米筐RQData一直以来是我们vn.py官方团队长期推荐的数据服务,对于大部分个人投资者应该是性价比最高的选择:

  • 项目地址:vnpy_rqdata
  • 数据分类:股票、期货、期权、基金、黄金TD
  • 数据周期:日线、小时线、分钟线、TICK(实时更新)
  • 注册申请:RICEQUANT

 

UData

恒有数UData是由恒生电子最新推出的云端数据服务,提供不限次、不限量的多种金融数据获取:

  • 项目地址:vnpy_udata
  • 数据分类:股票、期货
  • 数据周期:分钟线(盘后更新)
  • 注册申请:恒有数UData

 

TuShare

最有名的开源Python金融数据接口项目,由大神Jimmy团队长期开发维护,除了行情数据外还提供许多另类数据:

 

TQSDK

天勤TQSDK是由信易科技推出的Python程序化交易解决方案,提供当前所有可交易合约上市以来的全部历史数据获取:

 

其他更新

 

价差交易模块优化

2.6.0版本中对SpreadTrading模块(vnpy_spreadtrading)进行了大幅优化,包括:

  1. 移除对反向合约、交易开平选项的支持,减少价差交易算法中的整体逻辑长度;
  2. 移除对老版本中线性价差的支持,统一改为使用灵活价差,提供更加灵活的价差构建方案;
  3. 当用户手动或者由策略停止价差交易算法时,必须等到全部委托结束且各条腿平衡后,算法才会结束运行,避免出现价差瘸腿;
  4. 价差的持仓数据不再使用账户底层各个合约的持仓计算,而是改为基于价差算法的成交结果进行维护,方便在多个价差中使用同一条合约腿。

以上功能优化涉及到对价差交易配置文件的修改,如果在启动VN Trader时出现和SpreadTrading相关的报错,请进入程序运行缓存目录(C:\Users\Administrator.vntrader),删除下述文件后重启即可:

  • spread_trading_setting.json
  • spread_trading_advanced.json
  • spread_trading_strategy.json

交易应用模块剥离

  1. OptionMaster期权波动率交易模块,剥离到vnpy_optionmaster项目中;
  2. ChartWizard图表分析模块,剥离到vnpy_chartwizard项目中;
  3. DataRecorder实盘数据录制模块,剥离到vnpy_datarecorder项目中。

 

CHANGELOG

 

新增

  1. 增加双边报价业务的发送和撤销函数功能
  2. 增加双边报价监控UI组件
  3. 增加用于对接数据库的抽象接口vnpy.trader.database
  4. 新增基于Arctic的MongoDB数据库接口项目vnpy_arctic
  5. 新增LevelDB数据库接口项目vnpy_leveldb
  6. 新增DolphinDB数据库接口项目vnpy_dolphindb
  7. 增加用于对接数据服务的抽象接口vnpy.trader.datafeed
  8. 新增TuShare数据服务项目vnpy_tushare
  9. 新增恒生UData数据服务项目vnpy_udata
  10. 新增天勤TQSDK数据服务项目vnpy_tqsdk
  11. 新增CoinAPI数据服务项目vnpy_coinapi

调整

  1. 移除批量委托和批量撤单相关的函数功能
  2. 移除老虎证券交易接口TigerGateway
  3. 移除鑫管家交易接口XgjGateway
  4. 移除AlgoTrading算法交易模块对于金纳算法服务的支持
  5. RestClient增加对操作系统代理配置的支持
  6. RestClient和WebsocketClient的默认异常处理逻辑由抛出异常修改为打印输出
  7. 价差交易模块移除对反向合约、线性价差、开平字段的支持
  8. 价差交易模块优化对灵活价差的支持,优化价差行情推送过滤判断
  9. 价差交易算法停止时,等待全部委托结束、各条腿平衡后,再结束算法

修复

  1. 修复在Linux/Mac系统上,运行多进程优化时的进程启动错误
  2. 修复WebsocketClient由于心跳机制不完善,导致的频繁断线问题

剥离

  1. 将米筐数据接口剥离到vnpy_rqdata项目中,并升级到2.9.38版本
  2. 将行情录制模块剥离到vnpy_datarecorder项目中
  3. 将K线图表模块剥离到vnpy_chartwizard项目中
  4. 将SQLite数据库接口剥离到vnpy_sqlite项目中
  5. 将MySQL数据库接口剥离到vnpy_mysql项目中
  6. 将PostgreSQL数据库接口剥离到vnpy_postgresql项目中
  7. 将MongoDB数据库接口剥离到vnpy_mongodb项目中
  8. 将InfluxDB数据库接口剥离到vnpy_influxdb项目中
  9. 将期权波动率交易模块剥离到vnpy_optionmaster项目中


CTP的综合业务流程——费率设置

【注:以下的内容来自SFIT的官方文档《综合交易平台结算平台业务操作手册》,上传以下的几个帖子的目的是:通过对《综合交易平台结算平台业务操作手册》中对费率的设置的研究,让大家明白手续费率和保证金率的概念和计算方法,进而正确地计算出交易中发生的手续费和保证金,以及为了计算交易手续费和保证金所需要的参数有哪些。】

3. 费率的设置

3.1 手续费率设置

手续费设置本平台分为交易所手续费率设置和投资者手续费率设置,主要是对期货合约的手续费率按照交易所规定和公司要求进行设置,分为开仓、平仓、平今、结算、交割、移仓等6种手续费率,其中结算、移仓手续费率并未启用,不需要设置。每种手续费率,以按金额、按手数等2种方式取和收取手续费。记按金额手续费率为R金额、按手数手续费率为R手数,手续费C手续费的计算公式如下:

C手续费=R金额×成交金额+R手数×成交手数

交易所手续费率设置:

对于新合约的上市,需要设置交易所手续费率,在flex柜台中进行新增、修改、删除、查询操作,交易所手续费设置,主要用于设置交易所手续费率,以计算每笔成交的上交手续费,分为按产品和合约设置,同一产品和该产品的合约,合约优先级大于产品。在结算柜台中的“手续费率”目录下,“交易所手续费率设置”“交易所手续费率查询”菜单可以进行交易所手续费率的新增、修改和查询操作。手续费率设置后重新结算,就按新设置的手续费率计算手续费,盘中设置手续费率是否实时上场。

Flex柜台界面操作:

“费率设置->手续费率->交易所手续费率设置->新增即可”
查询:选择交易所、产品/合约,若均为空的话,则是对所有产品和合约进行查询;
新增:选择交易所;选择产品/合约,合约的优先级高于产品;对开仓、平仓、平今、结算、交割四个手续费率进行添加,结算、移仓手续费率并未启用,不需要设置,按金额、按手数等2种方式取和收取手续费;
修改:选中需要修改的记录,点击“修改”按钮,进入“交易所手续费率(修改)”界面,可以修改费率,不能修改交易所和产品/合约;
删除:选中需要删除的记录,即可进行删除操作。

投资者手续费率的设置

投资者手续费设置,主要用于设置投资者手续费率,以计算每笔成交的投资者手续费。在结算柜台中的“手续费率”目录下,“投资者手续费率设置”菜单可以进行投资者手续费率的新增、修改和查询操作。投资者手续费率分为公司标准、模板、单一投资者,按照所述对象范围的粒度大小,这3种情况对应的保证金率设置存在如下的优先级关系:

    单一投资者>模板>公司标准

系统按照投资者范围优先级的先后关系,三种情况设置分为按品种和按合约。对某一个投资者手续费率生效的优先级别是单一投资者>手续费率模板>公司标准,同一产品和该产品的合约,合约优先级别大于产品,系统依次去寻找投资者的手续费率设置,直到找到相应设置为止。
手续费率设置后重新结算,就按新设置的手续费率计算手续费,盘中设置手续费率是实时上场。手续费率实时同步的设置在flex柜台界面的操作流程:
“费率设置->手续费率盘中同步参数设置->选择“是”->点击确认”。
针对投资者手续费率设置中,分为三种情况,一、设置所有投资者手续费率,二、设置手续费率模板手续费率,三、 设置单一投资者手续费率,单一投资者设置与公司标准设置相对比较简单,下面主要介绍一新开户投资者模板手续费率设置步骤。

对于新开户投资者手续费率设置步骤(模板):

  1. 设置投资者手续费率之前,要进行一些参数设置;
    ➢ 投资者手续费率设置是否启用复核流程,若启动复核流程,设置投资者手续费率时候要进行复核操作,复核通过之后手续费率设置才生效;
    Flex柜台界面操作:
    “流程管理->复核流程管理->流程ID选中投资者手续费率设置->查询”,
    “选中记录->修改(流程ID不允许修改) ->保存”
    是否启用:“是/否”
    最高复核级别:“零级复核”即不需要符合;“一级复核”即需要一次复核;“二级复核”需要复核两次;
    是否允许自复核:“是/否”,即选择一级、二级复核是可否由修改手续费的操作员自己进行复核。
    ➢ 投资者手续费率模板对应关系设置是否启动复核流程,若启动复核流程,设置投资者手续费率模板对应关系时候要进行复核操作,复核通过之后投资者手续费率模板对应关系设置才生效;
    Flex柜台界面操作:
    “流程管理->复核流程管理->流程ID选中投资者手续费率模板关系设置->查询”,选中记录->修改(流程ID不允许修改) ->保存
    是否启用:“是/否”
    最高复核级别:“零级复核”即不需要符合;“一级复核”即需要一次复核;“二级复核”需要复核两次;
    是否允许自复核:“是/否”,即选择- -级、二级复核是可否由修改手续费的操作员自己进行复核。
    ➢ 手续费率模板数据权限是否启用,若启用,则操作员只能操作和自己有关联关系的手续费率模板;若不启用,操作员可以操作所有手续费率模板;
    Flex柜台界面操作:
    “交易管理->基本参数设置->结算参数->手续费模板数据权限->启动”
  2. 创建投 资者手续费率模板;
    在实际业务操作中,为每一个投资者都单独设置一套 手续费率是不明智的。为了便于管理和操作的简洁性,手续费率设置中可以采用手续费率模板形式为投资者设置手续费率,以简化操作,故需要先创建一个模板。
    Flex柜台界面操作:
    “费率设置->手续费率->手续费率模板设置->新增(填写手续费模板代码、手续费模板名称及备注) ->点击确认
    查询:输入代码可以精确查询,“空”则为查询全部,同时在左下角可以查询模板手续费率及模板对应投资者;
    新增:添加手续费率模板;
    修改:选中所需修改手续费率模板,点击“修改”按钮,只能修改手续费率模板名称及备注;
    删除:选中所需修改手续费率模板,点击“删除”按钮。
  3. 建立操作 员和手续费率模板对应关系;
    模板创建完成之后,需要将某一操作员与此模板建立起对应关系,即此模板只有该操作员有权限。
    Flex柜台界面操作:
    “费率设置->手续费率->操作员对应手续费率模板权限管理->点击新增(填写之前创建的手续费率模板、操作员代码) ->点击确认”
    查询:选择手续费率模板或操作员代码,点击“查询”,若为“空”则查询所有模板;
    新增:添加手续费率模板与对应操作员之间的关系;
    删除:选中记录,点击“删除”按钮即可。
  4. 建立投资者手续 费率模板和投资者对应关系;
    需要将投资者归入一一个手续费率模板,这样投资者的手续费率会跟随该模板进行变动,且一个投资者只可以属于一个手续费率模板。
    Flex柜台界面操作:
    “费率设置->手续费率->投资者手续费率模板对应关系->点击新增(填写之前创建的手续费率模板、投资者代码) ->点击确认’
    查询:可以按照投资者代码、手续费率模板、投资者属性进行查询,若为空,则查询全部;
    新增:添加投资者手续费率模板对应关系;
    修改:只能修改手续费率模板字段,不能修改投资者代码;
    删除:选中记录,点击“删除”按钮即可;
    批量新增:新开户的同一属性投资者可以同时建立投资者手续费率模板对应关系;
    批量修改:同一属性投资者可以实现同步修改手续费率模板;
    批量删除:查询到的投资者与模板对应关系可以实现同时删除。
  5. 设置模板手续费率;
    在flex柜台中进行新增、修改操作,该项内容设置完成后就实现了对同一属性的投资者通过模板进行手续费率的设置。
    Flex界面操作流程:
    “费率设置->手续费率->模板手续费率设置->新增->填写费率模板、产品/合约、选择交易所->添加开仓、平仓、平今、交割手续费率(按金额和按手数) ->点击确认按钮”
    查询:可以按照费率模板、交易所进行查询,若为空,则查询全部;
    新增:添加模板手续费率的设置;
    修改:只能修改相关费率字段,不能修改费率模板、交易所、产品/合约;
    删除:选中记录,点击“删除”按钮即可;
    批量删除:可以对查询到的记录进行全部删除;
    复制:分为投资者复制和交易所复制,投资者复制将某一投资的手续费率设置复制到所创建的模板中:交易所产品复制是将所创建模板中某交易所的产品/合约复制到某交易所的相关产品/合约中。
    当然,以上过程的设置也可以通过“投资者手续费率设置->新增-> 投资者范围选模板->填写费率模板、产品/合约、选择交易所->添加开仓、平仓、平今、交割手续费率(按金额和按手数) ->点击确认按钮”来实现;
    按照模板手续费率设置流程,对于单一投资者和公司标准可以分别通过“投资者手续费率设置”和“单一投资者手续费率设置”来实现,步骤同上。
  6. 设置完成之后可以到“投资者费率查询”里进行确认投资者的手续费率设置是否正确;在“手续费率修改查询”里能查询修改记录。

新功能——手续费率模型:

对投资者手续费率进行批量调整时,可以通过手续费率模型来实现。可以通过创建一个模型A实现对当前投资者手续费率数据进行备份,同时将数据复制到一个新模型B,如果手续费进行批量调整时可以对模型B进行批量修改,激活启用,那么投资者手续费率即按照模型B来进行收取;若过段时间手续费率需要进行恢复以前设置,则激活模型A即可。
手续费率模型应用步骤如下:

  1. 创建投资者手续费率模型;
  2. 设置手续费率模型的相关费率;
  3. 对所创建模型进行激活操作。

在flex中界面操作:
▶“ 费率设置->手续费率模型管理->手续费率模型-> 点击新增->填写手续费率模型代码、名称、选择交易所和产品、备注->从当前数据创建或确认”
注:若点击“确认”,手续费率需要到“模型投资者手续费率”中进行添加设置,添加过程中是与所创建模型是相对应的,包括:交易所得选择、产品/合约的选择;若是“从当前数据创建”,则所创建模型即为当前手续费率数据的设置;注:当交易所为空时,表示该手续费率模型的适用范围为所有交易所所有产品,激活时将覆盖所有手续费率;创建模型时,当交易所不为空,产品为空时,表示该手续费率模型的适用范围为特定交易所所有产品,激活时将覆盖特定交易所的所有产品;当交易所、产品都不为空时,表示该手续费率模型的适用范围为特定交易所特定产品,激活时将覆盖特定交易所特定产品的手续费率。
▶“费率设置-> 手续费率模型管理->模型手续费率->新增->填写模型代码、选择交易所、产品/合约、投资者范围、添加开仓、平仓、平今、交割手续费率(按金额和按手数) ->点击确认按钮”
查询:可以按照模型代码、交易所、产品/合约、投资者范围、组织架构进行查询,若为空,则查询全部;
新增:添加模型手续费率的设置;
修改:只能修改相关费率字段,不能修改费率模板、交易所、产品/合约;
删除:选中记录,点击“删除”按钮即可;
批量修改:可以实现对公司范围、模板、单一投资者进行批量修改,可分为:绝对调整(调整后的值为当前调整)和相对调整(调整后的值为调整值加上原有值),调整完成后,可以进行试算操作,检查是否正确,若正确则点击确认按钮。
批量删除:可以对查询到的记录进行全部删除;
复制:是将一个手续费率模型复制到另一个手续费率模型,可以按照投资者范围、交易所的产品/合约进行复制,则目标的模型手续费率设置将被删除、替换为源的模型手续
费率。
▶在“手续费率模型”界面中,选中所需要模型,点击“激活”即可,这样就实现对一个新建模型的启用。
▲交易所手续费率、投资者手续费率设置都是实时生效的。



分析一下盘中启动CTA策略带来的第一根$K线错误

说明一下,本贴中的bar数据和K线数据其实是一回事,有时候是方便读代码就按照代码说,有时候为了尊崇人们的习惯来说,不必在意。

1 通常CTA策略都是读取和合成K线的

1.1 从一个有代表性的策略DemoStrategy谈起

代码是这样的:

from typing import Any

from vnpy.app.cta_strategy import (
    CtaTemplate,
    BarGenerator,
    ArrayManager
)

from vnpy.trader.object import (
    BarData,
    TickData
)

from vnpy.trader.constant import Interval

class DemoStrategy(CtaTemplate):
    """ 一个演示策略 """
    author = "hxxjava"

    fast_window = 10
    slow_window = 20

    fast_ma0 = 0
    fast_ma1 = 0
    slow_ma0 = 0
    slow_ma1 = 0

    parameters = [
        "fast_window",
        "slow_window"
    ]

    variables = [
        "fast_ma0",
        "fast_ma1",
        "slow_ma0",
        "slow_ma1",
    ]

    def __init__(
        self,
        cta_engine: Any,
        strategy_name: str,
        vt_symbol: str,
        setting: dict 
    ):
        """构造函数"""
        super().__init__(cta_engine,strategy_name,vt_symbol,setting)

        self.bg = BarGenerator(
            on_bar=self.on_bar,
            window=7,
            on_window_bar=on_7min_bar,
            interval=Interval.Minute)

        self.am = ArrayManager()


    def on_init(self):
        """"""
        self.write_log("策略初始化")
        # account_data = self.cta_engine.get_account()
        self.load_bar(10)

    def on_start(self):
        """策略启动"""
        self.write_log("策略启动")

    def on_stop(self):
        """ 策略停止 """
        self.write_log(" 策略停止 ")

    def on_tick(self,tick:TickData):
        """ Tick更新 """
        self.bg.update_tick(tick) 

    def on_bar(self, bar: BarData):
        """K线更新"""
        self.bg.update_bar(bar)

    def on_7min_bar(self, bar: BarData):
        """K线更新"""
        am = self.am
        am.update_bar(bar)
        if not am.inited:
            return

        """ 计算均线 """
        fast_ma = am.sma(self.fast_window,True)
        self.fast_ma0 = fast_ma[-1]
        self.fast_ma1 = fast_ma[-2]

        slow_ma = am.sma(self.slow_window,True)
        self.slow_ma0 = slow_ma[-1]
        self.slow_ma1 = slow_ma[-2]

        """ 定义金叉和死叉 """

        cross_over = (self.fast_ma0>= self.fast_ma1 and
                      self.slow_ma0<self.slow_ma1)  

        cross_below = (self.slow_ma0>self.slow_ma1 and 
                      self.slow_ma0<=self.slow_ma1)

        if cross_over:
            price = bar.close_price + 5

            if not self.pos:
                self.buy(price,1)
            elif self.pos < 0:
                self.cover(price,1)
                self.buy(price,1)
        elif cross_below:
            price = bar.close_price - 5

            if not self.pos:
                self.short(price,1)
            elif self.pos>0:
                self.sell(price,1)
                self.short(price,1)

        # 更新图形界面 
        self.put_event()

这个策略是演示如何利用1分钟K线合成7分钟K线,然后在on_7min_bar()里面利用7分钟K线计算快慢两根移动均线,
然后更加快慢移动均线的金叉和死叉信号来进行多空的开仓和平仓操作,如此实现一个自动策略买卖交易。

1.2 策略工作的过程是这样的:

1.2.1 首先执行构造函数init()

在构造函数init()中创建BarGenerator类型self.bg和管理bar的ArrayManager类型的self.am

1.2.2 然后执行on_init()函数

这里的重点是self.load_bar(10),该函数是策略的父类CtaTemplate的函数,代码是这样的:

    def load_bar(
        self,
        days: int,
        interval: Interval = Interval.MINUTE,
        callback: Callable = None,
        use_database: bool = False
    ):
        """
        Load historical bar data for initializing strategy.
        """
        if not callback:
            callback = self.on_bar

        self.cta_engine.load_bar(
            self.vt_symbol,
            days,
            interval,
            callback,
            use_database
        )

self.cta_engine.load_bar()位于vnpy\app\cta_strategy.py中的CtaEngine类中,代码是这样的:

    def load_bar(
        self,
        vt_symbol: str,
        days: int,
        interval: Interval,
        callback: Callable[[BarData], None],load_bar
        use_database: bool
    ):
        """"""
        symbol, exchange = extract_vt_symbol(vt_symbol)
        end = datetime.now(get_localzone())
        start = end - timedelta(days)
        bars = []

        # Pass gateway and RQData if use_database set to True
        if not use_database:
            # Query bars from gateway if available
            contract = self.main_engine.get_contract(vt_symbol)

            if contract and contract.history_data:
                req = HistoryRequest(
                    symbol=symbol,
                    exchange=exchange,
                    interval=interval,
                    start=start,
                    end=end
                )
                bars = self.main_engine.query_history(req, contract.gateway_name)

            # Try to query bars from RQData, if not found, load from database.
            else:
                bars = self.query_bar_from_rq(symbol, exchange, interval, start, end)

        if not bars:
            bars = database_manager.load_bar_data(
                symbol=symbol,
                exchange=exchange,
                interval=interval,
                start=start,
                end=end,
            )

        for bar in bars:
            callback(bar)

因为在策略中使用这样的语句self.load_bar(10),所以use_database参数为默认值False,可是我们知道目前CTP接口是不支持历史数据查询的,所以contract and contract.history_data的条件为假,导致bars 为空, 最终执行了:

bars = self.query_bar_from_rq(symbol, exchange, interval, start, end)

而self.query_bar_from_rq的代码是这样的:

    def query_bar_from_rq(
        self, symbol: str, exchange: Exchange, interval: Interval, start: datetime, end: datetime
    ):
        """
        Query bar data from RQData.
        """
        req = HistoryRequest(
            symbol=symbol,
            exchange=exchange,
            interval=interval,
            start=start,
            end=end
        )
        data = rqdata_client.query_history(req)
        return data

再看看rqdata_client.query_history(req)的代码,它把产生req的symbol,interval,start 和end各字段,转换成米筐接口可以接受的rq_symbol,rq_interval ,interval,start 和end等4个变量中,然后把end加上1天的时间【注意:这是非常重要的一个技巧,不然无法取出截止到当前交易时刻的1分钟bar!】,最后执行米筐接口函数rqdata_get_price()读取所有的10天多的bar数据,注意:是10天多的bar,而不是整10天的bar!

def query_history(self, req: HistoryRequest) -> Optional[List[BarData]]:
        """
        Query history bar data from RQData.
        """
        if self.symbols is None:
            return None

        symbol = req.symbol
        exchange = req.exchange
        interval = req.interval
        start = req.start
        end = req.end

        rq_symbol = self.to_rq_symbol(symbol, exchange)
        if rq_symbol not in self.symbols:
            return None

        rq_interval = INTERVAL_VT2RQ.get(interval)
        if not rq_interval:
            return None

        # For adjust timestamp from bar close point (RQData) to open point (VN Trader)
        adjustment = INTERVAL_ADJUSTMENT_MAP[interval]

        # For querying night trading period data
        end += timedelta(1)

        # Only query open interest for futures contract
        fields = ["open", "high", "low", "close", "volume"]
        if not symbol.isdigit():
            fields.append("open_interest")

        df = rqdata_get_price(
            rq_symbol,
            frequency=rq_interval,
            fields=fields,
            start_date=start,
            end_date=end,
            adjust_type="none"
        )

        data: List[BarData] = []

        if df is not None:
            for ix, row in df.iterrows():
                dt = row.name.to_pydatetime() - adjustment
                dt = CHINA_TZ.localize(dt)

                bar = BarData(
                    symbol=symbol,
                    exchange=exchange,
                    interval=interval,
                    datetime=dt,
                    open_price=row["open"],
                    high_price=row["high"],
                    low_price=row["low"],
                    close_price=row["close"],
                    volume=row["volume"],
                    open_interest=row.get("open_interest", 0),
                    gateway_name="RQ"
                )

                data.append(bar)

        return data

同时可以知道interval的默认值为Interval.MINUTE。
至此我们可以看出,self.load_bar(10)其实就是从米筐接口获取的1分钟历史数据。

1.2.3 当策略启动后,接收到tick数据推送时执行on_tick()

这里执行了

self.bg.update_tick(tick)

这是在调用策略的K线合成器self.bg的update_tick() 函数,这个函数是用来把tick数据按照1分钟为间隔来产生1分钟bar的,当1分钟bar合成之时再次调用策略的on_bar()。
BarGenerator的update_tick()的函数代码如下:

    def update_tick(self, tick: TickData) -> None:
        """
        Update new tick data into generator.
        """
        new_minute = False

        # Filter tick data with 0 last price
        if not tick.last_price:
            return

        # Filter tick data with less intraday trading volume (i.e. older timestamp)
        if self.last_tick and tick.volume and tick.volume < self.last_tick.volume:
            return

        if not self.bar:
            new_minute = True
        elif self.bar.datetime.minute != tick.datetime.minute:
            self.bar.datetime = self.bar.datetime.replace(
                second=0, microsecond=0
            )
            self.on_bar(self.bar)

            new_minute = True

        if new_minute:
            self.bar = BarData(
                symbol=tick.symbol,
                exchange=tick.exchange,
                interval=Interval.MINUTE,
                datetime=tick.datetime,
                gateway_name=tick.gateway_name,
                open_price=tick.last_price,
                high_price=tick.last_price,
                low_price=tick.last_price,
                close_price=tick.last_price,
                open_interest=tick.open_interest
            )
        else:
            self.bar.high_price = max(self.bar.high_price, tick.last_price)
            self.bar.low_price = min(self.bar.low_price, tick.last_price)
            self.bar.close_price = tick.last_price
            self.bar.open_interest = tick.open_interest
            self.bar.datetime = tick.datetime

        if self.last_tick:
            volume_change = tick.volume - self.last_tick.volume
            self.bar.volume += max(volume_change, 0)

        self.last_tick = tick

分析得知它开始生成self.bar的条件是:

 if not self.bar:
    new_minute = True
   ....

 if new_minute:
            self.bar = BarData(
                symbol=tick.symbol,
                exchange=tick.exchange,
                interval=Interval.MINUTE,
                datetime=tick.datetime,
                gateway_name=tick.gateway_name,
                open_price=tick.last_price,
                high_price=tick.last_price,
                low_price=tick.last_price,
                close_price=tick.last_price,
                open_interest=tick.open_interest
            )

也就是说只要刚刚启动策略,就会立即生成一根新bar,而没有寻求对齐整分钟,这样会造成首个bar的合成非常可能是不完整的!

1.2.4 策略的on_bar()的执行:

self.bg.update_bar(bar)

这个函数是用1分钟bar来合成7分钟bar的,当7分钟bar合成完成后,它会以7分钟bar为参数调用策略的on_7min_bar()。

1.2.5 策略的on_7min_bar()的执行

        am = self.am
        am.update_bar(bar)
        if not am.inited:
            return

        """ 计算均线 """
        fast_ma = am.sma(self.fast_window,True)
        self.fast_ma0 = fast_ma[-1]
        self.fast_ma1 = fast_ma[-2]

        slow_ma = am.sma(self.slow_window,True)
        self.slow_ma0 = slow_ma[-1]
        self.slow_ma1 = slow_ma[-2]
        后面的代码就省略了

姑且不论策略是否可以赚钱,因为后面还要针对特定合约进行优化,这不是本帖讨论的重点!
从代码来看,一切都是那么自然,一个完美的例子!

2 如果你是在盘中启动将带来第一根K线错误

这里分析的重点是假如我们在盘中启动策略的话,会发生什么问题,请看图:

description

2.1 第一根合成1分钟K线的丢失部分

如上图中所示:

  1. 灰色的部分为策略利用self.load_bar(10)从米筐读取从启动之时起10日内历史1分钟bar,这就是1.2.2节中描述的那部分bar;
  2. 绿色的部分为策略利用接收到tick合成的1分钟bar,这就是1.2.3节和1.2.4节中描述的那部分bar;
  3. 黄色的部分为第一根合成1分钟K线的丢失部分,这是产生问题的主要原因!

我们知道从米筐接口读取的只有整分的K线数据,它不会提供没有走完的1分钟bar,所以如果你没有在整分钟结束的那一刻启动策略的话(做到这一点的概率太低了!),那么就一定会产生黄色的丢失部分。

2.2 第一根合成1分钟K线的丢失部分的影响

因为第一根合成1分钟K线出现丢失部分,导致第一根合成1分钟K线的开、高、收、低、成交量和开仓兴趣都可能是错误的,进而导致利用1分钟K线合成的7分钟K线也是错误的,这可以说是连锁反应,当然也就会导致利用7分钟K线进行信号计算和交易查询问题!
也许你会说,有那么夸张吗?我不知道!不过这个丢失部分的时间长度在0~59.99秒之间,再说了就算是只有3秒的丢失,也可能是这1分钟中几乎全部的成交量,创新高、创新低都是有可能的,它的缺失也可能是让7分钟K线严重失真的重要原因,谁知道呢!我们这里分析目前的代码就是这样的,从原理上讲它确实是会出错的!

3 怎么解决问题?

解决方法:

  1. 尽量不要在盘中启动策略,在盘前启动好要交易的策略,但这个方法仍然没有解决策略软件的问题。
  2. 在策略中增加是否盘中启动的判断,如果是盘中启动,则在第一根1分钟K线合成之时,抛弃不要,立即从米筐取得前1分钟的K线数据,这样就可以替换掉这个不完整的第一根合成1分钟K线,那么也就解决了第一根7分钟K线错误的问题,完美地解决问题。
  3. 那么解决该问题就需要知道启动策略的时刻是否在交易合约的交易时间段内,那么就需要知道合约的交易时间段信息。米筐接口时提供合约的交易时间段信息的,函数如下:
         get_trading_dates() # 合约的所有的交易日
         get_trading_hours() # 合约的所有的交易时间段
    如果策略启动后最后一个历史1分钟bar与第一个tick数据在一个交易时间段(如9:00-10:15)中, 那么就可以判断出第一个1分钟K线出现了数据丢失,在这个第一个1分钟K线走完之时,就应该从米筐接口立即读取这个刚刚生成的历史1分钟bar,替换掉策略合成的第一个1分钟K线,其他的处理逻辑继续执行就可以了。
  4. 另外一个简单解决方法是: 修改BarGenerator的update_tick(),当其返回合成第一个1分钟bar时,直接从米筐读取这个历史1分钟bar,以此替代之,后续的处理逻辑与目前的代码相同即可。这种方法的好处是不要根据合约的交易时间段来判断,简单,但是可能回因为读取米筐接口需要时间,会否影响tick数据的处理还有待编写代码来测试。
  5. 问题已经发现了,怎么实现代码还在思考中,应该不难。这个问题就难在你可能根本没有意识到它可能有问题!

4 一种解决第一根合成1分钟K线的方法:

按照第3节中的4的方法,修改BarGenerator,代码如下,可以解决问题:

class BarGenerator:
    """
    For:
    1. generating 1 minute bar data from tick data
    2. generateing x minute bar/x hour bar data from 1 minute data

    Notice:
    1. for x minute bar, x must be able to divide 60: 2, 3, 5, 6, 10, 15, 20, 30
    2. for x hour bar, x can be any number
    """

    def __init__(
        self,
        on_bar: Callable,
        window: int = 0,
        on_window_bar: Callable = None,
        interval: Interval = Interval.MINUTE
    ):
        """Constructor"""
        self.bar: BarData = None
        self.on_bar: Callable = on_bar

        self.interval: Interval = interval
        self.interval_count: int = 0

        self.window: int = window
        self.window_bar: BarData = None
        self.on_window_bar: Callable = on_window_bar

        self.last_tick: TickData = None
        self.last_bar: BarData = None
        self.is_first_bar = True            # hxxjava add

    def update_tick(self, tick: TickData) -> None:
        """
        Update new tick data into generator.
        """
        from vnpy.trader.rqdata import rqdata_client    # hxxjava add
        from vnpy.trader.object import HistoryRequest   # hxxjava add

        new_minute = False

        # Filter tick data with 0 last price
        if not tick.last_price:
            return False

        # Filter tick data with less intraday trading volume (i.e. older timestamp)
        if self.last_tick and tick.volume and tick.volume < self.last_tick.volume:
            return False

        if not self.bar:
            new_minute = True
        elif self.bar.datetime.minute != tick.datetime.minute:
            self.bar.datetime = self.bar.datetime.replace(
                second=0, microsecond=0
            )

            # hxxjava add start
            if self.is_first_bar:   
                self.is_first_bar = False

                symbol,exchange = extract_vt_symbol(self.bar.vt_symbol)
                bar_datetime = self.bar.datetime
                req = HistoryRequest(
                    symbol=symbol,
                    exchange=exchange,
                    start = bar_datetime,
                    end=bar_datetime,
                    interval=Interval.MINUTE
                )
                bars = rqdata_client.query_history(req)
                self.bar = bars[-1]
                print(f"【first bar time = {bar_datetime} history bar time = {self.bar.datetime},bars count={len(bars)}】")
            # hxxjava add end

            self.on_bar(self.bar)

            new_minute = True

        if new_minute:
            print(f"【tick.datetime = {tick.datetime} is_first_bar={self.is_first_bar}】")
            self.bar = BarData(
                symbol=tick.symbol,
                exchange=tick.exchange,
                interval=Interval.MINUTE,
                datetime=tick.datetime,
                gateway_name=tick.gateway_name,
                open_price=tick.last_price,
                high_price=tick.last_price,
                low_price=tick.last_price,
                close_price=tick.last_price,
                open_interest=tick.open_interest
            )
        else:
            self.bar.high_price = max(self.bar.high_price, tick.last_price)
            self.bar.low_price = min(self.bar.low_price, tick.last_price)
            self.bar.close_price = tick.last_price
            self.bar.open_interest = tick.open_interest
            self.bar.datetime = tick.datetime

        if self.last_tick:
            volume_change = tick.volume - self.last_tick.volume
            self.bar.volume += max(volume_change, 0)

        self.last_tick = tick

    def update_bar(self, bar: BarData) -> None:
        """
        Update 1 minute bar into generator
        """
        # If not inited, creaate window bar object
        if not self.window_bar:
            # Generate timestamp for bar data
            if self.interval == Interval.MINUTE:
                dt = bar.datetime.replace(second=0, microsecond=0)
            else:
                dt = bar.datetime.replace(minute=0, second=0, microsecond=0)

            self.window_bar = BarData(
                symbol=bar.symbol,
                exchange=bar.exchange,
                datetime=dt,
                gateway_name=bar.gateway_name,
                open_price=bar.open_price,
                high_price=bar.high_price,
                low_price=bar.low_price
            )
        # Otherwise, update high/low price into window bar
        else:
            self.window_bar.high_price = max(
                self.window_bar.high_price, bar.high_price)
            self.window_bar.low_price = min(
                self.window_bar.low_price, bar.low_price)

        # Update close price/volume into window bar
        self.window_bar.close_price = bar.close_price
        self.window_bar.volume += int(bar.volume)
        self.window_bar.open_interest = bar.open_interest

        # Check if window bar completed
        finished = False

        if self.interval == Interval.MINUTE:
            # x-minute bar
            if not (bar.datetime.minute + 1) % self.window:
                finished = True
        elif self.interval == Interval.HOUR:
            if self.last_bar and bar.datetime.hour != self.last_bar.datetime.hour:
                # 1-hour bar
                if self.window == 1:
                    finished = True
                # x-hour bar
                else:
                    self.interval_count += 1

                    if not self.interval_count % self.window:
                        finished = True
                        self.interval_count = 0

        if finished:
            self.on_window_bar(self.window_bar)
            self.window_bar = None

        # Cache last bar object
        self.last_bar = bar

    def generate(self) -> Optional[BarData]:
        """
        Generate the bar data and call callback immediately.
        """
        bar = self.bar

        if self.bar:
            bar.datetime = bar.datetime.replace(second=0, microsecond=0)
            self.on_bar(bar)

        self.bar = None
        return bar


《30天解锁python量化开发》课程中与现版本(2.6.0)不符合的地方

这里更新《30天解锁python量化开发》课程中与现版本(2.6.0)不符的地方,如果还有其他跟课程相关的问题,欢迎在该贴进行补充。

(1)由于2.6.0版本将一些应用模块进行了剥离,因此如果课程中提及的目录不在vnpy目录下,可以在vnpy同级目录,也就是site-packages目录下寻找

(2)课时36-载入CSV历史数据中的load_data_36代码应修改为:

import csv
import pytz
from datetime import datetime

from vnpy.trader.constant import Exchange, Interval
from vnpy.trader.object import BarData
from vnpy.trader.database import get_database

database_manager = get_database()
CHINA_TZ = pytz.timezone("Asia/Shanghai")


with open("if_Data.csv") as f:
    reader = csv.DictReader(f)

    bars = []
    for d in reader:
        dt = datetime.strptime(d["datetime"], "%Y-%m-%d %H:%M:%S")
        dt = CHINA_TZ.localize(dt)

        bar = BarData(
            symbol=d["symbol"],
            exchange=Exchange(d["exchange"]),
            interval=Interval.MINUTE,
            datetime=dt,
            open_price=d["open"],
            high_price=d['high'],
            low_price=d["low"],
            close_price=d["close"],
            volume=d["volume"],
            open_interest=d["open_interest"],
            gateway_name="DB"
        )
        bars.append(bar)

    database_manager.save_bar_data(bars)
    print(f"完成数据插入,起始点{bars[0].datetime},结束点{bars[-1].datetime},总数据量{len(bars)}")

(3)课时44-日志引擎中代码run_44应该修改为:

# flake8: noqa
from vnpy.event import EventEngine

from vnpy.trader.engine import MainEngine
from vnpy.trader.ui import MainWindow, create_qapp

from vnpy.gateway.ctp import CtpGateway
from vnpy_ctastrategy import CtaStrategyApp
from vnpy_ctastrategy.base import EVENT_CTA_LOG


def main():
    """"""
    qapp = create_qapp()

    event_engine = EventEngine()

    main_engine = MainEngine(event_engine)
    main_engine.add_gateway(CtpGateway)
    main_engine.add_app(CtaStrategyApp)

    log_engine = main_engine.get_engine("log")
    event_engine.register(EVENT_CTA_LOG, log_engine.process_log_event)

    main_window = MainWindow(main_engine, event_engine)
    main_window.showMaximized()

    qapp.exec()


if __name__ == "__main__":
    main()


Mac下vnpy安装运行交易的图文教程

尽管使用vn.py的用户使用windows或者linux服务器居多,但是社区内也有不少用户希望能在mac下运行vn.py进行数字货币的交易,本篇教程就针对如何在Mac系列的机器上成功运行vn.py连接coinbase数字货币交易所进行讲解。本文主要分以下几个部分:

  • python环境的安装
  • vn.py及依赖项的安装
  • 运行vn.py
  • 使用vn.py

 
python环境的安装
在mac下使用miniconda可以省去很多不必要的麻烦。
打开Minconda官网:https://docs.conda.io/en/latest/miniconda.html, 选择MacOSX Installer下的Python3.7,选择64位的pkg安装包进行下载并解压:
description
安装完毕后,在terminal输入python,会发现terminal自动将miniconda附带的python作为系统默认python了。
description
 
安装vn.py
因为mac自带git神器,因此直接在terminal里下载vnpy即可:打开terminal,使用cd切换到想要下载的路径,然后输入以下命令:
description
上图我将vnpy整个文件夹clone至Desktop。当clone完毕后,在cd进入vnpy文件夹,输入如下命令:
description
(注,cd的是一个命令,例如我要切换到/Users/limingfang/Desktop/,则只需要在terminal输入

cd /Users/limingfang/Desktop

安装的时间比较久,需要耐心等待。
在安装完成后,vn.py以及依赖项就被安装至Miniconda的python库中了。
注意:如果启动VN Trader时报错说缺少了pyqtgraph和zmq等库,即出现如图一的报错,直接用pip工具安装即可,在terminal中运行图二中的命令即可(笔者在解压完vnpy包后,发现还需要安装pyqtgraph和zmq)。
description
description

 

启动vn.py
在上述安装全部完成后,即可开始运行vn.py。
在terminal中将路径切换至clone的vnpy路径,比如vn.py被我cloned在/Users/limingfang/Documents/Github/cloned,那么输入以下命令即可切换到vn.py中的vn_trader目录并启动vn.py图形化界面:
description
其中红框内的路径每个用户都不一样。紧接着会跳出一个比较精致的VN Trader图形化界面。
description
 

使用vn.py
该部分主要说两点:简要说明图形化界面的使用,vn.py在mac上可使用的接口。
在图形化界面上方,“系统”栏提供了连接接口的选项,“功能”栏提供了vn.py开放给用户的一些App例如CTA回测等,“帮助”栏提供了查询合约的功能,用于提供交易所合约代码与vn.py内部合约代码映射的信息。注意,在mac下没有“配置”栏,如果想要修改全局配置,可以直接修改源文件。VN Trader界面启动后默认会在当前用户主目录下创建一些隐藏文件夹以存放一些配置信息,在mac下用户目录是在 “/Users/limingfang/”(limingfang是我的mac用户名)。
description
上图是最初.vntrader文件夹内的内容,其中connect_bitmex.json等connect_xxx.json类文件存放接口配置信息,database是默认的sqlite数据库的文件,vt_setting.json用于配置图形化界面字体,rqdata,数据库等,rpc_service_setting.json一般不需要修改。
如果需要修改某个文件,按下图操作即可打开相应文件。
description
 
目前2.x版本的vn.py,在windows下所有交易接口都可以使用,而在OSX系统下只能使用一部分:
纯Python类接口:IB、TIGER、FUTU
IB(盈透证券)、TIGER(老虎证券)、FUTU(证券)这三个接口,使用的是其官方提供的纯Python SDK,直接进行接口函数的对接开发。得益于Python本身的跨平台解析性语言特点,这类接口在OSX系统下也能直接使用。
REST/WebSocket协议类接口:所有数字货币、Alpaca
当今几乎所有的数字货币交易所,都提供了基于REST协议(下单、查询)和WebSocket协议(行情、推送)的API,部分外盘的股票期货经纪商也开始提供这块的支持(如美股0佣金券商Alpaca)。

此外,对于在mac电脑上使用vn.py的用户,交易一些数字货币的时候,可能会碰到需要交易某些标的(例如某些数字货币合约)而网络不通的问题,因此接下来介绍如何利用shadowsocks在mac上进行翻墙从而交易国外的一些合约。
最近比较敏感,本来笔者有详细的介绍如何安装及使用,现在只简单说说如何在vnpy连接接口的时候使用shadowsocks。

使用Shadowsocks
下载配置Shadowsocks后,在vn.py中连接一些大陆外的接口(例如Coinbase)时,需要提供:
- proxy_host
- proxy_port  
一般如下图所示进行配置即可:
description



视频记录如何编写一个完整的可以实盘的策略(简单框架,止损止盈,跨周期,逻辑优化)

闲来无事,在家把之前的编写的策略如何一步一步实现的记录下来了!

在B站发了些自己做的关于量化的视频,只是简单的记录一下自己是

如何一步一步走进这个量化坑的,希望可以帮助新手少走一些坑。

这个策略,实盘肯定不行的,只是记录下编写策略的一个流程。

我在B站发的视频,欢迎大家点赞关注 /😊/😊/😊/😊/😊/😊/😊

https://www.bilibili.com/video/BV1pV411r7Kt?p=10

视频只是简单的记录自己之前做的一些思路,高手别喷(●ˇ∀ˇ●)

质量肯定没法和官方的比 最后还是希望大家多多支持官方

from vnpy.app.cta_strategy import (
    CtaTemplate,
    StopOrder,
    TickData,
    BarData,
    TradeData,
    OrderData,
    BarGenerator,
    ArrayManager,
)
from vnpy.trader.constant import Interval


class Demo01(CtaTemplate):
    """"""

    # 参数
    fast_window = 30  # 快速均线
    slow_window = 60  # 慢速均线
    x_min = 30  # 交易周期  默认是15分钟
    lots = 1  # 开仓手数
    save = 20  # 止损参数
    startstop = 100  # 开始止盈
    stoploss = 40  # 回撤点位
    daily_window = 10  # 日线的参数
    max_lots = 3
    flag=0

    # 变量
    fast_ma = 0
    fast_ma_pre = 0
    slow_ma = 0
    slow_ma_pre = 0
    price = 0  # tick的实时价格
    bartime = ""  # 时间的显示
    avg_buy_price = 0
    avg_sell_price = 0
    highest = 0
    lowest = 0
    daily_ma = 0

    liqDays = 60
    liqpoint = 0
    holding_days = 0

    run_buy=False
    run_sell=False



    parameters = ["fast_window", "slow_window", "x_min", "lots", 'stoploss', 'startstop', 'save']

    variables = ["bartime", "price", "fast_ma", "slow_ma", 'highest', 'lowest']

    def __init__(self, cta_engine, strategy_name, vt_symbol, setting):
        """"""
        super().__init__(cta_engine, strategy_name, vt_symbol, setting)
        self.bg_x = BarGenerator(self.on_bar, self.x_min, self.on_x_bar, Interval.MINUTE)
        self.am_x = ArrayManager()

        self.bg_daily = BarGenerator(self.on_bar, 1, self.on_daily_bar, Interval.DAILY)
        self.am_daliy = ArrayManager()

    def on_init(self):
        """
        Callback when strategy is inited.
        """
        self.write_log("策略初始化")
        self.load_bar(30)

    def on_start(self):
        """
        Callback when strategy is started.
        """
        self.write_log("策略启动")

    def on_stop(self):
        """
        Callback when strategy is stopped.
        """
        self.write_log("策略停止")

    def on_tick(self, tick: TickData):
        """
        Callback of new tick data update.
        """
        self.bg_x.update_tick(tick)
        self.price = tick.last_price


        if self.pos > 0:
            if tick.last_price < self.avg_buy_price - self.save*2:
                self.sell(tick.last_price*0.9, abs(self.pos))
                print(tick.datetime, tick.last_price, "保命出场")
                self.flag+=1

        elif self.pos < 0:
            if tick.last_price > self.avg_sell_price + self.save*2:
                self.cover(tick.last_price*1.2, abs(self.pos))
                print(tick.datetime, tick.last_price, "保命出场")
                self.flag+=1

        self.put_event()

    def on_bar(self, bar: BarData):

        self.bg_x.update_bar(bar)
        self.bg_daily.update_bar(bar)

    def on_x_bar(self, bar: BarData):
        """
        Callback of new bar data update.
        """
        self.cancel_all()

        am = self.am_x

        am.update_bar(bar)

        if not am.inited:
            return
        # ma30的计算
        self.fast_ma = am.close_array[-self.fast_window:-1].mean()
        # 之前的ma30的计算
        self.fast_ma_pre = am.close_array[-self.fast_window - 1:-2].mean()
        # ma60的计算
        self.slow_ma = am.close_array[-self.slow_window:-1].mean()
        # 之前的ma60的计算
        self.slow_ma_pre = am.close_array[-self.slow_window - 1:-2].mean()

        self.run_buy=bar.close_price<=am.close_array[-2]*1.05

        self.run_sell=bar.close_price>=am.close_array[-2]*0.95





        if self.pos == 0:
            # 金叉开多
            if self.fast_ma_pre < self.slow_ma_pre and self.fast_ma > self.slow_ma:
                if bar.close_price > self.daily_ma and self.run_buy:
                    self.buy(bar.close_price,min(self.lots,self.max_lots))
                    self.highest = bar.close_price
                    self.avg_buy_price = bar.close_price
                    self.holding_days = 0
                    print(bar.datetime, bar.close_price, "开多单","开仓手数:",min(self.lots,self.max_lots))

            elif self.fast_ma_pre > self.slow_ma_pre and self.fast_ma < self.slow_ma:
                # 死叉开空
                if bar.close_price < self.daily_ma and self.run_sell:
                    self.short(bar.close_price,min(self.lots,self.max_lots))
                    self.lowest = bar.close_price
                    self.avg_sell_price = bar.close_price
                    self.holding_days = 0
                    print(bar.datetime, bar.close_price, "开空单","开仓手数:",min(self.lots,self.max_lots))

        elif self.pos > 0:
            # 持有多单的最高价记录
            self.highest = max(bar.high_price, self.highest)
            # 多单止损的逻辑
            if bar.close_price < self.avg_buy_price - self.save:
                self.sell(bar.close_price, abs(self.pos))
                print(bar.datetime, bar.close_price, "多单止损","止损手数:",abs(self.pos))
                self.flag+=1

            # 多单止盈
            elif self.highest > self.avg_buy_price + self.startstop:
                if bar.close_price < self.highest - self.stoploss:
                    self.sell(bar.close_price, abs(self.pos))
                    print(bar.datetime, bar.close_price, "多单止盈","止盈手数:",abs(self.pos))
                    self.flag=0

            elif self.holding_days > 20 and bar.close_price < self.liqpoint:
                self.sell(bar.close_price, abs(self.pos))
                print(bar.datetime, bar.close_price, "多单自适应均线出场","平仓手数:",abs(self.pos))
                # if self.avg_buy_price<bar.close_price:
                #     self.flag=0
                # else:
                #     self.flag+=1


        elif self.pos < 0:
            # 持有空单的最低价记录
            self.lowest = min(bar.low_price, self.lowest)
            # 空单止损的逻辑
            if bar.close_price > self.avg_sell_price + self.save:
                self.cover(bar.close_price, abs(self.pos))
                print(bar.datetime, bar.close_price, "空单止损","止损手数:",abs(self.pos))
                self.flag+=1

            # 空单止盈
            elif self.lowest < self.avg_sell_price - self.startstop:
                if bar.close_price > self.lowest + self.stoploss:
                    self.cover(bar.close_price, abs(self.pos))
                    print(bar.datetime, bar.close_price, "空单止盈", "止盈手数:", abs(self.pos))
                    self.flag=0


            elif self.holding_days > 20 and bar.close_price > self.liqpoint:
                self.cover(bar.close_price, abs(self.pos))
                print(bar.datetime, bar.close_price, "空单自适应均线出场","平仓手数:",abs(self.pos))
                # if self.avg_sell_price > bar.close_price:
                #     self.flag =0
                # else:
                #     self.flag += 1

        if self.pos != 0:
            self.holding_days += 1
        else:
            self.liqDays = self.slow_window

        if self.pos != 0 and self.holding_days >= 20:
            self.liqDays -= 1
            self.liqDays = max(self.liqDays, 50)

        self.liqpoint = am.close_array[-self.liqDays:].mean()

        self.lots=2 if self.flag>=2 else 1

        self.put_event()

    def on_daily_bar(self, bar: BarData):

        self.cancel_all()

        am = self.am_daliy

        am.update_bar(bar)

        if not am.inited:
            return

        self.daily_ma = am.close_array[-self.daily_window:-1].mean()

    def on_order(self, order: OrderData):
        """
        Callback of new order data update.
        """
        pass

    def on_trade(self, trade: TradeData):
        """
        Callback of new trade data update.
        """
        self.put_event()

    def on_stop_order(self, stop_order: StopOrder):
        """
        Callback of stop order update.
        """
        pass


如何解决界面栏宽度高度过小问题

因为每个电脑的使用分辨率都不太一样,vnpy界面出现如下混乱问题:如下图
description
解决办法:
1.主窗口下>配置>全局配置>font.size改小,比如设置为9.
2.vnpy>app>cya_backtester>widget.py下184行设置为较小值,比如600
description
完美解决如下:
description



VNPY 的EVENT事件作为 pyQT5的信号触发函数

最近在做vnpy的一些界面修改,里面很多要做内容就是按照Event事件,作为pyqt5信号,来触发信号槽函数。

简单介绍下pyqt5的信号和信号槽,信号是就是QtCore.pyqtSignal类的实实例对象。信号有个emit() 方法,当emit执行时候,相当于触发信号,激活对应的信号槽。信号槽就是对应函数,将会被操作。界面按钮点击或者屏幕双击,都是一些原生的信号事件,直接调用就是可以,至于更多相关内容可以网上找找资料。

我们这里说的是当VNPY的EVENT事件做为pyQT5信号,触发对应信号槽函数。

比如一笔的成交,在vnpy中定义好一个成交事件

EVENT_CTA_TRADE = 'eCtaTrade'
此时,这里在建立是对应的信号实例,参数类型是Event。然后把这个信号发送函数和event绑定,代码如下

signal_trade_strategy = QtCore.pyqtSignal(Event)
event_engine.register(
EVENT_CTA_TRADE, signal_trade_strategy.emit
)
这个时候需要定义对应的信号槽,如果process_cta_trade_event是处理函数,定义如下

self.signal_trade_strategy.connect(process_cta_trade_event)

在处理函数中,Event 事件可以作为参数直接获取。之后可以做为内容更新

process_cta_trade_event(, event):
data = event.data
这里没有什么难点,如果信号槽事件不触及pyqt5 的UI 绘制的化,其实直接在EVENT绑定时候直接绑定信号槽函数。

但是如果信号槽事件涉及到UI绘制的化,必须要用自定义pyqt5信号,因为UI更新绘制是一个子线程,如果整个VNPY是作为子线程运行,比如用child模式,子线程定时触发运行,那么就会报错下面错误。唯一要注意地方。

QObject::connect: Cannot queue arguments of type 'QVector<int>'
(Make sure 'QVector<int>' is registered using qRegisterMetaType().)


新消息

统计

主题
6871
帖子
26638
已注册用户
28635
最新用户
在线用户
644
在线来宾用户
3401
© 2015-2019 上海韦纳软件科技有限公司
备案服务号:沪ICP备18006526号-3

沪公网安备 31011502017034号