diff --git a/.github/issue_template.md b/.github/issue_template.md new file mode 100644 index 000000000..75a077d9f --- /dev/null +++ b/.github/issue_template.md @@ -0,0 +1,17 @@ +# 提 ISSUE 须知 + +请先阅读文档 [rqalpha文档](http://rqalpha.readthedocs.io/) + +如果仍有问题的话请在 [issue列表](https://github.com/ricequant/rqalpha/issues) 中寻找是否有相关问题的解决方案 + +如果没有的话 麻烦开一个issue 描述以下问题: + +## 1. RQAlpha的版本 + +## 2. Python的版本 + +## 3. 是Windows/Linux/MacOS or others? + +## 4. 您出现问题对应的源码/或者能复现问题的简易代码 以及对应的配置 + +## 5. 您出现的错误堆栈日志信息 diff --git a/.gitignore b/.gitignore index 254ea9cac..723dfa5cb 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,6 @@ rqalpha.system.log *.pk *.h5 .vscode -.idea *.c *.so *.pyc @@ -20,4 +19,4 @@ persist/ .coverage .tox rqalpha_test_coverage_report/ -.tmp.pkl +.tmp.pkl \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index b213ed9ae..03bb8c35f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,24 +24,25 @@ install: # Install bcolz - pip install Cython - pip install bcolz - - pip install -r requirements.txt # Install Test Deps - pip install TA-Lib - pip install coveralls - pip install -U setuptools - pip install funcat + - pip install ipython==5.3.0 - pip install . script: - - ls $HOME/.rqalpha/bundle/ + - ls -al $HOME/.rqalpha/ + - ls -al $HOME/.rqalpha/bundle/ - | - if [ ! -d $HOME/.rqalpha/config.yml ]; then + if [ ! -f $HOME/.rqalpha/bundle/instruments.pk ]; then rqalpha update_bundle fi - coverage run --source=rqalpha test.py cache: directories: - $HOME/.cache/pip - - $HOME/.rqalpha + - $HOME/.rqalpha/bundle after_success: coveralls diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4b86430c0..4dd6145e6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,194 @@ +================== +CHANGELOG +================== + +2.1.4 +================== + +- 解决 history_bars 在 before_trading 获取的是未来数据的问题 +- 解决 before_trading 获取结算价是当前交易日结算价的问题 +- 增加 RQAlpha 向前兼容(0.3.x) `Issue 100 `_ +- 期货增加强平机制: 及当前账户权益<=0时,清空仓位,资金置0 `Issue 108 `_ +- 解决回测时只有一个交易日时,只有回测数据显示的问题 + +2.1.3 +================== + +- Fix `Issue 101 `_ +- Fix `Issue 105 `_ +- 解决运行 RQAlpha 时缺少 `six` | `requests` 库依赖的问题 +- 解决安装RQAlpha时在某些情况下报错的问题 +- 解决第三方 Mod 安装后配置文件路径有误的问题 +- 现在可以通过 `rqalpha mod install -e .` 的方式来安装依赖 Mod 了 +- 现在运行策略时会检测当前目录是否存在 `config.yml` 或者 `config.json` 来作为配置文件 +- 解决股票下单就存在 `position` 的问题,现在只有成交后才会产生 `position` 了。 +- 修复 `portfolio` 和 `future_account` 计算逻辑的一些问题 +- 修复 `transaction_cost` 在某个 position 清空以后计算不准确的问题 +- 在信号模式下 `price_limit` 表示是否输出涨跌停买入/卖出的报警信息,但不会阻止其买入/卖出 + +2.1.2 +================== + +- 提供 :code:`from rqalpha import cli` 方便第三方 Mod 扩展 `rqalpha` command +- :code:`history_bars` 增加 :code:`include_now` option +- Fix `Issue 90 `_ +- Fix `Issue 94 `_ + +2.1.0 +================== + +- Fix `Issue 87 `_ +- Fix `Issue 89 `_ +- Fix 无法通过 :code:`env.config.mod` 获取全部 `mod` 的配置信息 +- 增加 :code:`context.config` 来获取配置信息 +- 提供 :code:`from rqalpha import export_as_api` 接口,方便扩展自定义 API + +2.0.9 +================== + +- Fix `Issue 79 `_ +- Fix `Issue 82 `_ +- Fix :code:`rqalpha cmd` 失效 + +2.0.8 +================== + +- Fix `Issue 81 `_ +- 解决 `mod_config.yml` 文件解析出错以后,所有的命令报错的问题 +- 默认在 Python 2.x 下 `sys.setdefaultencoding("utf-8")` +- 优化 `UNIVERSE_CHANGED` 事件,现在只有在universe真正变化时才触发 + +2.0.7 +================== + +- Fix `Issue 78 `_ +- `is_st_stock` | `is_suspended` 支持 `count` 参数 +- 解决大量 Python 2.x 下中文乱码问题 + +2.0.6 +================== + +- 解决在 Python 2.x 下安装 RQAlpha 提示 `requirements-py2.txt Not Found` 的问题 +- 解决 `Benchmark` 无法显示的问题 +- 解决 `rqalpha mod list` 显示不正确的问题 +- 现在可以通过配置 `base.extra_vars` 向策略中预定义变量了。用法如下: + +.. code-block:: python3 + + from rqalpha import run + + config = { + "base": { + "strategy_file": "strategy.py", + "start_date": "2016-06-01", + "end_date": "2016-07-01", + "stock_starting_cash":100000, + "benchmark": '000300.XSHG' + }, + "extra":{ + "context_vars":{ + "short":5, + "middle":10, + "long":21 + } + } + } + + result_dict = run(config) + + # 以下是策略代码: + + def handle_bar(context): + print(context.short) # 5 + print(context.middle) # 10 + print(context.long) # 21 + +2.0.1 +================== + +- 修改配置的读取方式,不再从 `~/.rqalpha/config.yml` 读取自定义配置信息,而是默认从当前路径读取 `config.yml`,如果没找到,则会读取系统默认配置信息 +- 现在不再对自定义信息进行版本检查 +- :code:`rqalpha generate_config` 现在会生成包含所有默认系统配置信息的 `config.yml` 文件。 +- :code:`RUN_TYPE` 增加 :code:`LIVE_TRADING` +- 修复 :code:`history_bars` 获取日期错误产生的问题 +- 修复执行 :code:`context.run_info` 会报错的问题 +- 修复持久化报错的问题 +- 增加 Order Persist 相关内容 + + +2.0.0 +================== + +2.0.0 详细修改内容请访问:`RQAlpha 2.0.0 `_ + +**Portfolio/Account/Position 相关** + +- 重新定义了 :code:`Portfolio`, :code:`Account` 和 :code:`Position` 的角色和关系 +- 删除大部分累计计算的属性,重新实现股票和期货的计算逻辑 +- 现在只有在 :code:`Portfolio` 层级进行净值/份额的计算,Account级别不再进行净值/份额/收益/相关的计算 +- 账户的恢复和初始化现在只需要 :code:`total_cash`, :code:`positions` 和 :code:`backward_trade_set` 即可完成 +- 精简 :code:`Position` 的初始化,可以从 :code:`real_broker` 直接进行恢复 +- :code:`Account` 提供 :code:`fast_forward` 函数,账户现在可以从任意时刻通过 :code:`orders` 和 :code:`trades` 快速前进至最新状态 +- 如果存在 Benchmark, 则创建一个 :code:`benchmark_portfolio`, 其包含一个 :code:`benchmark_account` +- 策略在调用 :code:`context.portfolio.positions[some_security]` 时候,如果 position 不存在,不再每次都创建临时仓位,而是会缓存,从而提高回测速度和性能 +- 不再使用 :code:`clone` 方法 +- 不再使用 :code:`PortfolioProxy` 和 :code:`PositionProxy` + +**Event 相关** + +- 规范 Event 的生成和相应逻辑, 使用 Event object 来替换原来的 Enum +- 抽离事件执行相关逻辑为 :code:`Executor` 模块 + +**Mod 相关** + +- 规范化 Mod 命名规则,需要以 `rqalpha_mod_xxx` 作为 Mod 依赖库命名 +- 抽离 :code:`slippage` 相关业务逻辑至 :code:`simulation mod` +- 抽离 :code:`commission` 相关业务逻辑至 :code:`simulation mod` +- 抽离 :code:`tax` 相关业务逻辑至 :code:`simulation mod` +- `rqalpha mod list` 命令现在可以格式化显示 Mod 当前的状态了 + +**Environment 和 ExecutionContext 相关** + +- 现在 :code:`ExecutionContext` 只负责上下文相关的内容,不再可以通过 :code:`ExecutionContext` 访问其他成员变量。 +- 扩展了 :code:`Environment` 的功能,RQAlpha 及 Mod 均可以直接通过 :code:`Environment.get_instance()` 来获取到环境中核心模块的引用 +- :code:`Environment` 还提供了很多常用的方法,具体请直接参考代码 + +**配置及参数相关** + +- 重构了配置相关的内容,`~/.rqalpha/config.yml` 现在类似于 Sublime/Atom 的用户配置文件,用于覆盖默认配置信息,因此只需要增加自定义配置项即可,不需要全部的配置内容都存在 +- 将Mod自己的默认配置从配置文件中删除,放在Mod中自行管理和维护 +- 独立存在 `~/.rqalpha/.mod_conifg.yml`, 提供 `rqalpha mod install/uninstall/enable/disable/list` 命令,RQAlpha 会通过该配置文件来对Mod进行管理。 +- 抽离 :code:`rqalpha run` 的参数,将其中属于 `Mod` 的参数全部删除,取代之为Mod提供了参数注入机制,所以现在 `Mod` 可以自行决定是否要注入参数或者命令来扩展 RQAlpha 的功能 +- 提供了 :code:`rqalpha-cmd` 命令,`Mod` 推荐在该命令下注入自己的命令来实现功能扩展 +- 不再使用 `--strategy-type`, 改为使用 `--security` 选项 +- `--output-file` | `--report` | `--plot` | `--plot-save`参数 转移至 `sys_analyser` Mod 中 +- `plot` | `report` 命令,转移至 `sys_analyser` Mod 中 +- `--signal` | `--slippage` | `--commission-multiplier` | `--matching-type` | `--rid` 转移至 `sys_simulation` Mod 中 + +**Risk 计算** + +- 修复 `tracking error `_ 计算错误 +- 修改 `sharpe `_ , `sortino `_ , `information ratio `_ , `alpha `_ 计算逻辑。参考 `晨星 `_ 的方法, 先计算单日级别指标, 再进行年化。与原本直接基于年化值计算相比, 在分析时间较短的情况下, 新的指标计算结果会系统性低于原指标结果。 +- 引入单日无风险利率作为中间变量计算上述指标。单日无风险利率为通过 `中国债券信息网 `_ 获取得到对应期限的年化国债到期收益率除以244得到 +- 修改指标说明若干 + +**其他** + +- 修改了 :code:`Order` 和 :code:`Trade` 的字段和函数,使其更通用 +- 为 :code:`RqAttrDict` 类增加 :code:`update` 方法,现在支持动态更新了 +- :code:`arg_checker` 增加 :code:`is_greater_or_equal_than` 和 :code:`is_less_or_equal_than` 函数 +- 删除 :code:`DEFAULT_FUTURE_INFO` 变量,现在可以直接通过 :code:`data_proxy` 获取相关数据 +- 通过 `better_exceptions `_ 提供更好的错误堆栈提示体验 +- 对字符串的处理进行了优化,现在可以正确在 Python2.x/3.x 下显示中文了 +- 修复 :code:`update_bundle` 直接在代码中调用会报错的问题 +- 增加对于下单量为0的订单过滤,不再会创建订单,也不再会输出警报日志 +- 增加 :code:`is_suspended` 和 :code:`is_st_stock` API 的支持 + +0.3.14 +================== + +- Hotfix :code:`UnboundLocalError: local variable 'signature' referenced before assignment` + 0.3.13 ================== diff --git a/MANIFEST.in b/MANIFEST.in index d903db033..8ee4a5f4d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,4 @@ include requirements.txt +include requirements-py2.txt include rqalpha/VERSION.txt recursive-include rqalpha/resource *.png diff --git a/README.rst b/README.rst index 9fcf2dc14..1a58223aa 100644 --- a/README.rst +++ b/README.rst @@ -1,8 +1,8 @@ -.. image:: https://raw.githubusercontent.com/ricequant/rqalpha/master/docs/source/_static/logo.jpg - -=============================== +======= RQAlpha -=============================== +======= + +.. image:: https://raw.githubusercontent.com/ricequant/rqalpha/master/docs/source/_static/logo.jpg .. image:: https://img.shields.io/travis/ricequant/rqalpha/master.svg :target: https://travis-ci.org/ricequant/rqalpha/branches @@ -58,9 +58,46 @@ RQAlpha API * `参数配置`_ : 启动 RQAlpha 参数配置 * `API`_ : RQAlpha API 文档 +Mod +============================ + +RQAlpha 提供了极具拓展性的 Mod Hook 接口,这意味着 RQAlpha 可以非常容易的对接其他量化库,安装和使用 Mod 也非常简单,当您安装好 RQAlpha 后,可以直接通过如下命令对管理您的Mod: + +.. code-block:: bash + + # 查看当前安装的 Mod 列表及状态 + $ rqalpha mod list + # 安装 Mod + $ rqalpha mod install xxx + # 卸载 Mod + $ rqalpha mod uninstall xxx + # 启用 Mod + $ rqalpha mod enable xxx + # 禁用 Mod + $ rqalpha mod disable xxx + +====================== ============================================================================================================== +Mod名 说明 +====================== ============================================================================================================== +`sys_analyser`_ 【系统模块】记录每天的下单、成交、投资组合、持仓等信息,并计算风险度指标,并以csv、plot图标等形式输出分析结果 +`sys_funcat`_ 【系统模块】支持以通达信公式的方式写策略 +`sys_progress`_ 【系统模块】在控制台输出当前策略的回测进度。 +`sys_risk`_ 【系统模块】对订单进行事前风控校验 +`sys_simulation`_ 【系统模块】支持回测、撮合、滑点控制等 +`sys_stock_realtime`_ 【系统模块】Demo 模块,用于展示如何接入自有行情进行回测/模拟/实盘 +`vnpy`_ 【第三方模块】通过 VNPY 对接期货实盘行情和实盘交易 +`sentry`_ 【第三方模块】集成 sentry 的扩展,实现错误日志全自动采集、处理 +`tushare`_ 【第三方模块】Demo Mod,用于展示如何通过tushare 获取实时Bar数据并组装以供RQAlpha使用 +`shipane`_ 【第三方模块】集成实盘易SDK,用于对接股票实盘跟单交易 +====================== ============================================================================================================== + +如果您基于 RQAlpha 进行了 Mod 扩展,欢迎告知我们,在审核通过后,会在 Mod 列表中添加您的 Mod 信息和链接。 + 机构版 ============================ +.. image:: https://raw.githubusercontent.com/ricequant/rqalpha/master/docs/source/_static/rqalpha_plus.png + 目前 RQAlpha 开源版仅开放了日级别的历史数据和日回测功能,如果您是机构用户,需要做算法交易亦或是量化研究,都可以联系我们的机构端产品销售获得机构端产品功能支持。「销售电话」:0755-33967716 「QQ」:4848371 机构端产品功能: @@ -73,6 +110,51 @@ RQAlpha API * 业绩分析和风险管理系统 * 技术支持及定制化开发 +Feature Status +============================ + +* VNPY 对接 --> `vnpy`_ + + * ✅ 扩展VNPY_Gateway + * ✅ 实盘交易对接 + * ✅ 数据源对接 + * ✅ 事件源对接 + +* Tushare 对接 + + * ✅ 数据源对接 --> `rqalpha_mod_sys_stock_realtime`_ + * ✅ 合成分钟线 --> `rqalpha_mod_tushare`_ + +* Tick 相关支持 + + * ✅ TICK 相关事件支持 --> `EVENT.PRE_TICK` | `EVENT.TICK` | `EVENT.POST_TICK` + * ✅ handle_tick 函数支持 + +* Mod Manager --> `通过 Mod 扩展 RQAlpha`_ + + * ✅ 定义 Mod 编写规范, workflow && Doc + * ✅ 提供 Mod Demo && Tutorial + * ✅ 提供 `rqalpha install xx_mod` 等命令 加载第三方 Mod + +* Third-party Tools Integration + + * ✅ 集成 Sentry --> `sentry`_ + +* i18n + + * 🚫 English Doc + +* Support Options + + * 🚫 OptionAccount + * 🚫 OptionPosition + +* Support BitCoin + + * 🚫 BitcoinAccount + * 🚫 BitcoinPosition + + 加入开发 ============================ @@ -80,43 +162,50 @@ RQAlpha API * `基本概念`_ * `RQAlpha 基于 Mod 进行扩展`_ -其他 -============================ - -* `FAQ`_ -* `History`_ -* `TODO`_ - 获取帮助 ============================ 关于RQAlpha的任何问题可以通过以下途径来获取帮助 * 查看 `FAQ`_ 页面找寻常见问题及解答 -* 可以通过 `索引`_ 或者 `搜索`_ 来查找特定问题 +* 可以通过 `索引`_ 或者使用搜索功能来查找特定问题 * 在 `Github Issue`_ 中提交issue * RQAlpha 交流群「487188429」 .. _Github Issue: https://github.com/ricequant/rqalpha/issues .. _Ricequant: https://www.ricequant.com/algorithms -.. _RQAlpha 文档: http://rqalpha.readthedocs.io/zh_CN/stable/ +.. _RQAlpha 文档: http://rqalpha.readthedocs.io/zh_CN/latest/ .. _Ricequant 文档: https://www.ricequant.com/api/python/chn .. _Ricequant 社区: https://www.ricequant.com/community/category/all/ -.. _FAQ: http://rqalpha.readthedocs.io/zh_CN/stable/faq.html -.. _索引: http://rqalpha.readthedocs.io/zh_CN/stable/genindex.html - -.. _RQAlpha 介绍: http://rqalpha.readthedocs.io/zh_CN/stable/intro/overview.html -.. _安装指南: http://rqalpha.readthedocs.io/zh_CN/stable/intro/install.html -.. _10分钟学会 RQAlpha: http://rqalpha.readthedocs.io/zh_CN/stable/intro/tutorial.html -.. _策略示例: http://rqalpha.readthedocs.io/zh_CN/stable/intro/examples.html - -.. _参数配置: http://rqalpha.readthedocs.io/zh_CN/stable/api/config.html -.. _API: http://rqalpha.readthedocs.io/zh_CN/stable/api/base_api.html - -.. _如何贡献代码: http://rqalpha.readthedocs.io/zh_CN/stable/development/make_contribute.html -.. _基本概念: http://rqalpha.readthedocs.io/zh_CN/stable/development/basic_concept.html -.. _RQAlpha 基于 Mod 进行扩展: http://rqalpha.readthedocs.io/zh_CN/stable/development/mod.html -.. _History: http://rqalpha.readthedocs.io/zh_CN/stable/history.html -.. _TODO: http://rqalpha.readthedocs.io/zh_CN/stable/todo.html - +.. _FAQ: http://rqalpha.readthedocs.io/zh_CN/latest/faq.html +.. _索引: http://rqalpha.readthedocs.io/zh_CN/latest/genindex.html + +.. _RQAlpha 介绍: http://rqalpha.readthedocs.io/zh_CN/latest/intro/overview.html +.. _安装指南: http://rqalpha.readthedocs.io/zh_CN/latest/intro/install.html +.. _10分钟学会 RQAlpha: http://rqalpha.readthedocs.io/zh_CN/latest/intro/tutorial.html +.. _策略示例: http://rqalpha.readthedocs.io/zh_CN/latest/intro/examples.html + +.. _参数配置: http://rqalpha.readthedocs.io/zh_CN/latest/api/config.html +.. _API: http://rqalpha.readthedocs.io/zh_CN/latest/api/base_api.html + +.. _如何贡献代码: http://rqalpha.readthedocs.io/zh_CN/latest/development/make_contribute.html +.. _基本概念: http://rqalpha.readthedocs.io/zh_CN/latest/development/basic_concept.html +.. _RQAlpha 基于 Mod 进行扩展: http://rqalpha.readthedocs.io/zh_CN/latest/development/mod.html +.. _History: http://rqalpha.readthedocs.io/zh_CN/latest/history.html +.. _TODO: https://github.com/ricequant/rqalpha/blob/master/TODO.md +.. _develop 分支: https://github.com/ricequant/rqalpha/tree/develop +.. _master 分支: https://github.com/ricequant/rqalpha +.. _rqalpha_mod_sys_stock_realtime: https://github.com/ricequant/rqalpha/blob/master/rqalpha/mod/rqalpha_mod_sys_stock_realtime/README.rst +.. _rqalpha_mod_tushare: https://github.com/ricequant/rqalpha-mod-tushare +.. _通过 Mod 扩展 RQAlpha: http://rqalpha.io/zh_CN/latest/development/mod.html +.. _sys_analyser: https://github.com/ricequant/rqalpha/blob/master/rqalpha/mod/rqalpha_mod_sys_analyser/README.rst +.. _sys_funcat: https://github.com/ricequant/rqalpha/blob/master/rqalpha/mod/rqalpha_mod_sys_funcat/README.rst +.. _sys_progress: https://github.com/ricequant/rqalpha/blob/master/rqalpha/mod/rqalpha_mod_sys_progress/README.rst +.. _sys_risk: https://github.com/ricequant/rqalpha/blob/master/rqalpha/mod/rqalpha_mod_sys_risk/README.rst +.. _sys_simulation: https://github.com/ricequant/rqalpha/blob/master/rqalpha/mod/rqalpha_mod_sys_simulation/README.rst +.. _sys_stock_realtime: https://github.com/ricequant/rqalpha/blob/master/rqalpha/mod/rqalpha_mod_sys_stock_realtime/README.rst +.. _vnpy: https://github.com/ricequant/rqalpha-mod-vnpy +.. _sentry: https://github.com/ricequant/rqalpha-mod-sentry +.. _tushare: https://github.com/ricequant/rqalpha-mod-tushare +.. _shipane: https://github.com/wh1100717/rqalpha-mod-ShiPanE diff --git a/TODO.rst b/TODO.rst deleted file mode 100644 index 4886c39f9..000000000 --- a/TODO.rst +++ /dev/null @@ -1,49 +0,0 @@ -=============================== -TODO -=============================== - -* 数据源 - - * tushare 数据对接 - * vnpy 数据对接 - * 量化掘金 数据对接 - -* 事件源 - - * Event 标准化、workflow && Doc - * 股票实盘 实盘事件源对接 - * 期货实盘 实盘事件源对接(基于Tick) - -* API 扩展 - - * API Extension Doc - * 支持 期货的 on_tick - -* 实盘交易接口 - - * vnpy 交易对接 - * easytrader 交易对接 - * 量化掘金 交易对接 - -* 风控模块 - - * 事前风控 - * 事中风控 - * 事后风控 - -* RESTful Server - - * 数据格式规范 - * 存储格式规范 - * HTTP 接口定义 - * 前端页面 - -* Mod Manager - - * 定义 Mod 编写规范, workflow && Doc - * 提供 Mod Demo && Tutorial - * 提供 `rqalpha install xx_mod` 等命令 加载第三方 Mod - -* i18n - - * English Doc \ No newline at end of file diff --git a/docs/Makefile b/docs/Makefile index 8db6d3e9e..569e0d6c9 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -2,7 +2,7 @@ # # You can set these variables from the command line. -PYTHON = python3 +PYTHON = python SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = diff --git a/docs/source/_static/pycharm_1.png b/docs/source/_static/pycharm_1.png new file mode 100644 index 000000000..cdfdc325c Binary files /dev/null and b/docs/source/_static/pycharm_1.png differ diff --git a/docs/source/_static/rqalpha_plus.png b/docs/source/_static/rqalpha_plus.png new file mode 100644 index 000000000..2f47cec0e Binary files /dev/null and b/docs/source/_static/rqalpha_plus.png differ diff --git a/docs/source/_static/virtualBox_1.png b/docs/source/_static/virtualBox_1.png new file mode 100644 index 000000000..ef852a805 Binary files /dev/null and b/docs/source/_static/virtualBox_1.png differ diff --git a/docs/source/api/base_api.rst b/docs/source/api/base_api.rst index 9a4faeeb5..8e1385bbe 100644 --- a/docs/source/api/base_api.rst +++ b/docs/source/api/base_api.rst @@ -560,8 +560,8 @@ concept - 概念股票列表 * 得到一个概念的股票列表: - .. code-block:: python3 - :linenos: + .. code-block:: python3 + :linenos: concept('民营医院') #[Out] @@ -573,8 +573,8 @@ concept - 概念股票列表 * 得到某几个概念的股票列表: - .. code-block:: python3 - :linenos: + .. code-block:: python3 + :linenos: concept('民营医院', '国企改革') #[Out] @@ -630,30 +630,12 @@ get_yield_curve - 收益率曲线 is_suspended - 全天停牌判断 ------------------------------------------------------ -.. py:function:: is_suspended(order_book_id, count) - - 判断某只股票是否全天停牌。 - - :param str order_book_id: 某只股票的代码或股票代码列表,可传入单只股票的order_book_id, symbol - - :param int count: 回溯获取的数据个数。默认为当前能够获取到的最近的数据 - - :return: count为1时 `bool`; count>1时 `pandas.DataFrame` +.. autofunction:: is_suspended(order_book_id) is_st_stock - ST股判断 ------------------------------------------------------ -.. py:function:: is_st_stock(order_book_id, count=1) - - 判断一只或多只股票在一段时间内是否为ST股(包括ST与*ST)。 - - ST股是有退市风险因此风险比较大的股票,很多时候您也会希望判断自己使用的股票是否是'ST'股来避开这些风险大的股票。另外,我们目前的策略比赛也禁止了使用'ST'股。 - - :param str order_book_id: 某只股票的代码或股票代码列表,可传入单只股票的order_book_id, symbol - - :param int count: 回溯获取的数据个数。默认为当前能够获取到的最近的数据 - - :return: count为1时 `bool`; count>1时 `pandas.DataFrame` +.. autofunction:: is_st_stock(order_book_id) 其他方法 ====================================================== @@ -717,30 +699,32 @@ Order :show-inheritance: :inherited-members: -MixedPortfolio +Portfolio ------------------------------------------------------ -.. module:: rqalpha.model.account.mixed_account -.. autoclass:: MixedPortfolio +.. module:: rqalpha.model.portfolio + +.. autoclass:: Portfolio :members: :show-inheritance: :inherited-members: - -StockPortfolio +StockAccount ------------------------------------------------------ -.. module:: rqalpha.model.portfolio.stock_portfolio -.. autoclass:: StockPortfolio +.. module:: rqalpha.model.account.stock_account + +.. autoclass:: StockAccount :members: :show-inheritance: :inherited-members: -FuturePortfolio +FutureAccount ------------------------------------------------------ -.. module:: rqalpha.model.portfolio.future_portfolio -.. autoclass:: FuturePortfolio +.. module:: rqalpha.model.account.future_account + +.. autoclass:: FutureAccount :members: :show-inheritance: :inherited-members: diff --git a/docs/source/api/config.rst b/docs/source/api/config.rst index 168405f5e..71d69fd94 100644 --- a/docs/source/api/config.rst +++ b/docs/source/api/config.rst @@ -11,8 +11,6 @@ * 想将回测好的策略直接用于实盘交易 * 不同的品种设置不同的风控标准 -等等。 - 在设计 RQAlpha API 的时候,考虑到以上随时变化的需求,将这部分需要以参数方式的配置严格从代码层面剥离。同一份策略代码,通过启动策略时传入不同的参数来实现完全不同的策略开发、风控、运行和调优等的功能。 通过启动策略传参的方式 @@ -27,30 +25,20 @@ -f `- -` strategy-file 启动的策略文件路径 -s `- -` start-date 回测起始日期 -e `- -` end-date 回测结束日期(如果是实盘,则忽略该配置) --r `- -` rid 可以指定回测的唯一ID,用户区分多次回测的结果 --i `- -` init-cash [Deprecated]股票起始资金,不建议使用该参数,使用 :code:`--stock-starting-cash` 代替。 --o `- -` output-file 指定回测结束时将回测数据输出到指定文件中 -sc `- -` stock-starting-cash 股票起始资金,默认为0 -fc `- -` future-starting-cash 期货起始资金,默认为0 -bm `- -` benchmark Benchmark,如果不设置,默认没有基准参照 --sp `- -` slippage 设置滑点 --cm `- -` commission-multiplier 设置手续费乘数,默认为1 -mm `- -` margin-multiplier 设置保证金乘数,默认为1 +-st `- -` security 设置 -st `- -` strategy-type 设置策略类型,目前支持 :code:`stock` (股票策略)、:code:`future` (期货策略)及 :code:`stock_future` (混合策略) -fq `- -` frequency 目前支持 :code:`1d` (日线回测) 和 :code:`1m` (分钟线回测),如果要进行分钟线,请注意是否拥有对应的数据源,目前开源版本是不提供对应的数据源的 --me `- -` match-engine 启用的回测引擎,目前支持 :code:`current_bar` (当前Bar收盘价撮合) 和 :code:`next_bar` (下一个Bar开盘价撮合) -rt `- -` run-type 运行类型,:code:`b` 为回测,:code:`p` 为模拟交易, :code:`r` 为实盘交易 N/A `- -` resume 在模拟交易和实盘交易中,RQAlpha支持策略的pause && resume,该选项表示开启 resume 功能 N/A `- -` handle-split 开启自动处理, 默认不开启 N/A `- -` not-handle-split 不开启自动处理, 默认不开启 -N/A `- -` risk-grid 开启Alpha/Beta 等风险指标的实时计算,默认开启 -N/A `- -` no-risk-grid 不开启Alpha/Beta 等风险指标的实时计算,默认开启 +-l `- -` log-level 选择日期的输出等级,有 :code:`verbose` | code:`info` | :code:`warning` | :code:`error` 等选项,您可以通过设置 :code:`verbose` 来查看最详细的日志,或者设置 :code:`error` 只查看错误级别的日志输出 +N/A `- -` locale 选择语言, 支持 :code:`en` | :code:`cn` N/A `- -` disable-user-system-log 关闭用户策略产生的系统日志(比如订单未成交等提示) --l `- -` log-level 选择日期的输出等级,有 :code:`verbose` | :code:`info` | :code:`warning` | :code:`error` 等选项,您可以通过设置 :code:`verbose` 来查看最详细的日志,或者设置 :code:`error` 只查看错误级别的日志输出 --p `- -` plot 在回测结束后,查看图形化的收益曲线 -N/A `- -` no-plot 在回测结束后,不查看图形化的收益曲线 -N/A `- -` progress 开启命令行显示回测进度条 -N/A `- -` no-progress 关闭命令行查看回测进度 N/A `- -` enable-profiler 启动策略逐行性能分析,启动后,在回测结束,会打印策略的运行性能分析报告,可以看到每一行消耗的时间 N/A `- -` config 设置配置文件路径 -mc `- -` mod-config 配置 mod ,支持多个。:code:`-mc funcat_api.enabled True` 就可以启动一个 mod @@ -58,12 +46,33 @@ N/A `- -` config 设置配置文件路径 对于 mod 的参数传递,可以使用 :code:`-mc` 传递 mod 设置。 -- :code:`-mc simple_stock_realtime_trade.enabled True` 启动 simple_stock_realtime_trade 这个 mod。 -- :code:`-mc simple_stock_realtime_trade.fps 60` 设置 simple_stock_realtime_trade 的 fps 参数为 60。 +- :code:`-mc sys_stock_realtime.enabled True` 启动 sys_stock_realtime 这个 mod。 +- :code:`-mc sys_stock_realtime.fps 60` 设置 sys_stock_realtime 的 fps 参数为 60。 .. code-block:: python3 - rqalpha run -rt p -fq 1m -f strategy.py -sc 100000 -mc simple_stock_realtime_trade.enabled True -mc simple_stock_realtime_trade.fps 60 + rqalpha run -rt p -fq 1m -f strategy.py -sc 100000 -mc sys_stock_realtime.enabled True -mc sys_stock_realtime.fps 60 + +系统内置 Mod 也提供了启动参数的扩展: + +=========== ============================= ============================================================================== +参数名缩写 参数名全称 说明 +=========== ============================= ============================================================================== +N/A `- -` report [sys_analyser]保存交易详情 +-o `- -` output-file [sys_analyser]指定回测结束时将回测数据输出到指定文件中 +-p `- -` plot [sys_analyser]在回测结束后,查看图形化的收益曲线 +N/A `- -` no-plot [sys_analyser]在回测结束后,不查看图形化的收益曲线 +N/A `- -` plot-save [sys_analyser]将plot的收益图以指定文件路径保存 +N/A `- -` progress [sys_progress]开启命令行显示回测进度条 +N/A `- -` no-progress [sys_progress]关闭命令行查看回测进度 +N/A `- -` short-stock [sys_risk]允许股票卖空 +N/A `- -` no-short-stock [sys_risk]不允许股票卖空 +N/A `- -` signal [sys_simulation]开启信号模式,不进行撮合,直接成交 +-sp `- -` slippage [sys_simulation]设置滑点 +-cm `- -` commission-multiplier [sys_simulation]设置手续费乘数,默认为1 +-me `- -` match-engine [sys_simulation]启用的回测引擎,目前支持 :code:`current_bar` (当前Bar收盘价撮合) 和 :code:`next_bar` (下一个Bar开盘价撮合) +-r `- -` rid [sys_simulation]可以指定回测的唯一ID,用户区分多次回测的结果 +=========== ============================= ============================================================================== .. _api-config-file: @@ -72,19 +81,22 @@ N/A `- -` config 设置配置文件路径 除了在启动策略的时候传入参数,还可以通过指定配置文件的方式来进行参数的配置, 配置文件的配置信息优先级低于启动参数,也就是说启动参数会覆盖配置文件的配置项,配置文件的格式如下: -注: 如果没有指定 `config.yml`, RQAlpha 在运行时会自动在 `~/.rqalpha/` 文件夹下创建 `config.yml` 文件作为默认配置文件。您也可以使用 `$ rqalpha generate_config` 来生成一份默认的配置文件。 +注1: 如果没有指定 `config.yml`, RQAlpha 在运行时会自动在当前目录下寻找 `config.yml` 文件作为用户配置文件,如果没有寻找到,则按照默认配置运行。 + +注2: 您只需要指定需要修改的配置信息,您没有指定的部分,会按照默认配置项来运行。 + +您也可以使用 `$ rqalpha generate_config` 来生成一份包含所有系统配置的默认的配置文件。 + .. code-block:: bash :linenos: - version: 0.1.0 + version: 0.1.5 # 白名单,设置可以直接在策略代码中指定哪些模块的配置项目 whitelist: [base, extra, validator, mod] base: - # 可以指定回测的唯一ID,用户区分多次回测的结果 - run_id: 9999 # 数据源所存储的文件路径 data_bundle_path: ~ # 启动的策略文件路径 @@ -97,22 +109,16 @@ N/A `- -` config 设置配置文件路径 stock_starting_cash: 0 # 期货起始资金,默认为0 future_starting_cash: 0 - # 设置策略类型,目前支持 `stock` (股票策略)、`future` (期货策略)及 `stock_future` (混合策略) - strategy_type: stock + # 设置策略可交易品种,目前支持 `stock` (股票策略)、`future` (期货策略) + securities: [stock] + # 设置保证金乘数,默认为1 + margin_multiplier: 1 # 运行类型,`b` 为回测,`p` 为模拟交易, `r` 为实盘交易。 run_type: b # 目前支持 `1d` (日线回测) 和 `1m` (分钟线回测),如果要进行分钟线,请注意是否拥有对应的数据源,目前开源版本是不提供对应的数据源的。 frequency: 1d - # 启用的回测引擎,目前支持 `current_bar` (当前Bar收盘价撮合) 和 `next_bar` (下一个Bar开盘价撮合) - matching_type: current_bar # Benchmark,如果不设置,默认没有基准参照。 benchmark: ~ - # 设置滑点 - slippage: 0 - # 设置手续费乘数,默认为1 - commission_multiplier: 1 - # 设置保证金乘数,默认为1 - margin_multiplier: 1 # 在模拟交易和实盘交易中,RQAlpha支持策略的pause && resume,该选项表示开启 resume 功能 resume_mode: false # 在模拟交易和实盘交易中,RQAlpha支持策略的pause && resume,该选项表示开启 persist 功能呢, @@ -134,63 +140,13 @@ N/A `- -` config 设置配置文件路径 # enable_profiler: 是否启动性能分析 enable_profiler: false is_hold: false + locale: zh_Hans_CN validator: # cash_return_by_stock_delisted: 开启该项,当持仓股票退市时,按照退市价格返还现金 cash_return_by_stock_delisted: false # close_amount: 在执行order_value操作时,进行实际下单数量的校验和scale,默认开启 close_amount: true - # bar_limit: 在处于涨跌停时,无法买进/卖出,默认开启 - bar_limit: true - - - mod: - # 回测 / 模拟交易 支持 Mod - simulation: - lib: 'rqalpha.mod.simulation' - enabled: true - priority: 100 - # 技术分析API - funcat_api: - lib: 'rqalpha.mod.funcat_api' - enabled: false - priority: 200 - # 开启该选项,可以在命令行查看回测进度 - progress: - lib: 'rqalpha.mod.progress' - enabled: false - priority: 400 - # 接收实时行情运行 - simple_stock_realtime_trade: - lib: 'rqalpha.mod.simple_stock_realtime_trade' - persist_path: "./persist/strategy/" - fps: 3 - enabled: false - priority: 500 - # 渐进式输出运行结果 - progressive_output_csv: - lib: 'rqalpha.mod.progressive_output_csv' - enabled: false - output_path: "./" - priority: 600 - risk_manager: - lib: 'rqalpha.mod.risk_manager' - enabled: true - priority: 700 - # available_cash: 查可用资金是否充足,默认开启 - available_cash: true - # available_position: 检查可平仓位是否充足,默认开启 - available_position: true - analyser: - priority: 100 - enabled: true - lib: 'rqalpha.mod.analyser' - record: true - output_file: ~ - plot: ~ - plot_save_file: ~ - report_save_path: ~ - 通过策略代码的方式 ------------------------------------------------------ @@ -229,23 +185,25 @@ N/A `- -` config 设置配置文件路径 __config__ = { "base": { - "strategy_type": "future", + "securities": ["future"], "start_date": "2015-01-09", "end_date": "2015-03-09", "frequency": "1d", - "matching_type": "next_bar", "future_starting_cash": 1000000, - "commission_multiplier": 0.01, "benchmark": None, }, "extra": { "log_level": "error", }, "mod": { - "progress": { + "sys_progress": { "enabled": True, - "priority": 400, + "show": True, }, + "sys_simulation": { + "commission_multiplier": 0.01, + "matching_type": "next_bar", + } }, } @@ -279,6 +237,6 @@ N/A `- -` config 设置配置文件路径 优先级 ------------------------------------------------------ -如果用户不指定 :code:`config.yml`, RQAlpha 会使用默认的 :code:`config.yml` 来配置所有参数的默认项,指定了配置文件,则不再使用默认配置文件,所以相对来说,:code:`config.yml` 的配置方式优先级是最低的。 +RQAlpha 在启动时会读取 `~/.rqalpha/config.yml` 作为用户配置文件,用于覆盖默认配置信息,您也可以通过 `--config` 来指定用户配置文件路径。 -策略代码中配置优先级 > 启动策略命令行传参 > 指定 :code:`config.yml` 文件 > 默认 :code:`config.yml` 文件 +策略代码中配置优先级 > 启动策略命令行传参 > 用户配置文件 > 系统配置文件 diff --git a/docs/source/development/basic_concept.rst b/docs/source/development/basic_concept.rst index 026599a19..5081ed103 100644 --- a/docs/source/development/basic_concept.rst +++ b/docs/source/development/basic_concept.rst @@ -47,6 +47,12 @@ Broker .. autoclass:: AbstractBroker :members: +PriceBoarder +------------------ + +.. autoclass:: AbstractPriceBoard + :members: + Mod ------------------ diff --git a/docs/source/development/data_source.rst b/docs/source/development/data_source.rst new file mode 100644 index 000000000..dd156d0f2 --- /dev/null +++ b/docs/source/development/data_source.rst @@ -0,0 +1,511 @@ +.. _development-data-source: + +================== +扩展数据源 +================== + +在程序化交易的过程中,数据的获取是非常重要的一个环节,而数据又包含很多种不同类型的数据,有行情数据、财务数据、指标因子数据以及自定义类型数据。 + +在实际交易过程中,对接数据源主要分为两种: + +* 增加自有数据源 + + * 策略中直接读取自有数据 + * 在策略中 `import` 自定义模块 + * 扩展 API 实现自有数据的读取 + +* 替换已有数据源 + + * 基础数据 + * 行情数据 + +增加自有数据源 +==================================== + +策略中直接读取自有数据 +------------------------------------ + +RQAlpha 不限制本地运行的策略调使用哪些库,因此您可以直接在策略中读取文件、访问数据库等,但需要关注如下两个注意事项: + +* 请在 :code:`init`, :code:`before_trading`, :code:`handle_bar`, :code:`handle_tick`, :code:`after_trading` 等函数中读取自有数据,而不要在函数外执行数据获取的代码,否则可能会产生异常。 +* RQAlpha 是读取策略代码并执行的,因此实际当前路径是运行 `rqalpha` 命令的路径,策略使用相对路径容易产生异常。如果您需要根据策略路径来定位相对路径可以通过 :code:`context.config.base.strategy_file` 来获取策略路径,从而获取相对策略文件的其他路径,具体使用方式请看下面的示例代码。 + +`read_csv_as_df `_ + +.. code-block:: python3 + + from rqalpha.api import * + + + def read_csv_as_df(csv_path): + # 通过 pandas 读取 csv 文件,并生成 DataFrame + import pandas as pd + data = pd.read_csv(csv_path) + return data + + + def init(context): + import os + # 获取当前运行策略的文件路径 + strategy_file_path = context.config.base.strategy_file + # 根据当前策略的文件路径寻找到相对路径为 "../IF1706_20161108.csv" 的 csv 文件 + csv_path = os.path.join(os.path.dirname(strategy_file_path), "../IF1706_20161108.csv") + # 读取 csv 文件并生成 df + IF1706_df = read_csv_as_df(csv_path) + # 传入 context 中 + context.IF1706_df = IF1706_df + + + def before_trading(context): + # 通过context 获取在 init 阶段读取的 csv 文件数据 + logger.info(context.IF1706_df) + + + def handle_bar(context, bar): + pass + + + __config__ = { + "base": { + "securities": "future", + "start_date": "2015-01-09", + "end_date": "2015-01-10", + "frequency": "1d", + "matching_type": "current_bar", + "future_starting_cash": 1000000, + "benchmark": None, + }, + "extra": { + "log_level": "verbose", + }, + } + +在策略中 import 自定义模块 +------------------------------------ + +如果您定义了自定义模块,希望在策略中引用,只需要在初始化的时候将模块对应的路径添加到 :code:`sys.path` 即可,但需要关注如下两个注意事项: + +* 如果没有特殊原因,请在 :code:`init` 阶段添加 :code:`sys.path` 路径。 +* 如果您的自定义模块是基于策略策略的相对路径,则需要在 :code:`init` 函数中通过 :code:`context.config.base.strategy_file` 获取到策略路径,然后再添加到 :code:`sys.path` 中。 +* RQAlpha 是读取策略代码并执行的,因此实际当前路径是执行 `rqalpha` 命令的路径,避免使用相对路径。 + +`get_csv_module `_ + + +.. code-block:: python3 + + import os + + + def read_csv_as_df(csv_path): + import pandas as pd + data = pd.read_csv(csv_path) + return data + + + def get_csv(): + csv_path = os.path.join(os.path.dirname(__file__), "../IF1706_20161108.csv") + return read_csv_as_df(csv_path) + +`import_get_csv_module `_ + +.. code-block:: python3 + + from rqalpha.api import * + + + def init(context): + import os + import sys + strategy_file_path = context.config.base.strategy_file + sys.path.append(os.path.realpath(os.path.dirname(strategy_file_path))) + + from get_csv_module import get_csv + + IF1706_df = get_csv() + context.IF1706_df = IF1706_df + + + def before_trading(context): + logger.info(context.IF1706_df) + + + __config__ = { + "base": { + "securities": "future", + "start_date": "2015-01-09", + "end_date": "2015-01-10", + "frequency": "1d", + "matching_type": "current_bar", + "future_starting_cash": 1000000, + "benchmark": None, + }, + "extra": { + "log_level": "verbose", + }, + } + +扩展 API 实现自有数据的读取 +------------------------------------ + +我们通过创建一个 Mod 来实现扩展 API,启动策略时,只需要开启该 Mod, 对应的扩展 API 便可以生效,在策略中直接使用。 + +`rqalpha_mod_extend_api_demo `_ + +.. code-block:: python3 + + import os + import pandas as pd + from rqalpha.interface import AbstractMod + + + __config__ = { + "csv_path": None + } + + + def load_mod(): + return ExtendAPIDemoMod() + + + class ExtendAPIDemoMod(AbstractMod): + def __init__(self): + # 注入API 一定要在初始化阶段,否则无法成功注入 + self._csv_path = None + self._inject_api() + + def start_up(self, env, mod_config): + self._csv_path = os.path.abspath(os.path.join(os.path.dirname(__file__), mod_config.csv_path)) + + def tear_down(self, code, exception=None): + pass + + def _inject_api(self): + from rqalpha import export_as_api + from rqalpha.execution_context import ExecutionContext + from rqalpha.const import EXECUTION_PHASE + + @export_as_api + @ExecutionContext.enforce_phase(EXECUTION_PHASE.ON_INIT, + EXECUTION_PHASE.BEFORE_TRADING, + EXECUTION_PHASE.ON_BAR, + EXECUTION_PHASE.AFTER_TRADING, + EXECUTION_PHASE.SCHEDULED) + def get_csv_as_df(): + data = pd.read_csv(self._csv_path) + return data + + +如上代码,我们定义了 :code:`rqalpha_mod_extend_api_demo` Mod,该 Mod 接受一个参数: :code:`csv_path`, 其会转换为基于 Mod 的相对路径来获取对应的 csv 地址。 + +在该Mod中通过 :code:`_inject_api` 方法,定义了 :code:`get_csv_ad_df` 函数,并通过 :code:`from rqalpha import export_as_api` 装饰器完成了 API 的注入。 + +如果想限制扩展API所运行使用的范围,可以通过 :code:`ExecutionContext.enforce_phase` 来控制. + +接下来我们看一下如何在策略中使用该扩展API: + +`test_extend_api `_ + +.. code-block:: python3 + + from rqalpha.api import * + + + def init(context): + IF1706_df = get_csv_as_df() + context.IF1706_df = IF1706_df + + + def before_trading(context): + logger.info(context.IF1706_df) + + + __config__ = { + "base": { + "securities": "future", + "start_date": "2015-01-09", + "end_date": "2015-01-10", + "frequency": "1d", + "matching_type": "current_bar", + "future_starting_cash": 1000000, + "benchmark": None, + }, + "extra": { + "log_level": "verbose", + }, + "mod": { + "extend_api_demo": { + "enabled": True, + "lib": "rqalpha.examples.extend_api.rqalpha_mod_extend_api_demo", + "csv_path": "../IF1706_20161108.csv" + } + } + } + +如上述代码,首先配置信息中添加 `extend_api_demo` 对应的配置 + +* :code:`enabled`: True 表示开启该 Mod +* :code:`lib`: 指定该 Mod 对应的加载位置(rqlalpha 会自动去寻找 `rqalpha_mod_xxx` 对应的库,如果该库已经通过 `pip install` 安装,则无需显式指定 lib) +* :code:`csv_path`: 指定 csv 所在位置 + +至此,我们就可以直接在策略中使用 `get_csv_as_df` 函数了。 + +替换已有数据源 +==================================== + +基础数据 +------------------------------------ + +通过 `$ rqalpha update_bundle` 下载的数据有如下文件: + +.. code-block:: bash + + $ cd ~/.rqalpha/bundle & tree -A -d -L 1 + + . + ├── adjusted_dividends.bcolz + ├── funds.bcolz + ├── futures.bcolz + ├── indexes.bcolz + ├── original_dividends.bcolz + ├── st_stock_days.bcolz + ├── stocks.bcolz + ├── suspended_days.bcolz + ├── trading_dates.bcolz + └── yield_curve.bcolz + +目前基础数据,比如 `Instruments`, `st_stocks`, `suspended_days`, `trading_dates` 都是全量数据,并且可以通过 `$ rqalpha update_bundle` 每天更新,因此没有相应的显式接口可以对其进行替换。 + +您如果想要替换,可以使用如下两种方式: + +* 写脚本将自有数据源按照相同的格式生成对应的文件,并进行文件替换。 +* 实现 `AbstractDataSource `_ 对应的接口,您可以继承 `BaseDataSource `_ 并 override 对应的接口即可完成替换。 + + +行情数据 - 五十行代码接入 tushare 行情数据 +------------------------------------------ + +RQAlpha 支持自定义扩展数据源。得益于 RQAlpha 的 mod 机制,我们可以很方便的替换或者扩展默认的数据接口。 + +RQAlpha 将提供给用户的数据 API 和回测所需的基础数据抽象成了若干个函数,这些函数被封于 :class:`~DataSource` 类中,并将在需要的时候被调用。简单的说,我们只需要在自己定义的 mod 中扩展或重写默认的 :class:`~DataSource` 类,就可以替换掉默认的数据源,接入自有数据。 + +:class:`~DataSource` 类的完整文档,请参阅 :ref:`development-basic-concept`。下面将用一个简单的例子,为大家介绍如何用五十行左右的代码将默认的行情数据替换为 `TuShare`_ 的行情数据。 + +.. _TuShare: http://tushare.org + +TushareKDataMod 的作用是使用 tushare 提供的k线数据替换 data_bundle 中的行情数据,由于目前 tushare 仅仅开放了日线、周线和月线的历史数据,所以该 mod 仍然只能提供日回测的功能,若未来 tushare 开放了60分钟或5分钟线的历史数据,只需进行简单修改,便可通过该 mod 使 RQAlpha 实现5分钟回测。 + +开工前,首先熟悉一下用到的 tushare 的k线接口,接口如下: + +.. code-block:: python3 + + get_k_data(code, ktype='D', autype='qfq', index=False, start=None, end=None) + + +如上文所说,我们要做的主要就是扩展或重写默认的 DataSource 类。在此处,我们选择建立一个新的 DataSource 类,该类继承于默认的 :class:`~BaseDataSource` 类。 + +这样做的好处在于我们不必重写 DataSource 需要实现的所有函数,而可以只实现与我们想替换的数据源相关的函数,其他数据的获取直接甩锅给父类 :class:`~BaseDataSource` 。 + +与行情数据密切相关的主要有以下三个函数: + +* :code:`current_snapshot(instrument, frequency, dt)` +* :code:`get_bar(instrument, dt, frequency)` +* :code:`history_bars(instrument, bar_count, frequency, fields, dt, skip_suspended=True)` +* :code:`available_data_range(frequency)` + +经过查看 :class:`DataProxy` 类的源代码,可以发现,提供日级别数据的 DataSource 类不需要实现 :code:`current_snapshot` 函数,所以我们只关注后三个函数的实现。 + +:code:`get_bar` 和 :code:`history_bars` 函数实现的主要功能都是传入 instrument 对象,从 tushare 获取指定时间或时间段的 bar 数据。我们把这一过程抽象为一个函数: + +.. code-block:: python3 + + class TushareKDataSource(BaseDataSource): + + ... + + @staticmethod + def get_tushare_k_data(instrument, start_dt, end_dt): + + # 首先获取 order_book_id 并将其转换为 tushare 所能识别的 code + order_book_id = instrument.order_book_id + code = order_book_id.split(".")[0] + + # tushare 行情数据目前仅支持股票和指数,并通过 index 参数进行区分 + if instrument.type == 'CS': + index = False + elif instrument.type == 'INDX': + index = True + else: + return None + + # 调用 tushare 函数,注意 datetime 需要转为指定格式的 str + return ts.get_k_data(code, index=index, start=start_dt.strftime('%Y-%m-%d'), end=end_dt.strftime('%Y-%m-%d')) + + +现在实现 :code:`get_bar` 函数: + +.. code-block:: python3 + + class TushareKDataSource(BaseDataSource): + + ... + + def get_bar(self, instrument, dt, frequency): + + # tushare k线数据暂时只能支持日级别的回测,其他情况甩锅给默认数据源 + if frequency != '1d': + return super(TushareKDataSource, self).get_bar(instrument, dt, frequency) + + # 调用上边写好的函数获取k线数据 + bar_data = self.get_tushare_k_data(instrument, dt, dt) + + # 遇到获取不到数据的情况,同样甩锅;若有返回值,注意转换格式。 + if bar_data is None or bar_data.empty: + return super(TushareKDataSource, self).get_bar(instrument, dt, frequency) + else: + return bar_data.iloc[0].to_dict() + + +然后是硬骨头 :code:`history_bars` 函数: + +.. code-block:: python3 + + class TushareKDataSource(BaseDataSource): + + ... + + def history_bars(self, instrument, bar_count, frequency, fields, dt, skip_suspended=True): + # tushare 的k线数据未对停牌日期做补齐,所以遇到不跳过停牌日期的情况我们先甩锅。有兴趣的开发者欢迎提交代码补齐停牌日数据。 + if frequency != '1d' or not skip_suspended: + return super(TushareKDataSource, self).history_bars(instrument, bar_count, frequency, fields, dt, skip_suspended) + + # 参数只提供了截止日期和天数,我们需要自己找到开始日期 + # 获取交易日列表,并拿到截止日期在列表中的索引,之后再算出开始日期的索引 + start_dt_loc = self.get_trading_calendar().get_loc(dt.replace(hour=0, minute=0, second=0, microsecond=0)) - bar_count + 1 + # 根据索引拿到开始日期 + start_dt = self.get_trading_calendar()[start_dt_loc] + + # 调用上边写好的函数获取k线数据 + bar_data = self.get_tushare_k_data(instrument, start_dt, dt) + + if bar_data is None or bar_data.empty: + return super(TushareKDataSource, self).get_bar(instrument, dt, frequency) + else: + # 注意传入的 fields 参数可能会有不同的数据类型 + if isinstance(fields, six.string_types): + fields = [fields] + fields = [field for field in fields if field in bar_data.columns] + + # 这样转换格式会导致返回值的格式与默认 DataSource 中该方法的返回值格式略有不同。欢迎有兴趣的开发者提交代码进行修改。 + return bar_data[fields].as_matrix() + +最后是 :code:`available_data_range` 函数 + +.. code-block:: python3 + + class TushareKDataSource(BaseDataSource): + + ... + + def available_data_range(self, frequency): + return date(2005, 1, 1), date.today() - relativedelta(days=1) + +把以上几个函数组合起来,并加入构造函数,就完成了我们重写的 DataSource 类。完整代码如下: + +.. code-block:: python3 + + import six + import tushare as ts + from datetime import date + from dateutil.relativedelta import relativedelta + from rqalpha.data.base_data_source import BaseDataSource + + + class TushareKDataSource(BaseDataSource): + def __init__(self, path): + super(TushareKDataSource, self).__init__(path) + + @staticmethod + def get_tushare_k_data(instrument, start_dt, end_dt): + order_book_id = instrument.order_book_id + code = order_book_id.split(".")[0] + + if instrument.type == 'CS': + index = False + elif instrument.type == 'INDX': + index = True + else: + return None + + return ts.get_k_data(code, index=index, start=start_dt.strftime('%Y-%m-%d'), end=end_dt.strftime('%Y-%m-%d')) + + def get_bar(self, instrument, dt, frequency): + if frequency != '1d': + return super(TushareKDataSource, self).get_bar(instrument, dt, frequency) + + bar_data = self.get_tushare_k_data(instrument, dt, dt) + + if bar_data is None or bar_data.empty: + return super(TushareKDataSource, self).get_bar(instrument, dt, frequency) + else: + return bar_data.iloc[0].to_dict() + + def history_bars(self, instrument, bar_count, frequency, fields, dt, skip_suspended=True): + if frequency != '1d' or not skip_suspended: + return super(TushareKDataSource, self).history_bars(instrument, bar_count, frequency, fields, dt, skip_suspended) + + start_dt_loc = self.get_trading_calendar().get_loc(dt.replace(hour=0, minute=0, second=0, microsecond=0)) - bar_count + 1 + start_dt = self.get_trading_calendar()[start_dt_loc] + + bar_data = self.get_tushare_k_data(instrument, start_dt, dt) + + if bar_data is None or bar_data.empty: + return super(TushareKDataSource, self).get_bar(instrument, dt, frequency) + else: + if isinstance(fields, six.string_types): + fields = [fields] + fields = [field for field in fields if field in bar_data.columns] + + return bar_data[fields].as_matrix() + + def available_data_range(self, frequency): + return date(2005, 1, 1), date.today() - relativedelta(days=1) + + +到目前为止,我们的主要工作已经完成了。想要将我们刚刚写好的 DataSource 类投入使用,还需要将其放入一个 mod 来被 RQAlpha 加载。 + +mod 的实现如下: + +.. code-block:: python3 + + from rqalpha.interface import AbstractMod + + from .data_source import TushareKDataSource + + + class TushareKDataMode(AbstractMod): + def __init__(self): + pass + + def start_up(self, env, mod_config): + # 设置 data_source 为 TushareKDataSource 类的对象 + env.set_data_source(TushareKDataSource(env.config.base.data_bundle_path)) + + def tear_down(self, code, exception=None): + pass + + +最后的最后,添加 :code:`load_mod` 函数,该函数将被 RQAlpha 调用以加载我们刚刚写好的 mod 。 + +.. code-block:: python3 + + from .mod import TushareKDataMode + + + def load_mod(): + return TushareKDataMode() + + +至此,我们已经完成了外部行情数据的接入,剩下要做的就是在 RQAlpha 启动时传入的配置信息中开启以上 mod。 + +该 mod 只是一个简单的 demo,仍存在一些问题,例如调用 tushare 接口速度较慢,频繁调用会消耗大量时间。如能将多次调用合并,或是将接口的调用改为异步,相信能够大幅提升回测速度。 diff --git a/docs/source/development/event_source.rst b/docs/source/development/event_source.rst index 1df9b2601..9f38070b7 100644 --- a/docs/source/development/event_source.rst +++ b/docs/source/development/event_source.rst @@ -131,10 +131,10 @@ ProgressMod 需要实现的需求非常的简单:在命令行输出目前回 def tear_down(self, success, exception=None): pass - def _init(self): + def _init(self, event): pass - def _tick(): + def _tick(self, event): pass 在 :code:`_init` 函数中,初始化 :code:`progressBar`,进度条的长度为回测的总时长 @@ -149,7 +149,7 @@ ProgressMod 需要实现的需求非常的简单:在命令行输出目前回 .. code-block:: python - def _tick(self): + def _tick(self, event): self.progress_bar.update(1) 在 :code:`tear_down` 函数中,终止进度条 @@ -179,11 +179,11 @@ ProgressMod 需要实现的需求非常的简单:在命令行输出目前回 env.event_bus.add_listener(EVENT.POST_AFTER_TRADING, self._tick) env.event_bus.add_listener(EVENT.POST_SYSTEM_INIT, self._init) - def _init(self): + def _init(self, event): trading_length = len(self._env.config.base.trading_calendar) self.progress_bar = click.progressbar(length=trading_length, show_eta=False) - def _tick(self): + def _tick(self, event): self.progress_bar.update(1) def tear_down(self, success, exception=None): @@ -209,11 +209,11 @@ ProgressMod 需要实现的需求非常的简单:在命令行输出目前回 env.event_bus.add_listener(EVENT.POST_AFTER_TRADING, self._tick) env.event_bus.add_listener(EVENT.POST_SYSTEM_INIT, self._init) - def _init(self): + def _init(self, event): trading_length = len(self._env.config.base.trading_calendar) self.progress_bar = click.progressbar(length=trading_length, show_eta=False) - def _tick(self): + def _tick(self, event): self.progress_bar.update(1) def tear_down(self, success, exception=None): @@ -229,7 +229,7 @@ ProgressMod 需要实现的需求非常的简单:在命令行输出目前回 上一节讲的是如何订阅事件源,那么如何发布事件呢?其实也很简单,只需要通过 :code:`publish_event` 就可以进行事件的发布。 -RQAlpha 整个回测模块是通过 :code:`SimulationMod` 实现的,其中定义了基于Bar回测的 :code:`event_source` 和 :code:`simulation_broker`, 其中包含了 MarketEvent 和 OrderEvent 大部分事件源的定义和发布。 +RQAlpha 整个回测模块是通过 :code:`rqalpha_mod_sys_simulation` 实现的,其中定义了基于Bar回测的 :code:`event_source` 和 :code:`simulation_broker`, 其中包含了 MarketEvent 和 OrderEvent 大部分事件源的定义和发布。 我们简单来分析一下日线回测 :code:`simulation_event_source` 中 MaketEvent 相关事件的触发流程。 @@ -248,13 +248,13 @@ RQAlpha 整个回测模块是通过 :code:`SimulationMod` 实现的,其中定 dt_after_trading = date.replace(hour=15, minute=30) dt_settlement = date.replace(hour=17, minute=0) - yield Event(EVENT.BEFORE_TRADING, dt_before_trading, dt_before_trading) - yield Event(EVENT.BAR, dt_bar, dt_bar) + yield Event(EVENT.BEFORE_TRADING, calendar_dt=dt_before_trading, trading_dt=dt_before_trading) + yield Event(EVENT.BAR, calendar_dt=dt_bar, trading_dt=dt_bar) - yield Event(EVENT.AFTER_TRADING, dt_after_trading, dt_after_trading) - yield Event(EVENT.SETTLEMENT, dt_settlement, dt_settlement) + yield Event(EVENT.AFTER_TRADING, calendar_dt=dt_after_trading, trading_dt=dt_after_trading) + yield Event(EVENT.SETTLEMENT, calendar_dt=dt_settlement, trading_dt=dt_settlement) -:code:`event` 函数是一个generator, 在 SimulationMod 中主要返回 :code:`BEFORE_TRADING`, :code:`BAR`, :code:`AFTER_TRADING` 和 :code:`SETTLEMENT` 事件。RQAlpha 在接受到对应的事件后,会自动的进行相应的 `publish_event` 操作,并且会自动 publish 相关的 `PRE_` 和 `POST_` 事件。 +:code:`event` 函数是一个generator, 在 rqalpha_mod_sys_simulation 中主要返回 :code:`BEFORE_TRADING`, :code:`BAR`, :code:`AFTER_TRADING` 和 :code:`SETTLEMENT` 事件。RQAlpha 在接受到对应的事件后,会自动的进行相应的 `publish_event` 操作,并且会自动 publish 相关的 `PRE_` 和 `POST_` 事件。 而在 :code:`simulation_broker` 中可以看到,当被调用 `cancel_order` 时,会模拟撤单的执行流程,分别触发 :code:`ORDER_PENDING_CANCEL` && :code:`ORDER_CANCELLATION_PASS` 事件,并将 :code:`account` 和 :code:`order` 传递给回调函数,使其可以获取其可能需要到的数据。 @@ -265,12 +265,11 @@ RQAlpha 整个回测模块是通过 :code:`SimulationMod` 实现的,其中定 def cancel_order(self, order): account = self._get_account_for(order.order_book_id) - self._env.event_bus.publish_event(EVENT.ORDER_PENDING_CANCEL, account, order) + self._env.event_bus.publish_event(Event(EVENT.ORDER_PENDING_CANCEL, account=account, order=order)) - # account.on_order_cancelling(order) order._mark_cancelled(_("{order_id} order has been cancelled by user.").format(order_id=order.order_id)) - self._env.event_bus.publish_event(EVENT.ORDER_CANCELLATION_PASS, account, order) + self._env.event_bus.publish_event(Event(EVENT.ORDER_CANCELLATION_PASS, account=account, order=order)) # account.on_order_cancellation_pass(order) try: @@ -281,7 +280,7 @@ RQAlpha 整个回测模块是通过 :code:`SimulationMod` 实现的,其中定 except ValueError: pass -如果想查看详细的事件源相关的内容,建议直接阅读 `SimulationMod` 源码,您会发现,扩展事件源比想象中要简单。 +如果想查看详细的事件源相关的内容,建议直接阅读 `rqalpha_mod_sys_simulation` 源码,您会发现,扩展事件源比想象中要简单。 -您也可以基于 `SimulationMod` 扩展一个自定义的回测引擎,实现您特定的回测需求。 +您也可以基于 `rqalpha_mod_sys_simulation` 扩展一个自定义的回测引擎,实现您特定的回测需求。 diff --git a/docs/source/development/mod.rst b/docs/source/development/mod.rst index 37b00c056..ff5393c11 100644 --- a/docs/source/development/mod.rst +++ b/docs/source/development/mod.rst @@ -1,7 +1,7 @@ .. _development-mod: ==================================== -RQAlpha 扩展 - Mod +Mod ==================================== 目前内置了几个简单的 mod 示例,在 :code:`rqalpha/mod/` 目录下面。 @@ -85,7 +85,7 @@ Hello World pass 以独立 Pypi 包作为 Mod -================ +================================ RQAlpha 支持安装、卸载、启用、停止第三方Mod。 @@ -94,22 +94,23 @@ RQAlpha 支持安装、卸载、启用、停止第三方Mod。 # 以名为 "xxx" 的 Mod 为例,介绍RQAlpha 第三方Mod的使用 # 安装 - $ rqalpha install xxx + $ rqalpha mod install xxx # 卸载 - $ rqalpha uninstall xxx + $ rqalpha mod uninstall xxx # 启用 - $ rqalpha enable xxx + $ rqalpha mod enable xxx # 关闭 - $ rqalpha disable xxx + $ rqalpha mod disable xxx 如果您希望发布自己的Mod并被 RQAlpha 的用户使用,只需要遵循简单的约定即可。 下面为一个 RQAlpha Mod 的模板: .. code-block:: python3 + from rqalpha.interface import AbstractMod diff --git a/docs/source/faq.rst b/docs/source/faq.rst deleted file mode 100644 index 7f339713c..000000000 --- a/docs/source/faq.rst +++ /dev/null @@ -1,27 +0,0 @@ -.. _faq: - -================== -FAQ -================== - -1. 运行回测时,matplotlib 报错怎么办?:code:`RuntimeError: Python is not installed as a framework`. - - 解决方案:创建文件 :code:`~/.matplotlib/matplotlibrc`,并加入代码 :code:`backend: TkAgg` - -2. 在Windows运行报 :code:`Error on import matplotlib.pyplot` - - 请访问 `Error on import matplotlib.pyplot (on Anaconda3 for Windows 10 Home 64-bit PC) `_ 解决。 - -3. 在Windows出现缺失 :code:`cl.exe` - - 请访问 https://wiki.python.org/moin/WindowsCompilers 下载VC并且安装。 - -4. Windows 安装时报 :code:`error: Microsoft Visual C++ 14.0 is required.` - - 请访问 https://wiki.python.org/moin/WindowsCompilers 下载VC并且安装。 - -5. 出现安装 :code:`bcolz` 编译失败 - - 在 Windows 环境下编译 :code:`bcolz` 需要 :code:`Cython` 和 :code:`VC`,请参考 FAQ 3 & 4 - - 或者不进行编译安装,访问 http://www.lfd.uci.edu/~gohlke/pythonlibs/#bcolz 下载 :code:`bcolz` 直接进行安装。 diff --git a/docs/source/history.rst b/docs/source/history.rst index 9556f3fdb..f57c08efe 100644 --- a/docs/source/history.rst +++ b/docs/source/history.rst @@ -4,6 +4,193 @@ History ================== +2.1.4 +================== + +- 解决 history_bars 在 before_trading 获取的是未来数据的问题 +- 解决 before_trading 获取结算价是当前交易日结算价的问题 +- 增加 RQAlpha 向前兼容(0.3.x) `Issue 100 `_ +- 期货增加强平机制: 及当前账户权益<=0时,清空仓位,资金置0 `Issue 108 `_ +- 解决回测时只有一个交易日时,只有回测数据显示的问题 + +2.1.3 +================== + +- Fix `Issue 101 `_ +- Fix `Issue 105 `_ +- 解决运行 RQAlpha 时缺少 `six` | `requests` 库依赖的问题 +- 解决安装RQAlpha时在某些情况下报错的问题 +- 解决第三方 Mod 安装后配置文件路径有误的问题 +- 现在可以通过 `rqalpha mod install -e .` 的方式来安装依赖 Mod 了 +- 现在运行策略时会检测当前目录是否存在 `config.yml` 或者 `config.json` 来作为配置文件 +- 解决股票下单就存在 `position` 的问题,现在只有成交后才会产生 `position` 了。 +- 修复 `portfolio` 和 `future_account` 计算逻辑的一些问题 +- 修复 `transaction_cost` 在某个 position 清空以后计算不准确的问题 +- 在信号模式下 `price_limit` 表示是否输出涨跌停买入/卖出的报警信息,但不会阻止其买入/卖出 + +2.1.2 +================== + +- 提供 :code:`from rqalpha import cli` 方便第三方 Mod 扩展 `rqalpha` command +- :code:`history_bars` 增加 :code:`include_now` option +- Fix `Issue 90 `_ +- Fix `Issue 94 `_ + +2.1.0 +================== + +- Fix `Issue 87 `_ +- Fix `Issue 89 `_ +- Fix 无法通过 :code:`env.config.mod` 获取全部 `mod` 的配置信息 +- 增加 :code:`context.config` 来获取配置信息 +- 提供 :code:`from rqalpha import export_as_api` 接口,方便扩展自定义 API + +2.0.9 +================== + +- Fix `Issue 79 `_ +- Fix `Issue 82 `_ +- Fix :code:`rqalpha cmd` 失效 + +2.0.8 +================== + +- Fix `Issue 81 `_ +- 解决 `mod_config.yml` 文件解析出错以后,所有的命令报错的问题 +- 默认在 Python 2.x 下 `sys.setdefaultencoding("utf-8")` +- 优化 `UNIVERSE_CHANGED` 事件,现在只有在universe真正变化时才触发 + +2.0.7 +================== + +- Fix `Issue 78 `_ +- `is_st_stock` | `is_suspended` 支持 `count` 参数 +- 解决大量 Python 2.x 下中文乱码问题 + +2.0.6 +================== + +- 解决在 Python 2.x 下安装 RQAlpha 提示 `requirements-py2.txt Not Found` 的问题 +- 解决 `Benchmark` 无法显示的问题 +- 解决 `rqalpha mod list` 显示不正确的问题 +- 现在可以通过配置 `base.extra_vars` 向策略中预定义变量了。用法如下: + +.. code-block:: python3 + + from rqalpha import run + + config = { + "base": { + "strategy_file": "strategy.py", + "start_date": "2016-06-01", + "end_date": "2016-07-01", + "stock_starting_cash":100000, + "benchmark": '000300.XSHG' + }, + "extra":{ + "context_vars":{ + "short":5, + "middle":10, + "long":21 + } + } + } + + result_dict = run(config) + + # 以下是策略代码: + + def handle_bar(context): + print(context.short) # 5 + print(context.middle) # 10 + print(context.long) # 21 + +2.0.1 +================== + +- 修改配置的读取方式,不再从 `~/.rqalpha/config.yml` 读取自定义配置信息,而是默认从当前路径读取 `config.yml`,如果没找到,则会读取系统默认配置信息 +- 现在不再对自定义信息进行版本检查 +- :code:`rqalpha generate_config` 现在会生成包含所有默认系统配置信息的 `config.yml` 文件。 +- :code:`RUN_TYPE` 增加 :code:`LIVE_TRADING` +- 修复 :code:`history_bars` 获取日期错误产生的问题 +- 修复执行 :code:`context.run_info` 会报错的问题 +- 修复持久化报错的问题 +- 增加 Order Persist 相关内容 + +2.0.0 +================== + +2.0.0 详细修改内容请访问:`RQAlpha 2.0.0 `_ + +**Portfolio/Account/Position 相关** + +- 重新定义了 :code:`Portfolio`, :code:`Account` 和 :code:`Position` 的角色和关系 +- 删除大部分累计计算的属性,重新实现股票和期货的计算逻辑 +- 现在只有在 :code:`Portfolio` 层级进行净值/份额的计算,Account级别不再进行净值/份额/收益/相关的计算 +- 账户的恢复和初始化现在只需要 :code:`total_cash`, :code:`positions` 和 :code:`backward_trade_set` 即可完成 +- 精简 :code:`Position` 的初始化,可以从 :code:`real_broker` 直接进行恢复 +- :code:`Account` 提供 :code:`fast_forward` 函数,账户现在可以从任意时刻通过 :code:`orders` 和 :code:`trades` 快速前进至最新状态 +- 如果存在 Benchmark, 则创建一个 :code:`benchmark_portfolio`, 其包含一个 :code:`benchmark_account` +- 策略在调用 :code:`context.portfolio.positions[some_security]` 时候,如果 position 不存在,不再每次都创建临时仓位,而是会缓存,从而提高回测速度和性能 +- 不再使用 :code:`clone` 方法 +- 不再使用 :code:`PortfolioProxy` 和 :code:`PositionProxy` + +**Event 相关** + +- 规范 Event 的生成和相应逻辑, 使用 Event object 来替换原来的 Enum +- 抽离事件执行相关逻辑为 :code:`Executor` 模块 + +**Mod 相关** + +- 规范化 Mod 命名规则,需要以 `rqalpha_mod_xxx` 作为 Mod 依赖库命名 +- 抽离 :code:`slippage` 相关业务逻辑至 :code:`simulation mod` +- 抽离 :code:`commission` 相关业务逻辑至 :code:`simulation mod` +- 抽离 :code:`tax` 相关业务逻辑至 :code:`simulation mod` +- `rqalpha mod list` 命令现在可以格式化显示 Mod 当前的状态了 + +**Environment 和 ExecutionContext 相关** + +- 现在 :code:`ExecutionContext` 只负责上下文相关的内容,不再可以通过 :code:`ExecutionContext` 访问其他成员变量。 +- 扩展了 :code:`Environment` 的功能,RQAlpha 及 Mod 均可以直接通过 :code:`Environment.get_instance()` 来获取到环境中核心模块的引用 +- :code:`Environment` 还提供了很多常用的方法,具体请直接参考代码 + +**配置及参数相关** + +- 重构了配置相关的内容,`~/.rqalpha/config.yml` 现在类似于 Sublime/Atom 的用户配置文件,用于覆盖默认配置信息,因此只需要增加自定义配置项即可,不需要全部的配置内容都存在 +- 将Mod自己的默认配置从配置文件中删除,放在Mod中自行管理和维护 +- 独立存在 `~/.rqalpha/.mod_conifg.yml`, 提供 `rqalpha mod install/uninstall/enable/disable/list` 命令,RQAlpha 会通过该配置文件来对Mod进行管理。 +- 抽离 :code:`rqalpha run` 的参数,将其中属于 `Mod` 的参数全部删除,取代之为Mod提供了参数注入机制,所以现在 `Mod` 可以自行决定是否要注入参数或者命令来扩展 RQAlpha 的功能 +- 提供了 :code:`rqalpha-cmd` 命令,`Mod` 推荐在该命令下注入自己的命令来实现功能扩展 +- 不再使用 `--strategy-type`, 改为使用 `--security` 选项 +- `--output-file` | `--report` | `--plot` | `--plot-save`参数 转移至 `sys_analyser` Mod 中 +- `plot` | `report` 命令,转移至 `sys_analyser` Mod 中 +- `--signal` | `--slippage` | `--commission-multiplier` | `--matching-type` | `--rid` 转移至 `sys_simulation` Mod 中 + +**Risk 计算** + +- 修复 `tracking error `_ 计算错误 +- 修改 `sharpe `_ , `sortino `_ , `information ratio `_ , `alpha `_ 计算逻辑。参考 `晨星 `_ 的方法, 先计算单日级别指标, 再进行年化。与原本直接基于年化值计算相比, 在分析时间较短的情况下, 新的指标计算结果会系统性低于原指标结果。 +- 引入单日无风险利率作为中间变量计算上述指标。单日无风险利率为通过 `中国债券信息网 `_ 获取得到对应期限的年化国债到期收益率除以244得到 +- 修改指标说明若干 + +**其他** + +- 修改了 :code:`Order` 和 :code:`Trade` 的字段和函数,使其更通用 +- 为 :code:`RqAttrDict` 类增加 :code:`update` 方法,现在支持动态更新了 +- :code:`arg_checker` 增加 :code:`is_greater_or_equal_than` 和 :code:`is_less_or_equal_than` 函数 +- 删除 :code:`DEFAULT_FUTURE_INFO` 变量,现在可以直接通过 :code:`data_proxy` 获取相关数据 +- 通过 `better_exceptions `_ 提供更好的错误堆栈提示体验 +- 对字符串的处理进行了优化,现在可以正确在 Python2.x/3.x 下显示中文了 +- 修复 :code:`update_bundle` 直接在代码中调用会报错的问题 +- 增加对于下单量为0的订单过滤,不再会创建订单,也不再会输出警报日志 +- 增加 :code:`is_suspended` 和 :code:`is_st_stock` API 的支持 + +0.3.14 +================== + +- Hotfix :code:`UnboundLocalError: local variable 'signature' referenced before assignment` + + 0.3.13 ================== diff --git a/docs/source/index.rst b/docs/source/index.rst index 361afc9ea..2b643fac8 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -33,112 +33,175 @@ RQAlpha 所有的策略都可以直接在 `Ricequant`_ 上进行回测和实盘 `Ricequant`_ 是一个开放的量化算法交易社区,为程序化交易者提供免费的回测和实盘模拟环境,并且会不间断举行实盘资金投入的量化比赛。 +特点 +============================ -快速指引 -================== +* 易于使用: RQAlpha让您集中于策略的开发,一行简单的命令就可以执行您的策略。 +* 完善的文档: 您可以直接访问 `RQAlpha 文档`_ 或者 `Ricequant 文档`_ 来获取您需要的信息。 +* 活跃的社区: 您可以通过访问 `Ricequant 社区`_ 获取和询问有关 RQAlpha 的一切问题,有很多优秀的童鞋会解答您的问题。 +* 稳定的环境: 每天都有会大量的算法交易在 Ricequant 上运行,无论是 RQAlpha,还是数据,我们能会做到问题秒处理,秒解决。 +* 灵活的配置: RQAlpha 提供了一系列的配置选项,用户可以通过简单的配置来构建适合自己的交易系统。 +* 强大的扩展性: RQAlpha 定义了一系列的 Mod Hook 接口,开发者可以基于 Mod 的开发模式,扩展 RQAlpha,无论是做实时监控,还是归因分析,RQAlpha 都可以通过扩展来实现。 -.. toctree:: - :caption: 快速指引 - :hidden: +Mod +============================ - intro/overview - intro/install - intro/detail_install - intro/tutorial - intro/examples +RQAlpha 提供了极具拓展性的 Mod Hook 接口,这意味着 RQAlpha 可以非常容易的对接其他量化库,安装和使用 Mod 也非常简单,当您安装好 RQAlpha 后,可以直接通过如下命令对管理您的Mod: -:doc:`intro/overview` - 了解RQAlpha +.. code-block:: bash + + # 查看当前安装的 Mod 列表及状态 + $ rqalpha mod list + # 安装 Mod + $ rqalpha mod install xxx + # 卸载 Mod + $ rqalpha mod uninstall xxx + # 启用 Mod + $ rqalpha mod enable xxx + # 禁用 Mod + $ rqalpha mod disable xxx -:doc:`intro/install` - 安装RQAlpha +====================== ============================================================================================================== +Mod名 说明 +====================== ============================================================================================================== +`sys_analyser`_ 【系统模块】记录每天的下单、成交、投资组合、持仓等信息,并计算风险度指标,并以csv、plot图标等形式输出分析结果 +`sys_funcat`_ 【系统模块】支持以通达信公式的方式写策略 +`sys_progress`_ 【系统模块】在控制台输出当前策略的回测进度。 +`sys_risk`_ 【系统模块】对订单进行事前风控校验 +`sys_simulation`_ 【系统模块】支持回测、撮合、滑点控制等 +`sys_stock_realtime`_ 【系统模块】Demo 模块,用于展示如何接入自有行情进行回测/模拟/实盘 +`vnpy`_ 【第三方模块】通过 VNPY 对接期货实盘行情和实盘交易 +`sentry`_ 【第三方模块】集成 sentry 的扩展,实现错误日志全自动采集、处理 +`tushare`_ 【第三方模块】Demo Mod,用于展示如何通过tushare 获取实时Bar数据并组装以供RQAlpha使用 +`shipane`_ 【第三方模块】集成实盘易SDK,用于对接股票实盘跟单交易 +====================== ============================================================================================================== -:doc:`intro/detail_install` - 如果对Python并不熟悉的话,我们提供了整套开发环境的详细安装教程 +如果您基于 RQAlpha 进行了 Mod 扩展,欢迎告知我们,在审核通过后,会在 Mod 列表中添加您的 Mod 信息和链接。 -:doc:`intro/tutorial` - 使用RQAlpha +Feature Status +============================ -:doc:`intro/examples` - 通过RQAlpha运行的策略示例 +* VNPY 对接 --> `vnpy`_ + * ✅ 扩展VNPY_Gateway + * ✅ 实盘交易对接 + * ✅ 数据源对接 + * ✅ 事件源对接 -RQAlpha API -================== +* Tushare 对接 -.. toctree:: - :caption: API - :hidden: + * ✅ 数据源对接 --> `rqalpha_mod_sys_stock_realtime`_ + * ✅ 合成分钟线 --> `rqalpha_mod_tushare`_ - api/config - api/base_api - api/extend_api +* Tick 相关支持 -:doc:`api/config` - 启动 RQAlpha 参数配置 + * ✅ TICK 相关事件支持 --> `EVENT.PRE_TICK` | `EVENT.TICK` | `EVENT.POST_TICK` + * ✅ handle_tick 函数支持 -:doc:`api/base_api` - 基础API(期货股票公用API) +* Mod Manager --> `通过 Mod 扩展 RQAlpha`_ -:doc:`api/extend_api` - 扩展API(开源版暂不支持,可以通过Ricequant平台或者商业版使用) + * ✅ 定义 Mod 编写规范, workflow && Doc + * ✅ 提供 Mod Demo && Tutorial + * ✅ 提供 `rqalpha install xx_mod` 等命令 加载第三方 Mod +* Third-party Tools Integration -扩展 RQAlpha -================== + * ✅ 集成 Sentry --> `sentry`_ -.. toctree:: - :caption: Development - :hidden: +* i18n - development/make_contribute - development/basic_concept - development/mod - development/event_source + * 🚫 English Doc -:doc:`development/make_contribute` - 如何加入 RQAlpha 的开发 +* Support Options -:doc:`development/basic_concept` - 基本概念 + * 🚫 OptionAccount + * 🚫 OptionPosition -:doc:`development/mod` - 基于Mod来开发和扩展RQAlpha +* Support BitCoin -:doc:`development/event_source` - 扩展事件源 + * 🚫 BitcoinAccount + * 🚫 BitcoinPosition -其他 -================== +获取帮助 +============================ -.. toctree:: - :caption: Extra - :hidden: +关于RQAlpha的任何问题可以通过以下途径来获取帮助 - faq - history - todo +* 查看 `FAQ`_ 页面找寻常见问题及解答 +* 可以通过 `索引`_ 或者使用搜索功能来查找特定问题 +* 在 `Github Issue`_ 中提交issue +* RQAlpha 交流群「487188429」 -:doc:`faq` - FAQ -:doc:`history` - 更新日志 +.. _Github Issue: https://github.com/ricequant/rqalpha/issues +.. _Ricequant: https://www.ricequant.com/algorithms +.. _RQAlpha 文档: http://rqalpha.readthedocs.io/zh_CN/latest/ +.. _Ricequant 文档: https://www.ricequant.com/api/python/chn +.. _Ricequant 社区: https://www.ricequant.com/community/category/all/ +.. _FAQ: http://rqalpha.readthedocs.io/zh_CN/latest/faq.html +.. _索引: http://rqalpha.readthedocs.io/zh_CN/latest/genindex.html + +.. _RQAlpha 介绍: http://rqalpha.readthedocs.io/zh_CN/latest/intro/overview.html +.. _安装指南: http://rqalpha.readthedocs.io/zh_CN/latest/intro/install.html +.. _10分钟学会 RQAlpha: http://rqalpha.readthedocs.io/zh_CN/latest/intro/tutorial.html +.. _策略示例: http://rqalpha.readthedocs.io/zh_CN/latest/intro/examples.html + +.. _参数配置: http://rqalpha.readthedocs.io/zh_CN/latest/api/config.html +.. _API: http://rqalpha.readthedocs.io/zh_CN/latest/api/base_api.html + +.. _如何贡献代码: http://rqalpha.readthedocs.io/zh_CN/latest/development/make_contribute.html +.. _基本概念: http://rqalpha.readthedocs.io/zh_CN/latest/development/basic_concept.html +.. _RQAlpha 基于 Mod 进行扩展: http://rqalpha.readthedocs.io/zh_CN/latest/development/mod.html +.. _History: http://rqalpha.readthedocs.io/zh_CN/latest/history.html +.. _TODO: https://github.com/ricequant/rqalpha/blob/master/TODO.md +.. _develop 分支: https://github.com/ricequant/rqalpha/tree/develop +.. _master 分支: https://github.com/ricequant/rqalpha +.. _rqalpha_mod_sys_stock_realtime: https://github.com/ricequant/rqalpha/blob/master/rqalpha/mod/rqalpha_mod_sys_stock_realtime/README.rst +.. _rqalpha_mod_tushare: https://github.com/ricequant/rqalpha-mod-tushare +.. _通过 Mod 扩展 RQAlpha: http://rqalpha.io/zh_CN/latest/development/mod.html +.. _sys_analyser: https://github.com/ricequant/rqalpha/blob/master/rqalpha/mod/rqalpha_mod_sys_analyser/README.rst +.. _sys_funcat: https://github.com/ricequant/rqalpha/blob/master/rqalpha/mod/rqalpha_mod_sys_funcat/README.rst +.. _sys_progress: https://github.com/ricequant/rqalpha/blob/master/rqalpha/mod/rqalpha_mod_sys_progress/README.rst +.. _sys_risk: https://github.com/ricequant/rqalpha/blob/master/rqalpha/mod/rqalpha_mod_sys_risk/README.rst +.. _sys_simulation: https://github.com/ricequant/rqalpha/blob/master/rqalpha/mod/rqalpha_mod_sys_simulation/README.rst +.. _sys_stock_realtime: https://github.com/ricequant/rqalpha/blob/master/rqalpha/mod/rqalpha_mod_sys_stock_realtime/README.rst +.. _vnpy: https://github.com/ricequant/rqalpha-mod-vnpy +.. _sentry: https://github.com/ricequant/rqalpha-mod-sentry +.. _tushare: https://github.com/ricequant/rqalpha-mod-tushare +.. _shipane: https://github.com/wh1100717/rqalpha-mod-ShiPanE -:doc:`todo` - TODO +.. toctree:: + :caption: 快速指引 + :hidden: + intro/overview + intro/install + intro/detail_install + intro/tutorial + intro/optimizing_parameters + intro/examples + intro/virtual_machine -获取帮助 -================== +.. toctree:: + :caption: API + :hidden: -关于RQAlpha的任何问题可以通过以下途径来获取帮助 + api/config + api/base_api + api/extend_api -* 查看 :doc:`FAQ ` 页面找寻常见问题及解答 -* 可以通过 :ref:`genindex` 或者 :ref:`search` 来查找特定问题 -* 在 `github issue page`_ 中提交issue -* RQAlpha 交流群「487188429」 +.. toctree:: + :caption: Development + :hidden: -.. _github issue page: https://github.com/ricequant/rqalpha/issues -.. _Ricequant: https://www.ricequant.com/algorithms + development/make_contribute + development/basic_concept + development/mod + development/event_source + development/data_source + +.. toctree:: + :caption: Extra + :hidden: + history \ No newline at end of file diff --git a/docs/source/intro/detail_install.rst b/docs/source/intro/detail_install.rst index f6b9d325b..8e1c4837b 100644 --- a/docs/source/intro/detail_install.rst +++ b/docs/source/intro/detail_install.rst @@ -1,7 +1,7 @@ .. _intro-detail-install: ================== -详细环境搭建 +环境搭建 ================== Anaconda diff --git a/docs/source/intro/install.rst b/docs/source/intro/install.rst index 2ef753382..ef6442bdf 100644 --- a/docs/source/intro/install.rst +++ b/docs/source/intro/install.rst @@ -4,22 +4,16 @@ 安装指南 ================== -兼容性 -================== - -RQAlpha目前只支持 python 3.4+ && Python 2.7+ - 安装前 ================== -我们强烈建议您在安装 RQAlpha 前,首先单独安装 bcolz 库,因为其编译时间较长,并且中间比较容易失败,单独安装好以后再继续安装RQAlpha。 +**我们强烈建议您如果安装过程中遇到了问题,先阅读该文档下面的 「FAQ」 章节来尝试着解决** -Windows 环境下编译安装 bcolz 需要使用 :code:`Visual C++ Compiler`,需要自行下载并安装 visual-cpp-build-tools, +.. image:: https://img.shields.io/pypi/pyversions/rqalpha.svg + :target: https://pypi.python.org/pypi/rqalpha + :alt: Python Version Support -如果觉得麻烦,也可以直接去 http://www.lfd.uci.edu/~gohlke/pythonlibs/#bcolz 下载相应版本的 :code:`bcolz wheel` 包,直接安装编译后的 bcolz 版本。 - -安装 -================== +注: 我们尽量保证 Python 2.7 的兼容,但如果没有特殊的需求,尽量使用 Python 3.4+ 为了避免一些安装问题,建议您先升级您的 pip 和 setuptools : @@ -27,14 +21,17 @@ Windows 环境下编译安装 bcolz 需要使用 :code:`Visual C++ Compiler`, $ pip install -U pip setuptools -因为 bcolz 对于一些用户可能会安装困难,可能需要重试多次,所以建议先安装 cython / bcolz 库: +bcolz 是 RQAlpha 的依赖库,因为其编译时间较长,并且中间比较容易失败,建议先单独安装 bcolz 库,安装好以后再安装 RQAlpha。如果在安装的过程中出现问题,请参考 「FAQ」 章节。 + +Windows 环境下因为默认没有安装 `Visual C++ Compiler`, 需要自行下载并安装 `visual-cpp-build-tools`,如果觉得麻烦,也可以直接去 http://www.lfd.uci.edu/~gohlke/pythonlibs/#bcolz 下载相应版本的 :code:`bcolz wheel` 包,直接安装编译后的 bcolz 版本。 .. code-block:: bash $ pip install cython - $ pip install bcolz==1.1.0 + $ pip install bcolz -安装 RQAlpha : +安装 +================== .. code-block:: bash @@ -88,3 +85,31 @@ bundle 默认存放在 :code:`~/.rqalpha` 下,您也可以指定 bundle 的存 $ rqalpha generate_config +FAQ +================== + +1. Bcolz 相关问题 + + 请首先 `pip install cython` 来安装cython + + `bcolz` 安装大部分问题都来自于没有安装 `Visual C++ Compiler`,建议您无论如何先成功安装 `Visual C++ Compiler`, 访问 https://wiki.python.org/moin/WindowsCompilers 根据自己的机器环境和Python版本选择安装对应的编译工具。 + + 不进行编译安装,访问 http://www.lfd.uci.edu/~gohlke/pythonlibs/#bcolz 下载 :code:`bcolz` 直接进行安装。 + +2. Matplotlib 相关问题 + + 1. 运行回测时,matplotlib 报错怎么办?:code:`RuntimeError: Python is not installed as a framework`: + + 解决方案:创建文件 :code:`~/.matplotlib/matplotlibrc`,并加入代码 :code:`backend: TkAgg` + + 2. 在 Python 3.6 下没有任何报错,但是就是没有plot输出: + + 解决方案:创建文件 :code:`~/.matplotlib/matplotlibrc`,并加入代码 :code:`backend: TkAgg` + + 3. 在Windows运行报 :code:`Error on import matplotlib.pyplot`: + + 解决方案: 请访问 `Error on import matplotlib.pyplot (on Anaconda3 for Windows 10 Home 64-bit PC) `_ 解决。 + +3. Python 2.7 在 Windows 下产生中文乱码的问题 + + RQAlpha 运行在 Windows(Python 2.x) 可能会遇到中文乱码的问题,这个并不是RQAlpha的问题,而是由于 Windows 的 cmd 本身是 `gbk` 编码而产生的,具体的解决方案可以参考 [Windows(Python 2.x) 命令行下输出日志中文乱码的问题](https://github.com/ricequant/rqalpha/issues/80) \ No newline at end of file diff --git a/docs/source/intro/optimizing_parameters.rst b/docs/source/intro/optimizing_parameters.rst new file mode 100644 index 000000000..db425e833 --- /dev/null +++ b/docs/source/intro/optimizing_parameters.rst @@ -0,0 +1,166 @@ +.. _intro-optimizing-parameters: + +================== +参数调优 +================== + +对于以下双均线策略,我们希望对其进行参数调优,我们可以通过命令行参数 :code:`--extra-vars` 或者通过配置 :code:`extra.context_vars` 传递变量到 :code:`context` 对象中。 + +.. code-block:: python + + from rqalpha.api import * + import talib + + + def init(context): + context.s1 = "000001.XSHE" + + context.SHORTPERIOD = 20 + context.LONGPERIOD = 120 + + + def handle_bar(context, bar_dict): + prices = history_bars(context.s1, context.LONGPERIOD+1, '1d', 'close') + + short_avg = talib.SMA(prices, context.SHORTPERIOD) + long_avg = talib.SMA(prices, context.LONGPERIOD) + + cur_position = context.portfolio.positions[context.s1].quantity + shares = context.portfolio.cash / bar_dict[context.s1].close + + if short_avg[-1] - long_avg[-1] < 0 and short_avg[-2] - long_avg[-2] > 0 and cur_position > 0: + order_target_value(context.s1, 0) + + if short_avg[-1] - long_avg[-1] > 0 and short_avg[-2] - long_avg[-2] < 0: + order_shares(context.s1, shares) + + +通过函数调用传递参数 +==================================== + +.. code-block:: python + + import concurrent.futures + import multiprocessing + from rqalpha import run + + + tasks = [] + for short_period in range(3, 10, 2): + for long_period in range(30, 90, 5): + config = { + "extra": { + "context_vars": { + "SHORTPERIOD": short_period, + "LONGPERIOD": long_period, + }, + "log_level": "error", + }, + "base": { + "securities": "stock", + "matching_type": "current_bar", + "start_date": "2015-01-01", + "end_date": "2016-01-01", + "stock_starting_cash": 100000, + "benchmark": "000001.XSHE", + "frequency": "1d", + "strategy_file": "rqalpha/examples/golden_cross.py", + }, + "mod": { + "sys_progress": { + "enabled": True, + "show": True, + }, + "sys_analyser": { + "enabled": True, + "output_file": "results/out-{short_period}-{long_period}.pkl".format( + short_period=short_period, + long_period=long_period, + ) + }, + }, + } + + tasks.append(config) + + + def run_bt(config): + run(config) + + + with concurrent.futures.ProcessPoolExecutor(max_workers=multiprocessing.cpu_count()) as executor: + for task in tasks: + executor.submit(run_bt, task) + + + +通过命令行传递参数 +==================================== + +.. code-block:: python + + import os + import json + import concurrent.futures + import multiprocessing + + + tasks = [] + for short_period in range(3, 10, 2): + for long_period in range(30, 90, 5): + extra_vars = { + "SHORTPERIOD": short_period, + "LONGPERIOD": long_period, + } + vars_params = json.dumps(extra_vars).encode("utf-8").decode("utf-8") + + cmd = ("rqalpha run -fq 1d -f rqalpha/examples/golden_cross.py --start-date 2015-01-01 --end-date 2016-01-01 " + "-o results/out-{short_period}-{long_period}.pkl -sc 100000 --progress -bm 000001.XSHE --extra-vars '{params}' ").format( + short_period=short_period, + long_period=long_period, + params=vars_params) + + tasks.append(cmd) + + + def run_bt(cmd): + print(cmd) + os.system(cmd) + + + with concurrent.futures.ProcessPoolExecutor(max_workers=multiprocessing.cpu_count()) as executor: + for task in tasks: + executor.submit(run_bt, task) + + + +分析批量回测结果 +==================================== + +.. code-block:: python + + import glob + import pandas as pd + + + results = [] + + for name in glob.glob("results/*.pkl"): + result_dict = pd.read_pickle(name) + summary = result_dict["summary"] + results.append({ + "name": name, + "annualized_returns": summary["annualized_returns"], + "sharpe": summary["sharpe"], + "max_drawdown": summary["max_drawdown"], + }) + + results_df = pd.DataFrame(results) + + print("-" * 50) + print("Sort by sharpe") + print(results_df.sort_values("sharpe", ascending=False)[:10]) + + print("-" * 50) + print("Sort by annualized_returns") + print(results_df.sort_values("annualized_returns", ascending=False)[:10]) diff --git a/docs/source/intro/overview.rst b/docs/source/intro/overview.rst index 78dae3892..5e86af50f 100644 --- a/docs/source/intro/overview.rst +++ b/docs/source/intro/overview.rst @@ -1,7 +1,7 @@ .. _intro-overview: ==================== -RQAlpha 介绍 +介绍 ==================== .. _Ricequant: https://www.ricequant.com/algorithms @@ -26,8 +26,6 @@ RQAlpha 所有的策略都可以直接在 `Ricequant`_ 上进行回测和实盘 RQAlpha 本身支持不同周期的回测和实盘交易,但是目前只开放A股市场日线数据,如果用户需要做分钟回测或者更细级别的回测可以在 `Ricequant`_ 上进行,也通过实现数据层接口函数来使用自己的数据。财务数据相关的API目前只能通过 `Ricequant`_ 平台来获取。 -RQAlpha 只是我们的商业版的一部分,如果您是机构希望采用我们包含数据的一体化策略开发、研究、评估系统,请邮件联系我们: public@ricequant.com,或加QQ:「4848371」 咨询,我们也会提供咨询帮助和系统维护服务等。 - RQAlpha 安装 ================== @@ -119,7 +117,6 @@ RQAlpha可以输出一个 pickle 文件,里面为一个 dict 。keys 包括 # 'max_drawdown': 0.087999999999999995, # 'pnl': 26624.358, # 'portfolio_value': 1026624.358, - # 'run_id': 9999, # 'run_type': 'BACKTEST', # 'sharpe': 0.016, # 'slippage': 0, diff --git a/docs/source/intro/tutorial.rst b/docs/source/intro/tutorial.rst index 5717ebe39..8fb5611fd 100644 --- a/docs/source/intro/tutorial.rst +++ b/docs/source/intro/tutorial.rst @@ -1,7 +1,7 @@ .. _intro-tutorial: ==================== -10分钟学会 RQAlpha +10分钟教程 ==================== 在本教程中,我们假设 RQAlpha 已经正确安装在您的系统中,并且已经完成了相应回测数据的同步,如果有任何安装相关的问题,请首先查看 :ref:`intro-install` diff --git a/docs/source/intro/virtual_machine.rst b/docs/source/intro/virtual_machine.rst new file mode 100644 index 000000000..abcdc2a0b --- /dev/null +++ b/docs/source/intro/virtual_machine.rst @@ -0,0 +1,72 @@ +.. _intro-virtual-machine: + +======================================== +开箱即用虚拟机 +======================================== + +为了您方便快速上手 RQAlpha,免去开发环境的安装和配置,我们为您提供了一个虚拟机镜像文件。该虚拟机已经安装了最新版本的 RQAlpha 及其他依赖组件。 + +使用虚拟机 +------------------------------------------------------ + +此处附上使用 VirtualBox 使用本镜像的简单教程 + +* 访问 `VirtualBox 官网`_ 下载 VirtualBox。 + +* 下载 `RQAlpha 开箱即用虚拟机镜像`_ 。 + +* 在 VirtualBox 中选择 "导入虚拟电脑", 选择刚刚下载的 .ova 文件。 + +* 点击 继续,进行虚拟机的一些设置,亦可维持默认。 + +* 点击 导入,坐等几分钟。 + +* 选择刚刚创建好的虚拟机,点击启动。 + +.. image:: ../_static/virtualBox_1.png + +* 选中创建好的虚拟机,点击 "启动" + +* 虚拟机的默认用户密码为: rqalpha + +.. _VirtualBox 官网: https://www.virtualbox.org/wiki/Downloads + +.. _RQAlpha 开箱即用虚拟机镜像: https://pan.baidu.com/s/1mhB3jfE + + +在终端运行 RQAlpha +------------------------------------------------------ + + +在您成功创建虚拟机并运行后,您可以按照以下步骤快速运行 RQAlpha + +打开终端并在终端输入如下命令以进入事先创建好的虚拟环境 + +.. code-block:: bash + + source activate py3 + +在终端输入如下命令以进入 RQAlpha 代码所在文件夹 + +.. code-block:: bash + + cd /home/rqalpha_user/rqalpha + +在终端输入如下命令以启动 RQAlpha,并运行 :ref:`intro-examples` 中的 :ref:`intro-examples-buy-and-hold` 进行回测。 + +.. code-block:: bash + + rqalpha run -f ./rqalpha/examples/buy_and_hold.py -s 2016-06-01 -e 2016-12-01 --stock-starting-cash 100000 --benchmark 000300.XSHG --plot + +使用 PyCharm 进行断点调试 +------------------------------------------------------ + +该虚拟机镜像中安装了 PyCharm,您可以方便的使用 PyCharm 进行代码的阅读、编写,或是通过断点调试了解 RQAlpha 的运行流程。 + +* 打开 PyCharm + +* 在右上角的下拉框中选择 debug_buy_and_hold + +* 点击右边的小虫子图标,运行期间可点击代码行号的右侧增加断点,点击左下方的箭头图标控制代码运行。 + +.. image:: ../_static/pycharm_1.png diff --git a/docs/source/todo.rst b/docs/source/todo.rst deleted file mode 100644 index 0e91b5668..000000000 --- a/docs/source/todo.rst +++ /dev/null @@ -1,51 +0,0 @@ -.. _todo: - -=============================== -TODO -=============================== - -* 数据源 - - * tushare 数据对接 - * vnpy 数据对接 - * 量化掘金 数据对接 - -* 事件源 - - * Event 标准化、workflow && Doc - * 股票实盘 实盘事件源对接 - * 期货实盘 实盘事件源对接(基于Tick) - -* API 扩展 - - * API Extension Doc - * 支持 期货的 on_tick - -* 实盘交易接口 - - * vnpy 交易对接 - * easytrader 交易对接 - * 量化掘金 交易对接 - -* 风控模块 - - * 事前风控 - * 事中风控 - * 事后风控 - -* RESTful Server - - * 数据格式规范 - * 存储格式规范 - * HTTP 接口定义 - * 前端页面 - -* Mod Manager - - * 定义 Mod 编写规范, workflow && Doc - * 提供 Mod Demo && Tutorial - * 提供 `rqalpha install xx_mod` 等命令 加载第三方 Mod - -* i18n - - * English Doc \ No newline at end of file diff --git a/messages.pot b/messages.pot index b02389fe3..290fcd1a6 100644 --- a/messages.pot +++ b/messages.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2017-02-28 15:22+0800\n" +"POT-Creation-Date: 2017-04-26 15:15+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,7 +17,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.3.4\n" -#: rqalpha/execution_context.py:141 +#: rqalpha/execution_context.py:105 #, python-format msgid "You cannot call %s when executing %s" msgstr "" @@ -38,7 +38,7 @@ msgstr "" msgid "benchmark {benchmark} has been de_listed on {end_date}" msgstr "" -#: rqalpha/main.py:119 +#: rqalpha/main.py:143 msgid "" "\n" "[WARNING]\n" @@ -47,423 +47,454 @@ msgid "" "Are you sure to continue?" msgstr "" -#: rqalpha/main.py:131 +#: rqalpha/main.py:155 msgid "try {} ..." msgstr "" -#: rqalpha/main.py:140 +#: rqalpha/main.py:164 msgid "downloading ..." msgstr "" -#: rqalpha/main.py:154 +#: rqalpha/main.py:178 msgid "Data bundle download successfully in {bundle_path}" msgstr "" -#: rqalpha/main.py:294 -msgid "unknown event from event source: {}" -msgstr "" - -#: rqalpha/main.py:300 +#: rqalpha/main.py:295 msgid "strategy run successfully, normal exit" msgstr "" -#: rqalpha/main.py:309 rqalpha/main.py:322 rqalpha/main.py:325 +#: rqalpha/main.py:301 rqalpha/main.py:318 rqalpha/main.py:321 msgid "strategy execute exception" msgstr "" -#: rqalpha/plot.py:116 -msgid "Total Returns" -msgstr "" - -#: rqalpha/plot.py:117 -msgid "Annual Returns" +#: rqalpha/api/api_base.py:132 rqalpha/api/api_base.py:232 +#: rqalpha/api/api_base.py:263 rqalpha/api/api_future.py:185 +#: rqalpha/api/api_stock.py:420 +msgid "unsupported order_book_id type" msgstr "" -#: rqalpha/plot.py:118 -msgid "Benchmark Returns" +#: rqalpha/api/api_base.py:176 +msgid "Cancel order fail: invalid order id" msgstr "" -#: rqalpha/plot.py:120 -msgid "Benchmark Annual" +#: rqalpha/api/api_base.py:705 +msgid "in get_dividend, start_date {} is later than the previous test day {}" msgstr "" -#: rqalpha/plot.py:123 -msgid "Alpha" +#: rqalpha/api/api_future.py:63 rqalpha/api/api_stock.py:99 +#: rqalpha/api/api_stock.py:211 +msgid "Limit order price should be positive" msgstr "" -#: rqalpha/plot.py:124 -msgid "Beta" +#: rqalpha/api/api_future.py:75 rqalpha/api/api_future.py:77 +#: rqalpha/api/api_stock.py:122 rqalpha/api/api_stock.py:124 +msgid "Order Creation Failed: [{order_book_id}] No market data" msgstr "" -#: rqalpha/plot.py:125 -msgid "Sharpe" +#: rqalpha/api/api_future.py:178 rqalpha/api/api_stock.py:415 +msgid "{order_book_id} is not supported in current strategy type" msgstr "" -#: rqalpha/plot.py:126 -msgid "Sortino" +#: rqalpha/api/api_stock.py:96 rqalpha/api/api_stock.py:208 +msgid "style should be OrderStyle" msgstr "" -#: rqalpha/plot.py:127 -msgid "Information Ratio" +#: rqalpha/api/api_stock.py:130 +msgid "Order Creation Failed: 0 order quantity" msgstr "" -#: rqalpha/plot.py:129 -msgid "Volatility" +#: rqalpha/api/api_stock.py:269 +msgid "percent should between -1 and 1" msgstr "" -#: rqalpha/plot.py:130 rqalpha/plot.py:160 -msgid "MaxDrawdown" +#: rqalpha/api/api_stock.py:348 +msgid "percent should between 0 and 1" msgstr "" -#: rqalpha/plot.py:131 -msgid "Tracking Error" +#: rqalpha/core/strategy.py:38 +msgid "deprecated parameter[bar_dict] in before_trading function." msgstr "" -#: rqalpha/plot.py:132 -msgid "Downside Risk" +#: rqalpha/core/strategy.py:55 +msgid "" +"[deprecated] before_day_trading is no longer used. use before_trading " +"instead." msgstr "" -#: rqalpha/plot.py:141 -msgid "MaxDD/MaxDDD" +#: rqalpha/core/strategy.py:57 +msgid "" +"[deprecated] before_night_trading is no longer used. use before_trading " +"instead." msgstr "" -#: rqalpha/plot.py:154 -msgid "strategy" +#: rqalpha/core/strategy_context.py:244 rqalpha/core/strategy_context.py:249 +#: rqalpha/core/strategy_context.py:254 rqalpha/core/strategy_context.py:258 +#: rqalpha/core/strategy_context.py:262 rqalpha/core/strategy_context.py:266 +#: rqalpha/core/strategy_context.py:270 +#: rqalpha/model/account/base_account.py:108 +#: rqalpha/model/account/base_account.py:116 +#: rqalpha/model/account/base_account.py:124 +#: rqalpha/model/account/base_account.py:132 +#: rqalpha/model/account/base_account.py:140 +#: rqalpha/model/account/future_account.py:204 +#: rqalpha/model/account/future_account.py:212 +msgid "[abandon] {} is no longer used." msgstr "" -#: rqalpha/plot.py:156 -msgid "benchmark" +#: rqalpha/mod/__init__.py:50 +msgid "loading mod {}" msgstr "" -#: rqalpha/plot.py:162 -msgid "MaxDDD" +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:114 +msgid "Total Returns" msgstr "" -#: rqalpha/api/api_base.py:123 rqalpha/api/api_base.py:224 -#: rqalpha/api/api_base.py:254 rqalpha/api/api_future.py:184 -#: rqalpha/api/api_stock.py:470 -msgid "unsupported order_book_id type" +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:115 +msgid "Annual Returns" msgstr "" -#: rqalpha/api/api_base.py:171 -msgid "Cancel order fail: invalid order id" +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:116 +msgid "Benchmark Returns" msgstr "" -#: rqalpha/api/api_base.py:685 -msgid "in get_dividend, start_date {} is later than the previous test day {}" +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:118 +msgid "Benchmark Annual" msgstr "" -#: rqalpha/api/api_future.py:62 rqalpha/api/api_stock.py:106 -#: rqalpha/api/api_stock.py:245 -msgid "Limit order price should be positive" +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:121 +msgid "Alpha" msgstr "" -#: rqalpha/api/api_future.py:76 rqalpha/api/api_future.py:77 -#: rqalpha/api/api_stock.py:131 rqalpha/api/api_stock.py:132 -msgid "Order Creation Failed: [{order_book_id}] No market data" +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:122 +msgid "Beta" msgstr "" -#: rqalpha/api/api_future.py:177 rqalpha/api/api_stock.py:465 -msgid "{order_book_id} is not supported in current strategy type" +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:123 +msgid "Sharpe" msgstr "" -#: rqalpha/api/api_stock.py:103 rqalpha/api/api_stock.py:242 -msgid "style should be OrderStyle" +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:124 +msgid "Sortino" msgstr "" -#: rqalpha/api/api_stock.py:138 -msgid "Order Creation Failed: 0 order quantity" +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:125 +msgid "Information Ratio" msgstr "" -#: rqalpha/api/api_stock.py:320 -msgid "percent should between -1 and 1" +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:127 +msgid "Volatility" msgstr "" -#: rqalpha/api/api_stock.py:439 -msgid "percent should between 0 and 1" +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:128 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:158 +msgid "MaxDrawdown" msgstr "" -#: rqalpha/core/strategy.py:38 -msgid "deprecated parameter[bar_dict] in before_trading function." +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:129 +msgid "Tracking Error" msgstr "" -#: rqalpha/core/strategy.py:55 -msgid "" -"[deprecated] before_day_trading is no longer used. use before_trading " -"instead." +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:130 +msgid "Downside Risk" msgstr "" -#: rqalpha/core/strategy.py:57 -msgid "" -"[deprecated] before_night_trading is no longer used. use before_trading " -"instead." +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:139 +msgid "MaxDD/MaxDDD" msgstr "" -#: rqalpha/core/strategy_context.py:312 rqalpha/core/strategy_context.py:316 -#: rqalpha/core/strategy_context.py:320 rqalpha/core/strategy_context.py:324 -#: rqalpha/core/strategy_context.py:328 -msgid "[deprecated] {} is no longer used." +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:152 +msgid "strategy" msgstr "" -#: rqalpha/mod/mod_handler.py:43 -msgid "loading mod {}" +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:154 +msgid "benchmark" msgstr "" -#: rqalpha/mod/risk_manager/frontend_validator.py:51 -msgid "Order Rejected: {order_book_id} is not listed!" +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:160 +msgid "MaxDDD" msgstr "" -#: rqalpha/mod/risk_manager/frontend_validator.py:55 -msgid "Order Rejected: {order_book_id} has been delisted!" +#: rqalpha/mod/rqalpha_mod_sys_risk/cash_validator.py:35 +#: rqalpha/mod/rqalpha_mod_sys_risk/cash_validator.py:54 +msgid "" +"Order Rejected: not enough money to buy {order_book_id}, needs " +"{cost_money:.2f}, cash {cash:.2f}" msgstr "" -#: rqalpha/mod/risk_manager/frontend_validator.py:59 -msgid "Order Rejected: {order_book_id} is not trading!" +#: rqalpha/mod/rqalpha_mod_sys_risk/is_trading_validator.py:29 +msgid "Order Rejected: {order_book_id} is not listed!" msgstr "" -#: rqalpha/mod/risk_manager/frontend_validator.py:68 -msgid "Order Rejected: {order_book_id} is suspended!" +#: rqalpha/mod/rqalpha_mod_sys_risk/is_trading_validator.py:35 +msgid "Order Rejected: {order_book_id} has been delisted!" msgstr "" -#: rqalpha/mod/risk_manager/frontend_validator.py:90 -#: rqalpha/mod/risk_manager/frontend_validator.py:124 -msgid "" -"Order Rejected: not enough money to buy {order_book_id}, needs " -"{cost_money:.2f}, cash {cash:.2f}" +#: rqalpha/mod/rqalpha_mod_sys_risk/is_trading_validator.py:41 +msgid "security {order_book_id} is suspended on {date}" msgstr "" -#: rqalpha/mod/risk_manager/frontend_validator.py:105 +#: rqalpha/mod/rqalpha_mod_sys_risk/position_validator.py:33 msgid "" "Order Rejected: not enough stock {order_book_id} to sell, you want to " "sell {quantity}, sellable {sellable}" msgstr "" -#: rqalpha/mod/risk_manager/frontend_validator.py:139 +#: rqalpha/mod/rqalpha_mod_sys_risk/position_validator.py:49 msgid "" "Order Rejected: not enough securities {order_book_id} to buy close, " "target sell quantity is {quantity}, sell_closable_quantity {closable}" msgstr "" -#: rqalpha/mod/risk_manager/frontend_validator.py:147 +#: rqalpha/mod/rqalpha_mod_sys_risk/position_validator.py:58 msgid "" "Order Rejected: not enough securities {order_book_id} to sell close, " "target sell quantity is {quantity}, buy_closable_quantity {closable}" msgstr "" -#: rqalpha/mod/simulation/matcher.py:57 -msgid "" -"Order Cancelled: current security [{order_book_id}] can not be traded in " -"listed date [{listed_date}]" -msgstr "" - -#: rqalpha/mod/simulation/matcher.py:62 -msgid "Order Cancelled: current bar [{order_book_id}] miss market data." -msgstr "" - -#: rqalpha/mod/simulation/matcher.py:70 +#: rqalpha/mod/rqalpha_mod_sys_risk/price_validator.py:33 msgid "" "Order Rejected: limit order price {limit_price} is higher than limit up " "{limit_up}." msgstr "" -#: rqalpha/mod/simulation/matcher.py:80 +#: rqalpha/mod/rqalpha_mod_sys_risk/price_validator.py:44 msgid "" "Order Rejected: limit order price {limit_price} is lower than limit down " "{limit_down}." msgstr "" -#: rqalpha/mod/simulation/matcher.py:95 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:74 +#: rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py:67 +msgid "" +"Order Cancelled: current security [{order_book_id}] can not be traded in " +"listed date [{listed_date}]" +msgstr "" + +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:79 +#: rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py:73 +msgid "Order Cancelled: current bar [{order_book_id}] miss market data." +msgstr "" + +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:104 msgid "Order Cancelled: current bar [{order_book_id}] reach the limit_up price." msgstr "" -#: rqalpha/mod/simulation/matcher.py:101 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:110 msgid "Order Cancelled: current bar [{order_book_id}] reach the limit_down price." msgstr "" -#: rqalpha/mod/simulation/matcher.py:118 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:117 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:123 +msgid "Order Cancelled: [{order_book_id}] has no liquidity." +msgstr "" + +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:136 msgid "" "Order Cancelled: market order {order_book_id} volume {order_volume} due " "to volume limit" msgstr "" -#: rqalpha/mod/simulation/matcher.py:140 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:171 msgid "" "Order Cancelled: market order {order_book_id} volume {order_volume} is " "larger than 25 percent of current bar volume, fill {filled_volume} " "actually" msgstr "" -#: rqalpha/mod/simulation/simulation_broker.py:130 +#: rqalpha/mod/rqalpha_mod_sys_simulation/mod.py:36 +msgid "invalid commission multiplier value: value range is [0, +∞)" +msgstr "" + +#: rqalpha/mod/rqalpha_mod_sys_simulation/mod.py:38 +msgid "invalid margin multiplier value: value range is (0, +∞]" +msgstr "" + +#: rqalpha/mod/rqalpha_mod_sys_simulation/mod.py:47 +#: rqalpha/mod/rqalpha_mod_sys_simulation/mod.py:53 +msgid "Not supported matching type {}" +msgstr "" + +#: rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py:54 +msgid "cancel_order function is not supported in signal mode" +msgstr "" + +#: rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py:89 +#: rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py:96 +msgid "You have traded {order_book_id} with {quantity} lots in {bar_status}" +msgstr "" + +#: rqalpha/mod/rqalpha_mod_sys_simulation/simulation_broker.py:97 msgid "{order_id} order has been cancelled by user." msgstr "" -#: rqalpha/mod/simulation/simulation_broker.py:150 +#: rqalpha/mod/rqalpha_mod_sys_simulation/simulation_broker.py:116 msgid "Order Rejected: {order_book_id} can not match. Market close." msgstr "" -#: rqalpha/model/bar.py:392 -msgid "id_or_symbols {} does not exist" +#: rqalpha/mod/rqalpha_mod_sys_simulation/simulation_event_source.py:184 +msgid "Frequency {} is not support." msgstr "" -#: rqalpha/model/slippage.py:43 +#: rqalpha/mod/rqalpha_mod_sys_simulation/decider/slippage.py:38 msgid "invalid slippage rate value: value range is [0, 1)" msgstr "" -#: rqalpha/model/account/future_account.py:82 -#: rqalpha/model/account/stock_account.py:71 -msgid "{order_book_id} is expired, close all positions by system" -msgstr "" - -#: rqalpha/model/account/stock_account.py:222 -msgid "no split data {}" +#: rqalpha/model/bar.py:345 +msgid "id_or_symbols {} does not exist" msgstr "" -#: rqalpha/model/account/stock_account.py:231 -#: rqalpha/model/account/stock_account.py:240 -msgid "split {order_book_id}, {position}" +#: rqalpha/model/account/future_account.py:144 +#: rqalpha/model/account/stock_account.py:141 +msgid "{order_book_id} is expired, close all positions by system" msgstr "" -#: rqalpha/model/account/stock_account.py:245 -msgid "split {order_book_id}, {series}" +#: rqalpha/model/position/base_position.py:79 +#: rqalpha/model/position/base_position.py:85 +#: rqalpha/model/position/stock_position.py:147 +#: rqalpha/model/position/stock_position.py:155 +#: rqalpha/model/position/stock_position.py:163 +#: rqalpha/model/position/stock_position.py:171 +#: rqalpha/model/position/stock_position.py:179 +msgid "[abandon] {} is no longer valid." msgstr "" -#: rqalpha/utils/__init__.py:150 +#: rqalpha/utils/__init__.py:171 msgid "not run {}({}, {}) because strategy is hold" msgstr "" -#: rqalpha/utils/arg_checker.py:58 +#: rqalpha/utils/arg_checker.py:48 msgid "" "function {}: invalid {} argument, expect a value of type {}, got {} " "(type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:67 +#: rqalpha/utils/arg_checker.py:57 msgid "" "function {}: invalid {} argument, expect a valid " "instrument/order_book_id/symbol, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:74 +#: rqalpha/utils/arg_checker.py:64 msgid "" "function {}: invalid {} argument, expect a valid stock " "instrument/order_book_id/symbol, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:81 +#: rqalpha/utils/arg_checker.py:71 msgid "" "function {}: invalid {} argument, expect a valid future " "instrument/order_book_id/symbol, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:92 rqalpha/utils/arg_checker.py:100 +#: rqalpha/utils/arg_checker.py:82 rqalpha/utils/arg_checker.py:90 msgid "Main Future contracts[88] are not supported in paper trading." msgstr "" -#: rqalpha/utils/arg_checker.py:94 rqalpha/utils/arg_checker.py:105 +#: rqalpha/utils/arg_checker.py:84 rqalpha/utils/arg_checker.py:95 msgid "Index Future contracts[99] are not supported in paper trading." msgstr "" -#: rqalpha/utils/arg_checker.py:165 +#: rqalpha/utils/arg_checker.py:155 msgid "function {}: invalid {} argument, expect a number, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:180 +#: rqalpha/utils/arg_checker.py:170 msgid "function {}: invalid {} argument, valid: {}, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:195 +#: rqalpha/utils/arg_checker.py:184 msgid "function {}: invalid {} argument, valid fields are {}, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:207 +#: rqalpha/utils/arg_checker.py:196 msgid "function {}: invalid field {}, valid fields are {}, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:212 rqalpha/utils/arg_checker.py:229 +#: rqalpha/utils/arg_checker.py:202 rqalpha/utils/arg_checker.py:220 msgid "" "function {}: invalid {} argument, expect a string or a list of string, " "got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:248 rqalpha/utils/arg_checker.py:253 +#: rqalpha/utils/arg_checker.py:240 rqalpha/utils/arg_checker.py:245 msgid "function {}: invalid {} argument, expect a valid date, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:264 +#: rqalpha/utils/arg_checker.py:256 +msgid "function {}: invalid {} argument, expect a value >= {}, got {} (type: {})" +msgstr "" + +#: rqalpha/utils/arg_checker.py:266 msgid "function {}: invalid {} argument, expect a value > {}, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:274 +#: rqalpha/utils/arg_checker.py:276 +msgid "function {}: invalid {} argument, expect a value <= {}, got {} (type: {})" +msgstr "" + +#: rqalpha/utils/arg_checker.py:287 msgid "function {}: invalid {} argument, expect a value < {}, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:291 +#: rqalpha/utils/arg_checker.py:304 msgid "" "function {}: invalid {} argument, interval should be in form of '1d', " "'3m', '4q', '2y', got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:304 +#: rqalpha/utils/arg_checker.py:326 +msgid "" +"function {}: invalid {} argument, quarter should be in form of '2012q3', " +"got {} (type: {})" +msgstr "" + +#: rqalpha/utils/arg_checker.py:340 msgid "" "function {}: invalid {} argument, should be entity like " "Fundamentals.balance_sheet.total_equity, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:323 +#: rqalpha/utils/arg_checker.py:359 msgid "" "function {}: invalid {} argument, frequency should be in form of '1m', " "'5m', '1d', got {} (type: {})" msgstr "" -#: rqalpha/utils/config.py:71 +#: rqalpha/utils/config.py:42 msgid "config.yml not found in {config_path}" msgstr "" -#: rqalpha/utils/config.py:92 -msgid "" -"\n" -"Your current config file {config_file_path} is too old and may cause " -"RQAlpha running error.\n" -"RQAlpha has replaced the config file with the newest one.\n" -"the backup config file has been saved in {back_config_file_path}.\n" -" " -msgstr "" - -#: rqalpha/utils/config.py:136 -msgid "invalid commission multiplier value: value range is [0, +∞)" +#: rqalpha/utils/config.py:94 +msgid "config path: {config_path} does not exist." msgstr "" -#: rqalpha/utils/config.py:138 -msgid "invalid margin multiplier value: value range is (0, +∞]" -msgstr "" - -#: rqalpha/utils/config.py:150 +#: rqalpha/utils/config.py:195 msgid "" "data bundle not found in {bundle_path}. Run `rqalpha update_bundle` to " "download data bundle." msgstr "" -#: rqalpha/utils/config.py:156 +#: rqalpha/utils/config.py:201 msgid "strategy file not found in {strategy_file}" msgstr "" -#: rqalpha/utils/config.py:175 +#: rqalpha/utils/config.py:218 msgid "invalid stock starting cash: {}" msgstr "" -#: rqalpha/utils/config.py:178 +#: rqalpha/utils/config.py:221 msgid "invalid future starting cash: {}" msgstr "" -#: rqalpha/utils/config.py:181 +#: rqalpha/utils/config.py:224 msgid "stock starting cash and future starting cash can not be both 0." msgstr "" -#: rqalpha/utils/config.py:218 +#: rqalpha/utils/config.py:258 msgid "in parse_user_config, exception: {e}" msgstr "" -#: rqalpha/utils/config.py:266 +#: rqalpha/utils/config.py:291 msgid "unknown persist mode: {persist_mode}" msgstr "" diff --git a/requirements-py2.txt b/requirements-py2.txt new file mode 100644 index 000000000..bea9b2d8a --- /dev/null +++ b/requirements-py2.txt @@ -0,0 +1,3 @@ +enum34>=1.1.6 +fastcache>=1.0.2 +funcsigs diff --git a/requirements.txt b/requirements.txt index d6e1198b1..0c40f270e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,22 @@ --e . +six +requests +numpy>=1.11.1 +pandas>=0.18.1 +python-dateutil>=2.5.3 +pytz>=2016.4 +six>=1.10.0 +pytest>=2.9.2 +logbook==1.0.0 +click>=6.6 +bcolz>=1.1.0 +matplotlib>=1.5.1 +jsonpickle==0.9.4 +simplejson>=3.10.0 +dill==0.2.5 +XlsxWriter>=0.9.6 +line-profiler>=2.0 +ruamel.yaml>=0.13.14 + +better_exceptions +tabulate +colorama==0.3.6 \ No newline at end of file diff --git a/rqalpha/VERSION.txt b/rqalpha/VERSION.txt index e4737652c..c346e7a04 100644 --- a/rqalpha/VERSION.txt +++ b/rqalpha/VERSION.txt @@ -1 +1 @@ -0.3.13 +2.1.4 \ No newline at end of file diff --git a/rqalpha/__init__.py b/rqalpha/__init__.py index 5f5a7cc08..9f1eb5ec4 100644 --- a/rqalpha/__init__.py +++ b/rqalpha/__init__.py @@ -19,6 +19,9 @@ """ import pkgutil +from .__main__ import cli +from .cmd import cmd_cli +from .api.api_base import export_as_api __all__ = [ '__version__', @@ -42,6 +45,6 @@ def run(config, source_code=None): return main.run(parse_config(config, click_type=False, source_code=source_code), source_code=source_code) -def update_bundle(data_bundle_path=None, confirm=True): +def update_bundle(data_bundle_path=None, locale="zh_Hans_CN", confirm=True): from . import main - main.update_bundle(data_bundle_path, confirm) + main.update_bundle(data_bundle_path=data_bundle_path, locale=locale, confirm=confirm) diff --git a/rqalpha/__main__.py b/rqalpha/__main__.py index e99e68966..b5083ca6e 100644 --- a/rqalpha/__main__.py +++ b/rqalpha/__main__.py @@ -14,15 +14,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -import click import errno import os import shutil +import six +import click import ruamel.yaml as yaml from importlib import import_module from .utils.click_helper import Date -from .utils.config import parse_config, get_default_config_path, load_config, dump_config +from .utils.config import parse_config, get_mod_config_path, dump_config, load_mod_config @click.group() @@ -33,6 +34,35 @@ def cli(ctx, verbose): def entry_point(): + from rqalpha.mod import SYSTEM_MOD_LIST + from rqalpha.utils.package_helper import import_mod + mod_config_path = get_mod_config_path() + mod_config = load_mod_config(mod_config_path, loader=yaml.RoundTripLoader) + + for mod_name, config in six.iteritems(mod_config['mod']): + lib_name = "rqalpha_mod_{}".format(mod_name) + if not config['enabled']: + continue + if mod_name in SYSTEM_MOD_LIST: + # inject system mod + import_mod("rqalpha.mod." + lib_name) + else: + # inject third part mod + import_mod(lib_name) + + # 原本是注入系统中所有的Mod对应的命令,现在修改为只注入用户启动的Mod对应的命令 + + # from . import mod + # from pkgutil import iter_modules + # from rqalpha.utils.package_helper import import_mod + # # inject system mod + # for package_name in mod.SYSTEM_MOD_LIST: + # import_mod("rqalpha.mod.rqalpha_mod_" + package_name) + # # inject user mod + # for package in iter_modules(): + # if "rqalpha_mod_" in package[1]: + # import_mod(package[1]) + cli(obj={}) @@ -54,16 +84,12 @@ def update_bundle(data_bundle_path, locale): @click.option('-f', '--strategy-file', 'base__strategy_file', type=click.Path(exists=True)) @click.option('-s', '--start-date', 'base__start_date', type=Date()) @click.option('-e', '--end-date', 'base__end_date', type=Date()) -@click.option('-r', '--rid', 'base__run_id', type=click.STRING) @click.option('-sc', '--stock-starting-cash', 'base__stock_starting_cash', type=click.FLOAT) @click.option('-fc', '--future-starting-cash', 'base__future_starting_cash', type=click.FLOAT) @click.option('-bm', '--benchmark', 'base__benchmark', type=click.STRING, default=None) -@click.option('-sp', '--slippage', 'base__slippage', type=click.FLOAT) -@click.option('-cm', '--commission-multiplier', 'base__commission_multiplier', type=click.FLOAT) @click.option('-mm', '--margin-multiplier', 'base__margin_multiplier', type=click.FLOAT) -@click.option('-st', '--strategy-type', 'base__strategy_type', type=click.Choice(['stock', 'future', 'stock_future'])) -@click.option('-fq', '--frequency', 'base__frequency', type=click.Choice(['1d', '1m'])) -@click.option('-me', '--match-engine', 'base__matching_type', type=click.Choice(['current_bar', 'next_bar'])) +@click.option('-st', '--security', 'base__securities', multiple=True, type=click.Choice(['stock', 'future'])) +@click.option('-fq', '--frequency', 'base__frequency', type=click.Choice(['1d', '1m', 'tick'])) @click.option('-rt', '--run-type', 'base__run_type', type=click.Choice(['b', 'p']), default="b") @click.option('--resume', 'base__resume_mode', is_flag=True) @click.option('--handle-split/--not-handle-split', 'base__handle_split', default=None, help="handle split") @@ -72,21 +98,14 @@ def update_bundle(data_bundle_path, locale): @click.option('--locale', 'extra__locale', type=click.Choice(['cn', 'en']), default="cn") @click.option('--disable-user-system-log', 'extra__user_system_log_disabled', is_flag=True, help='disable user system log') @click.option('--extra-vars', 'extra__context_vars', type=click.STRING, help="override context vars") -@click.option("--enable-profiler", "extra__enable_profiler", is_flag=True, - help="add line profiler to profile your strategy") +@click.option("--enable-profiler", "extra__enable_profiler", is_flag=True, help="add line profiler to profile your strategy") @click.option('--config', 'config_path', type=click.STRING, help="config file path") # -- Mod Configuration @click.option('-mc', '--mod-config', 'mod_configs', nargs=2, multiple=True, type=click.STRING, help="mod extra config") -@click.option('-p', '--plot/--no-plot', 'mod__analyser__plot', default=None, help="plot result") -@click.option('--plot-save', 'mod__analyser__plot_save_file', default=None, help="save plot to file") -@click.option('--report', 'mod__analyser__report_save_path', type=click.Path(writable=True), help="save report") -@click.option('-o', '--output-file', 'mod__analyser__output_file', type=click.Path(writable=True), - help="output result pickle file") -@click.option('--progress/--no-progress', 'mod__progress__enabled', default=None, help="show progress bar") -@click.option('--short-stock', 'mod__risk_manager__short_stock', is_flag=True, help="enable stock shorting") -# -- DEPRECATED ARGS && WILL BE REMOVED AFTER VERSION 1.0.0 -@click.option('-i', '--init-cash', 'base__stock_starting_cash', type=click.FLOAT) -@click.option('-k', '--kind', 'base__strategy_type', type=click.Choice(['stock', 'future', 'stock_future'])) +# -- DEPRECATED ARGS && WILL BE REMOVED AFTER VERSION 3.0.0 +@click.option('-i', '--init-cash', 'base__stock_starting_cash', type=click.FLOAT, help="[Deprecated]") +@click.option('-k', '--kind', 'base__securities', type=click.Choice(['stock', 'future', 'stock_future']), help="[Deprecated]") +@click.option('--strategy-type', 'base__securities', type=click.Choice(['stock', 'future']), help="[Deprecated]") def run(**kwargs): """ Start to run a strategy @@ -94,6 +113,7 @@ def run(**kwargs): config_path = kwargs.get('config_path', None) if config_path is not None: config_path = os.path.abspath(config_path) + kwargs.pop('config_path') from . import main main.run(parse_config(kwargs, config_path)) @@ -114,38 +134,6 @@ def examples(directory): print("Folder examples is exists.") -@cli.command() -@click.argument('result_dict_file', type=click.Path(exists=True), required=True) -@click.option('--show/--hide', 'is_show', default=True) -@click.option('--plot-save', 'plot_save_file', default=None, type=click.Path(), help="save plot result to file") -def plot(result_dict_file, is_show, plot_save_file): - """ - Draw result DataFrame - """ - import pandas as pd - from rqalpha.plot import plot_result - - result_dict = pd.read_pickle(result_dict_file) - if is_show: - plot_result(result_dict) - if plot_save_file: - plot_result(result_dict, show_windows=False, savefile=plot_save_file) - - -@cli.command() -@click.argument('result_pickle_file_path', type=click.Path(exists=True), required=True) -@click.argument('target_report_csv_path', type=click.Path(exists=True, writable=True), required=True) -def report(result_pickle_file_path, target_report_csv_path): - """ - Generate report from backtest output file - """ - import pandas as pd - result_dict = pd.read_pickle(result_pickle_file_path) - - from rqalpha.utils.report import generate_report - generate_report(result_dict, target_report_csv_path) - - @cli.command() @click.option('-v', '--verbose', is_flag=True) def version(**kwargs): @@ -162,7 +150,7 @@ def generate_config(directory): """ Generate default config file """ - default_config = os.path.join(os.path.dirname(os.path.realpath(__file__)), "config_template.yml") + default_config = os.path.join(os.path.dirname(os.path.realpath(__file__)), "default_config.yml") target_config_path = os.path.abspath(os.path.join(directory, 'config.yml')) shutil.copy(default_config, target_config_path) print("Config file has been generated in", target_config_path) @@ -170,164 +158,189 @@ def generate_config(directory): # For Mod Cli -system_mod = [ - 'simulation', - 'funcat_api', - 'progress', - 'simple_stock_realtime_trade', - 'progressive_output_csv', - 'risk_manager', - 'analyser', -] - - @cli.command(context_settings=dict( ignore_unknown_options=True, )) +@click.help_option('-h', '--help') +@click.argument('cmd', nargs=1, type=click.Choice(['list', 'enable', 'disable', 'install', 'uninstall'])) @click.argument('params', nargs=-1) -def install(params): +def mod(cmd, params): """ - Install third-party Mod - """ - from pip import main as pip_main - from pip.commands.install import InstallCommand - - params = [param for param in params] - - options, mod_list = InstallCommand().parse_args(params) - - params = ["install"] + params - - for mod_name in mod_list: - mod_name_index = params.index(mod_name) - if mod_name in system_mod: - print('System Mod can not be installed or uninstalled') - return - if "rqalpha_mod_" in mod_name: - lib_name = mod_name - mod_name = lib_name.replace("rqalpha_mod_", "") - else: - lib_name = "rqalpha_mod_" + mod_name - params[mod_name_index] = lib_name - - # Install Mod - pip_main(params) - - # Export config - config_path = get_default_config_path() - config = load_config(config_path, loader=yaml.RoundTripLoader) - - for mod_name in mod_list: - if "rqalpha_mod_" in mod_name: - lib_name = mod_name - mod_name = lib_name.replace("rqalpha_mod_", "") - else: - lib_name = "rqalpha_mod_" + mod_name - - mod = import_module(lib_name) - - mod_config = yaml.load(mod.__mod_config__, yaml.RoundTripLoader) + Mod management command - config['mod'][mod_name] = mod_config - config['mod'][mod_name]['lib'] = lib_name - config['mod'][mod_name]['enabled'] = False - config['mod'][mod_name]['priority'] = 1000 + rqalpha mod list \n + rqalpha mod install xxx \n + rqalpha mod uninstall xxx \n + rqalpha mod enable xxx \n + rqalpha mod disable xxx \n - dump_config(config_path, config) - - -@cli.command(context_settings=dict( - ignore_unknown_options=True, -)) -@click.argument('params', nargs=-1) -def uninstall(params): """ - Uninstall third-party Mod - """ - from pip import main as pip_main - from pip.commands.uninstall import UninstallCommand - - params = [param for param in params] - - options, mod_list = UninstallCommand().parse_args(params) - - params = ["uninstall"] + params - - for mod_name in mod_list: - mod_name_index = params.index(mod_name) - if mod_name in system_mod: - print('System Mod can not be installed or uninstalled') - return - if "rqalpha_mod_" in mod_name: - lib_name = mod_name - mod_name = lib_name.replace("rqalpha_mod_", "") - else: - lib_name = "rqalpha_mod_" + mod_name - params[mod_name_index] = lib_name - - # Uninstall Mod - pip_main(params) - - # Remove Mod Config - config_path = get_default_config_path() - config = load_config(config_path, loader=yaml.RoundTripLoader) - - for mod_name in mod_list: + def list(params): + """ + List all mod configuration + """ + from colorama import init, Fore + from tabulate import tabulate + init() + mod_config_path = get_mod_config_path(generate=True) + mod_config = load_mod_config(mod_config_path, loader=yaml.RoundTripLoader) + + table = [] + + for mod_name, mod in six.iteritems(mod_config['mod']): + table.append([ + Fore.RESET + mod_name, + Fore.GREEN + "enabled" + Fore.RESET if mod['enabled'] else Fore.RED + "disabled" + Fore.RESET + ]) + + headers = [ + Fore.CYAN + "name", + Fore.CYAN + "status" + Fore.RESET + ] + + print(tabulate(table, headers=headers, tablefmt="psql")) + print(Fore.LIGHTYELLOW_EX + "You can use `rqalpha mod list/install/uninstall/enable/disable` to manage your mods") + + def install(params): + """ + Install third-party Mod + """ + from pip import main as pip_main + from pip.commands.install import InstallCommand + + params = [param for param in params] + + options, mod_list = InstallCommand().parse_args(params) + + params = ["install"] + params + + for mod_name in mod_list: + mod_name_index = params.index(mod_name) + if mod_name.startswith("rqalpha_mod_sys_"): + print('System Mod can not be installed or uninstalled') + return + if "rqalpha_mod_" in mod_name: + lib_name = mod_name + else: + lib_name = "rqalpha_mod_" + mod_name + params[mod_name_index] = lib_name + + # Install Mod + pip_main(params) + + # Export config + mod_config_path = get_mod_config_path(generate=True) + mod_config = load_mod_config(mod_config_path, loader=yaml.RoundTripLoader) + + if len(mod_list) == 0: + """ + 主要是方便 `pip install -e .` 这种方式 本地调试 Mod 使用,需要满足以下条件: + 1. `rqalpha mod install -e .` 命令是在对应 自定义 Mod 的根目录下 + 2. 该 Mod 必须包含 `setup.py` 文件(否则也不能正常的 `pip install -e .` 来安装) + 3. 该 Mod 包名必须按照 RQAlpha 的规范来命名,具体规则如下 + * 必须以 `rqalpha-mod-` 来开头,比如 `rqalpha-mod-xxx-yyy` + * 对应import的库名必须要 `rqalpha_mod_` 来开头,并且需要和包名后半部分一致,但是 `-` 需要替换为 `_`, 比如 `rqalpha_mod_xxx_yyy` + """ + mod_name = _detect_package_name_from_dir() + mod_name = mod_name.replace("-", "_").replace("rqalpha_mod_", "") + mod_list.append(mod_name) + + for mod_name in mod_list: + if "rqalpha_mod_" in mod_name: + mod_name = mod_name.replace("rqalpha_mod_", "") + mod_config['mod'][mod_name] = {} + mod_config['mod'][mod_name]['enabled'] = False + + dump_config(mod_config_path, mod_config) + list({}) + + def uninstall(params): + """ + Uninstall third-party Mod + """ + + from pip import main as pip_main + from pip.commands.uninstall import UninstallCommand + + params = [param for param in params] + + options, mod_list = UninstallCommand().parse_args(params) + + params = ["uninstall"] + params + + for mod_name in mod_list: + mod_name_index = params.index(mod_name) + if mod_name.startswith("rqalpha_mod_sys_"): + print('System Mod can not be installed or uninstalled') + return + if "rqalpha_mod_" in mod_name: + lib_name = mod_name + else: + lib_name = "rqalpha_mod_" + mod_name + params[mod_name_index] = lib_name + + # Uninstall Mod + pip_main(params) + + # Remove Mod Config + mod_config_path = get_mod_config_path(generate=True) + mod_config = load_mod_config(mod_config_path, loader=yaml.RoundTripLoader) + + for mod_name in mod_list: + if "rqalpha_mod_" in mod_name: + mod_name = mod_name.replace("rqalpha_mod_", "") + + del mod_config['mod'][mod_name] + + dump_config(mod_config_path, mod_config) + list({}) + + def enable(params): + """ + enable mod + """ + mod_name = params[0] if "rqalpha_mod_" in mod_name: mod_name = mod_name.replace("rqalpha_mod_", "") - del config['mod'][mod_name] + # check whether is installed + module_name = "rqalpha_mod_" + mod_name + try: + import_module(module_name) + except ImportError: + install([module_name]) - dump_config(config_path, config) + mod_config_path = get_mod_config_path(generate=True) + mod_config = load_mod_config(mod_config_path, loader=yaml.RoundTripLoader) + mod_config['mod'][mod_name]['enabled'] = True + dump_config(mod_config_path, mod_config) + list({}) -@cli.command() -def list(): - """ - List all mod configuration - """ - config_path = get_default_config_path() - config = load_config(config_path, loader=yaml.RoundTripLoader) - - print(yaml.dump(config['mod'], Dumper=yaml.RoundTripDumper)) - - -@cli.command() -@click.argument('mod_name') -def enable(mod_name): - """ - enable mod - """ - if mod_name not in system_mod and "rqalpha_mod_" in mod_name: - mod_name = mod_name.replace("rqalpha_mod_", "") + def disable(params): + """ + disable mod + """ + mod_name = params[0] - config_path = get_default_config_path() - config = load_config(config_path, loader=yaml.RoundTripLoader) + if "rqalpha_mod_" in mod_name: + mod_name = mod_name.replace("rqalpha_mod_", "") - try: - config['mod'][mod_name]['enabled'] = True - dump_config(config_path, config) - except Exception as e: - pass + mod_config_path = get_mod_config_path(generate=True) + mod_config = load_mod_config(mod_config_path, loader=yaml.RoundTripLoader) + mod_config['mod'][mod_name]['enabled'] = False + dump_config(mod_config_path, mod_config) + list({}) -@cli.command() -@click.argument('mod_name') -def disable(mod_name): - """ - disable mod - """ - if mod_name not in system_mod and "rqalpha_mod_" in mod_name: - mod_name = mod_name.replace("rqalpha_mod_", "") + locals()[cmd](params) - config_path = get_default_config_path() - config = load_config(config_path, loader=yaml.RoundTripLoader) - try: - config['mod'][mod_name]['enabled'] = False - dump_config(config_path, config) - except Exception as e: - pass +def _detect_package_name_from_dir(): + setup_path = os.path.join(os.path.abspath('.'), 'setup.py') + if not os.path.exists(setup_path): + return None + return os.path.split(os.path.dirname(setup_path))[1] if __name__ == '__main__': diff --git a/rqalpha/api/__init__.py b/rqalpha/api/__init__.py index 11501b457..d56912103 100644 --- a/rqalpha/api/__init__.py +++ b/rqalpha/api/__init__.py @@ -18,5 +18,3 @@ from .api_base import * from .api_stock import * from .api_future import * - -from ..utils.logger import user_log as logger diff --git a/rqalpha/api/api_base.py b/rqalpha/api/api_base.py index 47d940178..bd7e778c9 100644 --- a/rqalpha/api/api_base.py +++ b/rqalpha/api/api_base.py @@ -20,37 +20,43 @@ ''' from __future__ import division -import sys + import datetime import inspect +import sys +from collections import Iterable +from functools import wraps +from types import FunctionType + import pandas as pd import six -from collections import Iterable from dateutil.parser import parse -from types import FunctionType -from functools import wraps -from typing import List +from . import names from ..environment import Environment -from ..model.instrument import Instrument, SectorCode as sector_code, IndustryCode as industry_code -from ..model.instrument import SectorCodeItem, IndustryCodeItem from ..execution_context import ExecutionContext -from ..const import EXECUTION_PHASE, EXC_TYPE, ORDER_STATUS, SIDE, POSITION_EFFECT, ORDER_TYPE, MATCHING_TYPE, RUN_TYPE from ..utils import to_industry_code, to_sector_name, unwrapper from ..utils.exception import patch_user_exc, patch_system_exc, EXC_EXT_NAME, RQInvalidArgument from ..utils.i18n import gettext as _ -from ..model.snapshot import SnapshotObject -from ..model.order import Order, MarketOrder, LimitOrder -from ..model.slippage import PriceRatioSlippage +# noinspection PyUnresolvedReferences +from ..utils.logger import user_log as logger + +from ..model.instrument import SectorCodeItem, IndustryCodeItem from ..utils.arg_checker import apply_rules, verify_that -from . import names +# noinspection PyUnresolvedReferences +from ..model.instrument import Instrument, SectorCode as sector_code, IndustryCode as industry_code +# noinspection PyUnresolvedReferences +from ..const import EXECUTION_PHASE, EXC_TYPE, ORDER_STATUS, SIDE, POSITION_EFFECT, ORDER_TYPE, MATCHING_TYPE, RUN_TYPE +# noinspection PyUnresolvedReferences +from ..model.order import Order, MarketOrder, LimitOrder + __all__ = [ + 'logger', 'sector_code', 'industry_code', 'LimitOrder', 'MarketOrder', - 'PriceRatioSlippage', 'ORDER_STATUS', 'SIDE', 'POSITION_EFFECT', @@ -112,6 +118,7 @@ def export_as_api(func): __all__.append(func.__name__) func = decorate_api_exc(func) + globals()[func.__name__] = func return func @@ -122,7 +129,7 @@ def assure_order_book_id(id_or_ins): elif isinstance(id_or_ins, six.string_types): order_book_id = instruments(id_or_ins).order_book_id else: - raise RQInvalidArgument(_("unsupported order_book_id type")) + raise RQInvalidArgument(_(u"unsupported order_book_id type")) return order_book_id @@ -133,11 +140,8 @@ def assure_order_book_id(id_or_ins): EXECUTION_PHASE.ON_TICK, EXECUTION_PHASE.AFTER_TRADING, EXECUTION_PHASE.SCHEDULED) -def get_order(order_id): - if isinstance(order_id, Order): - return order_id - else: - return ExecutionContext.account.get_order(order_id) +def get_order(order): + return order @export_as_api @@ -152,7 +156,7 @@ def get_open_orders(): :return: List[:class:`~Order` object] """ - return ExecutionContext.account.get_open_orders() + return Environment.get_instance().broker.get_open_orders() @export_as_api @@ -168,10 +172,12 @@ def cancel_order(order): :param order: 需要撤销的order对象 :type order: :class:`~Order` object """ - order = order if isinstance(order, Order) else get_order(order) if order is None: - patch_user_exc(KeyError(_("Cancel order fail: invalid order id"))) - ExecutionContext.broker.cancel_order(order) + patch_user_exc(KeyError(_(u"Cancel order fail: invalid order id"))) + env = Environment.get_instance() + if env.can_cancel_order(order): + env.broker.cancel_order(order) + return order @export_as_api @@ -192,7 +198,7 @@ def update_universe(id_or_symbols): if isinstance(id_or_symbols, (six.string_types, Instrument)): id_or_symbols = [id_or_symbols] order_book_ids = set(assure_order_book_id(order_book_id) for order_book_id in id_or_symbols) - if order_book_ids != Environment.get_instance().universe: + if order_book_ids != Environment.get_instance().get_universe(): Environment.get_instance().update_universe(order_book_ids) @@ -213,7 +219,7 @@ def subscribe(id_or_symbols): :param id_or_ins: 标的物 :type id_or_ins: :class:`~Instrument` object | `str` | List[:class:`~Instrument`] | List[`str`] """ - current_universe = Environment.get_instance().universe + current_universe = Environment.get_instance().get_universe() if isinstance(id_or_symbols, six.string_types): order_book_id = instruments(id_or_symbols).order_book_id current_universe.add(order_book_id) @@ -223,7 +229,7 @@ def subscribe(id_or_symbols): for item in id_or_symbols: current_universe.add(assure_order_book_id(item)) else: - raise RQInvalidArgument(_("unsupported order_book_id type")) + raise RQInvalidArgument(_(u"unsupported order_book_id type")) verify_that('id_or_symbols')._are_valid_instruments("subscribe", id_or_symbols) Environment.get_instance().update_universe(current_universe) @@ -243,7 +249,7 @@ def unsubscribe(id_or_symbols): :param id_or_ins: 标的物 :type id_or_ins: :class:`~Instrument` object | `str` | List[:class:`~Instrument`] | List[`str`] """ - current_universe = Environment.get_instance().universe + current_universe = Environment.get_instance().get_universe() if isinstance(id_or_symbols, six.string_types): order_book_id = instruments(id_or_symbols).order_book_id current_universe.discard(order_book_id) @@ -254,7 +260,7 @@ def unsubscribe(id_or_symbols): i = assure_order_book_id(item) current_universe.discard(i) else: - raise RQInvalidArgument(_("unsupported order_book_id type")) + raise RQInvalidArgument(_(u"unsupported order_book_id type")) Environment.get_instance().update_universe(current_universe) @@ -297,10 +303,9 @@ def get_yield_curve(date=None, tenor=None): 2013-01-04 0.0314 0.0318 ... 0.0342 0.0350 0.0353 0.0357 0.0361 ... """ - data_proxy = ExecutionContext.data_proxy - dt = ExecutionContext.get_current_trading_dt().date() - - yesterday = data_proxy.get_previous_trading_date(dt) + env = Environment.get_instance() + trading_date = env.trading_dt.date() + yesterday = env.data_proxy.get_previous_trading_date(trading_date) if date is None: date = yesterday @@ -309,7 +314,7 @@ def get_yield_curve(date=None, tenor=None): if date > yesterday: raise RQInvalidArgument('get_yield_curve: {} >= now({})'.format(date, yesterday)) - return data_proxy.get_yield_curve(start_date=date, end_date=date, tenor=tenor) + return env.data_proxy.get_yield_curve(start_date=date, end_date=date, tenor=tenor) @export_as_api @@ -320,10 +325,12 @@ def get_yield_curve(date=None, tenor=None): EXECUTION_PHASE.SCHEDULED) @apply_rules(verify_that('order_book_id').is_valid_instrument(), verify_that('bar_count').is_instance_of(int).is_greater_than(0), - verify_that('frequency').is_in(('1m', '1d')), + verify_that('frequency').is_valid_frequency(), verify_that('fields').are_valid_fields(names.VALID_HISTORY_FIELDS, ignore_none=True), - verify_that('skip_suspended').is_instance_of(bool)) -def history_bars(order_book_id, bar_count, frequency, fields=None, skip_suspended=True): + verify_that('skip_suspended').is_instance_of(bool), + verify_that('include_now').is_instance_of(bool)) +def history_bars(order_book_id, bar_count, frequency, fields=None, skip_suspended=True, + include_now=False): """ 获取指定合约的历史行情,同时支持日以及分钟历史数据。不能在init中调用。 注意,该API会自动跳过停牌数据。 @@ -356,7 +363,7 @@ def history_bars(order_book_id, bar_count, frequency, fields=None, skip_suspende T日handle_bar T日当前minute bar ========================= =================================================== - :param order_book_id: 合约代码或者合约代码列表 + :param order_book_id: 合约代码 :type order_book_id: `str` :param int bar_count: 获取的历史数据数量,必填项 @@ -382,6 +389,9 @@ def history_bars(order_book_id, bar_count, frequency, fields=None, skip_suspende prev_settlement 结算价(期货日线专用) ========================= =================================================== + :param bool skip_suspended: 是否跳过停牌数据 + :param bool include_now: 是否包含当前数据 + :return: `ndarray`, 方便直接与talib等计算库对接,效率较history返回的DataFrame更高。 :example: @@ -397,18 +407,24 @@ def history_bars(order_book_id, bar_count, frequency, fields=None, skip_suspende [ 8.69 8.7 8.71 8.81 8.81] """ order_book_id = assure_order_book_id(order_book_id) - data_proxy = ExecutionContext.data_proxy - dt = ExecutionContext.get_current_calendar_dt() + env = Environment.get_instance() + dt = env.calendar_dt - if frequency == '1m' and Environment.get_instance().config.base.frequency == '1d': + if frequency[-1] == 'm' and Environment.get_instance().config.base.frequency == '1d': raise RQInvalidArgument('can not get minute history in day back test') - if (Environment.get_instance().config.base.frequency == '1m' and frequency == '1d') or \ - (frequency == '1d' and ExecutionContext.get_active().phase == EXECUTION_PHASE.BEFORE_TRADING): - # 在分钟回测获取日线数据, 应该推前一天,这里应该使用 trading date - dt = data_proxy.get_previous_trading_date(ExecutionContext.get_current_trading_dt().date()) + if frequency == '1d': + sys_frequency = Environment.get_instance().config.base.frequency + if ((sys_frequency in ['1m', 'tick'] and not include_now) or ExecutionContext.phase() == EXECUTION_PHASE.BEFORE_TRADING): + dt = env.data_proxy.get_previous_trading_date(env.trading_dt.date()) + # 当 EXECUTION_PHASE.BEFORE_TRADING 的时候,强制 include_now 为 False + include_now = False + if sys_frequency == "1d": + # 日回测不支持 include_now + include_now = False - return data_proxy.history_bars(order_book_id, bar_count, frequency, fields, dt, skip_suspended) + return env.data_proxy.history_bars(order_book_id, bar_count, frequency, fields, dt, + skip_suspended=skip_suspended, include_now=include_now) @export_as_api @@ -462,7 +478,7 @@ def all_instruments(type=None): ... """ - return ExecutionContext.data_proxy.all_instruments(type) + return Environment.get_instance().data_proxy.all_instruments(type) @export_as_api @@ -518,7 +534,7 @@ def instruments(id_or_symbols): instruments('IF1701').days_to_expire() """ - return ExecutionContext.data_proxy.instruments(id_or_symbols) + return Environment.get_instance().data_proxy.instruments(id_or_symbols) @export_as_api @@ -535,7 +551,7 @@ def sector(code): else: code = to_sector_name(code) - return ExecutionContext.data_proxy.sector(code) + return Environment.get_instance().data_proxy.sector(code) @export_as_api @@ -552,7 +568,7 @@ def industry(code): else: code = to_industry_code(code) - return ExecutionContext.data_proxy.industry(code) + return Environment.get_instance().data_proxy.industry(code) @export_as_api @@ -563,7 +579,7 @@ def industry(code): EXECUTION_PHASE.AFTER_TRADING, EXECUTION_PHASE.SCHEDULED) def concept(*concept_names): - return ExecutionContext.data_proxy.concept(*concept_names) + return Environment.get_instance().data_proxy.concept(*concept_names) @export_as_api @@ -596,7 +612,7 @@ def get_trading_dates(start_date, end_date): [Out] [datetime.date(2016, 5, 5)] """ - return ExecutionContext.data_proxy.get_trading_dates(start_date, end_date) + return Environment.get_instance().data_proxy.get_trading_dates(start_date, end_date) @export_as_api @@ -625,7 +641,7 @@ def get_previous_trading_date(date): [Out] [datetime.date(2016, 4, 29)] """ - return ExecutionContext.data_proxy.get_previous_trading_date(date) + return Environment.get_instance().data_proxy.get_previous_trading_date(date) @export_as_api @@ -654,7 +670,7 @@ def get_next_trading_date(date): [Out] [datetime.date(2016, 5, 3)] """ - return ExecutionContext.data_proxy.get_next_trading_date(date) + return Environment.get_instance().data_proxy.get_next_trading_date(date) def to_date(date): @@ -666,7 +682,7 @@ def to_date(date): return date.date() except AttributeError: return date - + raise RQInvalidArgument('unknown date value: {}'.format(date)) @@ -681,15 +697,16 @@ def to_date(date): verify_that('start_date').is_valid_date(ignore_none=False), verify_that('adjusted').is_instance_of(bool)) def get_dividend(order_book_id, start_date, adjusted=True): - dt = ExecutionContext.get_current_trading_dt().date() - datetime.timedelta(days=1) + env = Environment.get_instance() + dt = env.trading_dt.date() - datetime.timedelta(days=1) start_date = to_date(start_date) if start_date > dt: raise RQInvalidArgument( - _('in get_dividend, start_date {} is later than the previous test day {}').format( + _(u"in get_dividend, start_date {} is later than the previous test day {}").format( start_date, dt )) order_book_id = assure_order_book_id(order_book_id) - df = ExecutionContext.data_proxy.get_dividend(order_book_id, adjusted) + df = env.data_proxy.get_dividend(order_book_id, adjusted) return df[start_date:dt] @@ -706,11 +723,7 @@ def plot(series_name, value): :param float value: the value of the series in this time :return: None """ - if ExecutionContext.plots is None: - # FIXME: this is ugly - from ..utils.plot_store import PlotStore - ExecutionContext.plots = PlotStore() - ExecutionContext.plots.add_plot(ExecutionContext.trading_dt.date(), series_name, value) + Environment.get_instance().add_plot(series_name, value) @export_as_api @@ -739,10 +752,7 @@ def current_snapshot(id_or_symbol): 2016-01-04 09:33:00.00 INFO Snapshot(order_book_id: '000001.XSHE', datetime: datetime.datetime(2016, 1, 4, 9, 33), open: 10.0, high: 10.025, low: 9.9667, last: 9.9917, volume: 2050320, total_turnover: 20485195, prev_close: 9.99) """ - data_proxy = ExecutionContext.data_proxy - cal_dt = ExecutionContext.get_current_calendar_dt() - - frequency = Environment.get_instance().config.base.frequency - + env = Environment.get_instance() + frequency = env.config.base.frequency order_book_id = assure_order_book_id(id_or_symbol) - return data_proxy.current_snapshot(order_book_id, frequency, cal_dt) + return env.data_proxy.current_snapshot(order_book_id, frequency, env.calendar_dt) diff --git a/rqalpha/api/api_future.py b/rqalpha/api/api_future.py index fdf55542d..ff4155bd7 100644 --- a/rqalpha/api/api_future.py +++ b/rqalpha/api/api_future.py @@ -25,15 +25,15 @@ from .api_base import decorate_api_exc, instruments from ..execution_context import ExecutionContext +from ..environment import Environment from ..model.order import Order, MarketOrder, LimitOrder, OrderStyle from ..const import EXECUTION_PHASE, SIDE, POSITION_EFFECT, ORDER_TYPE from ..model.instrument import Instrument -from ..utils.exception import patch_user_exc, RQInvalidArgument +from ..utils.exception import RQInvalidArgument from ..utils.logger import user_system_log from ..utils.i18n import gettext as _ from ..utils.arg_checker import apply_rules, verify_that - __all__ = [ ] @@ -60,27 +60,28 @@ def order(id_or_ins, amount, side, position_effect, style): if amount <= 0: raise RuntimeError if isinstance(style, LimitOrder) and style.get_limit_price() <= 0: - raise RQInvalidArgument(_("Limit order price should be positive")) + raise RQInvalidArgument(_(u"Limit order price should be positive")) order_book_id = assure_future_order_book_id(id_or_ins) - - price = ExecutionContext.get_current_close_price(order_book_id) - + env = Environment.get_instance() + price = env.get_last_price(order_book_id) amount = int(amount) - calendar_dt = ExecutionContext.get_current_calendar_dt() - trading_dt = ExecutionContext.get_current_trading_dt() - r_order = Order.__from_create__(calendar_dt, trading_dt, order_book_id, amount, side, style, position_effect) + r_order = Order.__from_create__(env.calendar_dt, env.trading_dt, order_book_id, amount, side, style, + position_effect) if np.isnan(price) or price == 0: - user_system_log.warn(_("Order Creation Failed: [{order_book_id}] No market data").format(order_book_id=order_book_id)) - r_order._mark_rejected(_("Order Creation Failed: [{order_book_id}] No market data").format(order_book_id=order_book_id)) + user_system_log.warn( + _(u"Order Creation Failed: [{order_book_id}] No market data").format(order_book_id=order_book_id)) + r_order.mark_rejected( + _(u"Order Creation Failed: [{order_book_id}] No market data").format(order_book_id=order_book_id)) return r_order if r_order.type == ORDER_TYPE.MARKET: - bar_dict = ExecutionContext.get_current_bar_dict() - r_order._frozen_price = bar_dict[order_book_id].close - ExecutionContext.broker.submit_order(r_order) + r_order.set_frozen_price(price) + + if env.can_submit_order(r_order): + env.broker.submit_order(r_order) return r_order @@ -174,14 +175,14 @@ def assure_future_order_book_id(id_or_symbols): if isinstance(id_or_symbols, Instrument): if id_or_symbols.type != "Future": raise RQInvalidArgument( - _("{order_book_id} is not supported in current strategy type").format( + _(u"{order_book_id} is not supported in current strategy type").format( order_book_id=id_or_symbols.order_book_id)) else: return id_or_symbols.order_book_id elif isinstance(id_or_symbols, six.string_types): return assure_future_order_book_id(instruments(id_or_symbols)) else: - raise RQInvalidArgument(_("unsupported order_book_id type")) + raise RQInvalidArgument(_(u"unsupported order_book_id type")) @export_as_api @@ -211,5 +212,5 @@ def get_future_contracts(underlying_symbol): [Out] ['IF1612', 'IF1701', 'IF1703', 'IF1706'] """ - dt = ExecutionContext.get_current_trading_dt() - return ExecutionContext.data_proxy.get_future_contracts(underlying_symbol, dt) + env = Environment.get_instance() + return env.data_proxy.get_future_contracts(underlying_symbol, env.trading_dt) diff --git a/rqalpha/api/api_stock.py b/rqalpha/api/api_stock.py index d256d64bf..522b365c7 100644 --- a/rqalpha/api/api_stock.py +++ b/rqalpha/api/api_stock.py @@ -31,10 +31,13 @@ from ..model.instrument import Instrument from ..model.order import Order, OrderStyle, MarketOrder, LimitOrder from ..utils.arg_checker import apply_rules, verify_that +# noinspection PyUnresolvedReferences from ..utils.exception import patch_user_exc, RQInvalidArgument from ..utils.i18n import gettext as _ from ..utils.logger import user_system_log +# noinspection PyUnresolvedReferences from ..utils.scheduler import market_close, market_open +# noinspection PyUnresolvedReferences from ..utils import scheduler # 使用Decimal 解决浮点数运算精度问题 @@ -86,31 +89,18 @@ def order_shares(id_or_ins, amount, style=MarketOrder()): #购买1000股的平安银行股票,并以限价单发送,价格为¥10: order_shares('000001.XSHG', 1000, style=LimitOrder(10)) """ - # Place an order by specified number of shares. Order type is also - # passed in as parameters if needed. If style is omitted, it fires a - # market order by default. - # :PARAM id_or_ins: the instrument to be ordered - # :type id_or_ins: str or Instrument - # :param float amount: Number of shares to order. Positive means buy, - # negative means sell. It will be rounded down to the closest - # integral multiple of the lot size - # :param style: Order type and default is `MarketOrder()`. The - # available order types are: `MarketOrder()` and - # `LimitOrder(limit_price)` - # :return: A unique order id. - # :rtype: int + if amount is 0: + # 如果下单量为0,则认为其并没有发单,则直接返回None + return None if not isinstance(style, OrderStyle): - raise RQInvalidArgument(_('style should be OrderStyle')) + raise RQInvalidArgument(_(u"style should be OrderStyle")) if isinstance(style, LimitOrder): if style.get_limit_price() <= 0: - raise RQInvalidArgument(_("Limit order price should be positive")) - + raise RQInvalidArgument(_(u"Limit order price should be positive")) order_book_id = assure_stock_order_book_id(id_or_ins) - bar_dict = ExecutionContext.get_current_bar_dict() - bar = bar_dict[order_book_id] - price = bar.close - calendar_dt = ExecutionContext.get_current_calendar_dt() - trading_dt = ExecutionContext.get_current_trading_dt() + env = Environment.get_instance() + + price = env.get_last_price(order_book_id) if amount > 0: side = SIDE.BUY @@ -118,29 +108,31 @@ def order_shares(id_or_ins, amount, style=MarketOrder()): amount = abs(amount) side = SIDE.SELL - round_lot = int(ExecutionContext.data_proxy.instruments(order_book_id).round_lot) + round_lot = int(env.get_instrument(order_book_id).round_lot) try: amount = int(Decimal(amount) / Decimal(round_lot)) * round_lot except ValueError: amount = 0 - r_order = Order.__from_create__(calendar_dt, trading_dt, order_book_id, amount, side, style, None) + r_order = Order.__from_create__(env.calendar_dt, env.trading_dt, order_book_id, amount, side, style, None) - if bar.isnan or price == 0: - user_system_log.warn(_("Order Creation Failed: [{order_book_id}] No market data").format(order_book_id=order_book_id)) - r_order._mark_rejected(_("Order Creation Failed: [{order_book_id}] No market data").format(order_book_id=order_book_id)) + if price == 0: + user_system_log.warn( + _(u"Order Creation Failed: [{order_book_id}] No market data").format(order_book_id=order_book_id)) + r_order.mark_rejected( + _(u"Order Creation Failed: [{order_book_id}] No market data").format(order_book_id=order_book_id)) return r_order if amount == 0: # 如果计算出来的下单量为0, 则不生成Order, 直接返回None # 因为很多策略会直接在handle_bar里面执行order_target_percent之类的函数,经常会出现下一个量为0的订单,如果这些订单都生成是没有意义的。 - r_order._mark_rejected(_("Order Creation Failed: 0 order quantity")) + r_order.mark_rejected(_(u"Order Creation Failed: 0 order quantity")) return r_order if r_order.type == ORDER_TYPE.MARKET: - bar_dict = ExecutionContext.get_current_bar_dict() - r_order._frozen_price = bar_dict[order_book_id].close - ExecutionContext.broker.submit_order(r_order) + r_order.set_frozen_price(price) + if env.can_submit_order(r_order): + env.broker.submit_order(r_order) return r_order @@ -175,21 +167,9 @@ def order_lots(id_or_ins, amount, style=MarketOrder()): order_lots('000001.XSHE', 10, style=LimitOrder(10)) """ - # Place an order by specified number of lots. Order type is also passed - # in as parameters if needed. If style is omitted, it fires a market - # order by default. - # :param id_or_ins: the instrument to be ordered - # :type id_or_ins: str or Instrument - # :param float amount: Number of lots to order. Positive means buy, - # negative means sell. - # :param style: Order type and default is `MarketOrder()`. The - # available order types are: `MarketOrder()` and - # `LimitOrder(limit_price)` - # :return: A unique order id. - # :rtype: int order_book_id = assure_stock_order_book_id(id_or_ins) - round_lot = int(ExecutionContext.get_instrument(order_book_id).round_lot) + round_lot = int(Environment.get_instance().get_instrument(order_book_id).round_lot) return order_shares(id_or_ins, amount * round_lot, style) @@ -224,40 +204,25 @@ def order_value(id_or_ins, cash_amount, style=MarketOrder()): order_value('000001.XSHE', -10000) """ - # Place an order by specified value amount rather than specific number - # of shares/lots. Negative cash_amount results in selling the given - # amount of value, if the cash_amount is larger than you current - # security’s position, then it will sell all shares of this security. - # Orders are always truncated to whole lot shares. - # :param id_or_ins: the instrument to be ordered - # :type id_or_ins: str or Instrument - # :param float cash_amount: Cash amount to buy / sell the given value of - # securities. Positive means buy, negative means sell. - # :param style: Order type and default is `MarketOrder()`. The - # available order types are: `MarketOrder()` and - # `LimitOrder(limit_price)` - # :return: A unique order id. - # :rtype: int if not isinstance(style, OrderStyle): - raise RQInvalidArgument(_('style should be OrderStyle')) + raise RQInvalidArgument(_(u"style should be OrderStyle")) if isinstance(style, LimitOrder): if style.get_limit_price() <= 0: - raise RQInvalidArgument(_("Limit order price should be positive")) + raise RQInvalidArgument(_(u"Limit order price should be positive")) order_book_id = assure_stock_order_book_id(id_or_ins) + env = Environment.get_instance() - bar_dict = ExecutionContext.get_current_bar_dict() - bar = bar_dict[order_book_id] - price = bar.close + price = env.get_last_price(order_book_id) - if bar.isnan or price == 0: + if price == 0: return order_shares(order_book_id, 0, style) - account = ExecutionContext.accounts[ACCOUNT_TYPE.STOCK] - round_lot = int(ExecutionContext.get_instrument(order_book_id).round_lot) + account = env.portfolio.accounts[ACCOUNT_TYPE.STOCK] + round_lot = int(env.get_instrument(order_book_id).round_lot) if cash_amount > 0: - cash_amount = min(cash_amount, account.portfolio.cash) + cash_amount = min(cash_amount, account.cash) if isinstance(style, MarketOrder): amount = int(Decimal(cash_amount) / Decimal(price) / Decimal(round_lot)) * round_lot @@ -267,7 +232,7 @@ def order_value(id_or_ins, cash_amount, style=MarketOrder()): # if the cash_amount is larger than you current security’s position, # then it will sell all shares of this security. - position = account.portfolio.positions[order_book_id] + position = account.positions[order_book_id] amount = downsize_amount(amount, position) return order_shares(order_book_id, amount, style) @@ -277,7 +242,7 @@ def order_value(id_or_ins, cash_amount, style=MarketOrder()): @ExecutionContext.enforce_phase(EXECUTION_PHASE.ON_BAR, EXECUTION_PHASE.SCHEDULED) @apply_rules(verify_that('id_or_ins').is_valid_stock(), - verify_that('percent').is_number().is_greater_than(-1).is_less_than(1), + verify_that('percent').is_number().is_greater_or_equal_than(-1).is_less_or_equal_than(1), verify_that('style').is_instance_of((MarketOrder, LimitOrder))) def order_percent(id_or_ins, percent, style=MarketOrder()): """ @@ -300,28 +265,11 @@ def order_percent(id_or_ins, percent, style=MarketOrder()): #买入等于现有投资组合50%价值的平安银行股票。如果现在平安银行的股价是¥10/股并且现在的投资组合总价值是¥2000,那么将会买入200股的平安银行股票。(不包含交易成本和滑点的损失): order_percent('000001.XSHG', 0.5) """ - # Place an order for a security for a given percent of the current - # portfolio value, which is the sum of the positions value and - # ending cash balance. A negative percent order will result in - # selling given percent of current portfolio value. Orders are - # always truncated to whole shares. Percent should be a decimal - # number (0.50 means 50%), and its absolute value is <= 1. - # :param id_or_ins: the instrument to be ordered - # :type id_or_ins: str or Instrument - # :param float percent: Percent of the current portfolio value. Positive - # means buy, negative means selling give percent of the current - # portfolio value. Orders are always truncated according to lot size. - # :param style: Order type and default is `MarketOrder()`. The - # available order types are: `MarketOrder()` and - # `LimitOrder(limit_price)` - # :return: A unique order id. - # :rtype: int if percent < -1 or percent > 1: - raise RQInvalidArgument(_('percent should between -1 and 1')) + raise RQInvalidArgument(_(u"percent should between -1 and 1")) - account = ExecutionContext.accounts[ACCOUNT_TYPE.STOCK] - portfolio_value = account.portfolio.portfolio_value - return order_value(id_or_ins, portfolio_value * percent, style) + account = Environment.get_instance().portfolio.accounts[ACCOUNT_TYPE.STOCK] + return order_value(id_or_ins, account.total_value * percent, style) @export_as_api @@ -351,38 +299,18 @@ def order_target_value(id_or_ins, cash_amount, style=MarketOrder()): #如果现在的投资组合中持有价值¥3000的平安银行股票的仓位并且设置其目标价值为¥10000,以下代码范例会发送价值¥7000的平安银行的买单到市场。(向下调整到最接近每手股数即100的倍数的股数): order_target_value('000001.XSHE', 10000) """ - # Place an order to adjust a position to a target value. If there is no - # position for the security, an order is placed for the whole amount - # of target value. If there is already a position for the security, - # an order is placed for the difference between target value and - # current position value. - # :param id_or_ins: the instrument to be ordered - # :type id_or_ins: str or Instrument - # :param float cash_amount: Target cash value for the adjusted position - # after placing order. - # :param style: Order type and default is `MarketOrder()`. The - # available order types are: `MarketOrder()` and - # `LimitOrder(limit_price)` - # :return: A unique order id. - # :rtype: int order_book_id = assure_stock_order_book_id(id_or_ins) + account = Environment.get_instance().portfolio.accounts[ACCOUNT_TYPE.STOCK] + position = account.positions[order_book_id] - bar_dict = ExecutionContext.get_current_bar_dict() - bar = bar_dict[order_book_id] - price = 0 if bar.isnan else bar.close - - position = ExecutionContext.accounts[ACCOUNT_TYPE.STOCK].portfolio.positions[order_book_id] - - current_value = position._quantity * price - - return order_value(order_book_id, cash_amount - current_value, style) + return order_value(order_book_id, cash_amount - position.market_value, style) @export_as_api @ExecutionContext.enforce_phase(EXECUTION_PHASE.ON_BAR, EXECUTION_PHASE.SCHEDULED) @apply_rules(verify_that('id_or_ins').is_valid_stock(), - verify_that('percent').is_number().is_greater_than(0).is_less_than(1), + verify_that('percent').is_number().is_greater_or_equal_than(0).is_less_or_equal_than(1), verify_that('style').is_instance_of((MarketOrder, LimitOrder))) def order_target_percent(id_or_ins, percent, style=MarketOrder()): """ @@ -416,39 +344,61 @@ def order_target_percent(id_or_ins, percent, style=MarketOrder()): #如果投资组合中已经有了平安银行股票的仓位,并且占据目前投资组合的10%的价值,那么以下代码会买入平安银行股票最终使其占据投资组合价值的15%: order_target_percent('000001.XSHE', 0.15) """ - # Place an order to adjust position to a target percent of the portfolio - # value, so that your final position value takes the percentage you - # defined of your whole portfolio. - # position_to_adjust = target_position - current_position - # Portfolio value is calculated as sum of positions value and ending - # cash balance. The order quantity will be rounded down to integral - # multiple of lot size. Percent should be a decimal number (0.50 - # means 50%), and its absolute value is <= 1. If the - # position_to_adjust calculated is positive, then it fires buy - # orders, otherwise it fires sell orders. - # :param id_or_ins: the instrument to be ordered - # :type id_or_ins: str or Instrument - # :param float percent: Number of percent to order. It will be rounded down - # to the closest integral multiple of the lot size - # :param style: Order type and default is `MarketOrder()`. The - # available order types are: `MarketOrder()` and - # `LimitOrder(limit_price)` - # :return: A unique order id. - # :rtype: int if percent < 0 or percent > 1: - raise RQInvalidArgument(_('percent should between 0 and 1')) + raise RQInvalidArgument(_(u"percent should between 0 and 1")) order_book_id = assure_stock_order_book_id(id_or_ins) - bar_dict = ExecutionContext.get_current_bar_dict() - bar = bar_dict[order_book_id] - price = 0 if bar.isnan else bar.close + account = Environment.get_instance().portfolio.accounts[ACCOUNT_TYPE.STOCK] + position = account.positions[order_book_id] + + return order_value(order_book_id, account.total_value * percent - position.market_value, style) + + +@export_as_api +@ExecutionContext.enforce_phase(EXECUTION_PHASE.ON_INIT, + EXECUTION_PHASE.BEFORE_TRADING, + EXECUTION_PHASE.ON_BAR, + EXECUTION_PHASE.AFTER_TRADING, + EXECUTION_PHASE.SCHEDULED) +@apply_rules(verify_that('order_book_id').is_valid_instrument(), + verify_that('count').is_greater_than(0)) +def is_suspended(order_book_id, count=1): + """ + 判断某只股票是否全天停牌。 + + :param str order_book_id: 某只股票的代码或股票代码,可传入单只股票的order_book_id, symbol + + :param int count: 回溯获取的数据个数。默认为当前能够获取到的最近的数据 - portfolio = ExecutionContext.accounts[ACCOUNT_TYPE.STOCK].portfolio - position = portfolio.positions[order_book_id] + :return: count为1时 `bool`; count>1时 `pandas.DataFrame` + """ + dt = Environment.get_instance().calendar_dt.date() + order_book_id = assure_stock_order_book_id(order_book_id) + return Environment.get_instance().data_proxy.is_suspended(order_book_id, dt, count) - current_value = position._quantity * price - return order_value(order_book_id, portfolio.portfolio_value * percent - current_value, style) +@export_as_api +@ExecutionContext.enforce_phase(EXECUTION_PHASE.ON_INIT, + EXECUTION_PHASE.BEFORE_TRADING, + EXECUTION_PHASE.ON_BAR, + EXECUTION_PHASE.AFTER_TRADING, + EXECUTION_PHASE.SCHEDULED) +@apply_rules(verify_that('order_book_id').is_valid_instrument()) +def is_st_stock(order_book_id, count=1): + """ + 判断股票在一段时间内是否为ST股(包括ST与*ST)。 + + ST股是有退市风险因此风险比较大的股票,很多时候您也会希望判断自己使用的股票是否是'ST'股来避开这些风险大的股票。另外,我们目前的策略比赛也禁止了使用'ST'股。 + + :param str order_book_id: 某只股票的代码,可传入单只股票的order_book_id, symbol + + :param int count: 回溯获取的数据个数。默认为当前能够获取到的最近的数据 + + :return: count为1时 `bool`; count>1时 `pandas.DataFrame` + """ + dt = Environment.get_instance().calendar_dt.date() + order_book_id = assure_stock_order_book_id(order_book_id) + return Environment.get_instance().data_proxy.is_st_stock(order_book_id, dt, count) def assure_stock_order_book_id(id_or_symbols): @@ -462,12 +412,12 @@ def assure_stock_order_book_id(id_or_symbols): return order_book_id else: raise RQInvalidArgument( - _("{order_book_id} is not supported in current strategy type").format( + _(u"{order_book_id} is not supported in current strategy type").format( order_book_id=order_book_id)) elif isinstance(id_or_symbols, six.string_types): return assure_stock_order_book_id(instruments(id_or_symbols)) else: - raise RQInvalidArgument(_("unsupported order_book_id type")) + raise RQInvalidArgument(_(u"unsupported order_book_id type")) def downsize_amount(amount, position): diff --git a/rqalpha/api/ext.py b/rqalpha/api/ext.py index 1532ed8d3..b753653bb 100644 --- a/rqalpha/api/ext.py +++ b/rqalpha/api/ext.py @@ -16,13 +16,12 @@ import six -from ..execution_context import ExecutionContext +from ..environment import Environment from .api_base import instruments def get_current_bar_dict(): - bar_dict = ExecutionContext.get_current_bar_dict() - return bar_dict + return Environment.get_instance().bar_dict def price_change(stock): @@ -39,5 +38,4 @@ def symbol(order_book_id, split=", "): def now_time_str(str_format="%H:%M:%S"): - dt = ExecutionContext.get_current_trading_dt() - return dt.strftime(str_format) + return Environment.get_instance().trading_dt.strftime(str_format) diff --git a/rqalpha/cmd.py b/rqalpha/cmd.py new file mode 100644 index 000000000..280caa203 --- /dev/null +++ b/rqalpha/cmd.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2017 Ricequant, Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click +import ruamel.yaml as yaml + + +@click.group() +@click.option('-v', '--verbose', count=True) +@click.pass_context +def cmd_cli(ctx, verbose): + ctx.obj["VERBOSE"] = verbose + + +def entry_point(): + import six + from rqalpha.mod import SYSTEM_MOD_LIST + from rqalpha.utils.config import get_mod_config_path, load_mod_config + from rqalpha.utils.package_helper import import_mod + mod_config_path = get_mod_config_path() + mod_config = load_mod_config(mod_config_path, loader=yaml.RoundTripLoader) + + for mod_name, config in six.iteritems(mod_config['mod']): + lib_name = "rqalpha_mod_{}".format(mod_name) + if not config['enabled']: + continue + if mod_name in SYSTEM_MOD_LIST: + # inject system mod + import_mod("rqalpha.mod." + lib_name) + else: + # inject third part mod + import_mod(lib_name) + + cmd_cli(obj={}) diff --git a/rqalpha/const.py b/rqalpha/const.py index 910d180be..6e006cd7d 100644 --- a/rqalpha/const.py +++ b/rqalpha/const.py @@ -23,6 +23,7 @@ def __repr__(self): self.__class__.__name__, self._name_) +# noinspection PyPep8Naming class EXECUTION_PHASE(CustomEnum): GLOBAL = "[全局]" ON_INIT = "[程序初始化]" @@ -33,103 +34,124 @@ class EXECUTION_PHASE(CustomEnum): FINALIZED = "[程序结束]" SCHEDULED = "[scheduler函数内]" -RUN_TYPE = CustomEnum("RUN_TYPE", [ + +# noinspection PyPep8Naming +class RUN_TYPE(CustomEnum): + # TODO 取消 RUN_TYPE, 取而代之的是使用开启哪些Mod来控制策略所运行的类型 # Back Test - "BACKTEST", + BACKTEST = "BACKTEST" # Paper Trading - "PAPER_TRADING", -]) - -ACCOUNT_TYPE = CustomEnum("ACCOUNT_TYPE", [ - "TOTAL", - "BENCHMARK", - "STOCK", - "FUTURE", -]) - -BAR_STATUS = CustomEnum("BAR_STATUS", [ - "LIMIT_UP", - "LIMIT_DOWN", - "NORMAL", - "ERROR", -]) - -MATCHING_TYPE = CustomEnum("MATCHING_TYPE", [ - "CURRENT_BAR_CLOSE", - "NEXT_BAR_OPEN", -]) - -ORDER_TYPE = CustomEnum("ORDER_TYPE", [ - "MARKET", - "LIMIT", -]) - -ORDER_STATUS = CustomEnum("ORDER_STATUS", [ - "PENDING_NEW", - "ACTIVE", - "FILLED", - "REJECTED", - "PENDING_CANCEL", - "CANCELLED", -]) - -SIDE = CustomEnum("SIDE", [ - "BUY", - "SELL", -]) - -POSITION_EFFECT = CustomEnum("POSITION_EFFECT", [ - "OPEN", - "CLOSE", - "CLOSE_TODAY", -]) - -EXC_TYPE = CustomEnum("EXC_TYPE", [ - "USER_EXC", - "SYSTEM_EXC", - "NOTSET", -]) - -INSTRUMENT_TYPE = CustomEnum("INSTRUMENT_TYPE", [ - "CS", - "FUTURE", - "OPTION", - "ETF", - "LOF", - "INDX", - "FENJI_MU", - "FENJI_A", - "FENJI_B", -]) - -PERSIST_MODE = CustomEnum("PERSIST_MODE", [ - "ON_CRASH", - "REAL_TIME" -]) - -MARGIN_TYPE = CustomEnum("MARGIN_TYPE", [ - "BY_MONEY", - "BY_VOLUME", -]) - -COMMISSION_TYPE = CustomEnum("COMMISSION_TYPE", [ - "BY_MONEY", - "BY_VOLUME", -]) - -EXIT_CODE = CustomEnum("EXIT_CODE", [ - "EXIT_SUCCESS", - "EXIT_USER_ERROR", - "EXIT_INTERNAL_ERROR", -]) + PAPER_TRADING = "PAPER_TRADING" + LIVE_TRADING = 'LIVE_TRADING' + + +# noinspection PyPep8Naming +class ACCOUNT_TYPE(CustomEnum): + TOTAL = 0 + BENCHMARK = 1 + STOCK = 2 + FUTURE = 3 + + +# noinspection PyPep8Naming +class BAR_STATUS(CustomEnum): + LIMIT_UP = "LIMIT_UP" + LIMIT_DOWN = "LIMIT_DOWN" + NORMAL = "NORMAL" + ERROR = "ERROR" + + +# noinspection PyPep8Naming +class MATCHING_TYPE(CustomEnum): + CURRENT_BAR_CLOSE = "CURRENT_BAR_CLOSE" + NEXT_BAR_OPEN = "NEXT_BAR_OPEN" + NEXT_TICK_LAST = "NEXT_TICK_LAST" + NEXT_TICK_BEST_OWN = "NEXT_TICK_BEST_OWN" + NEXT_TICK_BEST_COUNTERPARTY = "NEXT_TICK_BEST_COUNTERPARTY" + + +# noinspection PyPep8Naming +class ORDER_TYPE(CustomEnum): + MARKET = "MARKET" + LIMIT = "LIMIT" + + +# noinspection PyPep8Naming +class ORDER_STATUS(CustomEnum): + PENDING_NEW = "PENDING_NEW" + ACTIVE = "ACTIVE" + FILLED = "FILLED" + REJECTED = "REJECTED" + PENDING_CANCEL = "PENDING_CANCEL" + CANCELLED = "CANCELLED" + + +# noinspection PyPep8Naming +class SIDE(CustomEnum): + BUY = "BUY" + SELL = "SELL" + + +# noinspection PyPep8Naming +class POSITION_EFFECT(CustomEnum): + OPEN = "OPEN" + CLOSE = "CLOSE" + CLOSE_TODAY = "CLOSE_TODAY" + + +# noinspection PyPep8Naming +class EXC_TYPE(CustomEnum): + USER_EXC = "USER_EXC" + SYSTEM_EXC = "SYSTEM_EXC" + NOTSET = "NOTSET" + + +# noinspection PyPep8Naming +class INSTRUMENT_TYPE(CustomEnum): + CS = "CS" + FUTURE = "FUTURE" + OPTION = "OPTION" + ETF = "ETF" + LOF = "LOF" + INDX = "INDX" + FENJI_MU = "FENJI_MU" + FENJI_A = "FENJI_A" + FENJI_B = "FENJI_B" + + +# noinspection PyPep8Naming +class PERSIST_MODE(CustomEnum): + ON_CRASH = "ON_CRASH" + REAL_TIME = "REAL_TIME" + + +# noinspection PyPep8Naming +class MARGIN_TYPE(CustomEnum): + BY_MONEY = "BY_MONEY" + BY_VOLUME = "BY_VOLUME" + + +# noinspection PyPep8Naming +class COMMISSION_TYPE(CustomEnum): + BY_MONEY = "BY_MONEY" + BY_VOLUME = "BY_VOLUME" + + +# noinspection PyPep8Naming +class EXIT_CODE(CustomEnum): + EXIT_SUCCESS = "EXIT_SUCCESS" + EXIT_USER_ERROR = "EXIT_USER_ERROR" + EXIT_INTERNAL_ERROR = "EXIT_INTERNAL_ERROR" +# noinspection PyPep8Naming class HEDGE_TYPE(CustomEnum): HEDGE = "hedge" SPECULATION = "speculation" ARBITRAGE = "arbitrage" +# noinspection PyPep8Naming class DAYS_CNT(object): DAYS_A_YEAR = 365 TRADING_DAYS_A_YEAR = 252 diff --git a/rqalpha/core/bar_dict_price_board.py b/rqalpha/core/bar_dict_price_board.py new file mode 100644 index 000000000..1f3c83169 --- /dev/null +++ b/rqalpha/core/bar_dict_price_board.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2017 Ricequant, Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import numpy as np + +from ..interface import AbstractPriceBoard +from ..environment import Environment +from ..events import EVENT +from ..const import ACCOUNT_TYPE + + +class BarDictPriceBoard(AbstractPriceBoard): + def __init__(self, bar_dict): + self._bar_dict = bar_dict + self._settlement_lock = False + self._settlement_dt = None + self._env = Environment.get_instance() + if ACCOUNT_TYPE.FUTURE in self._env.config.base.account_list: + self._env.event_bus.prepend_listener(EVENT.PRE_SETTLEMENT, self._lock_settlement) + self._env.event_bus.add_listener(EVENT.POST_BEFORE_TRADING, self._unlock_settlement) + + def get_last_price(self, order_book_id): + if self._settlement_lock and self._env.get_instrument(order_book_id).type == 'Future': + return self._env.data_proxy.get_settle_price(order_book_id, self._settlement_dt) + else: + return self._bar_dict[order_book_id].last + + def get_limit_up(self, order_book_id): + return self._bar_dict[order_book_id].limit_up + + def get_limit_down(self, order_book_id): + return self._bar_dict[order_book_id].limit_down + + def _lock_settlement(self, event): + self._settlement_lock = True + self._settlement_dt = self._env.trading_dt + + def _unlock_settlement(self, event): + self._settlement_lock = False + + def get_a1(self, order_book_id): + return np.nan + + def get_b1(self, order_book_id): + return np.nan diff --git a/rqalpha/core/executor.py b/rqalpha/core/executor.py new file mode 100644 index 000000000..ed07e9781 --- /dev/null +++ b/rqalpha/core/executor.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2017 Ricequant, Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ..events import EVENT, Event + +PRE_BEFORE_TRADING = Event(EVENT.PRE_BEFORE_TRADING) +POST_BEFORE_TRADING = Event(EVENT.POST_BEFORE_TRADING) +PRE_BAR = Event(EVENT.PRE_BAR) +POST_BAR = Event(EVENT.POST_BAR) +PRE_TICK = Event(EVENT.PRE_TICK) +POST_TICK = Event(EVENT.POST_TICK) +PRE_AFTER_TRADING = Event(EVENT.PRE_AFTER_TRADING) +POST_AFTER_TRADING = Event(EVENT.POST_AFTER_TRADING) +PRE_SETTLEMENT = Event(EVENT.PRE_SETTLEMENT) +POST_SETTLEMENT = Event(EVENT.POST_SETTLEMENT) + + +class Executor(object): + def __init__(self, env): + self._env = env + + KNOWN_EVENTS = { + EVENT.TICK, + EVENT.BAR, + EVENT.BEFORE_TRADING, + EVENT.AFTER_TRADING, + EVENT.POST_SETTLEMENT, + } + + def run(self, bar_dict): + PRE_BAR.bar_dict = bar_dict + POST_BAR.bar_dict = bar_dict + + start_date = self._env.config.base.start_date + end_date = self._env.config.base.end_date + frequency = self._env.config.base.frequency + event_bus = self._env.event_bus + + for event in self._env.event_source.events(start_date, end_date, frequency): + if event.event_type in self.KNOWN_EVENTS: + self._env.calendar_dt = event.calendar_dt + self._env.trading_dt = event.trading_dt + + if event.event_type == EVENT.TICK: + event_bus.publish_event(PRE_TICK) + event_bus.publish_event(event) + event_bus.publish_event(POST_TICK) + elif event.event_type == EVENT.BAR: + bar_dict.update_dt(event.calendar_dt) + event_bus.publish_event(PRE_BAR) + event.bar_dict = bar_dict + event_bus.publish_event(event) + event_bus.publish_event(POST_BAR) + elif event.event_type == EVENT.BEFORE_TRADING: + event_bus.publish_event(PRE_BEFORE_TRADING) + event_bus.publish_event(event) + event_bus.publish_event(POST_BEFORE_TRADING) + elif event.event_type == EVENT.AFTER_TRADING: + event_bus.publish_event(PRE_AFTER_TRADING) + event_bus.publish_event(event) + event_bus.publish_event(POST_AFTER_TRADING) + elif event.event_type == EVENT.SETTLEMENT: + event_bus.publish_event(PRE_SETTLEMENT) + event_bus.publish_event(event) + event_bus.publish_event(POST_SETTLEMENT) + else: + event_bus.publish_event(event) diff --git a/rqalpha/core/strategy.py b/rqalpha/core/strategy.py index caaa7fc9a..83ac4d318 100644 --- a/rqalpha/core/strategy.py +++ b/rqalpha/core/strategy.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from ..events import EVENT +from ..events import EVENT, Event from ..utils import run_when_strategy_not_hold from ..utils.logger import user_system_log from ..utils.i18n import gettext as _ @@ -35,7 +35,7 @@ def __init__(self, event_bus, scope, ucontext): func_before_trading = scope.get('before_trading', None) if func_before_trading is not None and func_before_trading.__code__.co_argcount > 1: self._before_trading = lambda context: func_before_trading(context, None) - user_system_log.warn(_("deprecated parameter[bar_dict] in before_trading function.")) + user_system_log.warn(_(u"deprecated parameter[bar_dict] in before_trading function.")) else: self._before_trading = func_before_trading self._after_trading = scope.get('after_trading', None) @@ -52,9 +52,9 @@ def __init__(self, event_bus, scope, ucontext): self._before_day_trading = scope.get('before_day_trading', None) self._before_night_trading = scope.get('before_night_trading', None) if self._before_day_trading is not None: - user_system_log.warn(_("[deprecated] before_day_trading is no longer used. use before_trading instead.")) + user_system_log.warn(_(u"[deprecated] before_day_trading is no longer used. use before_trading instead.")) if self._before_night_trading is not None: - user_system_log.warn(_("[deprecated] before_night_trading is no longer used. use before_trading instead.")) + user_system_log.warn(_(u"[deprecated] before_night_trading is no longer used. use before_trading instead.")) @property def user_context(self): @@ -68,28 +68,30 @@ def init(self): with ModifyExceptionFromType(EXC_TYPE.USER_EXC): self._init(self._user_context) - Environment.get_instance().event_bus.publish_event(EVENT.POST_USER_INIT) + Environment.get_instance().event_bus.publish_event(Event(EVENT.POST_USER_INIT)) @run_when_strategy_not_hold - def before_trading(self): + def before_trading(self, event): with ExecutionContext(EXECUTION_PHASE.BEFORE_TRADING): with ModifyExceptionFromType(EXC_TYPE.USER_EXC): self._before_trading(self._user_context) @run_when_strategy_not_hold - def handle_bar(self, bar_dict): - with ExecutionContext(EXECUTION_PHASE.ON_BAR, bar_dict): + def handle_bar(self, event): + bar_dict = event.bar_dict + with ExecutionContext(EXECUTION_PHASE.ON_BAR): with ModifyExceptionFromType(EXC_TYPE.USER_EXC): self._handle_bar(self._user_context, bar_dict) @run_when_strategy_not_hold - def handle_tick(self, tick): - with ExecutionContext(EXECUTION_PHASE.ON_TICK, tick): + def handle_tick(self, event): + tick = event.tick + with ExecutionContext(EXECUTION_PHASE.ON_TICK): with ModifyExceptionFromType(EXC_TYPE.USER_EXC): self._handle_tick(self._user_context, tick) @run_when_strategy_not_hold - def after_trading(self): + def after_trading(self, event): with ExecutionContext(EXECUTION_PHASE.AFTER_TRADING): with ModifyExceptionFromType(EXC_TYPE.USER_EXC): self._after_trading(self._user_context) diff --git a/rqalpha/core/strategy_context.py b/rqalpha/core/strategy_context.py index 3867dc29a..2778121a9 100644 --- a/rqalpha/core/strategy_context.py +++ b/rqalpha/core/strategy_context.py @@ -22,7 +22,6 @@ from ..utils.logger import user_system_log, system_log from ..utils.i18n import gettext as _ from ..utils.repr import property_repr -from ..utils.proxy import PortfolioProxy class RunInfo(object): @@ -32,25 +31,19 @@ class RunInfo(object): __repr__ = property_repr def __init__(self, config): - self._run_id = config.base.run_id self._start_date = config.base.start_date self._end_date = config.base.end_date self._frequency = config.base.frequency self._stock_starting_cash = config.base.stock_starting_cash self._future_starting_cash = config.base.future_starting_cash - self._slippage = config.base.slippage self._benchmark = config.base.benchmark - self._matching_type = config.base.matching_type - self._commission_multiplier = config.base.commission_multiplier self._margin_multiplier = config.base.margin_multiplier self._run_type = config.base.run_type - @property - def run_id(self): - """ - :property getter: 标识策略每次运行的唯一id - """ - return self._run_id + # For Mod + self._matching_type = config.mod.sys_simulation.matching_type + self._slippage = config.mod.sys_simulation.slippage + self._commission_multiplier = config.mod.sys_simulation.commission_multiplier @property def start_date(self): @@ -138,7 +131,6 @@ def __repr__(self): return "Context({%s})" % (', '.join(items),) def __init__(self): - self._proxy_portfolio_dict = None self._config = None def get_state(self): @@ -170,7 +162,7 @@ def universe(self): :property getter: list[`str`] """ - return Environment.get_instance().universe + return Environment.get_instance().get_universe() @property def now(self): @@ -190,102 +182,40 @@ def run_info(self): @property def portfolio(self): """ - 该投资组合在单一股票或期货策略中分别为股票投资组合和期货投资组合。在股票+期货的混合策略中代表汇总之后的总投资组合。 + 投资组合 ========================= ========================= ============================================================================== 属性 类型 注释 ========================= ========================= ============================================================================== - starting_cash float 初始资金,为子组合初始资金的加总 - cash float 可用资金,为子组合可用资金的加总 - total_returns float 投资组合至今的累积收益率。计算方法是现在的投资组合价值/投资组合的初始资金 - daily_returns float 投资组合每日收益率 - daily_pnl float 当日盈亏,子组合当日盈亏的加总 - market_value float 投资组合当前的市场价值,为子组合市场价值的加总 - portfolio_value float 总权益,为子组合总权益加总 - pnl float 当前投资组合的累计盈亏 + accounts dict 账户字典 start_date datetime.datetime 策略投资组合的回测/实时模拟交易的开始日期 + units float 份额 + unit_net_value float 净值 + daily_pnl float 当日盈亏,当日盈亏的加总 + daily_returns float 投资组合每日收益率 + total_returns float 投资组合总收益率 annualized_returns float 投资组合的年化收益率 + total_value float 投资组合总权益 positions dict 一个包含所有仓位的字典,以order_book_id作为键,position对象作为值 + cash float 总的可用资金 + market_value float 投资组合当前的市场价值,为子组合市场价值的加总 ========================= ========================= ============================================================================== - :property getter: :class:`~MixedPortfolio` | :class:`~StockPortfolio` | :class:`~FuturePortfolio` + :property getter: :class:`~Portfolio` """ - env = Environment.get_instance() - account_list = env.config.base.account_list - if len(account_list) == 1: - if account_list[0] == ACCOUNT_TYPE.STOCK: - return self.stock_portfolio - elif account_list[0] == ACCOUNT_TYPE.FUTURE: - return self.future_portfolio - return Environment.get_instance().account.portfolio + return Environment.get_instance().portfolio @property - def stock_portfolio(self): - """ - 获取股票投资组合信息。 - - 在单独股票类型策略中,内容与portfolio一致,都代表当前投资组合;在期货+股票混合策略中代表股票子组合;在单独期货策略中,不能被访问。 - - ========================= ========================= ============================================================================== - 属性 类型 注释 - ========================= ========================= ============================================================================== - starting_cash float 回测或实盘交易给算法策略设置的初始资金 - cash float 可用资金 - total_returns float 投资组合至今的累积收益率。计算方法是现在的投资组合价值/投资组合的初始资金 - daily_returns float 当前最新一天的每日收益 - daily_pnl float 当日盈亏,当日投资组合总权益-昨日投资组合总权益 - market_value float 投资组合当前所有证券仓位的市值的加总 - portfolio_value float 总权益,包含市场价值和剩余现金 - pnl float 当前投资组合的累计盈亏 - start_date date 策略投资组合的回测/实时模拟交易的开始日期 - annualized_returns float 投资组合的年化收益率 - positions dict 一个包含所有证券仓位的字典,以order_book_id作为键,position对象作为值 - dividend_receivable float 投资组合在分红现金收到账面之前的应收分红部分 - ========================= ========================= ============================================================================== - - :property getter: :class:`~StockPortfolio` - """ - if getattr(self, "_proxy_portfolio_dict", None) is None: - self._proxy_portfolio_dict = {} - self._proxy_portfolio_dict[ACCOUNT_TYPE.STOCK] = PortfolioProxy( - Environment.get_instance().accounts[ACCOUNT_TYPE.STOCK].portfolio) - return self._proxy_portfolio_dict[ACCOUNT_TYPE.STOCK] + def stock_account(self): + return self.portfolio.accounts[ACCOUNT_TYPE.STOCK] @property - def future_portfolio(self): - """ - 获取期货投资组合信息。 - - 在单独期货类型策略中,内容与portfolio一致,都代表当前投资组合;在期货+股票混合策略中代表期货子组合;在单独股票策略中,不能被访问。 - - ========================= ========================= ============================================================================== - 属性 类型 注释 - ========================= ========================= ============================================================================== - starting_cash float 初始资金 - cash float 可用资金 - frozen_cash float 冻结资金 - total_returns float 投资组合至今的累积收益率,当前总权益/初始资金 - daily_returns float 当日收益率 = 当日收益 / 昨日总权益 - market_value float 投资组合当前所有期货仓位的名义市值的加总 - pnl float 累计盈亏,当前投资组合总权益-初始资金 - daily_pnl float 当日盈亏,当日浮动盈亏 + 当日平仓盈亏 - 当日费用 - daily_holding_pnl float 当日浮动盈亏 - daily_realized_pnl float 当日平仓盈亏 - portfolio_value float 总权益,昨日总权益+当日盈亏 - transaction_cost float 总费用 - start_date date 回测开始日期 - annualized_returns float 投资组合的年化收益率 - positions dict 一个包含期货仓位的字典,以order_book_id作为键,position对象作为值 - margin float 已占用保证金 - ========================= ========================= ============================================================================== + def future_account(self): + return self.portfolio.accounts[ACCOUNT_TYPE.FUTURE] - :property getter: :class:`~FuturePortfolio` - """ - if getattr(self, "_proxy_portfolio_dict", None) is None: - self._proxy_portfolio_dict = {} - self._proxy_portfolio_dict[ACCOUNT_TYPE.FUTURE] = PortfolioProxy( - Environment.get_instance().accounts[ACCOUNT_TYPE.FUTURE].portfolio) - return self._proxy_portfolio_dict[ACCOUNT_TYPE.FUTURE] + @property + def config(self): + return Environment.get_instance().config @property def slippage(self): @@ -307,22 +237,34 @@ def commission(self): def short_selling_allowed(self): raise NotImplementedError + # ------------------------------------ Abandon Property ------------------------------------ + + @property + def stock_portfolio(self): + user_system_log.warn(_(u"[abandon] {} is no longer used.").format('context.stock_portfolio')) + return self.stock_account + + @property + def future_portfolio(self): + user_system_log.warn(_(u"[abandon] {} is no longer used.").format('context.future_portfolio')) + return self.future_account + @slippage.setter def slippage(self, value): - user_system_log.warn(_("[deprecated] {} is no longer used.").format('context.slippage')) + user_system_log.warn(_(u"[abandon] {} is no longer used.").format('context.slippage')) @benchmark.setter def benchmark(self, value): - user_system_log.warn(_("[deprecated] {} is no longer used.").format('context.benchmark')) + user_system_log.warn(_(u"[abandon] {} is no longer used.").format('context.benchmark')) @margin_rate.setter def margin_rate(self, value): - user_system_log.warn(_("[deprecated] {} is no longer used.").format('context.margin_rate')) + user_system_log.warn(_(u"[abandon] {} is no longer used.").format('context.margin_rate')) @commission.setter def commission(self, value): - user_system_log.warn(_("[deprecated] {} is no longer used.").format('context.commission')) + user_system_log.warn(_(u"[abandon] {} is no longer used.").format('context.commission')) @short_selling_allowed.setter def short_selling_allowed(self, value): - user_system_log.warn(_("[deprecated] {} is no longer used.").format('context.short_selling_allowed')) + user_system_log.warn(_(u"[abandon] {} is no longer used.").format('context.short_selling_allowed')) diff --git a/rqalpha/core/strategy_universe.py b/rqalpha/core/strategy_universe.py index a41c5aa73..a9f8485b0 100644 --- a/rqalpha/core/strategy_universe.py +++ b/rqalpha/core/strategy_universe.py @@ -15,10 +15,11 @@ # limitations under the License. import json +import copy import six -from ..events import EVENT +from ..events import EVENT, Event from ..environment import Environment from ..model.instrument import Instrument @@ -38,18 +39,21 @@ def set_state(self, state): def update(self, universe): if isinstance(universe, (six.string_types, Instrument)): universe = [universe] - self._set = set(universe) - Environment.get_instance().event_bus.publish_event(EVENT.POST_UNIVERSE_CHANGED, self._set) + new_set = set(universe) + if new_set != self._set: + self._set = new_set + Environment.get_instance().event_bus.publish_event(Event(EVENT.POST_UNIVERSE_CHANGED, universe=self._set)) def get(self): - return self._set + return copy.copy(self._set) - def _clear_de_listed(self): + def _clear_de_listed(self, event): de_listed = set() + env = Environment.get_instance() for o in self._set: - i = Environment.get_instance().data_proxy.instruments(o) - if i.de_listed_date <= Environment.get_instance().trading_dt: + i = env.data_proxy.instruments(o) + if i.de_listed_date <= env.trading_dt: de_listed.add(o) if de_listed: self._set -= de_listed - Environment.get_instance().event_bus.publish_event(EVENT.POST_UNIVERSE_CHANGED, self._set) + env.event_bus.publish_event(Event(EVENT.POST_UNIVERSE_CHANGED, universe=self._set)) diff --git a/rqalpha/data/base_data_source.py b/rqalpha/data/base_data_source.py index c00000a28..cfcff1141 100644 --- a/rqalpha/data/base_data_source.py +++ b/rqalpha/data/base_data_source.py @@ -13,18 +13,15 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import os import six -import os import numpy as np -try: - # For Python 2 兼容 - from functools import lru_cache -except Exception as e: - from fastcache import lru_cache +from ..utils.py2 import lru_cache from ..utils.datetime_func import convert_date_to_int, convert_int_to_date from ..interface import AbstractDataSource +from .future_info_cn import CN_FUTURE_INFO from .converter import StockBarConverter, IndexBarConverter from .converter import FutureDayBarConverter, FundDayBarConverter from .daybar_store import DayBarStore @@ -74,11 +71,11 @@ def get_trading_calendar(self): def get_all_instruments(self): return self._instruments.get_all_instruments() - def is_suspended(self, order_book_id, dt): - return self._suspend_days.contains(order_book_id, dt) + def is_suspended(self, order_book_id, dates): + return self._suspend_days.contains(order_book_id, dates) - def is_st_stock(self, order_book_id, dt): - return self._st_stock_days.contains(order_book_id, dt) + def is_st_stock(self, order_book_id, dates): + return self._st_stock_days.contains(order_book_id, dates) INSTRUMENT_TYPE_MAP = { 'CS': 0, @@ -137,7 +134,8 @@ def _are_fields_valid(fields, valid_fields): return False return True - def history_bars(self, instrument, bar_count, frequency, fields, dt, skip_suspended=True): + def history_bars(self, instrument, bar_count, frequency, fields, dt, + skip_suspended=True, include_now=False): if frequency != '1d': raise NotImplementedError @@ -170,9 +168,14 @@ def get_split(self, order_book_id): return None def available_data_range(self, frequency): - if frequency == '1d': + if frequency in ['tick', '1d']: s, e = self._day_bars[self.INSTRUMENT_TYPE_MAP['INDX']].get_date_range('000001.XSHG') return convert_int_to_date(s).date(), convert_int_to_date(e).date() - if frequency == '1m': - raise NotImplementedError + raise NotImplementedError + + def get_future_info(self, instrument, hedge_type): + return CN_FUTURE_INFO[instrument.underlying_symbol][hedge_type.value] + + def get_ticks(self, order_book_id, date): + raise NotImplementedError diff --git a/rqalpha/data/converter.py b/rqalpha/data/converter.py index 9804feca8..0aaf2a8bb 100644 --- a/rqalpha/data/converter.py +++ b/rqalpha/data/converter.py @@ -50,20 +50,20 @@ def field_type(self, name, dt): 'close': Rule(float64, 1 / 10000.0, 4), 'high': Rule(float64, 1 / 10000.0, 4), 'low': Rule(float64, 1 / 10000.0, 4), - 'limit_up': Rule(float64, 1/10000.0, 4), - 'limit_down': Rule(float64, 1/10000.0, 4), + 'limit_up': Rule(float64, 1 / 10000.0, 4), + 'limit_down': Rule(float64, 1 / 10000.0, 4), }) FutureDayBarConverter = Converter({ - 'open': Rule(float64, 1 / 10000.0, 2), - 'close': Rule(float64, 1 / 10000.0, 2), - 'high': Rule(float64, 1 / 10000.0, 2), - 'low': Rule(float64, 1 / 10000.0, 2), - 'limit_up': Rule(float64, 1 / 10000.0, 2), - 'limit_down': Rule(float64, 1 / 10000.0, 2), + 'open': Rule(float64, 1 / 10000.0, 3), + 'close': Rule(float64, 1 / 10000.0, 3), + 'high': Rule(float64, 1 / 10000.0, 3), + 'low': Rule(float64, 1 / 10000.0, 3), + 'limit_up': Rule(float64, 1 / 10000.0, 3), + 'limit_down': Rule(float64, 1 / 10000.0, 3), 'basis_spread': Rule(float64, 1 / 10000.0, 4), - 'settlement': Rule(float64, 1 / 10000.0, 2), - 'prev_settlement': Rule(float64, 1 / 10000.0, 2), + 'settlement': Rule(float64, 1 / 10000.0, 3), + 'prev_settlement': Rule(float64, 1 / 10000.0, 3), }) FundDayBarConverter = Converter({ @@ -74,8 +74,8 @@ def field_type(self, name, dt): 'acc_net_value': Rule(float64, 1 / 10000.0, 4), 'unit_net_value': Rule(float64, 1 / 10000.0, 4), 'discount_rate': Rule(float64, 1 / 10000.0, 4), - 'limit_up': Rule(float64, 1/10000.0, 4), - 'limit_down': Rule(float64, 1/10000.0, 4), + 'limit_up': Rule(float64, 1 / 10000.0, 4), + 'limit_down': Rule(float64, 1 / 10000.0, 4), }) IndexBarConverter = Converter({ diff --git a/rqalpha/data/data_proxy.py b/rqalpha/data/data_proxy.py index ea0d036b5..564d52491 100644 --- a/rqalpha/data/data_proxy.py +++ b/rqalpha/data/data_proxy.py @@ -17,17 +17,13 @@ import six import numpy as np import pandas as pd -try: - # For Python 2 兼容 - from functools import lru_cache -except Exception as e: - from fastcache import lru_cache from . import risk_free_helper from .instrument_mixin import InstrumentMixin from .trading_dates_mixin import TradingDatesMixin from ..model.bar import BarObject from ..model.snapshot import SnapshotObject +from ..utils.py2 import lru_cache from ..utils.datetime_func import convert_int_to_datetime from ..const import HEDGE_TYPE @@ -67,6 +63,10 @@ def get_risk_free_rate(self, start_date, end_date): def get_dividend(self, order_book_id, adjusted=True): return self._data_source.get_dividend(order_book_id, adjusted) + @lru_cache(128) + def get_split(self, order_book_id): + return self._data_source.get_split(order_book_id) + def get_dividend_by_book_date(self, order_book_id, date, adjusted=True): df = self.get_dividend(order_book_id, adjusted) if df is None or df.empty: @@ -80,11 +80,21 @@ def get_dividend_by_book_date(self, order_book_id, date, adjusted=True): return df.iloc[pos] + def get_split_by_ex_date(self, order_book_id, date): + df = self.get_split(order_book_id) + if df is None or df.empty: + return + try: + return df.loc[date] + except KeyError: + pass + @lru_cache(10240) def _get_prev_close(self, order_book_id, dt): prev_trading_date = self.get_previous_trading_date(dt) instrument = self.instruments(order_book_id) - bar = self._data_source.history_bars(instrument, 1, '1d', 'close', prev_trading_date, False) + bar = self._data_source.history_bars(instrument, 1, '1d', 'close', prev_trading_date, + skip_suspended=False) if bar is None or len(bar) == 0: return np.nan return bar[0] @@ -95,7 +105,8 @@ def get_prev_close(self, order_book_id, dt): @lru_cache(10240) def _get_prev_settlement(self, instrument, dt): prev_trading_date = self.get_previous_trading_date(dt) - bar = self._data_source.history_bars(instrument, 1, '1d', 'settlement', prev_trading_date, False) + bar = self._data_source.history_bars(instrument, 1, '1d', 'settlement', prev_trading_date, + skip_suspended=False) if bar is None or len(bar) == 0: return np.nan return bar[0] @@ -112,6 +123,10 @@ def get_settle_price(self, order_book_id, date): return np.nan return self._data_source.get_settle_price(instrument, date) + def get_last_price(self, order_book_id, dt): + instrument = self.instruments(order_book_id) + return self._data_source.get_last_price(instrument, dt) + def get_bar(self, order_book_id, dt, frequency='1d'): instrument = self.instruments(order_book_id) bar = self._data_source.get_bar(instrument, dt, frequency) @@ -128,9 +143,11 @@ def history(self, order_book_id, bar_count, frequency, field, dt): def fast_history(self, order_book_id, bar_count, frequency, field, dt): return self.history_bars(order_book_id, bar_count, frequency, field, dt, skip_suspended=False) - def history_bars(self, order_book_id, bar_count, frequency, field, dt, skip_suspended=True): + def history_bars(self, order_book_id, bar_count, frequency, field, dt, + skip_suspended=True, include_now=False): instrument = self.instruments(order_book_id) - return self._data_source.history_bars(instrument, bar_count, frequency, field, dt, skip_suspended) + return self._data_source.history_bars(instrument, bar_count, frequency, field, dt, + skip_suspended=skip_suspended, include_now=include_now) def current_snapshot(self, order_book_id, frequency, dt): instrument = self.instruments(order_book_id) @@ -149,4 +166,26 @@ def available_data_range(self, frequency): return self._data_source.available_data_range(frequency) def get_future_info(self, order_book_id, hedge_type=HEDGE_TYPE.SPECULATION): - return self._data_source.get_future_info(order_book_id, hedge_type) + instrument = self.instruments(order_book_id) + return self._data_source.get_future_info(instrument, hedge_type) + + def get_ticks(self, order_book_id, date): + instrument = self.instruments(order_book_id) + return self._data_source.get_ticks(instrument, date) + + def get_merge_ticks(self, order_book_id_list, trading_date, last_dt=None): + return self._data_source.get_merge_ticks(order_book_id_list, trading_date, last_dt) + + def is_suspended(self, order_book_id, dt, count=1): + if count == 1: + return self._data_source.is_suspended(order_book_id, [dt])[0] + + trading_dates = self.get_n_trading_dates_until(dt, count) + return self._data_source.is_suspended(order_book_id, trading_dates) + + def is_st_stock(self, order_book_id, dt, count=1): + if count == 1: + return self._data_source.is_st_stock(order_book_id, [dt])[0] + + trading_dates = self.get_n_trading_dates_until(dt, count) + return self._data_source.is_st_stock(order_book_id, trading_dates) diff --git a/rqalpha/data/date_set.py b/rqalpha/data/date_set.py index 028055d2d..d2b190bb2 100644 --- a/rqalpha/data/date_set.py +++ b/rqalpha/data/date_set.py @@ -16,17 +16,15 @@ import bcolz import numpy as np -try: - # For Python 2 兼容 - from functools import lru_cache -except Exception as e: - from fastcache import lru_cache + +from ..utils.py2 import lru_cache class DateSet(object): def __init__(self, f): - self._dates = bcolz.open(f, 'r') - self._index = self._dates.attrs['line_map'] + dates = bcolz.open(f, 'r') + self._index = dates.attrs['line_map'] + self._dates = [int(d) for d in dates] @lru_cache(None) def _get_set(self, s, e): @@ -40,16 +38,19 @@ def get_days(self, order_book_id): return self._get_set(s, e) - def contains(self, order_book_id, dt): + def contains(self, order_book_id, dates): try: s, e = self._index[order_book_id] except KeyError: - return False + return [False] * len(dates) + + def _to_dt_int(d): + if isinstance(d, (int, np.int64, np.uint64)): + if d > 100000000: + return int(d // 1000000) + else: + return d.year*10000 + d.month*100 + d.day - if isinstance(dt, (int, np.int64, np.uint64)): - if dt > 100000000: - dt //= 1000000 - else: - dt = dt.year*10000 + dt.month*100 + dt.day + date_set = self._get_set(s, e) - return dt in self._get_set(s, e) + return [(_to_dt_int(d) in date_set) for d in dates] diff --git a/rqalpha/data/daybar_store.py b/rqalpha/data/daybar_store.py index f30b5ea38..34a802628 100644 --- a/rqalpha/data/daybar_store.py +++ b/rqalpha/data/daybar_store.py @@ -51,7 +51,7 @@ def get_bars(self, order_book_id, fields=None): dtype = np.dtype([('datetime', np.uint64)] + [(f, self._converter.field_type(f, self._table.cols[f].dtype)) for f in fields]) - result = np.empty(shape=(e-s, ), dtype=dtype) + result = np.empty(shape=(e - s, ), dtype=dtype) for f in fields: result[f][:] = self._converter.convert(f, self._table.cols[f][s:e]) result['datetime'][:] = self._table.cols['date'][s:e].astype(np.uint64) * 1000000 diff --git a/rqalpha/data/dividend_store.py b/rqalpha/data/dividend_store.py index 8fffd686a..f4aaaa39f 100644 --- a/rqalpha/data/dividend_store.py +++ b/rqalpha/data/dividend_store.py @@ -16,11 +16,8 @@ import bcolz import pandas as pd -try: - # For Python 2 兼容 - from functools import lru_cache -except Exception as e: - from fastcache import lru_cache + +from ..utils.py2 import lru_cache class DividendStore(object): diff --git a/rqalpha/data/future_info_cn.py b/rqalpha/data/future_info_cn.py new file mode 100644 index 000000000..b71b514ca --- /dev/null +++ b/rqalpha/data/future_info_cn.py @@ -0,0 +1,1646 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2017 Ricequant, Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ..const import COMMISSION_TYPE, MARGIN_TYPE + + +CN_FUTURE_INFO = { + "SM": { + "hedge": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 3.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 3.0 + }, + "speculation": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 3.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 3.0 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 3.0, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 3.0 + } + }, + "SR": { + "hedge": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 3.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 3.0 + }, + "speculation": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 3.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 3.0 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 3.0, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 3.0 + } + }, + "JD": { + "hedge": { + "short_margin_ratio": 0.08, + "close_commission_today_ratio": 0.00015, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 0.00015, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.08, + "open_commission_ratio": 0.00015 + }, + "speculation": { + "short_margin_ratio": 0.08, + "close_commission_today_ratio": 0.00015, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 0.00015, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.08, + "open_commission_ratio": 0.00015 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 0.00015, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 0.00015, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 0.00015 + } + }, + "T": { + "hedge": { + "short_margin_ratio": 0.02, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 3.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.02, + "open_commission_ratio": 3.0 + }, + "speculation": { + "short_margin_ratio": 0.02, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 3.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.02, + "open_commission_ratio": 3.0 + }, + "arbitrage": { + "short_margin_ratio": 0.02, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 3.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.02, + "open_commission_ratio": 3.0 + } + }, + "P": { + "hedge": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 2.5, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 2.5 + }, + "speculation": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 2.5, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 2.5 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 2.5, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 2.5 + } + }, + "BB": { + "hedge": { + "short_margin_ratio": 0.2, + "close_commission_today_ratio": 5e-05, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 0.0001, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.2, + "open_commission_ratio": 0.0001 + }, + "speculation": { + "short_margin_ratio": 0.2, + "close_commission_today_ratio": 5e-05, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 0.0001, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.2, + "open_commission_ratio": 0.0001 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 5e-05, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 0.0001, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 0.0001 + } + }, + "RM": { + "hedge": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 1.5, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 1.5 + }, + "speculation": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 1.5, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 1.5 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 1.5, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 1.5 + } + }, + "RS": { + "hedge": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 2.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 2.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 2.0 + }, + "speculation": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 2.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 2.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 2.0 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 2.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 2.0, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 2.0 + } + }, + "J": { + "hedge": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 3e-05, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 6e-05, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 6e-05 + }, + "speculation": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 3e-05, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 6e-05, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 6e-05 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 3e-05, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 6e-05, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 6e-05 + } + }, + "RI": { + "hedge": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 2.5, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 2.5, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 2.5 + }, + "speculation": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 2.5, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 2.5, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 2.5 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 2.5, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 2.5, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 2.5 + } + }, + "ER": { + "hedge": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 2.5, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 2.5, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 2.5 + }, + "speculation": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 2.5, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 2.5, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 2.5 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 2.5, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 2.5, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 2.5 + } + }, + "L": { + "hedge": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 2.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 2.0 + }, + "speculation": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 2.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 2.0 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 2.0, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 2.0 + } + }, + "PP": { + "hedge": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 2.5e-05, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 5e-05, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 5e-05 + }, + "speculation": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 2.5e-05, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 5e-05, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 5e-05 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 2.5e-05, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 5e-05, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 5e-05 + } + }, + "SN": { + "hedge": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 3.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 3.0 + }, + "speculation": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 3.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 3.0 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 3.0, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 3.0 + } + }, + "I": { + "hedge": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 3e-05, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 6e-05, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 6e-05 + }, + "speculation": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 3e-05, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 6e-05, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 6e-05 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 3e-05, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 6e-05, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 6e-05 + } + }, + "TA": { + "hedge": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 3.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 3.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 3.0 + }, + "speculation": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 3.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 3.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 3.0 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 3.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 3.0, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 3.0 + } + }, + "AL": { + "hedge": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 3.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 3.0 + }, + "speculation": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 3.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 3.0 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 3.0, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 3.0 + } + }, + "ZC": { + "hedge": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 4.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 4.0 + }, + "speculation": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 4.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 4.0 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 4.0, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 4.0 + } + }, + "TC": { + "hedge": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 4.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 4.0 + }, + "speculation": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 4.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 4.0 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 4.0, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 4.0 + } + }, + "LR": { + "hedge": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 3.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 3.0 + }, + "speculation": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 3.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 3.0 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 3.0, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 3.0 + } + }, + "RU": { + "hedge": { + "short_margin_ratio": 0.08, + "close_commission_today_ratio": 4.5e-05, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 4.5e-05, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.08, + "open_commission_ratio": 4.5e-05 + }, + "speculation": { + "short_margin_ratio": 0.08, + "close_commission_today_ratio": 4.5e-05, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 4.5e-05, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.08, + "open_commission_ratio": 4.5e-05 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 4.5e-05, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 4.5e-05, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 4.5e-05 + } + }, + "M": { + "hedge": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 1.5, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 1.5 + }, + "speculation": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 1.5, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 1.5 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 1.5, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 1.5 + } + }, + "TF": { + "hedge": { + "short_margin_ratio": 0.012, + "close_commission_today_ratio": 3.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 3.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.012, + "open_commission_ratio": 3.0 + }, + "speculation": { + "short_margin_ratio": 0.012, + "close_commission_today_ratio": 3.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 3.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.012, + "open_commission_ratio": 3.0 + }, + "arbitrage": { + "short_margin_ratio": 0.012, + "close_commission_today_ratio": 3.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 3.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.012, + "open_commission_ratio": 3.0 + } + }, + "MA": { + "hedge": { + "short_margin_ratio": 0.07, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 1.4, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.07, + "open_commission_ratio": 1.4 + }, + "speculation": { + "short_margin_ratio": 0.07, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 1.4, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.07, + "open_commission_ratio": 1.4 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 1.4, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 1.4 + } + }, + "ME": { + "hedge": { + "short_margin_ratio": 0.07, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 1.4, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.07, + "open_commission_ratio": 1.4 + }, + "speculation": { + "short_margin_ratio": 0.07, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 1.4, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.07, + "open_commission_ratio": 1.4 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 1.4, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 1.4 + } + }, + "WR": { + "hedge": { + "short_margin_ratio": 0.2, + "close_commission_today_ratio": 4e-05, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 4e-05, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.2, + "open_commission_ratio": 4e-05 + }, + "speculation": { + "short_margin_ratio": 0.2, + "close_commission_today_ratio": 4e-05, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 4e-05, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.2, + "open_commission_ratio": 4e-05 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 4e-05, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 4e-05, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 4e-05 + } + }, + "RB": { + "hedge": { + "short_margin_ratio": 0.06, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 4.5e-05, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.06, + "open_commission_ratio": 4.5e-05 + }, + "speculation": { + "short_margin_ratio": 0.06, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 4.5e-05, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.06, + "open_commission_ratio": 4.5e-05 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 4.5e-05, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 4.5e-05 + } + }, + "C": { + "hedge": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 1.2, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 1.2 + }, + "speculation": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 1.2, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 1.2 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 1.2, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 1.2 + } + }, + "JR": { + "hedge": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 3.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 3.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 3.0 + }, + "speculation": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 3.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 3.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 3.0 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 3.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 3.0, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 3.0 + } + }, + "SF": { + "hedge": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 3.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 3.0 + }, + "speculation": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 3.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 3.0 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 3.0, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 3.0 + } + }, + "OI": { + "hedge": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 2.5, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 2.5 + }, + "speculation": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 2.5, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 2.5 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 2.5, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 2.5 + } + }, + "RO": { + "hedge": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 2.5, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 2.5 + }, + "speculation": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 2.5, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 2.5 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 2.5, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 2.5 + } + }, + "CF": { + "hedge": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 4.3, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 4.3 + }, + "speculation": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 4.3, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 4.3 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 4.3, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 4.3 + } + }, + "BU": { + "hedge": { + "short_margin_ratio": 0.07, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 3e-05, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.07, + "open_commission_ratio": 3e-05 + }, + "speculation": { + "short_margin_ratio": 0.07, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 3e-05, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.07, + "open_commission_ratio": 3e-05 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 3e-05, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 3e-05 + } + }, + "JM": { + "hedge": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 3e-05, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 6e-05, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 6e-05 + }, + "speculation": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 3e-05, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 6e-05, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 6e-05 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 3e-05, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 6e-05, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 6e-05 + } + }, + "IH": { + "hedge": { + "short_margin_ratio": 0.2, + "close_commission_today_ratio": 0.000115, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 2.5e-05, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.2, + "open_commission_ratio": 2.5e-05 + }, + "speculation": { + "short_margin_ratio": 0.4, + "close_commission_today_ratio": 0.0023, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 2.3e-05, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.4, + "open_commission_ratio": 2.3e-05 + }, + "arbitrage": { + "short_margin_ratio": 0.2, + "close_commission_today_ratio": 0.000115, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 2.5e-05, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.2, + "open_commission_ratio": 2.5e-05 + } + }, + "FG": { + "hedge": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 3.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 3.0 + }, + "speculation": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 3.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 3.0 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 3.0, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 3.0 + } + }, + "PM": { + "hedge": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 5.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 5.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 5.0 + }, + "speculation": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 5.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 5.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 5.0 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 5.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 5.0, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 5.0 + } + }, + "FB": { + "hedge": { + "short_margin_ratio": 0.2, + "close_commission_today_ratio": 5e-05, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 0.0001, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.2, + "open_commission_ratio": 0.0001 + }, + "speculation": { + "short_margin_ratio": 0.2, + "close_commission_today_ratio": 5e-05, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 0.0001, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.2, + "open_commission_ratio": 0.0001 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 5e-05, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 0.0001, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 0.0001 + } + }, + "CS": { + "hedge": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 1.5, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 1.5 + }, + "speculation": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 1.5, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 1.5 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 1.5, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 1.5 + } + }, + "B": { + "hedge": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 2.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 2.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 2.0 + }, + "speculation": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 2.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 2.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 2.0 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 2.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 2.0, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 2.0 + } + }, + "IC": { + "hedge": { + "short_margin_ratio": 0.2, + "close_commission_today_ratio": 0.000115, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 2.5e-05, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.2, + "open_commission_ratio": 2.5e-05 + }, + "speculation": { + "short_margin_ratio": 0.4, + "close_commission_today_ratio": 0.0023, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 2.3e-05, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.4, + "open_commission_ratio": 2.3e-05 + }, + "arbitrage": { + "short_margin_ratio": 0.2, + "close_commission_today_ratio": 0.000115, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 2.5e-05, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.2, + "open_commission_ratio": 2.5e-05 + } + }, + "WH": { + "hedge": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 2.5, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 2.5 + }, + "speculation": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 2.5, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 2.5 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 2.5, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 2.5 + } + }, + "WS": { + "hedge": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 2.5, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 2.5 + }, + "speculation": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 2.5, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 2.5 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 2.5, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 2.5 + } + }, + "FU": { + "hedge": { + "short_margin_ratio": 0.2, + "close_commission_today_ratio": 2e-05, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 2e-05, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.2, + "open_commission_ratio": 2e-05 + }, + "speculation": { + "short_margin_ratio": 0.2, + "close_commission_today_ratio": 2e-05, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 2e-05, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.2, + "open_commission_ratio": 2e-05 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 2e-05, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 2e-05, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 2e-05 + } + }, + "AU": { + "hedge": { + "short_margin_ratio": 0.06, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 10.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.06, + "open_commission_ratio": 10.0 + }, + "speculation": { + "short_margin_ratio": 0.06, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 10.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.06, + "open_commission_ratio": 10.0 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 10.0, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 10.0 + } + }, + "CU": { + "hedge": { + "short_margin_ratio": 0.08, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 2.5e-05, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.08, + "open_commission_ratio": 2.5e-05 + }, + "speculation": { + "short_margin_ratio": 0.08, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 2.5e-05, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.08, + "open_commission_ratio": 2.5e-05 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 2.5e-05, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 2.5e-05 + } + }, + "V": { + "hedge": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 2.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 2.0 + }, + "speculation": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 2.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 2.0 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 2.0, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 2.0 + } + }, + "Y": { + "hedge": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 2.5, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 2.5 + }, + "speculation": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 2.5, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 2.5 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 2.5, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 2.5 + } + }, + "AG": { + "hedge": { + "short_margin_ratio": 0.08, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 5e-05, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.08, + "open_commission_ratio": 5e-05 + }, + "speculation": { + "short_margin_ratio": 0.08, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 5e-05, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.08, + "open_commission_ratio": 5e-05 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 5e-05, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 5e-05 + } + }, + "PB": { + "hedge": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 4e-05, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 4e-05 + }, + "speculation": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 4e-05, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 4e-05 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 4e-05, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 4e-05 + } + }, + "IF": { + "hedge": { + "short_margin_ratio": 0.2, + "close_commission_today_ratio": 0.000115, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 2.5e-05, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.2, + "open_commission_ratio": 2.5e-05 + }, + "speculation": { + "short_margin_ratio": 0.4, + "close_commission_today_ratio": 0.0023, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 2.3e-05, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.4, + "open_commission_ratio": 2.3e-05 + }, + "arbitrage": { + "short_margin_ratio": 0.2, + "close_commission_today_ratio": 0.000115, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 2.5e-05, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.2, + "open_commission_ratio": 2.5e-05 + } + }, + "A": { + "hedge": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 2.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 2.0 + }, + "speculation": { + "short_margin_ratio": 0.05, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 2.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.05, + "open_commission_ratio": 2.0 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 2.0, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 2.0 + } + }, + "NI": { + "hedge": { + "short_margin_ratio": 0.07, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 6.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.07, + "open_commission_ratio": 6.0 + }, + "speculation": { + "short_margin_ratio": 0.07, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 6.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.07, + "open_commission_ratio": 6.0 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 6.0, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 6.0 + } + }, + "HC": { + "hedge": { + "short_margin_ratio": 0.06, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 4e-05, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.06, + "open_commission_ratio": 4e-05 + }, + "speculation": { + "short_margin_ratio": 0.06, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 4e-05, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.06, + "open_commission_ratio": 4e-05 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_MONEY, + "close_commission_ratio": 4e-05, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 4e-05 + } + }, + "ZN": { + "hedge": { + "short_margin_ratio": 0.06, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 3.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.06, + "open_commission_ratio": 3.0 + }, + "speculation": { + "short_margin_ratio": 0.06, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 3.0, + "margin_type": MARGIN_TYPE.BY_MONEY, + "long_margin_ratio": 0.06, + "open_commission_ratio": 3.0 + }, + "arbitrage": { + "short_margin_ratio": 0.0, + "close_commission_today_ratio": 0.0, + "commission_type": COMMISSION_TYPE.BY_VOLUME, + "close_commission_ratio": 3.0, + "margin_type": None, + "long_margin_ratio": 0.0, + "open_commission_ratio": 3.0 + } + } +} + diff --git a/rqalpha/data/trading_dates_mixin.py b/rqalpha/data/trading_dates_mixin.py index 9b08b872c..f4f5f5bfc 100644 --- a/rqalpha/data/trading_dates_mixin.py +++ b/rqalpha/data/trading_dates_mixin.py @@ -14,13 +14,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -import pandas as pd import datetime -try: - # For Python 2 兼容 - from functools import lru_cache -except Exception as e: - from fastcache import lru_cache +import pandas as pd + +from ..utils.py2 import lru_cache class TradingDatesMixin(object): @@ -52,6 +49,11 @@ def get_next_trading_date(self, date): pos = self._dates.searchsorted(date, side='right') return self._dates[pos] + def is_trading_date(self, date): + date = pd.Timestamp(date).replace(hour=0, minute=0, second=0) + pos = self._dates.searchsorted(date) + return pos < len(self._dates) and self._dates[pos] == date + @lru_cache(512) def _get_future_trading_date(self, dt): dt1 = dt - datetime.timedelta(hours=4) @@ -78,3 +80,11 @@ def get_nth_previous_trading_date(self, date, n): return self._dates[pos - n] else: return self._dates[0] + + def get_n_trading_dates_until(self, dt, n): + date = pd.Timestamp(dt).replace(hour=0, minute=0, second=0) + pos = self._dates.searchsorted(date) + if pos >= n: + return self._dates[pos - n:pos] + + return self._dates[:pos] diff --git a/rqalpha/config_template.yml b/rqalpha/default_config.yml similarity index 54% rename from rqalpha/config_template.yml rename to rqalpha/default_config.yml index 41f08f1b3..ff277034d 100644 --- a/rqalpha/config_template.yml +++ b/rqalpha/default_config.yml @@ -1,11 +1,11 @@ -version: 0.1.2 +# see more config +# http://rqalpha.readthedocs.io/zh_CN/stable/api/config.html +version: 0.1.5 # 白名单,设置可以直接在策略代码中指定哪些模块的配置项目 whitelist: [base, extra, validator, mod] base: - # 可以指定回测的唯一ID,用户区分多次回测的结果 - run_id: 9999 # 数据源所存储的文件路径 data_bundle_path: ~ # 启动的策略文件路径 @@ -18,22 +18,16 @@ base: stock_starting_cash: 0 # 期货起始资金,默认为0 future_starting_cash: 0 - # 设置策略类型,目前支持 `stock` (股票策略)、`future` (期货策略)及 `stock_future` (混合策略) - strategy_type: stock + # 设置策略可交易品种,目前支持 `stock` (股票策略)、`future` (期货策略) + securities: [stock] + # 设置保证金乘数,默认为1 + margin_multiplier: 1 # 运行类型,`b` 为回测,`p` 为模拟交易, `r` 为实盘交易。 run_type: b # 目前支持 `1d` (日线回测) 和 `1m` (分钟线回测),如果要进行分钟线,请注意是否拥有对应的数据源,目前开源版本是不提供对应的数据源的。 frequency: 1d - # 启用的回测引擎,目前支持 `current_bar` (当前Bar收盘价撮合) 和 `next_bar` (下一个Bar开盘价撮合) - matching_type: current_bar # Benchmark,如果不设置,默认没有基准参照。 benchmark: ~ - # 设置滑点 - slippage: 0 - # 设置手续费乘数,默认为1 - commission_multiplier: 1 - # 设置保证金乘数,默认为1 - margin_multiplier: 1 # 在模拟交易和实盘交易中,RQAlpha支持策略的pause && resume,该选项表示开启 resume 功能 resume_mode: false # 在模拟交易和实盘交易中,RQAlpha支持策略的pause && resume,该选项表示开启 persist 功能呢, @@ -48,7 +42,7 @@ extra: # 或者设置 `error` 只查看错误级别的日志输出 log_level: info user_system_log_disabled: false - # 在回测结束后,选择是否查看图形化的收益曲线 + # 通过该参数可以将预定义变量传入 `context` 内。 context_vars: ~ # force_run_init_when_pt_resume: 在PT的resume模式时,是否强制执行用户init。主要用于用户改代码。 force_run_init_when_pt_resume: false @@ -62,55 +56,3 @@ validator: cash_return_by_stock_delisted: false # close_amount: 在执行order_value操作时,进行实际下单数量的校验和scale,默认开启 close_amount: true - # bar_limit: 在处于涨跌停时,无法买进/卖出,默认开启 - bar_limit: true - - -mod: - # 回测 / 模拟交易 支持 Mod - simulation: - lib: 'rqalpha.mod.simulation' - enabled: true - priority: 100 - # 技术分析API - funcat_api: - lib: 'rqalpha.mod.funcat_api' - enabled: false - priority: 200 - # 开启该选项,可以在命令行查看回测进度 - progress: - lib: 'rqalpha.mod.progress' - enabled: false - priority: 400 - # 接收实时行情运行 - simple_stock_realtime_trade: - lib: 'rqalpha.mod.simple_stock_realtime_trade' - persist_path: "./persist/strategy/" - fps: 3 - enabled: false - priority: 500 - # 渐进式输出运行结果 - progressive_output_csv: - lib: 'rqalpha.mod.progressive_output_csv' - enabled: false - output_path: "./" - priority: 600 - risk_manager: - lib: 'rqalpha.mod.risk_manager' - enabled: true - priority: 700 - # available_cash: 查可用资金是否充足,默认开启 - available_cash: true - # available_position: 检查可平仓位是否充足,默认开启 - available_position: true - # short_stock: 允许裸卖空 - short_stock: false - analyser: - priority: 100 - enabled: true - lib: 'rqalpha.mod.analyser' - record: true - output_file: ~ - plot: ~ - plot_save_file: ~ - report_save_path: ~ diff --git a/rqalpha/environment.py b/rqalpha/environment.py index 53c66648b..4abd3bfbd 100644 --- a/rqalpha/environment.py +++ b/rqalpha/environment.py @@ -14,9 +14,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +from .events import EventBus +from .utils import get_account_type from .utils.logger import system_log, user_log, user_detail_log -from .events import EVENT, EventBus -from .model.commission import init_commission class Environment(object): @@ -24,62 +24,48 @@ class Environment(object): def __init__(self, config): Environment._env = self - self._config = config - self._data_proxy = None - self._data_source = None - self._event_source = None - self._strategy_loader = None - self._global_vars = None - self._persist_provider = None - self._commission_initializer = init_commission + self.config = config + self._universe = None + self.data_proxy = None + self.data_source = None + self.price_board = None + self.event_source = None + self.strategy_loader = None + self.global_vars = None + self.persist_provider = None + self.broker = None + self.profile_deco = None self.system_log = system_log self.user_log = user_log self.user_detail_log = user_detail_log self.event_bus = EventBus() - self._broker = None - self.account = None - self.accounts = None + self.portfolio = None + self.benchmark_portfolio = None self.calendar_dt = None self.trading_dt = None self.mod_dict = None - self._universe = None - self.profile_deco = None + self.plot_store = None + self.bar_dict = None + self._frontend_validators = [] @classmethod def get_instance(cls): return Environment._env - @property - def config(self): - return self._config - def set_data_proxy(self, data_proxy): - self._data_proxy = data_proxy - - @property - def data_proxy(self): - return self._data_proxy + self.data_proxy = data_proxy def set_data_source(self, data_source): - self._data_source = data_source + self.data_source = data_source - @property - def data_source(self): - return self._data_source - - @property - def strategy_loader(self): - return self._strategy_loader + def set_price_board(self, price_board): + self.price_board = price_board def set_strategy_loader(self, strategy_loader): - self._strategy_loader = strategy_loader - - @property - def global_vars(self): - return self._global_vars + self.strategy_loader = strategy_loader def set_global_vars(self, global_vars): - self._global_vars = global_vars + self.global_vars = global_vars def set_hold_strategy(self): self.config.extra.is_hold = True @@ -87,35 +73,75 @@ def set_hold_strategy(self): def cancel_hold_strategy(self): self.config.extra.is_hold = False - @property - def is_strategy_hold(self): - return self.config.extra.is_hold - def set_persist_provider(self, provider): - self._persist_provider = provider - - @property - def persist_provider(self): - return self._persist_provider + self.persist_provider = provider def set_event_source(self, event_source): - self._event_source = event_source + self.event_source = event_source - @property - def event_source(self): - return self._event_source + def set_broker(self, broker): + self.broker = broker - @property - def broker(self): - return self._broker + def add_frontend_validator(self, validator): + self._frontend_validators.append(validator) - def set_broker(self, broker): - self._broker = broker + def can_submit_order(self, order): + account = self.get_account(order.order_book_id) + for v in self._frontend_validators: + if not v.can_submit_order(account, order): + return False + return True - @property - def universe(self): + def can_cancel_order(self, order): + account = self.get_account(order.order_book_id) + for v in self._frontend_validators: + if not v.can_cancel_order(account, order): + return False + return True + + def set_bar_dict(self, bar_dict): + self.bar_dict = bar_dict + + def get_universe(self): return self._universe.get() def update_universe(self, universe): self._universe.update(universe) - self.event_bus.publish_event(EVENT.POST_UNIVERSE_CHANGED, universe) + + def get_plot_store(self): + if self.plot_store is None: + from .utils.plot_store import PlotStore + self.plot_store = PlotStore() + return self.plot_store + + def add_plot(self, series_name, value): + self.get_plot_store().add_plot(self.trading_dt.date(), series_name, value) + + def get_bar(self, order_book_id): + return self.bar_dict[order_book_id] + + def get_last_price(self, order_book_id): + return self.price_board.get_last_price(order_book_id) + + def get_instrument(self, order_book_id): + return self.data_proxy.instruments(order_book_id) + + def get_future_commission_info(self, order_book_id, hedge_type): + return self.data_proxy.get_future_info(order_book_id, hedge_type) + + def get_future_margin_rate(self, order_book_id): + # FIXME + try: + return self.data_proxy.instruments(order_book_id).margin_rate + except NotImplementedError: + return self.data_proxy.get_future_info(order_book_id)['long_margin_ratio'] + + def get_future_info(self, order_book_id, hedge_type): + return self.data_proxy.get_future_info(order_book_id, hedge_type) + + def get_account(self, order_book_id): + account_type = get_account_type(order_book_id) + return self.portfolio.accounts[account_type] + + def get_open_orders(self, order_book_id=None): + return self.broker.get_open_orders(order_book_id) diff --git a/rqalpha/events.py b/rqalpha/events.py index 8245dde88..26021185d 100644 --- a/rqalpha/events.py +++ b/rqalpha/events.py @@ -19,11 +19,12 @@ class Event(object): - def __init__(self, event_type, calendar_dt, trading_dt, data={}): + def __init__(self, event_type, **kwargs): + self.__dict__ = kwargs self.event_type = event_type - self.calendar_dt = calendar_dt - self.trading_dt = trading_dt - self.data = data + + def __repr__(self): + return ' '.join('{}:{}'.format(k, v) for k, v in self.__dict__.items()) class EventBus(object): @@ -36,10 +37,10 @@ def add_listener(self, event, listener): def prepend_listener(self, event, listener): self._listeners[event].insert(0, listener) - def publish_event(self, event, *args, **kwargs): - for l in self._listeners[event]: + def publish_event(self, event): + for l in self._listeners[event.event_type]: # 如果返回 True ,那么消息不再传递下去 - if l(*args, **kwargs): + if l(event): break diff --git a/rqalpha/examples/IF1706_20161108.csv b/rqalpha/examples/IF1706_20161108.csv new file mode 100644 index 000000000..1ac2343e3 --- /dev/null +++ b/rqalpha/examples/IF1706_20161108.csv @@ -0,0 +1,241 @@ +MDEntryTime,CalendarDate,OpeningPx,HighPx,LowPx,ClosingPx,OpenInterest,TotalVolumeTraded,TotalTurnover,LowLimitPx,HighLimitPx,RefrencePx,BasisSpread +93100000,20161108,3209.8,3209.8,3209.8,3209.8,537,1,962940.0,2877.6,3516.8,3372.6206,-162.8206 +93200000,20161108,3215.8,3215.8,3215.8,3215.8,536,1,964740.0,2877.6,3516.8,3371.7776,-155.9776 +93300000,20161108,3215.8,3215.8,3215.8,3215.8,536,0,0.0,2877.6,3516.8,3369.8866,-154.0866 +93400000,20161108,3215.8,3215.8,3215.8,3215.8,536,0,0.0,2877.6,3516.8,3369.6326,-153.8326 +93500000,20161108,3217.6,3217.6,3217.6,3217.6,535,1,965280.0,2877.6,3516.8,3370.1556,-152.5556 +93600000,20161108,3218.6,3218.6,3218.6,3218.6,535,2,1930920.0,2877.6,3516.8,3370.1786,-151.5786 +93700000,20161108,3215.0,3215.0,3214.8,3214.8,535,4,3857820.0,2877.6,3516.8,3368.6136,-153.8136 +93800000,20161108,3214.8,3214.8,3214.8,3214.8,535,0,0.0,2877.6,3516.8,3367.1286,-152.3286 +93900000,20161108,3214.4,3214.8,3214.4,3214.8,535,2,1928760.0,2877.6,3516.8,3367.1556,-152.3556 +94000000,20161108,3212.2,3214.2,3212.2,3213.8,536,4,3855780.0,2877.6,3516.8,3367.9366,-154.1366 +94100000,20161108,3215.2,3215.2,3215.2,3215.2,537,2,1929120.0,2877.6,3516.8,3368.0386,-152.8386 +94200000,20161108,3216.8,3217.2,3216.8,3217.2,540,3,2895300.0,2877.6,3516.8,3368.0946,-150.8946 +94300000,20161108,3217.2,3217.2,3217.2,3217.2,540,0,0.0,2877.6,3516.8,3367.9696,-150.7696 +94400000,20161108,3212.6,3212.6,3212.6,3212.6,539,1,963780.0,2877.6,3516.8,3367.3106,-154.7106 +94500000,20161108,3212.6,3212.6,3212.6,3212.6,539,0,0.0,2877.6,3516.8,3366.2786,-153.6786 +94600000,20161108,3207.6,3207.6,3207.6,3207.6,539,1,962280.0,2877.6,3516.8,3364.8616,-157.2616 +94700000,20161108,3208.2,3208.2,3206.2,3206.2,540,2,1924320.0,2877.6,3516.8,3364.5916,-158.3916 +94800000,20161108,3206.6,3208.2,3206.6,3208.2,541,2,1924440.0,2877.6,3516.8,3364.1186,-155.9186 +94900000,20161108,3208.2,3208.2,3208.2,3208.2,541,0,0.0,2877.6,3516.8,3365.3646,-157.1646 +95000000,20161108,3208.2,3208.2,3208.2,3208.2,541,0,0.0,2877.6,3516.8,3365.0106,-156.8106 +95100000,20161108,3208.2,3208.2,3208.2,3208.2,541,0,0.0,2877.6,3516.8,3365.2286,-157.0286 +95200000,20161108,3208.2,3208.2,3208.2,3208.2,541,0,0.0,2877.6,3516.8,3365.5466,-157.3466 +95300000,20161108,3208.8,3208.8,3208.8,3208.8,541,1,962640.0,2877.6,3516.8,3366.2176,-157.4176 +95400000,20161108,3208.8,3208.8,3208.8,3208.8,541,0,0.0,2877.6,3516.8,3366.3106,-157.5106 +95500000,20161108,3208.8,3208.8,3208.8,3208.8,541,0,0.0,2877.6,3516.8,3366.8716,-158.0716 +95600000,20161108,3208.8,3208.8,3208.8,3208.8,541,0,0.0,2877.6,3516.8,3368.0906,-159.2906 +95700000,20161108,3208.8,3208.8,3208.8,3208.8,541,0,0.0,2877.6,3516.8,3368.4706,-159.6706 +95800000,20161108,3208.8,3208.8,3208.8,3208.8,541,0,0.0,2877.6,3516.8,3369.1116,-160.3116 +95900000,20161108,3208.8,3208.8,3208.8,3208.8,541,0,0.0,2877.6,3516.8,3368.6266,-159.8266 +100000000,20161108,3208.8,3208.8,3208.8,3208.8,541,0,0.0,2877.6,3516.8,3368.6136,-159.8136 +100100000,20161108,3208.8,3208.8,3208.8,3208.8,541,0,0.0,2877.6,3516.8,3368.5366,-159.7366 +100200000,20161108,3208.8,3208.8,3208.8,3208.8,541,0,0.0,2877.6,3516.8,3368.3886,-159.5886 +100300000,20161108,3208.8,3208.8,3208.8,3208.8,541,0,0.0,2877.6,3516.8,3367.9986,-159.1986 +100400000,20161108,3208.8,3208.8,3208.8,3208.8,541,0,0.0,2877.6,3516.8,3367.8496,-159.0496 +100500000,20161108,3208.8,3208.8,3208.8,3208.8,541,0,0.0,2877.6,3516.8,3368.4416,-159.6416 +100600000,20161108,3213.2,3213.2,3213.2,3213.2,540,1,963960.0,2877.6,3516.8,3368.8746,-155.6746 +100700000,20161108,3213.2,3213.2,3213.2,3213.2,540,0,0.0,2877.6,3516.8,3368.9566,-155.7566 +100800000,20161108,3213.2,3213.2,3213.2,3213.2,540,0,0.0,2877.6,3516.8,3368.8746,-155.6746 +100900000,20161108,3213.2,3213.2,3213.2,3213.2,540,0,0.0,2877.6,3516.8,3369.1056,-155.9056 +101000000,20161108,3213.2,3213.2,3213.2,3213.2,540,0,0.0,2877.6,3516.8,3369.0436,-155.8436 +101100000,20161108,3213.2,3213.2,3213.2,3213.2,540,0,0.0,2877.6,3516.8,3367.7006,-154.5006 +101200000,20161108,3213.2,3213.2,3213.2,3213.2,540,0,0.0,2877.6,3516.8,3367.0576,-153.8576 +101300000,20161108,3210.0,3210.0,3210.0,3210.0,541,2,1926000.0,2877.6,3516.8,3367.0096,-157.0096 +101400000,20161108,3210.0,3210.0,3210.0,3210.0,541,0,0.0,2877.6,3516.8,3366.8666,-156.8666 +101500000,20161108,3208.2,3208.2,3208.2,3208.2,542,1,962460.0,2877.6,3516.8,3366.3056,-158.1056 +101600000,20161108,3208.2,3208.2,3208.2,3208.2,542,0,0.0,2877.6,3516.8,3367.4646,-159.2646 +101700000,20161108,3208.2,3208.2,3208.2,3208.2,542,0,0.0,2877.6,3516.8,3367.5206,-159.3206 +101800000,20161108,3208.2,3208.2,3208.2,3208.2,542,0,0.0,2877.6,3516.8,3366.7836,-158.5836 +101900000,20161108,3208.2,3208.2,3208.2,3208.2,542,0,0.0,2877.6,3516.8,3367.0826,-158.8826 +102000000,20161108,3208.0,3208.0,3208.0,3208.0,543,1,962400.0,2877.6,3516.8,3366.2286,-158.2286 +102100000,20161108,3208.0,3208.0,3208.0,3208.0,543,0,0.0,2877.6,3516.8,3365.3306,-157.3306 +102200000,20161108,3208.0,3208.0,3208.0,3208.0,543,0,0.0,2877.6,3516.8,3365.6416,-157.6416 +102300000,20161108,3205.0,3205.0,3205.0,3205.0,543,1,961500.0,2877.6,3516.8,3365.3626,-160.3626 +102400000,20161108,3205.0,3205.0,3205.0,3205.0,543,0,0.0,2877.6,3516.8,3365.0536,-160.0536 +102500000,20161108,3205.0,3205.0,3205.0,3205.0,543,0,0.0,2877.6,3516.8,3365.4466,-160.4466 +102600000,20161108,3205.0,3205.0,3205.0,3205.0,543,0,0.0,2877.6,3516.8,3365.3886,-160.3886 +102700000,20161108,3205.0,3205.0,3205.0,3205.0,543,0,0.0,2877.6,3516.8,3365.8446,-160.8446 +102800000,20161108,3208.0,3209.0,3208.0,3209.0,544,2,1925100.0,2877.6,3516.8,3365.5696,-156.5696 +102900000,20161108,3209.0,3209.0,3209.0,3209.0,544,0,0.0,2877.6,3516.8,3365.5176,-156.5176 +103000000,20161108,3205.0,3205.0,3205.0,3205.0,544,1,961500.0,2877.6,3516.8,3365.5566,-160.5566 +103100000,20161108,3205.0,3205.0,3205.0,3205.0,544,0,0.0,2877.6,3516.8,3365.6336,-160.6336 +103200000,20161108,3205.8,3205.8,3205.8,3205.8,544,1,961740.0,2877.6,3516.8,3364.8526,-159.0526 +103300000,20161108,3205.8,3205.8,3205.8,3205.8,544,0,0.0,2877.6,3516.8,3364.5216,-158.7216 +103400000,20161108,3205.8,3205.8,3205.8,3205.8,544,0,0.0,2877.6,3516.8,3362.8846,-157.0846 +103500000,20161108,3203.0,3203.0,3202.8,3202.8,544,2,1921740.0,2877.6,3516.8,3362.4556,-159.6556 +103600000,20161108,3202.8,3202.8,3202.8,3202.8,544,0,0.0,2877.6,3516.8,3361.6266,-158.8266 +103700000,20161108,3202.8,3202.8,3202.8,3202.8,544,0,0.0,2877.6,3516.8,3361.7806,-158.9806 +103800000,20161108,3202.8,3202.8,3202.8,3202.8,544,0,0.0,2877.6,3516.8,3361.8146,-159.0146 +103900000,20161108,3202.8,3202.8,3202.8,3202.8,544,0,0.0,2877.6,3516.8,3362.2296,-159.4296 +104000000,20161108,3202.8,3202.8,3202.8,3202.8,544,0,0.0,2877.6,3516.8,3362.3646,-159.5646 +104100000,20161108,3202.8,3202.8,3202.8,3202.8,544,0,0.0,2877.6,3516.8,3362.9646,-160.1646 +104200000,20161108,3202.8,3202.8,3202.8,3202.8,544,0,0.0,2877.6,3516.8,3362.8576,-160.0576 +104300000,20161108,3202.4,3202.4,3202.4,3202.4,545,4,3842880.0,2877.6,3516.8,3363.0706,-160.6706 +104400000,20161108,3202.4,3202.4,3202.4,3202.4,545,0,0.0,2877.6,3516.8,3363.5376,-161.1376 +104500000,20161108,3200.8,3200.8,3200.8,3200.8,546,1,960240.0,2877.6,3516.8,3363.7636,-162.9636 +104600000,20161108,3201.4,3204.0,3201.4,3204.0,547,2,1921620.0,2877.6,3516.8,3363.8926,-159.8926 +104700000,20161108,3204.0,3204.0,3204.0,3204.0,547,0,0.0,2877.6,3516.8,3364.3336,-160.3336 +104800000,20161108,3204.0,3204.0,3204.0,3204.0,547,0,0.0,2877.6,3516.8,3363.9176,-159.9176 +104900000,20161108,3204.0,3204.0,3204.0,3204.0,547,0,0.0,2877.6,3516.8,3364.0866,-160.0866 +105000000,20161108,3204.0,3204.0,3204.0,3204.0,547,0,0.0,2877.6,3516.8,3364.4266,-160.4266 +105100000,20161108,3204.0,3204.0,3204.0,3204.0,547,0,0.0,2877.6,3516.8,3364.9256,-160.9256 +105200000,20161108,3205.6,3205.6,3205.6,3205.6,547,1,961680.0,2877.6,3516.8,3365.4326,-159.8326 +105300000,20161108,3205.6,3205.6,3205.6,3205.6,547,0,0.0,2877.6,3516.8,3365.9316,-160.3316 +105400000,20161108,3205.6,3205.6,3205.6,3205.6,547,0,0.0,2877.6,3516.8,3366.5076,-160.9076 +105500000,20161108,3205.6,3205.6,3205.6,3205.6,547,0,0.0,2877.6,3516.8,3366.5786,-160.9786 +105600000,20161108,3205.6,3205.6,3205.6,3205.6,547,0,0.0,2877.6,3516.8,3366.0816,-160.4816 +105700000,20161108,3205.6,3205.6,3205.6,3205.6,547,0,0.0,2877.6,3516.8,3365.9616,-160.3616 +105800000,20161108,3205.6,3205.6,3205.6,3205.6,547,0,0.0,2877.6,3516.8,3365.9396,-160.3396 +105900000,20161108,3205.6,3205.6,3205.6,3205.6,547,0,0.0,2877.6,3516.8,3366.5606,-160.9606 +110000000,20161108,3205.6,3205.6,3205.6,3205.6,547,0,0.0,2877.6,3516.8,3366.9026,-161.3026 +110100000,20161108,3205.6,3205.6,3205.6,3205.6,547,0,0.0,2877.6,3516.8,3367.2956,-161.6956 +110200000,20161108,3205.6,3205.6,3205.6,3205.6,547,0,0.0,2877.6,3516.8,3367.0836,-161.4836 +110300000,20161108,3205.6,3205.6,3205.6,3205.6,547,0,0.0,2877.6,3516.8,3367.2016,-161.6016 +110400000,20161108,3205.6,3205.6,3205.6,3205.6,547,0,0.0,2877.6,3516.8,3366.6516,-161.0516 +110500000,20161108,3205.6,3205.6,3205.6,3205.6,547,0,0.0,2877.6,3516.8,3366.7746,-161.1746 +110600000,20161108,3205.6,3205.6,3205.6,3205.6,547,0,0.0,2877.6,3516.8,3366.6516,-161.0516 +110700000,20161108,3205.6,3205.6,3205.6,3205.6,547,0,0.0,2877.6,3516.8,3366.7876,-161.1876 +110800000,20161108,3205.6,3205.6,3205.6,3205.6,547,0,0.0,2877.6,3516.8,3366.6816,-161.0816 +110900000,20161108,3205.6,3205.6,3205.6,3205.6,547,0,0.0,2877.6,3516.8,3366.5956,-160.9956 +111000000,20161108,3205.6,3205.6,3205.6,3205.6,547,0,0.0,2877.6,3516.8,3366.3466,-160.7466 +111100000,20161108,3205.6,3205.6,3205.6,3205.6,547,0,0.0,2877.6,3516.8,3366.4586,-160.8586 +111200000,20161108,3208.8,3208.8,3208.8,3208.8,548,1,962640.0,2877.6,3516.8,3366.5896,-157.7896 +111300000,20161108,3208.8,3208.8,3208.8,3208.8,548,0,0.0,2877.6,3516.8,3366.2936,-157.4936 +111400000,20161108,3208.8,3208.8,3208.8,3208.8,548,0,0.0,2877.6,3516.8,3366.4636,-157.6636 +111500000,20161108,3208.8,3208.8,3208.8,3208.8,548,0,0.0,2877.6,3516.8,3366.7246,-157.9246 +111600000,20161108,3208.8,3208.8,3208.8,3208.8,548,0,0.0,2877.6,3516.8,3366.8836,-158.0836 +111700000,20161108,3208.8,3208.8,3208.8,3208.8,548,0,0.0,2877.6,3516.8,3367.1606,-158.3606 +111800000,20161108,3208.8,3208.8,3208.8,3208.8,548,0,0.0,2877.6,3516.8,3367.5316,-158.7316 +111900000,20161108,3208.8,3208.8,3208.8,3208.8,548,0,0.0,2877.6,3516.8,3368.1796,-159.3796 +112000000,20161108,3211.6,3211.6,3211.6,3211.6,549,2,1926960.0,2877.6,3516.8,3369.3456,-157.7456 +112100000,20161108,3211.6,3211.6,3211.6,3211.6,549,0,0.0,2877.6,3516.8,3370.2416,-158.6416 +112200000,20161108,3211.6,3211.6,3211.6,3211.6,549,0,0.0,2877.6,3516.8,3371.7636,-160.1636 +112300000,20161108,3211.6,3211.6,3211.6,3211.6,549,0,0.0,2877.6,3516.8,3372.5186,-160.9186 +112400000,20161108,3214.8,3214.8,3214.8,3214.8,549,1,964440.0,2877.6,3516.8,3374.6516,-159.8516 +112500000,20161108,3217.8,3218.0,3217.8,3218.0,546,4,3861540.0,2877.6,3516.8,3376.0876,-158.0876 +112600000,20161108,3223.2,3223.2,3223.2,3223.2,546,1,966960.0,2877.6,3516.8,3376.6216,-153.4216 +112700000,20161108,3223.2,3223.2,3223.2,3223.2,546,0,0.0,2877.6,3516.8,3377.0826,-153.8826 +112800000,20161108,3223.2,3223.2,3223.2,3223.2,546,0,0.0,2877.6,3516.8,3377.1626,-153.9626 +112900000,20161108,3223.2,3223.2,3223.2,3223.2,546,0,0.0,2877.6,3516.8,3378.5326,-155.3326 +113000000,20161108,3223.2,3223.2,3223.2,3223.2,546,0,0.0,2877.6,3516.8,3379.7756,-156.5756 +130100000,20161108,3223.2,3223.2,3223.2,3223.2,546,0,0.0,2877.6,3516.8,3383.3496,-160.1496 +130200000,20161108,3223.2,3223.2,3223.2,3223.2,546,0,0.0,2877.6,3516.8,3381.8826,-158.6826 +130300000,20161108,3223.2,3223.2,3223.2,3223.2,546,0,0.0,2877.6,3516.8,3381.6346,-158.4346 +130400000,20161108,3223.2,3223.2,3223.2,3223.2,546,0,0.0,2877.6,3516.8,3382.5376,-159.3376 +130500000,20161108,3223.2,3223.2,3223.2,3223.2,546,0,0.0,2877.6,3516.8,3381.8676,-158.6676 +130600000,20161108,3223.2,3223.2,3223.2,3223.2,546,0,0.0,2877.6,3516.8,3381.4786,-158.2786 +130700000,20161108,3223.2,3223.2,3223.2,3223.2,546,0,0.0,2877.6,3516.8,3382.0286,-158.8286 +130800000,20161108,3223.4,3223.6,3223.4,3223.6,548,2,1934100.0,2877.6,3516.8,3381.0416,-157.4416 +130900000,20161108,3223.6,3223.6,3223.6,3223.6,548,0,0.0,2877.6,3516.8,3380.4806,-156.8806 +131000000,20161108,3223.6,3223.6,3223.6,3223.6,548,0,0.0,2877.6,3516.8,3380.2566,-156.6566 +131100000,20161108,3223.6,3223.6,3223.6,3223.6,548,0,0.0,2877.6,3516.8,3379.7096,-156.1096 +131200000,20161108,3223.6,3223.6,3223.6,3223.6,548,0,0.0,2877.6,3516.8,3380.0466,-156.4466 +131300000,20161108,3223.6,3223.6,3223.6,3223.6,548,0,0.0,2877.6,3516.8,3380.2386,-156.6386 +131400000,20161108,3223.6,3223.6,3223.6,3223.6,548,0,0.0,2877.6,3516.8,3380.3006,-156.7006 +131500000,20161108,3223.6,3223.6,3223.6,3223.6,548,0,0.0,2877.6,3516.8,3380.0226,-156.4226 +131600000,20161108,3227.2,3229.6,3227.2,3229.6,548,2,1937040.0,2877.6,3516.8,3381.3146,-151.7146 +131700000,20161108,3231.4,3231.4,3226.8,3230.4,550,3,2906580.0,2877.6,3516.8,3382.0446,-151.6446 +131800000,20161108,3230.4,3230.4,3230.4,3230.4,549,1,969120.0,2877.6,3516.8,3382.4426,-152.0426 +131900000,20161108,3226.2,3226.2,3226.2,3226.2,550,1,967860.0,2877.6,3516.8,3382.8386,-156.6386 +132000000,20161108,3226.2,3226.2,3226.2,3226.2,550,0,0.0,2877.6,3516.8,3382.4006,-156.2006 +132100000,20161108,3227.6,3227.6,3227.6,3227.6,551,1,968280.0,2877.6,3516.8,3382.2436,-154.6436 +132200000,20161108,3227.6,3227.6,3227.6,3227.6,551,0,0.0,2877.6,3516.8,3381.4466,-153.8466 +132300000,20161108,3226.6,3226.8,3226.6,3226.8,551,2,1936020.0,2877.6,3516.8,3381.2196,-154.4196 +132400000,20161108,3227.0,3227.0,3227.0,3227.0,551,1,968100.0,2877.6,3516.8,3380.6936,-153.6936 +132500000,20161108,3227.0,3227.0,3227.0,3227.0,551,0,0.0,2877.6,3516.8,3380.4976,-153.4976 +132600000,20161108,3225.2,3225.2,3225.2,3225.2,552,1,967560.0,2877.6,3516.8,3379.3746,-154.1746 +132700000,20161108,3225.2,3225.2,3225.2,3225.2,552,0,0.0,2877.6,3516.8,3378.2846,-153.0846 +132800000,20161108,3225.2,3225.2,3225.2,3225.2,552,0,0.0,2877.6,3516.8,3378.1746,-152.9746 +132900000,20161108,3225.2,3225.2,3225.2,3225.2,552,0,0.0,2877.6,3516.8,3377.7696,-152.5696 +133000000,20161108,3225.2,3225.2,3225.2,3225.2,552,0,0.0,2877.6,3516.8,3377.7176,-152.5176 +133100000,20161108,3225.0,3225.0,3225.0,3225.0,555,5,4837500.0,2877.6,3516.8,3377.3476,-152.3476 +133200000,20161108,3225.0,3225.0,3225.0,3225.0,555,0,0.0,2877.6,3516.8,3377.0746,-152.0746 +133300000,20161108,3224.0,3224.0,3224.0,3224.0,560,5,4836000.0,2877.6,3516.8,3375.9236,-151.9236 +133400000,20161108,3221.2,3221.2,3221.2,3221.2,561,1,966360.0,2877.6,3516.8,3375.6036,-154.4036 +133500000,20161108,3221.2,3221.2,3221.2,3221.2,561,0,0.0,2877.6,3516.8,3374.9526,-153.7526 +133600000,20161108,3221.2,3221.2,3221.2,3221.2,561,0,0.0,2877.6,3516.8,3374.5466,-153.3466 +133700000,20161108,3221.2,3221.2,3221.2,3221.2,561,0,0.0,2877.6,3516.8,3374.8086,-153.6086 +133800000,20161108,3221.2,3221.2,3221.2,3221.2,561,0,0.0,2877.6,3516.8,3375.1126,-153.9126 +133900000,20161108,3221.2,3221.2,3221.2,3221.2,561,0,0.0,2877.6,3516.8,3375.5156,-154.3156 +134000000,20161108,3222.8,3222.8,3222.8,3222.8,562,1,966840.0,2877.6,3516.8,3375.0776,-152.2776 +134100000,20161108,3222.0,3222.0,3222.0,3222.0,563,1,966600.0,2877.6,3516.8,3374.6396,-152.6396 +134200000,20161108,3222.0,3222.0,3222.0,3222.0,564,1,966600.0,2877.6,3516.8,3373.2166,-151.2166 +134300000,20161108,3222.0,3222.0,3222.0,3222.0,566,3,2899800.0,2877.6,3516.8,3372.3566,-150.3566 +134400000,20161108,3222.0,3222.0,3222.0,3222.0,566,0,0.0,2877.6,3516.8,3371.0946,-149.0946 +134500000,20161108,3220.0,3220.0,3220.0,3220.0,567,1,966000.0,2877.6,3516.8,3371.6596,-151.6596 +134600000,20161108,3220.0,3220.0,3220.0,3220.0,567,0,0.0,2877.6,3516.8,3371.7436,-151.7436 +134700000,20161108,3220.0,3220.0,3220.0,3220.0,567,0,0.0,2877.6,3516.8,3371.7956,-151.7956 +134800000,20161108,3220.0,3220.0,3220.0,3220.0,567,0,0.0,2877.6,3516.8,3371.6546,-151.6546 +134900000,20161108,3220.0,3220.0,3220.0,3220.0,568,1,966000.0,2877.6,3516.8,3371.2666,-151.2666 +135000000,20161108,3222.0,3222.0,3221.8,3221.8,569,2,1933140.0,2877.6,3516.8,3372.4176,-150.6176 +135100000,20161108,3221.8,3221.8,3221.8,3221.8,569,0,0.0,2877.6,3516.8,3372.9016,-151.1016 +135200000,20161108,3221.8,3221.8,3221.8,3221.8,569,0,0.0,2877.6,3516.8,3373.1456,-151.3456 +135300000,20161108,3221.8,3221.8,3221.8,3221.8,569,0,0.0,2877.6,3516.8,3372.8346,-151.0346 +135400000,20161108,3221.8,3221.8,3221.8,3221.8,569,0,0.0,2877.6,3516.8,3373.5086,-151.7086 +135500000,20161108,3221.8,3221.8,3221.8,3221.8,569,0,0.0,2877.6,3516.8,3373.4806,-151.6806 +135600000,20161108,3223.0,3223.0,3223.0,3223.0,570,1,966900.0,2877.6,3516.8,3372.7866,-149.7866 +135700000,20161108,3223.0,3223.0,3223.0,3223.0,570,0,0.0,2877.6,3516.8,3372.3676,-149.3676 +135800000,20161108,3223.0,3223.0,3223.0,3223.0,570,0,0.0,2877.6,3516.8,3371.6816,-148.6816 +135900000,20161108,3220.6,3220.6,3220.6,3220.6,571,1,966180.0,2877.6,3516.8,3371.7246,-151.1246 +140000000,20161108,3220.0,3220.0,3220.0,3220.0,571,1,966000.0,2877.6,3516.8,3371.6196,-151.6196 +140100000,20161108,3221.2,3221.2,3220.0,3220.0,573,2,1932360.0,2877.6,3516.8,3371.5536,-151.5536 +140200000,20161108,3220.0,3220.0,3220.0,3220.0,573,0,0.0,2877.6,3516.8,3371.2536,-151.2536 +140300000,20161108,3220.0,3220.0,3220.0,3220.0,573,0,0.0,2877.6,3516.8,3370.8476,-150.8476 +140400000,20161108,3220.0,3220.0,3220.0,3220.0,573,0,0.0,2877.6,3516.8,3370.7436,-150.7436 +140500000,20161108,3220.0,3220.0,3220.0,3220.0,573,0,0.0,2877.6,3516.8,3370.7096,-150.7096 +140600000,20161108,3220.0,3220.0,3220.0,3220.0,573,0,0.0,2877.6,3516.8,3370.6156,-150.6156 +140700000,20161108,3220.0,3220.0,3220.0,3220.0,573,0,0.0,2877.6,3516.8,3370.6166,-150.6166 +140800000,20161108,3217.8,3217.8,3217.8,3217.8,574,1,965340.0,2877.6,3516.8,3371.4586,-153.6586 +140900000,20161108,3217.8,3217.8,3217.8,3217.8,574,0,0.0,2877.6,3516.8,3371.8116,-154.0116 +141000000,20161108,3217.8,3217.8,3217.8,3217.8,574,0,0.0,2877.6,3516.8,3371.9966,-154.1966 +141100000,20161108,3217.8,3217.8,3217.8,3217.8,574,0,0.0,2877.6,3516.8,3372.3326,-154.5326 +141200000,20161108,3217.8,3217.8,3217.8,3217.8,574,0,0.0,2877.6,3516.8,3371.6636,-153.8636 +141300000,20161108,3217.8,3217.8,3217.8,3217.8,574,0,0.0,2877.6,3516.8,3372.6486,-154.8486 +141400000,20161108,3217.0,3217.2,3217.0,3217.2,576,2,1930260.0,2877.6,3516.8,3372.3206,-155.1206 +141500000,20161108,3217.2,3217.2,3217.2,3217.2,577,1,965160.0,2877.6,3516.8,3372.5556,-155.3556 +141600000,20161108,3217.2,3217.2,3217.2,3217.2,577,0,0.0,2877.6,3516.8,3372.5116,-155.3116 +141700000,20161108,3216.2,3216.2,3216.2,3216.2,577,1,964860.0,2877.6,3516.8,3372.2986,-156.0986 +141800000,20161108,3216.2,3216.2,3216.2,3216.2,577,0,0.0,2877.6,3516.8,3372.0416,-155.8416 +141900000,20161108,3216.0,3216.0,3216.0,3216.0,577,1,964800.0,2877.6,3516.8,3371.9466,-155.9466 +142000000,20161108,3216.0,3216.0,3216.0,3216.0,577,0,0.0,2877.6,3516.8,3371.3386,-155.3386 +142100000,20161108,3216.0,3216.0,3216.0,3216.0,577,0,0.0,2877.6,3516.8,3370.5546,-154.5546 +142200000,20161108,3211.6,3211.6,3211.6,3211.6,576,1,963480.0,2877.6,3516.8,3370.2546,-158.6546 +142300000,20161108,3211.6,3211.6,3211.6,3211.6,576,0,0.0,2877.6,3516.8,3369.8696,-158.2696 +142400000,20161108,3211.6,3211.6,3211.6,3211.6,576,0,0.0,2877.6,3516.8,3369.8006,-158.2006 +142500000,20161108,3211.6,3211.6,3211.6,3211.6,576,0,0.0,2877.6,3516.8,3368.6266,-157.0266 +142600000,20161108,3211.2,3211.2,3211.2,3211.2,577,1,963360.0,2877.6,3516.8,3367.9416,-156.7416 +142700000,20161108,3211.2,3211.2,3211.2,3211.2,577,0,0.0,2877.6,3516.8,3367.6396,-156.4396 +142800000,20161108,3211.8,3212.0,3211.8,3212.0,579,2,1927140.0,2877.6,3516.8,3367.9196,-155.9196 +142900000,20161108,3212.0,3212.0,3212.0,3212.0,579,0,0.0,2877.6,3516.8,3367.8386,-155.8386 +143000000,20161108,3213.0,3213.0,3213.0,3213.0,579,1,963900.0,2877.6,3516.8,3368.4656,-155.4656 +143100000,20161108,3214.0,3214.2,3214.0,3214.2,581,2,1928460.0,2877.6,3516.8,3369.7536,-155.5536 +143200000,20161108,3214.8,3214.8,3214.8,3214.8,582,1,964440.0,2877.6,3516.8,3369.8956,-155.0956 +143300000,20161108,3214.8,3214.8,3214.8,3214.8,582,0,0.0,2877.6,3516.8,3370.2326,-155.4326 +143400000,20161108,3214.4,3214.4,3214.4,3214.4,582,1,964320.0,2877.6,3516.8,3370.3386,-155.9386 +143500000,20161108,3214.4,3214.4,3214.4,3214.4,582,0,0.0,2877.6,3516.8,3369.6726,-155.2726 +143600000,20161108,3214.4,3214.4,3214.4,3214.4,582,0,0.0,2877.6,3516.8,3369.7156,-155.3156 +143700000,20161108,3214.4,3214.4,3214.4,3214.4,582,0,0.0,2877.6,3516.8,3369.5976,-155.1976 +143800000,20161108,3214.4,3214.4,3214.4,3214.4,582,0,0.0,2877.6,3516.8,3369.9536,-155.5536 +143900000,20161108,3214.4,3214.4,3214.4,3214.4,582,0,0.0,2877.6,3516.8,3369.9456,-155.5456 +144000000,20161108,3214.4,3214.4,3214.4,3214.4,582,0,0.0,2877.6,3516.8,3369.8996,-155.4996 +144100000,20161108,3214.4,3214.4,3214.4,3214.4,582,0,0.0,2877.6,3516.8,3369.7076,-155.3076 +144200000,20161108,3214.4,3214.4,3214.4,3214.4,582,0,0.0,2877.6,3516.8,3369.5626,-155.1626 +144300000,20161108,3214.4,3214.4,3214.4,3214.4,582,0,0.0,2877.6,3516.8,3368.8096,-154.4096 +144400000,20161108,3211.2,3211.2,3211.2,3211.2,583,1,963360.0,2877.6,3516.8,3368.5416,-157.3416 +144500000,20161108,3211.2,3211.2,3211.2,3211.2,583,0,0.0,2877.6,3516.8,3368.2766,-157.0766 +144600000,20161108,3211.2,3211.2,3211.2,3211.2,583,0,0.0,2877.6,3516.8,3367.8196,-156.6196 +144700000,20161108,3211.2,3211.2,3211.2,3211.2,583,0,0.0,2877.6,3516.8,3367.4246,-156.2246 +144800000,20161108,3210.8,3211.0,3210.8,3211.0,582,2,1926540.0,2877.6,3516.8,3367.5086,-156.5086 +144900000,20161108,3211.0,3211.0,3211.0,3211.0,582,0,0.0,2877.6,3516.8,3367.3326,-156.3326 +145000000,20161108,3211.0,3211.0,3211.0,3211.0,582,0,0.0,2877.6,3516.8,3367.5136,-156.5136 +145100000,20161108,3211.0,3211.0,3211.0,3211.0,582,0,0.0,2877.6,3516.8,3367.1606,-156.1606 +145200000,20161108,3211.0,3211.0,3211.0,3211.0,582,0,0.0,2877.6,3516.8,3367.0826,-156.0826 +145300000,20161108,3211.0,3211.0,3211.0,3211.0,582,0,0.0,2877.6,3516.8,3366.8816,-155.8816 +145400000,20161108,3211.0,3211.0,3211.0,3211.0,582,0,0.0,2877.6,3516.8,3368.1856,-157.1856 +145500000,20161108,3211.0,3211.0,3211.0,3211.0,582,0,0.0,2877.6,3516.8,3368.3956,-157.3956 +145600000,20161108,3211.0,3211.0,3211.0,3211.0,582,0,0.0,2877.6,3516.8,3368.8866,-157.8866 +145700000,20161108,3219.4,3219.4,3219.4,3219.4,583,1,965820.0,2877.6,3516.8,3369.7466,-150.3466 +145800000,20161108,3219.4,3219.4,3219.4,3219.4,583,0,0.0,2877.6,3516.8,3370.4546,-151.0546 +145900000,20161108,3219.4,3219.4,3219.4,3219.4,583,0,0.0,2877.6,3516.8,3370.7196,-151.3196 +150000000,20161108,3219.4,3219.4,3219.4,3219.4,584,1,965820.0,2877.6,3516.8,3371.1176,-151.7176 diff --git a/rqalpha/examples/IF_macd.py b/rqalpha/examples/IF_macd.py index dda55b47f..139fc141d 100644 --- a/rqalpha/examples/IF_macd.py +++ b/rqalpha/examples/IF_macd.py @@ -14,7 +14,7 @@ def init(context): context.SMOOTHPERIOD = 9 context.OBSERVATION = 50 - #初始化时订阅合约行情。订阅之后的合约行情会在handle_bar中进行更新 + # 初始化时订阅合约行情。订阅之后的合约行情会在handle_bar中进行更新 subscribe(context.s1) @@ -43,4 +43,4 @@ def handle_bar(context, bar_dict): if buy_qty > 0: sell_close(context.s1, 1) # 卖出开仓 - sell_open(context.s1, 1) \ No newline at end of file + sell_open(context.s1, 1) diff --git a/rqalpha/mod/risk_manager/__init__.py b/rqalpha/examples/data_source/__init__.py similarity index 88% rename from rqalpha/mod/risk_manager/__init__.py rename to rqalpha/examples/data_source/__init__.py index 2d5e1bc25..2f1988b5a 100644 --- a/rqalpha/mod/risk_manager/__init__.py +++ b/rqalpha/examples/data_source/__init__.py @@ -13,9 +13,3 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - -from .mod import RiskManagerMod - - -def load_mod(): - return RiskManagerMod() diff --git a/rqalpha/examples/data_source/get_csv_module.py b/rqalpha/examples/data_source/get_csv_module.py new file mode 100644 index 000000000..637f83694 --- /dev/null +++ b/rqalpha/examples/data_source/get_csv_module.py @@ -0,0 +1,12 @@ +import os + + +def read_csv_as_df(csv_path): + import pandas as pd + data = pd.read_csv(csv_path) + return data + + +def get_csv(): + csv_path = os.path.join(os.path.dirname(__file__), "../IF1706_20161108.csv") + return read_csv_as_df(csv_path) diff --git a/rqalpha/examples/data_source/import_get_csv_module.py b/rqalpha/examples/data_source/import_get_csv_module.py new file mode 100644 index 000000000..e76f56321 --- /dev/null +++ b/rqalpha/examples/data_source/import_get_csv_module.py @@ -0,0 +1,33 @@ +from rqalpha.api import * +import os +import sys + + +def init(context): + strategy_file_path = context.config.base.strategy_file + sys.path.append(os.path.realpath(os.path.dirname(strategy_file_path))) + + from get_csv_module import get_csv + + IF1706_df = get_csv() + context.IF1706_df = IF1706_df + + +def before_trading(context): + logger.info(context.IF1706_df) + + +__config__ = { + "base": { + "securities": "future", + "start_date": "2015-01-09", + "end_date": "2015-01-10", + "frequency": "1d", + "matching_type": "current_bar", + "future_starting_cash": 1000000, + "benchmark": None, + }, + "extra": { + "log_level": "verbose", + }, +} \ No newline at end of file diff --git a/rqalpha/examples/data_source/read_csv_as_df.py b/rqalpha/examples/data_source/read_csv_as_df.py new file mode 100644 index 000000000..ff24855bb --- /dev/null +++ b/rqalpha/examples/data_source/read_csv_as_df.py @@ -0,0 +1,35 @@ +from rqalpha.api import * + + +def read_csv_as_df(csv_path): + import pandas as pd + data = pd.read_csv(csv_path) + return data + + +def init(context): + import os + strategy_file_path = context.config.base.strategy_file + csv_path = os.path.join(os.path.dirname(strategy_file_path), "../IF1706_20161108.csv") + IF1706_df = read_csv_as_df(csv_path) + context.IF1706_df = IF1706_df + + +def before_trading(context): + logger.info(context.IF1706_df) + + +__config__ = { + "base": { + "securities": "future", + "start_date": "2015-01-09", + "end_date": "2015-01-10", + "frequency": "1d", + "matching_type": "current_bar", + "future_starting_cash": 1000000, + "benchmark": None, + }, + "extra": { + "log_level": "verbose", + }, +} \ No newline at end of file diff --git a/rqalpha/mod/simulation/__init__.py b/rqalpha/examples/extend_api/__init__.py similarity index 88% rename from rqalpha/mod/simulation/__init__.py rename to rqalpha/examples/extend_api/__init__.py index f79f43f8a..f7b4f888c 100644 --- a/rqalpha/mod/simulation/__init__.py +++ b/rqalpha/examples/extend_api/__init__.py @@ -14,8 +14,3 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .mod import SimulationMod - - -def load_mod(): - return SimulationMod() diff --git a/rqalpha/examples/extend_api/rqalpha_mod_extend_api_demo.py b/rqalpha/examples/extend_api/rqalpha_mod_extend_api_demo.py new file mode 100644 index 000000000..e2dd87072 --- /dev/null +++ b/rqalpha/examples/extend_api/rqalpha_mod_extend_api_demo.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2017 Ricequant, Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import pandas as pd +from rqalpha.interface import AbstractMod + + +__config__ = { + "csv_path": None +} + + +def load_mod(): + return ExtendAPIDemoMod() + + +class ExtendAPIDemoMod(AbstractMod): + def __init__(self): + # 注入API 一定要在初始化阶段,否则无法成功注入 + self._csv_path = None + self._inject_api() + + def start_up(self, env, mod_config): + self._csv_path = os.path.abspath(os.path.join(os.path.dirname(__file__), mod_config.csv_path)) + + def tear_down(self, code, exception=None): + pass + + def _inject_api(self): + from rqalpha import export_as_api + from rqalpha.execution_context import ExecutionContext + from rqalpha.const import EXECUTION_PHASE + + @export_as_api + @ExecutionContext.enforce_phase(EXECUTION_PHASE.ON_INIT, + EXECUTION_PHASE.BEFORE_TRADING, + EXECUTION_PHASE.ON_BAR, + EXECUTION_PHASE.AFTER_TRADING, + EXECUTION_PHASE.SCHEDULED) + def get_csv_as_df(): + data = pd.read_csv(self._csv_path) + return data diff --git a/rqalpha/examples/extend_api/test_extend_api.py b/rqalpha/examples/extend_api/test_extend_api.py new file mode 100644 index 000000000..b3cbcc86f --- /dev/null +++ b/rqalpha/examples/extend_api/test_extend_api.py @@ -0,0 +1,35 @@ +from rqalpha.api import * + + +def init(context): + IF1706_df = get_csv_as_df() + context.IF1706_df = IF1706_df + + +def before_trading(context): + logger.info(context.IF1706_df) + + +__config__ = { + "base": { + "securities": "future", + "start_date": "2015-01-09", + "end_date": "2015-01-10", + "frequency": "1d", + "matching_type": "current_bar", + "future_starting_cash": 1000000, + "benchmark": None, + }, + "extra": { + "log_level": "verbose", + }, + "mod": { + "extend_api_demo": { + "enabled": True, + "lib": "rqalpha.examples.extend_api.rqalpha_mod_extend_api_demo", + "csv_path": "../IF1706_20161108.csv" + } + } +} + + diff --git a/rqalpha/examples/golden_cross.py b/rqalpha/examples/golden_cross.py index b7ef8375a..37ad04e38 100644 --- a/rqalpha/examples/golden_cross.py +++ b/rqalpha/examples/golden_cross.py @@ -36,7 +36,7 @@ def handle_bar(context, bar_dict): # 计算现在portfolio中股票的仓位 cur_position = context.portfolio.positions[context.s1].quantity # 计算现在portfolio中的现金可以购买多少股票 - shares = context.portfolio.cash/bar_dict[context.s1].close + shares = context.portfolio.cash / bar_dict[context.s1].close # 如果短均线从上往下跌破长均线,也就是在目前的bar短线平均值低于长线平均值,而上一个bar的短线平均值高于长线平均值 if short_avg[-1] - long_avg[-1] < 0 and short_avg[-2] - long_avg[-2] > 0 and cur_position > 0: diff --git a/rqalpha/examples/macd.py b/rqalpha/examples/macd.py index 24a342ebb..9a0e23f65 100644 --- a/rqalpha/examples/macd.py +++ b/rqalpha/examples/macd.py @@ -26,7 +26,7 @@ def handle_bar(context, bar_dict): # TODO: 开始编写你的算法吧! # 读取历史数据,使用sma方式计算均线准确度和数据长度无关,但是在使用ema方式计算均线时建议将历史数据窗口适当放大,结果会更加准确 - prices = history_bars(context.s1, context.OBSERVATION,'1d','close') + prices = history_bars(context.s1, context.OBSERVATION, '1d', 'close') # 用Talib计算MACD取值,得到三个时间序列数组,分别为macd, signal 和 hist macd, signal, hist = talib.MACD(prices, context.SHORTPERIOD, @@ -42,11 +42,11 @@ def handle_bar(context, bar_dict): if macd[-1] - signal[-1] < 0 and macd[-2] - signal[-2] > 0: # 计算现在portfolio中股票的仓位 curPosition = context.portfolio.positions[context.s1].quantity - #进行清仓 + # 进行清仓 if curPosition > 0: order_target_value(context.s1, 0) # 如果短均线从下往上突破长均线,为入场信号 if macd[-1] - signal[-1] > 0 and macd[-2] - signal[-2] < 0: # 满仓入股 - order_target_percent(context.s1, 1) \ No newline at end of file + order_target_percent(context.s1, 1) diff --git a/rqalpha/examples/technical_analysis/dma.py b/rqalpha/examples/technical_analysis/dma.py index a7e042211..b9352517f 100644 --- a/rqalpha/examples/technical_analysis/dma.py +++ b/rqalpha/examples/technical_analysis/dma.py @@ -1,13 +1,13 @@ -# rqalpha run -f rqalpha/examples/technical_analysis/dma.py -sc 100000 -p -bm 000300.XSHG -mc funcat_api.enabled True +# Run `rqalpha mod enable sys_funcat` first. from rqalpha.api import * def init(context): context.s1 = "600275.XSHG" - S(context.s1) def handle_bar(context, bar_dict): + S(context.s1) # 自己实现 DMA指标(Different of Moving Average) M1 = 5 M2 = 89 @@ -25,5 +25,5 @@ def handle_bar(context, bar_dict): order_target_percent(context.s1, 0) if (HHV(MAX(O, C), 50) / LLV(MIN(O, C), 50) < 2 - and cross(DDD, AMA) and cur_position == 0): + and CROSS(DDD, AMA) and cur_position == 0): order_target_percent(context.s1, 1) diff --git a/rqalpha/examples/technical_analysis/golden_cross.py b/rqalpha/examples/technical_analysis/golden_cross.py index 7f46addd4..08f85534a 100644 --- a/rqalpha/examples/technical_analysis/golden_cross.py +++ b/rqalpha/examples/technical_analysis/golden_cross.py @@ -1,4 +1,4 @@ -# rqalpha run -f rqalpha/examples/technical_analysis/golden_cross.py -sc 100000 -p -bm 000001.XSHE -mc funcat_api.enabled True +# Run `rqalpha mod enable sys_funcat` first. from rqalpha.api import * @@ -26,11 +26,11 @@ def handle_bar(context, bar_dict): cur_position = context.portfolio.positions[context.s1].quantity # 如果短均线从上往下跌破长均线,也就是在目前的bar短线平均值低于长线平均值,而上一个bar的短线平均值高于长线平均值 - if cross(long_ma, short_ma) and cur_position > 0: + if CROSS(long_ma, short_ma) and cur_position > 0: # 进行清仓 order_target_percent(context.s1, 0) # 如果短均线从下往上突破长均线,为入场信号 - if cross(short_ma, long_ma): + if CROSS(short_ma, long_ma): # 满仓入股 order_target_percent(context.s1, 1) diff --git a/rqalpha/examples/turtle.py b/rqalpha/examples/turtle.py index 334430868..adcf46a8f 100644 --- a/rqalpha/examples/turtle.py +++ b/rqalpha/examples/turtle.py @@ -13,15 +13,14 @@ def get_extreme(array_high_price_result, array_low_price_result): return [max_result, min_result] -def get_atr_and_unit( atr_array_result, atr_length_result, portfolio_value_result): - atr = atr_array_result[ atr_length_result-1] +def get_atr_and_unit(atr_array_result, atr_length_result, portfolio_value_result): + atr = atr_array_result[atr_length_result-1] unit = math.floor(portfolio_value_result * .01 / atr) return [atr, unit] def get_stop_price(first_open_price_result, units_hold_result, atr_result): - stop_price = first_open_price_result - 2 * atr_result \ - + (units_hold_result - 1) * 0.5 * atr_result + stop_price = first_open_price_result - 2 * atr_result + (units_hold_result - 1) * 0.5 * atr_result return stop_price @@ -44,10 +43,10 @@ def init(context): def handle_bar(context, bar_dict): portfolio_value = context.portfolio.portfolio_value - high_price = history_bars(context.s, context.open_observe_time+1, '1d', 'high') - low_price_for_atr = history_bars(context.s, context.open_observe_time+1, '1d', 'low') - low_price_for_extreme = history_bars(context.s, context.close_observe_time+1, '1d', 'low') - close_price = history_bars(context.s, context.open_observe_time+2, '1d', 'close') + high_price = history_bars(context.s, context.open_observe_time + 1, '1d', 'high') + low_price_for_atr = history_bars(context.s, context.open_observe_time + 1, '1d', 'low') + low_price_for_extreme = history_bars(context.s, context.close_observe_time + 1, '1d', 'low') + close_price = history_bars(context.s, context.open_observe_time + 2, '1d', 'close') close_price_for_atr = close_price[:-1] atr_array = talib.ATR(high_price, low_price_for_atr, close_price_for_atr, timeperiod=context.atr_time) @@ -113,4 +112,4 @@ def handle_bar(context, bar_dict): order_shares(context.s, -cur_position) context.units_hold = 0 - context.pre_trading_signal = context.trading_signal \ No newline at end of file + context.pre_trading_signal = context.trading_signal diff --git a/rqalpha/execution_context.py b/rqalpha/execution_context.py index 7c4a03b40..fb9979992 100644 --- a/rqalpha/execution_context.py +++ b/rqalpha/execution_context.py @@ -19,8 +19,7 @@ from .utils.i18n import gettext as _ from .utils.exception import CustomException, patch_user_exc -from .utils import get_upper_underlying_symbol -from .utils.default_future_info import DEFAULT_FUTURE_INFO +from .environment import Environment class ContextStack(object): @@ -54,18 +53,9 @@ def top(self): class ExecutionContext(object): stack = ContextStack() - config = None - data_proxy = None - account = None - accounts = None - broker = None - calendar_dt = None - trading_dt = None - plots = None - - def __init__(self, phase, bar_dict=None): + + def __init__(self, phase): self.phase = phase - self.bar_dict = bar_dict def _push(self): self.stack.push(self) @@ -100,74 +90,23 @@ def __exit__(self, exc_type, exc_val, exc_tb): raise last_exc_val from .utils import create_custom_exception - user_exc = create_custom_exception(exc_type, exc_val, exc_tb, self.config.base.strategy_file) + strategy_file = Environment.get_instance().config.base.strategy_file + user_exc = create_custom_exception(exc_type, exc_val, exc_tb, strategy_file) raise user_exc - @classmethod - def get_active(cls): - return cls.stack.top - - @classmethod - def get_current_bar_dict(cls): - ctx = cls.get_active() - return ctx.bar_dict - - @classmethod - def get_current_calendar_dt(cls): - return ExecutionContext.calendar_dt - - @classmethod - def get_current_trading_dt(cls): - return ExecutionContext.trading_dt - - @classmethod - def get_current_run_id(cls): - return ExecutionContext.config.base.run_id - - @classmethod - def get_instrument(cls, order_book_id): - return ExecutionContext.data_proxy.instruments(order_book_id) - - @classmethod - def get_data_proxy(cls): - return ExecutionContext.data_proxy - @classmethod def enforce_phase(cls, *phases): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): - phase = cls.get_active().phase + phase = cls.stack.top.phase if phase not in phases: raise patch_user_exc( - RuntimeError(_("You cannot call %s when executing %s") % (func.__name__, phase.value))) + RuntimeError(_(u"You cannot call %s when executing %s") % (func.__name__, phase.value))) return func(*args, **kwargs) return wrapper return decorator @classmethod - def get_current_close_price(cls, order_book_id): - return ExecutionContext.data_proxy.current_snapshot( - order_book_id, - ExecutionContext.config.base.frequency, - ExecutionContext.calendar_dt - ).last - - @classmethod - def get_future_commission_info(cls, order_book_id, hedge_type): - try: - return ExecutionContext.data_proxy.get_future_info(order_book_id, hedge_type) - except NotImplementedError: - underlying_symbol = get_upper_underlying_symbol(order_book_id) - return DEFAULT_FUTURE_INFO[underlying_symbol][hedge_type.value] - - @classmethod - def get_future_margin(cls, order_book_id): - try: - return ExecutionContext.data_proxy.get_future_info(order_book_id)['long_margin_ratio'] - except NotImplementedError: - return ExecutionContext.data_proxy.instruments(order_book_id).margin_rate - - @classmethod - def get_future_info(cls, order_book_id, hedge_type): - return ExecutionContext.data_proxy.get_future_info(order_book_id, hedge_type) + def phase(cls): + return cls.stack.top.phase diff --git a/rqalpha/interface.py b/rqalpha/interface.py index e133b9130..f3e93fc25 100644 --- a/rqalpha/interface.py +++ b/rqalpha/interface.py @@ -16,7 +16,6 @@ import abc - from six import with_metaclass @@ -29,7 +28,7 @@ class AbstractStrategyLoader(with_metaclass(abc.ABCMeta)): @abc.abstractmethod def load(self, strategy, scope): """ - 【Required】 + [Required] load 函数负责组装策略代码和策略代码所在的域,并输出最终组装好的可执行域。 @@ -51,7 +50,7 @@ class AbstractEventSource(with_metaclass(abc.ABCMeta)): @abc.abstractmethod def events(self, start_date, end_date, frequency): """ - 【Required】 + [Required] 扩展 EventSource 必须实现 events 函数。 @@ -74,6 +73,34 @@ def events(self, start_date, end_date, frequency): raise NotImplementedError +class AbstractPriceBoard(with_metaclass(abc.ABCMeta)): + """ + RQAlpha多个地方需要使用最新「行情」,不同的数据源其最新价格获取的方式不尽相同 + + 因此抽离出 `AbstractPriceBoard`, 您可以自行进行扩展并替换默认 PriceBoard + """ + @abc.abstractmethod + def get_last_price(self, order_book_id): + """ + 获取证券的最新价格 + """ + raise NotImplementedError + + @abc.abstractmethod + def get_limit_up(self, order_book_id): + raise NotImplementedError + + @abc.abstractmethod + def get_limit_down(self, order_book_id): + raise NotImplementedError + + def get_a1(self, order_book_id): + raise NotImplementedError + + def get_b1(self, order_book_id): + raise NotImplementedError + + class AbstractDataSource(object): """ 数据源接口。RQAlpha 中通过 :class:`DataProxy` 进一步进行了封装,向上层提供更易用的接口。 @@ -157,7 +184,7 @@ def get_settle_price(self, instrument, date): """ raise NotImplementedError - def history_bars(self, instrument, bar_count, frequency, fields, dt, skip_suspended=True): + def history_bars(self, instrument, bar_count, frequency, fields, dt, skip_suspended=True, include_now=False): """ 获取历史数据 @@ -188,6 +215,7 @@ def history_bars(self, instrument, bar_count, frequency, fields, dt, skip_suspen :param datetime.datetime dt: 时间 :param bool skip_suspended: 是否跳过停牌日 + :param bool include_now: 是否包含当天最新数据 :return: `numpy.ndarray` @@ -236,16 +264,28 @@ def available_data_range(self, frequency): """ raise NotImplementedError - def get_future_info(self, order_book_id, hedge_type): + def get_future_info(self, instrument, hedge_type): """ 获取期货合约手续费、保证金等数据 - :param str order_book_id: 合约名 + :param instrument: 合约对象 :param HEDGE_TYPE hedge_type: 枚举类型,账户对冲类型 :return: dict """ raise NotImplementedError + def get_merge_ticks(self, order_book_id_list, trading_date, last_dt=None): + """ + 获取合并的 ticks + + :param list order_book_id_list: 合约名列表 + :param datetime.date trading_date: 交易日 + :param datetime.datetime last_dt: 仅返回 last_dt 之后的时间 + + :return: Tick + """ + raise NotImplementedError + class AbstractBroker(with_metaclass(abc.ABCMeta)): """ @@ -256,24 +296,21 @@ class AbstractBroker(with_metaclass(abc.ABCMeta)): 在扩展模块中,可以通过调用 ``env.set_broker`` 来替换默认的 Broker。 """ + @abc.abstractmethod - def get_accounts(self): + def get_portfolio(self): """ [Required] - 获取账号信息。系统初始化时,RQAlpha 会调用此接口,获取账户信息。 + 获取投资组合。系统初始化时,会调用此接口,获取包含账户信息、净值、份额等内容的投资组合 - RQAlpha 支持混合策略,因此返回的账户信息为一个字典(dict),key 为账户类型(``rqalpha.const.ACCOUNT_TYPE``), - value 为对应的 :class:`~Account` 对象。 - - :return: dict + :return: Portfolio """ - raise NotImplementedError @abc.abstractmethod def submit_order(self, order): """ - 【Required】 + [Required] 提交订单。在当前版本,RQAlpha 会生成 :class:`~Order` 对象,再通过此接口提交到 Broker。 TBD: 由 Broker 对象生成 Order 并返回? @@ -283,7 +320,7 @@ def submit_order(self, order): @abc.abstractmethod def cancel_order(self, order): """ - 【Required】 + [Required] 撤单。 @@ -293,9 +330,9 @@ def cancel_order(self, order): raise NotImplementedError @abc.abstractmethod - def get_open_orders(self): + def get_open_orders(self, order_book_id=None): """ - 【Required】 + [Required] 获得当前未完成的订单。 @@ -303,36 +340,6 @@ def get_open_orders(self): """ raise NotImplementedError - def before_trading(self): - """ - 【Optional】 - - RQAlpha 会在 `EVENT.BEFORE_TRADING` 事件被触发前调用此接口。 - """ - pass - - def after_trading(self): - """ - 【Optional】 - - RQAlpha 会在 `EVENT.AFTER_TRADING` 事件被触发前调用此接口。 - """ - pass - - def bar(self, bar_dict): - """ - 【Optional】 - - RQAlpha 会在 `EVENT.BAR` 事件被出发前调用此接口。 - - Broker 根据自己的业务场景来选择是否实现,比如说自带撮合引擎的Broker,会通过 `update` 函数来触发撮合。 - - :param calendar_dt: 实际时间 - :param trading_dt: 交易时间 - :param bar_dict: dict[:class:`~BarObject`] - """ - pass - class AbstractMod(with_metaclass(abc.ABCMeta)): """ @@ -409,3 +416,15 @@ def __subclasshook__(cls, C): any("set_state" in B.__dict__ for B in C.__mro__)): return True return NotImplemented + + +class AbstractFrontendValidator(with_metaclass(abc.ABCMeta)): + @abc.abstractmethod + def can_submit_order(self, account, order): + # FIXME need a better name + raise NotImplementedError + + @abc.abstractmethod + def can_cancel_order(self, account, order): + # FIXME need a better name + raise NotImplementedError diff --git a/rqalpha/main.py b/rqalpha/main.py index 35e44d95b..d06033472 100644 --- a/rqalpha/main.py +++ b/rqalpha/main.py @@ -26,6 +26,7 @@ import pytz import requests import six +import better_exceptions from . import const from .api import helper as api_helper @@ -37,12 +38,11 @@ from .data.base_data_source import BaseDataSource from .data.data_proxy import DataProxy from .environment import Environment -from .events import EVENT +from .events import EVENT, Event from .execution_context import ExecutionContext from .interface import Persistable -from .mod.mod_handler import ModHandler +from .mod import ModHandler from .model.bar import BarMap -from .model.account import MixedAccount from .utils import create_custom_exception, run_with_user_log_disabled, scheduler as mod_scheduler from .utils.exception import CustomException, is_user_exc, patch_user_exc from .utils.i18n import gettext as _ @@ -64,7 +64,7 @@ def _adjust_start_date(config, data_proxy): config.base.end_date = min(end, config.base.end_date) config.base.trading_calendar = data_proxy.get_trading_dates(config.base.start_date, config.base.end_date) if len(config.base.trading_calendar) == 0: - raise patch_user_exc(ValueError(_('There is no trading day between {start_date} and {end_date}.').format( + raise patch_user_exc(ValueError(_(u"There is no trading day between {start_date} and {end_date}.").format( start_date=origin_start_date, end_date=origin_end_date))) config.base.start_date = config.base.trading_calendar[0].date() config.base.end_date = config.base.trading_calendar[-1].date() @@ -77,7 +77,7 @@ def _validate_benchmark(config, data_proxy): return instrument = data_proxy.instruments(benchmark) if instrument is None: - raise patch_user_exc(ValueError(_('invalid benchmark {}').format(benchmark))) + raise patch_user_exc(ValueError(_(u"invalid benchmark {}").format(benchmark))) if instrument.order_book_id == "000300.XSHG": # 000300.XSHG 数据进行了补齐,因此认为只要benchmark设置了000300.XSHG,就存在数据,不受限于上市日期。 @@ -87,12 +87,35 @@ def _validate_benchmark(config, data_proxy): end_date = config.base.end_date if instrument.listed_date.date() > start_date: raise patch_user_exc(ValueError( - _("benchmark {benchmark} has not been listed on {start_date}").format(benchmark=benchmark, - start_date=start_date))) + _(u"benchmark {benchmark} has not been listed on {start_date}").format(benchmark=benchmark, + start_date=start_date))) if instrument.de_listed_date.date() < end_date: raise patch_user_exc(ValueError( - _("benchmark {benchmark} has been de_listed on {end_date}").format(benchmark=benchmark, - end_date=end_date))) + _(u"benchmark {benchmark} has been de_listed on {end_date}").format(benchmark=benchmark, + end_date=end_date))) + + +def create_benchmark_portfolio(env): + if env.config.base.benchmark is None: + return None + + from .const import ACCOUNT_TYPE + from .model.account import BenchmarkAccount + from .model.position import Positions, StockPosition + from .model.portfolio import Portfolio + accounts = {} + config = env.config + start_date = config.base.start_date + total_cash = 0 + for account_type in config.base.account_list: + if account_type == ACCOUNT_TYPE.STOCK: + total_cash += config.base.stock_starting_cash + elif account_type == ACCOUNT_TYPE.FUTURE: + total_cash += config.base.future_starting_cash + else: + raise NotImplementedError + accounts[ACCOUNT_TYPE.BENCHMARK] = BenchmarkAccount(total_cash, Positions(StockPosition)) + return Portfolio(start_date, 1, total_cash, accounts) def create_base_scope(): @@ -117,7 +140,7 @@ def update_bundle(data_bundle_path=None, locale="zh_Hans_CN", confirm=True): data_bundle_path = os.path.abspath(os.path.join(data_bundle_path, './bundle/')) if (confirm and os.path.exists(data_bundle_path) and data_bundle_path != default_bundle_path and os.listdir(data_bundle_path)): - click.confirm(_(""" + click.confirm(_(u""" [WARNING] Target bundle path {data_bundle_path} is not empty. The content of this folder will be REMOVED before updating. @@ -129,7 +152,7 @@ def update_bundle(data_bundle_path=None, locale="zh_Hans_CN", confirm=True): while True: url = 'http://7xjci3.com1.z0.glb.clouddn.com/bundles_v2/rqbundle_%04d%02d%02d.tar.bz2' % ( day.year, day.month, day.day) - six.print_(_('try {} ...').format(url)) + six.print_(_(u"try {} ...").format(url)) r = requests.get(url, stream=True) if r.status_code != 200: day = day - datetime.timedelta(days=1) @@ -138,7 +161,7 @@ def update_bundle(data_bundle_path=None, locale="zh_Hans_CN", confirm=True): out = open(tmp, 'wb') total_length = int(r.headers.get('content-length')) - with click.progressbar(length=total_length, label=_('downloading ...')) as bar: + with click.progressbar(length=total_length, label=_(u"downloading ...")) as bar: for data in r.iter_content(chunk_size=8192): bar.update(len(data)) out.write(data) @@ -152,7 +175,7 @@ def update_bundle(data_bundle_path=None, locale="zh_Hans_CN", confirm=True): tar.extractall(data_bundle_path) tar.close() os.remove(tmp) - six.print_(_("Data bundle download successfully in {bundle_path}").format(bundle_path=data_bundle_path)) + six.print_(_(u"Data bundle download successfully in {bundle_path}").format(bundle_path=data_bundle_path)) def run(config, source_code=None): @@ -171,7 +194,6 @@ def run(config, source_code=None): env.set_data_source(BaseDataSource(config.base.data_bundle_path)) env.set_data_proxy(DataProxy(env.data_source)) - ExecutionContext.data_proxy = env.data_proxy Scheduler.set_trading_dates_(env.data_source.get_trading_calendar()) scheduler = Scheduler(config.base.frequency) mod_scheduler._scheduler = scheduler @@ -184,27 +206,28 @@ def run(config, source_code=None): broker = env.broker assert broker is not None - env.accounts = accounts = broker.get_accounts() - env.account = account = MixedAccount(accounts) - - ExecutionContext.broker = broker - ExecutionContext.accounts = accounts - ExecutionContext.account = account - ExecutionContext.config = env.config + env.portfolio = broker.get_portfolio() + env.benchmark_portfolio = create_benchmark_portfolio(env) event_source = env.event_source assert event_source is not None bar_dict = BarMap(env.data_proxy, config.base.frequency) - ctx = ExecutionContext(const.EXECUTION_PHASE.GLOBAL, bar_dict) + env.set_bar_dict(bar_dict) + + if env.price_board is None: + from .core.bar_dict_price_board import BarDictPriceBoard + env.price_board = BarDictPriceBoard(bar_dict) + + ctx = ExecutionContext(const.EXECUTION_PHASE.GLOBAL) ctx._push() # FIXME start_dt = datetime.datetime.combine(config.base.start_date, datetime.datetime.min.time()) - env.calendar_dt = ExecutionContext.calendar_dt = start_dt - env.trading_dt = ExecutionContext.trading_dt = start_dt + env.calendar_dt = start_dt + env.trading_dt = start_dt - env.event_bus.publish_event(EVENT.POST_SYSTEM_INIT) + env.event_bus.publish_event(Event(EVENT.POST_SYSTEM_INIT)) scope = create_base_scope() scope.update({ @@ -240,16 +263,18 @@ def run(config, source_code=None): persist_helper.register('universe', env._universe) if isinstance(event_source, Persistable): persist_helper.register('event_source', event_source) - for k, v in six.iteritems(accounts): - persist_helper.register('{}_account'.format(k.name.lower()), v) + persist_helper.register('portfolio', env.portfolio) + if env.benchmark_portfolio: + persist_helper.register('benchmark_portfolio', env.benchmark_portfolio) for name, module in six.iteritems(env.mod_dict): if isinstance(module, Persistable): persist_helper.register('mod_{}'.format(name), module) # broker will restore open orders from account - persist_helper.register('broker', broker) + if isinstance(broker, Persistable): + persist_helper.register('broker', broker) persist_helper.restore() - env.event_bus.publish_event(EVENT.POST_SYSTEM_RESTORED) + env.event_bus.publish_event(Event(EVENT.POST_SYSTEM_RESTORED)) init_succeed = True @@ -260,56 +285,24 @@ def run(config, source_code=None): with run_with_user_log_disabled(disabled=False): user_strategy.init() - for event in event_source.events(config.base.start_date, config.base.end_date, config.base.frequency): - calendar_dt = event.calendar_dt - trading_dt = event.trading_dt - event_type = event.event_type - ExecutionContext.calendar_dt = calendar_dt - ExecutionContext.trading_dt = trading_dt - env.calendar_dt = calendar_dt - env.trading_dt = trading_dt - for account in accounts.values(): - account.portfolio._current_date = trading_dt.date() - - if event_type == EVENT.BEFORE_TRADING: - env.event_bus.publish_event(EVENT.PRE_BEFORE_TRADING) - env.event_bus.publish_event(EVENT.BEFORE_TRADING) - env.event_bus.publish_event(EVENT.POST_BEFORE_TRADING) - elif event_type == EVENT.BAR: - bar_dict.update_dt(calendar_dt) - env.event_bus.publish_event(EVENT.PRE_BAR) - env.event_bus.publish_event(EVENT.BAR, bar_dict) - env.event_bus.publish_event(EVENT.POST_BAR) - elif event_type == EVENT.TICK: - env.event_bus.publish_event(EVENT.PRE_TICK) - env.event_bus.publish_event(EVENT.TICK, event.data['tick']) - env.event_bus.publish_event(EVENT.POST_TICK) - elif event_type == EVENT.AFTER_TRADING: - env.event_bus.publish_event(EVENT.PRE_AFTER_TRADING) - env.event_bus.publish_event(EVENT.AFTER_TRADING) - env.event_bus.publish_event(EVENT.POST_AFTER_TRADING) - elif event_type == EVENT.SETTLEMENT: - env.event_bus.publish_event(EVENT.PRE_SETTLEMENT) - env.event_bus.publish_event(EVENT.SETTLEMENT) - env.event_bus.publish_event(EVENT.POST_SETTLEMENT) - else: - raise RuntimeError(_('unknown event from event source: {}').format(event)) + from .core.executor import Executor + Executor(env).run(bar_dict) if env.profile_deco: output_profile_result(env) - mod_handler.tear_down(const.EXIT_CODE.EXIT_SUCCESS) - system_log.debug(_("strategy run successfully, normal exit")) - - # FIXME - if 'analyser' in env.mod_dict: - return env.mod_dict['analyser']._result + result = mod_handler.tear_down(const.EXIT_CODE.EXIT_SUCCESS) + system_log.debug(_(u"strategy run successfully, normal exit")) + return result except CustomException as e: if init_succeed and env.config.base.persist and persist_helper: persist_helper.persist() - user_detail_log.exception(_("strategy execute exception")) + user_detail_log.exception(_(u"strategy execute exception")) user_system_log.error(e.error) + + better_exceptions.excepthook(e.error.exc_type, e.error.exc_val, e.error.exc_tb) + mod_handler.tear_down(const.EXIT_CODE.EXIT_USER_ERROR, e) except Exception as e: if init_succeed and env.config.base.persist and persist_helper: @@ -318,13 +311,14 @@ def run(config, source_code=None): exc_type, exc_val, exc_tb = sys.exc_info() user_exc = create_custom_exception(exc_type, exc_val, exc_tb, config.base.strategy_file) + better_exceptions.excepthook(exc_type, exc_val, exc_tb) user_system_log.error(user_exc.error) code = const.EXIT_CODE.EXIT_USER_ERROR if not is_user_exc(exc_val): - system_log.exception(_("strategy execute exception")) + system_log.exception(_(u"strategy execute exception")) code = const.EXIT_CODE.EXIT_INTERNAL_ERROR else: - user_detail_log.exception(_("strategy execute exception")) + user_detail_log.exception(_(u"strategy execute exception")) mod_handler.tear_down(code, user_exc) @@ -353,4 +347,4 @@ def output_profile_result(env): profile_output = stdout_trap.getvalue() profile_output = profile_output.rstrip() print(profile_output) - env.event_bus.publish_event(EVENT.ON_LINE_PROFILER_RESULT, profile_output) + env.event_bus.publish_event(Event(EVENT.ON_LINE_PROFILER_RESULT, result=profile_output)) diff --git a/rqalpha/mod/__init__.py b/rqalpha/mod/__init__.py index 2f1988b5a..fac0dc9bc 100644 --- a/rqalpha/mod/__init__.py +++ b/rqalpha/mod/__init__.py @@ -13,3 +13,74 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + +import copy +from collections import OrderedDict + +from rqalpha.utils.package_helper import import_mod +from rqalpha.utils.logger import system_log +from rqalpha.utils.i18n import gettext as _ +from rqalpha.utils import RqAttrDict + + +class ModHandler(object): + def __init__(self): + self._env = None + self._mod_list = list() + self._mod_dict = OrderedDict() + + def set_env(self, environment): + self._env = environment + + config = environment.config + + for mod_name in config.mod.__dict__: + mod_config = getattr(config.mod, mod_name) + if not mod_config.enabled: + continue + self._mod_list.append((mod_name, mod_config)) + + for idx, (mod_name, user_mod_config) in enumerate(self._mod_list): + if hasattr(user_mod_config, 'lib'): + lib_name = user_mod_config.lib + elif mod_name in SYSTEM_MOD_LIST: + lib_name = "rqalpha.mod.rqalpha_mod_" + mod_name + else: + lib_name = "rqalpha_mod_" + mod_name + system_log.debug(_(u"loading mod {}").format(lib_name)) + mod_module = import_mod(lib_name) + if mod_module is None: + del self._mod_list[idx] + return + mod = mod_module.load_mod() + + mod_config = RqAttrDict(copy.deepcopy(getattr(mod_module, "__config__", {}))) + mod_config.update(user_mod_config) + setattr(config.mod, mod_name, mod_config) + self._mod_list[idx] = (mod_name, mod_config) + self._mod_dict[mod_name] = mod + + self._mod_list.sort(key=lambda item: getattr(item[1], "priority", 100)) + environment.mod_dict = self._mod_dict + + def start_up(self): + for mod_name, mod_config in self._mod_list: + self._mod_dict[mod_name].start_up(self._env, mod_config) + + def tear_down(self, *args): + result = {} + for mod_name, __ in reversed(self._mod_list): + ret = self._mod_dict[mod_name].tear_down(*args) + if ret is not None: + result[mod_name] = ret + return result + + +SYSTEM_MOD_LIST = [ + "sys_analyser", + "sys_progress", + "sys_funcat", + "sys_risk", + "sys_simulation", + "sys_stock_realtime", +] diff --git a/rqalpha/mod/analyser/mod.py b/rqalpha/mod/analyser/mod.py deleted file mode 100644 index 794dde43c..000000000 --- a/rqalpha/mod/analyser/mod.py +++ /dev/null @@ -1,243 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2017 Ricequant, Inc -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -import pickle -from collections import defaultdict - -import six -from enum import Enum -import numpy as np -import pandas as pd - -from rqalpha.interface import AbstractMod -from rqalpha.events import EVENT -from rqalpha.const import ACCOUNT_TYPE, EXIT_CODE -from rqalpha.utils.risk import Risk -from rqalpha.utils.repr import properties -from rqalpha.execution_context import ExecutionContext - - -class AnalyserMod(AbstractMod): - def __init__(self): - self._env = None - self._mod_config = None - self._enabled = False - self._result = None - - self._orders = defaultdict(list) - self._trades = [] - self._total_portfolios = [] - self._sub_portfolios = defaultdict(list) - self._positions = defaultdict(list) - - self._benchmark_daily_returns = [] - self._portfolio_daily_returns = [] - self._latest_portfolio = None - self._latest_benchmark_portfolio = None - - def start_up(self, env, mod_config): - self._env = env - self._mod_config = mod_config - self._enabled = (self._mod_config.record or self._mod_config.plot or self._mod_config.output_file or - self._mod_config.plot_save_file or self._mod_config.report_save_path) - - if self._enabled: - env.event_bus.add_listener(EVENT.POST_SETTLEMENT, self._collect_daily) - env.event_bus.add_listener(EVENT.TRADE, self._collect_trade) - env.event_bus.add_listener(EVENT.ORDER_CREATION_PASS, self._collect_order) - - def _collect_trade(self, account, trade): - self._trades.append(self._to_trade_record(trade)) - - def _collect_order(self, account, order): - self._orders[order.trading_datetime.date()].append(order) - - def _collect_daily(self): - date = self._env.calendar_dt.date() - portfolio = self._env.account.get_portfolio(date) - - self._latest_portfolio = portfolio - self._portfolio_daily_returns.append(portfolio.daily_returns) - self._total_portfolios.append(self._to_portfolio_record(date, portfolio)) - - if ACCOUNT_TYPE.BENCHMARK in self._env.accounts: - self._latest_benchmark_portfolio = self._env.accounts[ACCOUNT_TYPE.BENCHMARK].portfolio - self._benchmark_daily_returns.append(self._latest_benchmark_portfolio.daily_returns) - else: - self._benchmark_daily_returns.append(0) - - for account_type, account in six.iteritems(self._env.accounts): - portfolio = account.get_portfolio(date) - self._sub_portfolios[account_type].append(self._to_portfolio_record2(date, portfolio)) - for order_book_id, position in six.iteritems(portfolio.positions): - self._positions[account_type].append(self._to_position_record(date, order_book_id, position)) - - def _symbol(self, order_book_id): - return self._env.data_proxy.instruments(order_book_id).symbol - - @staticmethod - def _safe_convert(value, ndigits=3): - if isinstance(value, Enum): - return value.name - - if isinstance(value, (float, np.float64, np.float32, np.float16, np.float)): - return round(value, ndigits) - - return value - - def _to_portfolio_record(self, date, portfolio): - data = { - k: self._safe_convert(v, 3) for k, v in six.iteritems(properties(portfolio)) - if not k.startswith('_') and not k.endswith('_') and k not in { - "positions", "start_date", "starting_cash" - } - } - data['date'] = date - return data - - def _to_portfolio_record2(self, date, portfolio): - data = { - k: self._safe_convert(v, 3) for k, v in six.iteritems(portfolio.__dict__) - if not k.startswith('_') and not k.endswith('_') and k not in { - "positions", "start_date", "starting_cash" - } - } - data['date'] = date - return data - - def _to_position_record(self, date, order_book_id, position): - data = { - k: self._safe_convert(v, 3) for k, v in six.iteritems(position.__dict__) - if not k.startswith('_') and not k.endswith('_') - } - data['order_book_id'] = order_book_id - data['symbol'] = self._symbol(order_book_id) - data['date'] = date - return data - - def _to_trade_record(self, trade): - data = { - k: self._safe_convert(v) for k, v in six.iteritems(properties(trade)) - if not k.startswith('_') and not k.endswith('_') and k != 'order' - } - data['order_book_id'] = trade.order.order_book_id - data['symbol'] = self._symbol(trade.order.order_book_id) - data['side'] = self._safe_convert(trade.order.side) - data['position_effect'] = self._safe_convert(trade.order.position_effect) - data['datetime'] = data['datetime'].strftime("%Y-%m-%d %H:%M:%S") - data['trading_datetime'] = data['trading_datetime'].strftime("%Y-%m-%d %H:%M:%S") - return data - - def tear_down(self, code, exception=None): - if code != EXIT_CODE.EXIT_SUCCESS or not self._enabled: - return - - strategy_name = os.path.basename(self._env.config.base.strategy_file).split(".")[0] - data_proxy = self._env.data_proxy - - summary = { - 'strategy_name': strategy_name, - } - for k, v in six.iteritems(self._env.config.base.__dict__): - if k in ["trading_calendar", "account_list", "timezone", "persist_mode", - "resume_mode", "data_bundle_path", "handle_split", "persist"]: - continue - summary[k] = self._safe_convert(v, 2) - - risk = Risk(np.array(self._portfolio_daily_returns), np.array(self._benchmark_daily_returns), - data_proxy.get_risk_free_rate(self._env.config.base.start_date, self._env.config.base.end_date), - (self._env.config.base.end_date - self._env.config.base.start_date).days + 1) - summary.update({ - 'alpha': self._safe_convert(risk.alpha, 3), - 'beta': self._safe_convert(risk.beta, 3), - 'sharpe': self._safe_convert(risk.sharpe, 3), - 'information_ratio': self._safe_convert(risk.information_ratio, 3), - 'downside_risk': self._safe_convert(risk.annual_downside_risk, 3), - 'tracking_error': self._safe_convert(risk.annual_tracking_error, 3), - 'sortino': self._safe_convert(risk.sortino, 3), - 'volatility': self._safe_convert(risk.annual_volatility, 3), - 'max_drawdown': self._safe_convert(risk.max_drawdown, 3), - }) - - summary.update({ - k: self._safe_convert(v, 3) for k, v in six.iteritems(properties(self._latest_portfolio)) - if k not in ["positions", "daily_returns", "daily_pnl"] - }) - - if self._latest_benchmark_portfolio: - summary['benchmark_total_returns'] = self._latest_benchmark_portfolio.total_returns - summary['benchmark_annualized_returns'] = self._latest_benchmark_portfolio.annualized_returns - - trades = pd.DataFrame(self._trades) - if 'datetime' in trades.columns: - trades = trades.set_index('datetime') - - df = pd.DataFrame(self._total_portfolios) - df['date'] = pd.to_datetime(df['date']) - total_portfolios = df.set_index('date').sort_index() - - result_dict = { - 'summary': summary, - 'trades': trades, - 'total_portfolios': total_portfolios, - } - - if ExecutionContext.plots is not None: - plots = ExecutionContext.plots.get_plots() - plots_items = defaultdict(dict) - for series_name, value_dict in six.iteritems(plots): - for date, value in six.iteritems(value_dict): - plots_items[date][series_name] = value - plots_items[date]["date"] = date - - df = pd.DataFrame([dict_data for date, dict_data in six.iteritems(plots_items)]) - df["date"] = pd.to_datetime(df["date"]) - df = df.set_index("date").sort_index() - result_dict["plots"] = df - - for account_type, account in six.iteritems(self._env.accounts): - account_name = account_type.name.lower() - portfolios_list = self._sub_portfolios[account_type] - df = pd.DataFrame(portfolios_list) - df["date"] = pd.to_datetime(df["date"]) - portfolios_df = df.set_index("date").sort_index() - result_dict["{}_portfolios".format(account_name)] = portfolios_df - - positions_list = self._positions[account_type] - positions_df = pd.DataFrame(positions_list) - if "date" in positions_df.columns: - positions_df["date"] = pd.to_datetime(positions_df["date"]) - positions_df = positions_df.set_index("date").sort_index() - result_dict["{}_positions".format(account_name)] = positions_df - - self._result = result_dict - - if self._mod_config.output_file: - with open(self._mod_config.output_file, 'wb') as f: - pickle.dump(result_dict, f) - - if self._mod_config.plot: - from rqalpha.plot import plot_result - plot_result(result_dict) - - if self._mod_config.plot_save_file: - from rqalpha.plot import plot_result - plot_result(result_dict, False, self._mod_config.plot_save_file) - - if self._mod_config.report_save_path: - from rqalpha.utils.report import generate_report - generate_report(result_dict, self._mod_config.report_save_path) diff --git a/rqalpha/mod/mod_handler.py b/rqalpha/mod/mod_handler.py deleted file mode 100644 index e77896d50..000000000 --- a/rqalpha/mod/mod_handler.py +++ /dev/null @@ -1,56 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2017 Ricequant, Inc -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from importlib import import_module -from collections import OrderedDict - -from rqalpha.utils.logger import system_log -from rqalpha.utils.i18n import gettext as _ - - -class ModHandler(object): - def __init__(self): - self._env = None - self._mod_list = [] - self._mod_dict = OrderedDict() - - def set_env(self, environment): - self._env = environment - - config = environment.config - - for mod_name in config.mod.__dict__: - mod_config = getattr(config.mod, mod_name) - if not mod_config.enabled: - continue - self._mod_list.append((mod_name, mod_config)) - - self._mod_list.sort(key=lambda item: item[1].priority) - for mod_name, mod_config in self._mod_list: - system_log.debug(_('loading mod {}').format(mod_name)) - mod_module = import_module(mod_config.lib) - mod = mod_module.load_mod() - self._mod_dict[mod_name] = mod - - environment.mod_dict = self._mod_dict - - def start_up(self): - for mod_name, mod_config in self._mod_list: - self._mod_dict[mod_name].start_up(self._env, mod_config) - - def tear_down(self, *args): - for mod_name, _ in reversed(self._mod_list): - self._mod_dict[mod_name].tear_down(*args) diff --git a/rqalpha/mod/progressive_output_csv/mod.py b/rqalpha/mod/progressive_output_csv/mod.py deleted file mode 100644 index 189311a0b..000000000 --- a/rqalpha/mod/progressive_output_csv/mod.py +++ /dev/null @@ -1,58 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2017 Ricequant, Inc -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -import csv - -from rqalpha.interface import AbstractMod -from rqalpha.events import EVENT - - -class ProgressiveOutputCSVMod(AbstractMod): - - def start_up(self, env, mod_config): - self._env = env - self._mod_config = mod_config - - env.event_bus.add_listener(EVENT.POST_BAR, self._output_feeds) - - output_path = mod_config.output_path - - filename = os.path.join(output_path, "portfolio.csv") - new_file = False - if not os.path.exists(filename): - new_file = True - self.csv_file = open(filename, 'a') - fieldnames = ["datetime", "portfolio_value", "market_value", "total_returns"] - self.csv_writer = csv.DictWriter(self.csv_file, fieldnames) - if new_file: - self.csv_writer.writeheader() - - def _output_feeds(self, *args, **kwargs): - misc_account = self._env.account - calendar_date = self._env.calendar_dt.date() - portfolio = misc_account.portfolio - - self.csv_writer.writerow({ - "datetime": calendar_date, - "total_returns": portfolio.total_returns, - "portfolio_value": portfolio.portfolio_value, - "market_value": portfolio.market_value, - }) - self.csv_file.flush() - - def tear_down(self, code, exception=None): - pass diff --git a/rqalpha/mod/risk_manager/frontend_validator.py b/rqalpha/mod/risk_manager/frontend_validator.py deleted file mode 100644 index 16bd8f485..000000000 --- a/rqalpha/mod/risk_manager/frontend_validator.py +++ /dev/null @@ -1,157 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2017 Ricequant, Inc -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from rqalpha.const import SIDE, POSITION_EFFECT -from rqalpha.utils.i18n import gettext as _ -from rqalpha.execution_context import ExecutionContext - - -class FrontendValidator(object): - def __init__(self, config): - self.config = config - - def order_pipeline(self, account, order): - order_book_id = order.order_book_id - bar_dict = ExecutionContext.get_current_bar_dict() - portfolio = account.portfolio - position = portfolio.positions[order_book_id] - bar = bar_dict[order_book_id] - - if not self.validate_trading(order, bar): - return False - if not self.validate_available_cash(order, account, bar): - return False - if self.config.short_stock: - # 如果开启做空,则不验证仓位是否足够 - return True - if not self.validate_available_position(order, position): - return False - return True - - def validate_trading(self, order, bar): - order_book_id = order.order_book_id - trading_date = ExecutionContext.get_current_trading_dt().date() - - if bar.isnan: - """ - 只有未上市/已退市,对应的bar才为NaN - """ - instrument = ExecutionContext.get_instrument(order_book_id) - if trading_date < instrument.listed_date.date(): - order._mark_rejected(_("Order Rejected: {order_book_id} is not listed!").format( - order_book_id=order_book_id, - )) - elif trading_date > instrument.de_listed_date.date(): - order._mark_rejected(_("Order Rejected: {order_book_id} has been delisted!").format( - order_book_id=order_book_id, - )) - else: - order._mark_rejected(_("Order Rejected: {order_book_id} is not trading!").format( - order_book_id=order_book_id, - )) - return False - elif not bar.is_trading: - """ - 如果bar.is_trading为False,还需要判断是否为停盘,如果不是停牌,则说明交易量为0. - """ - if bar.suspended: - order._mark_rejected(_("Order Rejected: {order_book_id} is suspended!").format( - order_book_id=order_book_id, - )) - return False - return True - - def validate_available_cash(self, order, account, bar): - raise NotImplementedError - - def validate_available_position(self, order, position): - raise NotImplementedError - - -class StockFrontendValidator(FrontendValidator): - def validate_available_cash(self, order, account, bar): - if not self.config.available_cash: - return True - if order.side != SIDE.BUY: - return True - # 检查可用资金是否充足 - cost_money = order._frozen_price * order.quantity - if cost_money > account.portfolio.cash: - order._mark_rejected(_( - "Order Rejected: not enough money to buy {order_book_id}, needs {cost_money:.2f}, cash {cash:.2f}").format( - order_book_id=order.order_book_id, - cost_money=cost_money, - cash=account.portfolio.cash, - )) - return False - return True - - def validate_available_position(self, order, position): - if not self.config.available_position: - return True - if order.side != SIDE.SELL: - return True - if order.quantity > position.sellable: - order._mark_rejected(_( - "Order Rejected: not enough stock {order_book_id} to sell, you want to sell {quantity}, sellable {sellable}").format( - order_book_id=order.order_book_id, - quantity=order.quantity, - sellable=position.sellable, - )) - return False - return True - - -class FutureFrontendValidator(FrontendValidator): - def validate_available_cash(self, order, account, bar): - if not self.config.available_cash: - return True - if order.position_effect != POSITION_EFFECT.OPEN: - return True - contract_multiplier = bar.instrument.contract_multiplier - cost_money = account.margin_decider.cal_margin(order.order_book_id, order.side, order._frozen_price * order.quantity * contract_multiplier) - if cost_money > account.portfolio.cash: - order._mark_rejected(_( - "Order Rejected: not enough money to buy {order_book_id}, needs {cost_money:.2f}, cash {cash:.2f}").format( - order_book_id=order.order_book_id, - cost_money=cost_money, - cash=account.portfolio.cash, - )) - return False - return True - - def validate_available_position(self, order, position): - if not self.config.available_position: - return True - if order.position_effect != POSITION_EFFECT.CLOSE: - return True - if order.side == SIDE.BUY and order.quantity > position._sell_closable_quantity: - order._mark_rejected(_( - "Order Rejected: not enough securities {order_book_id} to buy close, target sell quantity is {quantity}, sell_closable_quantity {closable}").format( - order_book_id=order.order_book_id, - quantity=order.quantity, - closable=position._sell_closable_quantity, - )) - return False - elif order.side == SIDE.SELL and order.quantity > position._buy_closable_quantity: - order._mark_rejected(_( - "Order Rejected: not enough securities {order_book_id} to sell close, target sell quantity is {quantity}, buy_closable_quantity {closable}").format( - order_book_id=order.order_book_id, - quantity=order.quantity, - closable=position._buy_closable_quantity, - )) - return False - return True diff --git a/rqalpha/mod/risk_manager/mod.py b/rqalpha/mod/risk_manager/mod.py deleted file mode 100644 index 29bb84e97..000000000 --- a/rqalpha/mod/risk_manager/mod.py +++ /dev/null @@ -1,57 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2017 Ricequant, Inc -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from rqalpha.interface import AbstractMod -from rqalpha.events import EVENT -from rqalpha.utils import get_account_type -from rqalpha.const import ACCOUNT_TYPE - -from .frontend_validator import StockFrontendValidator, FutureFrontendValidator - - -class RiskManagerMod(AbstractMod): - - def __init__(self): - self._env = None - self.mod_config = None - self._frontend_validator = {} - - def start_up(self, env, mod_config): - self._env = env - self.mod_config = mod_config - self._env.event_bus.prepend_listener(EVENT.ORDER_PENDING_NEW, self._frontend_validate) - - def tear_down(self, code, exception=None): - pass - - def _frontend_validate(self, account, order): - frontend_validator = self._get_frontend_validator_for(order.order_book_id) - frontend_validator.order_pipeline(account, order) - - def _get_frontend_validator_for(self, order_book_id): - account_type = get_account_type(order_book_id) - try: - return self._frontend_validator[account_type] - except KeyError: - if account_type == ACCOUNT_TYPE.STOCK: - validator = StockFrontendValidator(self.mod_config) - elif account_type == ACCOUNT_TYPE.FUTURE: - validator = FutureFrontendValidator(self.mod_config) - else: - raise RuntimeError('account type {} not supported yet'.format(account_type)) - self._frontend_validator[account_type] = validator - return validator - diff --git a/rqalpha/mod/rqalpha_mod_sys_analyser/README.rst b/rqalpha/mod/rqalpha_mod_sys_analyser/README.rst new file mode 100644 index 000000000..7cdc2c052 --- /dev/null +++ b/rqalpha/mod/rqalpha_mod_sys_analyser/README.rst @@ -0,0 +1,93 @@ +=============================== +sys_analyser Mod +=============================== + +RQAlpha 策略分析 Mod。 + +启用该 Mod 后会记录下来每天的下单、成交、投资组合、持仓等信息,并计算风险度指标,最终以csv、plot图标等形式输出分析结果。 + +该模块是系统模块,不可删除 + +开启或关闭策略分析 Mod +=============================== + +.. code-block:: bash + + # 关闭策略分析 Mod + $ rqalpha mod disable sys_analyser + + # 启用策略分析 Mod + $ rqalpha mod enable sys_analyser + +模块配置项 +=============================== + +您可以通过直接修改 `sys_analyser` Mod 的配置信息来选择需要启用的功能。 + +默认配置项如下: + +.. code-block:: python + + { + # 当不输出csv/pickle/plot 等内容时,可以通过 record 来决定是否执行该 Mod 的计算逻辑 + "record": True, + # 如果指定路径,则输出计算后的 pickle 文件 + "output_file": None, + # 如果指定路径,则输出 report csv 文件 + "report_save_path": None, + # 画图 + 'plot': False, + # 如果指定路径,则输出 plot 对应的图片文件 + 'plot_save_file': None + } + +您可以通过如下方式来修改模块的配置信息,比如下面的示例中介绍了如何开启显示回测收益曲线图 + +.. code-block:: python + + from rqalpha import run + config = { + "base": { + "strategy_file": "strategy.py", + "securities": ["stock"], + "start_date": "2015-01-09", + "end_date": "2015-03-09", + "frequency": "1d", + "stock_starting_cash": 100000, + } + "mod": { + "sys_analyser": { + "enabled": True, + "plot": True + } + } + } + run(config) + +扩展命令 +=============================== + +在启用该 Mod 的情况下,您可以使用如下功能: + +* :code:`rqalpha run` 命令增加 :code:`--report target_csv_path` 选项,您可以指定将报告以 :code:`csv` 格式输出至 :code:`target_csv_path` 路径 +* :code:`rqalpha run` 命令增加 :code:`--output-file target_pickle_path` / :code:`-o target_pickle_path` 选项,您可以将每日 :code:`Portfolio` / :code:`Trade` 数据以 :code:`pickle` 文件格式输出到 :code:`target_pickle_path` 路径 +* :code:`rqalpha run` 命令增加 :code:`--plot/--no-plot` / :code:`-p` 选项,您可以以图形的方式显示收益曲线图 +* :code:`rqalpha run` 命令增加 :code:`--plot-save target_plot_img_path` 选项,您可以将收益曲线图输出至 :code:`target_plot img_path` 路径 + +.. code-block:: bash + + $ rqalpha run -f strategy.py --report target_csv_path -o target_pickle_path --plot --plot-save target_plot_img_path + +* 增加 :code:`rqalpha plot` 命令,根据生成的 :code:`pickle` 文件来显示收益曲线图 + * :code:`--show/--hide` 选项,是否显示收益曲线图 + * :code:`--plot-save target_plot_img_path` 选项,您可以将收益曲线图输出至 :code:`target_plot img_path` 路径 + +.. code-block:: bash + + $ rqalpha plot result_pickle_file_path --hide --plot-save target_plot_img_path + +* 增加 :code:`rqalpha report` 命令,根据生成的 :code:`pickle` 文件来生成报告 :code:`csv` 文件 + +.. code-block:: bash + + $ rqalpha report result_pickle_file_path target_report_csv_path diff --git a/rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py b/rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py new file mode 100644 index 000000000..3ddc7105f --- /dev/null +++ b/rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2017 Ricequant, Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click +from rqalpha.__main__ import cli + +__config__ = { + # 当不输出csv/pickle/plot 等内容时,可以通过 record 来决定是否执行该 Mod 的计算逻辑 + "record": True, + # 如果指定路径,则输出计算后的 pickle 文件 + "output_file": None, + # 如果指定路径,则输出 report csv 文件 + "report_save_path": None, + # 画图 + 'plot': False, + # 如果指定路径,则输出 plot 对应的图片文件 + 'plot_save_file': None, +} + + +def load_mod(): + from .mod import AnalyserMod + return AnalyserMod() + + +""" +--report +--output-file + +""" +cli.commands['run'].params.append( + click.Option( + ('--report', 'mod__sys_analyser__report_save_path'), + type=click.Path(writable=True), + help="[sys_analyser] save report" + ) +) +cli.commands['run'].params.append( + click.Option( + ('-o', '--output-file', 'mod__sys_analyser__output_file'), + type=click.Path(writable=True), + help="[sys_analyser] output result pickle file" + ) +) +cli.commands['run'].params.append( + click.Option( + ('-p', '--plot/--no-plot', 'mod__sys_analyser__plot'), + default=None, + help="[sys_analyser] plot result" + ) +) +cli.commands['run'].params.append( + click.Option( + ('--plot-save', 'mod__sys_analyser__plot_save_file'), + default=None, + help="[sys_analyser] save plot to file" + ) +) + + +@cli.command() +@click.argument('result_pickle_file_path', type=click.Path(exists=True), required=True) +@click.option('--show/--hide', 'show', default=True) +@click.option('--plot-save', 'plot_save_file', default=None, type=click.Path(), help="save plot result to file") +def plot(result_dict_file, show, plot_save_file): + """ + [sys_analyser] draw result DataFrame + """ + import pandas as pd + from .plot import plot_result + + result_dict = pd.read_pickle(result_dict_file) + plot_result(result_dict, show, plot_save_file) + + +@cli.command() +@click.argument('result_pickle_file_path', type=click.Path(exists=True), required=True) +@click.argument('target_report_csv_path', type=click.Path(exists=True, writable=True), required=True) +def report(result_pickle_file_path, target_report_csv_path): + """ + [sys_analyser] Generate report from backtest output file + """ + import pandas as pd + result_dict = pd.read_pickle(result_pickle_file_path) + + from .report import generate_report + generate_report(result_dict, target_report_csv_path) diff --git a/rqalpha/mod/rqalpha_mod_sys_analyser/mod.py b/rqalpha/mod/rqalpha_mod_sys_analyser/mod.py new file mode 100644 index 000000000..3a1795e44 --- /dev/null +++ b/rqalpha/mod/rqalpha_mod_sys_analyser/mod.py @@ -0,0 +1,279 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2017 Ricequant, Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import pickle +from collections import defaultdict +from enum import Enum + +import numpy as np +import pandas as pd +import six + +from rqalpha.const import EXIT_CODE, ACCOUNT_TYPE +from rqalpha.events import EVENT +from rqalpha.interface import AbstractMod +from rqalpha.utils.risk import Risk + + +class AnalyserMod(AbstractMod): + def __init__(self): + self._env = None + self._mod_config = None + self._enabled = False + + self._orders = [] + self._trades = [] + self._total_portfolios = [] + self._total_benchmark_portfolios = [] + self._sub_accounts = defaultdict(list) + self._positions = defaultdict(list) + + self._benchmark_daily_returns = [] + self._portfolio_daily_returns = [] + + def start_up(self, env, mod_config): + self._env = env + self._mod_config = mod_config + self._enabled = (self._mod_config.record or self._mod_config.plot or self._mod_config.output_file or + self._mod_config.plot_save_file or self._mod_config.report_save_path) + + if self._enabled: + env.event_bus.add_listener(EVENT.PRE_SETTLEMENT, self._collect_daily) + env.event_bus.add_listener(EVENT.TRADE, self._collect_trade) + env.event_bus.add_listener(EVENT.ORDER_CREATION_PASS, self._collect_order) + + def _collect_trade(self, event): + self._trades.append(self._to_trade_record(event.trade)) + + def _collect_order(self, event): + self._orders.append(event.order) + + def _collect_daily(self, event): + date = self._env.calendar_dt.date() + portfolio = self._env.portfolio + benchmark_portfolio = self._env.benchmark_portfolio + + self._portfolio_daily_returns.append(portfolio.daily_returns) + self._total_portfolios.append(self._to_portfolio_record(date, portfolio)) + + if benchmark_portfolio is None: + self._benchmark_daily_returns.append(0) + else: + self._benchmark_daily_returns.append(benchmark_portfolio.daily_returns) + self._total_benchmark_portfolios.append(self._to_portfolio_record(date, benchmark_portfolio)) + + for account_type, account in six.iteritems(self._env.portfolio.accounts): + self._sub_accounts[account_type].append(self._to_account_record(date, account)) + for order_book_id, position in six.iteritems(account.positions): + self._positions[account_type].append(self._to_position_record(date, order_book_id, position)) + + def _symbol(self, order_book_id): + return self._env.data_proxy.instruments(order_book_id).symbol + + @staticmethod + def _safe_convert(value, ndigits=3): + if isinstance(value, Enum): + return value.name + + if isinstance(value, (float, np.float64, np.float32, np.float16, np.float)): + return round(value, ndigits) + + return value + + def _to_portfolio_record(self, date, portfolio): + return { + 'date': date, + 'cash': self._safe_convert(portfolio.cash), + 'total_returns': self._safe_convert(portfolio.total_returns), + 'daily_returns': self._safe_convert(portfolio.daily_returns), + 'daily_pnl': self._safe_convert(portfolio.daily_pnl), + 'total_value': self._safe_convert(portfolio.total_value), + 'market_value': self._safe_convert(portfolio.market_value), + 'annualized_returns': self._safe_convert(portfolio.annualized_returns), + 'unit_net_value': self._safe_convert(portfolio.unit_net_value), + 'units': portfolio.units, + 'static_unit_net_value': self._safe_convert(portfolio.static_unit_net_value), + } + + ACCOUNT_FIELDS_MAP = { + ACCOUNT_TYPE.STOCK: ['dividend_receivable'], + ACCOUNT_TYPE.FUTURE: ['holding_pnl', 'realized_pnl', 'daily_pnl', 'margin'], + } + + def _to_account_record(self, date, account): + data = { + 'date': date, + 'total_cash': self._safe_convert(account.cash + account.frozen_cash), + 'transaction_cost': self._safe_convert(account.transaction_cost), + 'market_value': self._safe_convert(account.market_value), + 'total_value': self._safe_convert(account.total_value), + } + + for f in self.ACCOUNT_FIELDS_MAP[account.type]: + data[f] = self._safe_convert(getattr(account, f)) + + return data + + POSITION_FIELDS_MAP = { + ACCOUNT_TYPE.STOCK: [ + 'quantity', 'last_price', 'avg_price', 'market_value', 'sellable' + ], + ACCOUNT_TYPE.FUTURE: [ + 'pnl', 'daily_pnl', 'holding_pnl', 'realized_pnl', 'margin', 'market_value', + 'buy_pnl', 'sell_pnl', 'closable_buy_quantity', 'buy_margin', 'buy_today_quantity', + 'buy_avg_open_price', 'buy_avg_holding_price', 'closable_sell_quantity', + 'sell_margin', 'sell_today_quantity', 'sell_quantity', 'sell_avg_open_price', + 'sell_avg_holding_price' + ], + } + + def _to_position_record(self, date, order_book_id, position): + data = { + 'order_book_id': order_book_id, + 'symbol': self._symbol(order_book_id), + 'date': date, + } + + for f in self.POSITION_FIELDS_MAP[position.type]: + data[f] = self._safe_convert(getattr(position, f)) + return data + + def _to_trade_record(self, trade): + return { + 'datetime': trade.datetime.strftime("%Y-%m-%d %H:%M:%S"), + 'trading_datetime': trade.trading_datetime.strftime("%Y-%m-%d %H:%M:%S"), + 'order_book_id': trade.order_book_id, + 'symbol': self._symbol(trade.order_book_id), + 'side': self._safe_convert(trade.side), + 'position_effect': self._safe_convert(trade.position_effect), + 'exec_id': trade.exec_id, + 'tax': trade.tax, + 'commission': trade.commission, + 'last_quantity': trade.last_quantity, + 'last_price': self._safe_convert(trade.last_price), + 'order_id': trade.order_id, + 'transaction_cost': trade.transaction_cost, + } + + def tear_down(self, code, exception=None): + if code != EXIT_CODE.EXIT_SUCCESS or not self._enabled: + return + + strategy_name = os.path.basename(self._env.config.base.strategy_file).split(".")[0] + data_proxy = self._env.data_proxy + + summary = { + 'strategy_name': strategy_name, + 'start_date': self._env.config.base.start_date.strftime('%Y-%m-%d'), + 'end_date': self._env.config.base.end_date.strftime('%Y-%m-%d'), + 'strategy_file': self._env.config.base.strategy_file, + 'securities': self._env.config.base.securities, + 'run_type': self._env.config.base.run_type.value, + 'stock_starting_cash': self._env.config.base.stock_starting_cash, + 'future_starting_cash': self._env.config.base.future_starting_cash, + } + + risk = Risk(np.array(self._portfolio_daily_returns), np.array(self._benchmark_daily_returns), + data_proxy.get_risk_free_rate(self._env.config.base.start_date, self._env.config.base.end_date), + (self._env.config.base.end_date - self._env.config.base.start_date).days + 1) + summary.update({ + 'alpha': self._safe_convert(risk.alpha, 3), + 'beta': self._safe_convert(risk.beta, 3), + 'sharpe': self._safe_convert(risk.sharpe, 3), + 'information_ratio': self._safe_convert(risk.information_ratio, 3), + 'downside_risk': self._safe_convert(risk.annual_downside_risk, 3), + 'tracking_error': self._safe_convert(risk.annual_tracking_error, 3), + 'sortino': self._safe_convert(risk.sortino, 3), + 'volatility': self._safe_convert(risk.annual_volatility, 3), + 'max_drawdown': self._safe_convert(risk.max_drawdown, 3), + }) + + summary.update({ + 'total_value': self._safe_convert(self._env.portfolio.total_value), + 'cash': self._safe_convert(self._env.portfolio.cash), + 'total_returns': self._safe_convert(self._env.portfolio.total_returns), + 'annualized_returns': self._safe_convert(self._env.portfolio.annualized_returns), + 'unit_net_value': self._safe_convert(self._env.portfolio.unit_net_value), + 'units': self._env.portfolio.units, + }) + + if self._env.benchmark_portfolio: + summary['benchmark_total_returns'] = self._safe_convert(self._env.benchmark_portfolio.total_returns) + summary['benchmark_annualized_returns'] = self._safe_convert( + self._env.benchmark_portfolio.annualized_returns) + + trades = pd.DataFrame(self._trades) + if 'datetime' in trades.columns: + trades = trades.set_index('datetime') + + df = pd.DataFrame(self._total_portfolios) + df['date'] = pd.to_datetime(df['date']) + total_portfolios = df.set_index('date').sort_index() + + result_dict = { + 'summary': summary, + 'trades': trades, + 'total_portfolios': total_portfolios, + } + + if self._env.benchmark_portfolio is not None: + b_df = pd.DataFrame(self._total_benchmark_portfolios) + df['date'] = pd.to_datetime(df['date']) + benchmark_portfolios = b_df.set_index('date').sort_index() + result_dict['benchmark_portfolios'] = benchmark_portfolios + + if self._env.plot_store is not None: + plots = self._env.get_plot_store().get_plots() + plots_items = defaultdict(dict) + for series_name, value_dict in six.iteritems(plots): + for date, value in six.iteritems(value_dict): + plots_items[date][series_name] = value + plots_items[date]["date"] = date + + df = pd.DataFrame([dict_data for date, dict_data in six.iteritems(plots_items)]) + df["date"] = pd.to_datetime(df["date"]) + df = df.set_index("date").sort_index() + result_dict["plots"] = df + + for account_type, account in six.iteritems(self._env.portfolio.accounts): + account_name = account_type.name.lower() + portfolios_list = self._sub_accounts[account_type] + df = pd.DataFrame(portfolios_list) + df["date"] = pd.to_datetime(df["date"]) + portfolios_df = df.set_index("date").sort_index() + result_dict["{}_portfolios".format(account_name)] = portfolios_df + + positions_list = self._positions[account_type] + positions_df = pd.DataFrame(positions_list) + if "date" in positions_df.columns: + positions_df["date"] = pd.to_datetime(positions_df["date"]) + positions_df = positions_df.set_index("date").sort_index() + result_dict["{}_positions".format(account_name)] = positions_df + + if self._mod_config.output_file: + with open(self._mod_config.output_file, 'wb') as f: + pickle.dump(result_dict, f) + + if self._mod_config.report_save_path: + from .report import generate_report + generate_report(result_dict, self._mod_config.report_save_path) + + if self._mod_config.plot or self._mod_config.plot_save_file: + from .plot import plot_result + plot_result(result_dict, self._mod_config.plot, self._mod_config.plot_save_file) + + return result_dict diff --git a/rqalpha/plot.py b/rqalpha/mod/rqalpha_mod_sys_analyser/plot.py similarity index 66% rename from rqalpha/plot.py rename to rqalpha/mod/rqalpha_mod_sys_analyser/plot.py index 86093b57c..d89657cf4 100644 --- a/rqalpha/plot.py +++ b/rqalpha/mod/rqalpha_mod_sys_analyser/plot.py @@ -14,14 +14,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .utils.logger import system_log -from .utils.i18n import gettext +import rqalpha +from rqalpha.utils.logger import system_log +from rqalpha.utils.i18n import gettext def plot_result(result_dict, show_windows=True, savefile=None): import os - from matplotlib import rcParams + from matplotlib import rcParams, gridspec, ticker, image as mpimg, pyplot as plt from matplotlib.font_manager import findfont, FontProperties + import numpy as np rcParams['font.family'] = 'sans-serif' rcParams['font.sans-serif'] = [ @@ -42,13 +44,8 @@ def plot_result(result_dict, show_windows=True, savefile=None): use_chinese_fonts = False system_log.warn("Missing Chinese fonts. Fallback to English.") - import numpy as np - import matplotlib - from matplotlib import gridspec - import matplotlib.image as mpimg - import matplotlib.pyplot as plt - summary = result_dict["summary"] + title = summary['strategy_file'] total_portfolios = result_dict["total_portfolios"] @@ -57,12 +54,13 @@ def plot_result(result_dict, show_windows=True, savefile=None): index = total_portfolios.index # maxdrawdown - xs = total_portfolios.portfolio_value.values + portfolio_value = total_portfolios.unit_net_value * total_portfolios.units + xs = portfolio_value.values rt = total_portfolios.total_returns.values max_dd_end = np.argmax(np.maximum.accumulate(xs) / xs) if max_dd_end == 0: max_dd_end = len(xs) - 1 - max_dd_start = np.argmax(xs[:max_dd_end]) + max_dd_start = np.argmax(xs[:max_dd_end]) if max_dd_end > 0 else 0 # maxdrawdown duration al_cum = np.maximum.accumulate(xs) @@ -96,7 +94,7 @@ def plot_result(result_dict, show_windows=True, savefile=None): # draw logo ax = plt.subplot(gs[:3, -1:]) ax.axis("off") - filename = os.path.join(os.path.dirname(os.path.realpath(__file__)), "resource") + filename = os.path.join(os.path.dirname(os.path.realpath(rqalpha.__file__)), "resource") filename = os.path.join(filename, "ricequant-logo.png") img = mpimg.imread(filename) ax.imshow(img, interpolation="nearest") @@ -113,23 +111,23 @@ def _(txt): return gettext(txt) if use_chinese_fonts else txt fig_data = [ - (0.00, label_height, value_height, _("Total Returns"), "{0:.3%}".format(summary["total_returns"]), red, black), - (0.15, label_height, value_height, _("Annual Returns"), "{0:.3%}".format(summary["annualized_returns"]), red, black), - (0.00, label_height2, value_height2, _("Benchmark Returns"), "{0:.3%}".format(summary.get("benchmark_total_returns", 0)), blue, + (0.00, label_height, value_height, _(u"Total Returns"), "{0:.3%}".format(summary["total_returns"]), red, black), + (0.15, label_height, value_height, _(u"Annual Returns"), "{0:.3%}".format(summary["annualized_returns"]), red, black), + (0.00, label_height2, value_height2, _(u"Benchmark Returns"), "{0:.3%}".format(summary.get("benchmark_total_returns", 0)), blue, black), - (0.15, label_height2, value_height2, _("Benchmark Annual"), "{0:.3%}".format(summary.get("benchmark_annualized_returns", 0)), + (0.15, label_height2, value_height2, _(u"Benchmark Annual"), "{0:.3%}".format(summary.get("benchmark_annualized_returns", 0)), blue, black), - (0.30, label_height, value_height, _("Alpha"), "{0:.4}".format(summary["alpha"]), black, black), - (0.40, label_height, value_height, _("Beta"), "{0:.4}".format(summary["beta"]), black, black), - (0.55, label_height, value_height, _("Sharpe"), "{0:.4}".format(summary["sharpe"]), black, black), - (0.70, label_height, value_height, _("Sortino"), "{0:.4}".format(summary["sortino"]), black, black), - (0.85, label_height, value_height, _("Information Ratio"), "{0:.4}".format(summary["information_ratio"]), black, black), + (0.30, label_height, value_height, _(u"Alpha"), "{0:.4}".format(summary["alpha"]), black, black), + (0.40, label_height, value_height, _(u"Beta"), "{0:.4}".format(summary["beta"]), black, black), + (0.55, label_height, value_height, _(u"Sharpe"), "{0:.4}".format(summary["sharpe"]), black, black), + (0.70, label_height, value_height, _(u"Sortino"), "{0:.4}".format(summary["sortino"]), black, black), + (0.85, label_height, value_height, _(u"Information Ratio"), "{0:.4}".format(summary["information_ratio"]), black, black), - (0.30, label_height2, value_height2, _("Volatility"), "{0:.4}".format(summary["volatility"]), black, black), - (0.40, label_height2, value_height2, _("MaxDrawdown"), "{0:.3%}".format(summary["max_drawdown"]), black, black), - (0.55, label_height2, value_height2, _("Tracking Error"), "{0:.4}".format(summary["tracking_error"]), black, black), - (0.70, label_height2, value_height2, _("Downside Risk"), "{0:.4}".format(summary["downside_risk"]), black, black), + (0.30, label_height2, value_height2, _(u"Volatility"), "{0:.4}".format(summary["volatility"]), black, black), + (0.40, label_height2, value_height2, _(u"MaxDrawdown"), "{0:.3%}".format(summary["max_drawdown"]), black, black), + (0.55, label_height2, value_height2, _(u"Tracking Error"), "{0:.4}".format(summary["tracking_error"]), black, black), + (0.70, label_height2, value_height2, _(u"Downside Risk"), "{0:.4}".format(summary["downside_risk"]), black, black), ] ax = plt.subplot(gs[:3, :-1]) @@ -138,28 +136,28 @@ def _(txt): ax.text(x, y1, label, color=label_color, fontsize=font_size) ax.text(x, y2, value, color=value_color, fontsize=value_font_size) for x, y1, y2, label, value, label_color, value_color in [ - (0.85, label_height2, value_height2, _("MaxDD/MaxDDD"), max_dd_info, black, black)]: + (0.85, label_height2, value_height2, _(u"MaxDD/MaxDDD"), max_dd_info, black, black)]: ax.text(x, y1, label, color=label_color, fontsize=font_size) ax.text(x, y2, value, color=value_color, fontsize=8) # strategy vs benchmark ax = plt.subplot(gs[4:10, :]) - ax.get_xaxis().set_minor_locator(matplotlib.ticker.AutoMinorLocator()) - ax.get_yaxis().set_minor_locator(matplotlib.ticker.AutoMinorLocator()) + ax.get_xaxis().set_minor_locator(ticker.AutoMinorLocator()) + ax.get_yaxis().set_minor_locator(ticker.AutoMinorLocator()) ax.grid(b=True, which='minor', linewidth=.2) ax.grid(b=True, which='major', linewidth=1) # plot two lines - ax.plot(total_portfolios["total_returns"], label=_("strategy"), alpha=1, linewidth=2, color=red) + ax.plot(total_portfolios["total_returns"], label=_(u"strategy"), alpha=1, linewidth=2, color=red) if benchmark_portfolios is not None: - ax.plot(benchmark_portfolios["total_returns"], label=_("benchmark"), alpha=1, linewidth=2, color=blue) + ax.plot(benchmark_portfolios["total_returns"], label=_(u"benchmark"), alpha=1, linewidth=2, color=blue) # plot MaxDD/MaxDDD ax.plot([index[max_dd_end], index[max_dd_start]], [rt[max_dd_end], rt[max_dd_start]], - 'v', color='Green', markersize=8, alpha=.7, label=_("MaxDrawdown")) + 'v', color='Green', markersize=8, alpha=.7, label=_(u"MaxDrawdown")) ax.plot([index[max_ddd_start_day], index[max_ddd_end_day]], - [rt[max_ddd_start_day], rt[max_ddd_end_day]], 'D', color='Blue', markersize=8, alpha=.7, label=_("MaxDDD")) + [rt[max_ddd_start_day], rt[max_ddd_end_day]], 'D', color='Blue', markersize=8, alpha=.7, label=_(u"MaxDDD")) # place legend leg = plt.legend(loc="best") diff --git a/rqalpha/utils/report.py b/rqalpha/mod/rqalpha_mod_sys_analyser/report.py similarity index 100% rename from rqalpha/utils/report.py rename to rqalpha/mod/rqalpha_mod_sys_analyser/report.py diff --git a/rqalpha/mod/rqalpha_mod_sys_funcat/README.rst b/rqalpha/mod/rqalpha_mod_sys_funcat/README.rst new file mode 100644 index 000000000..f99e41591 --- /dev/null +++ b/rqalpha/mod/rqalpha_mod_sys_funcat/README.rst @@ -0,0 +1,123 @@ +=============================== +sys_funcat Mod +=============================== + +该模块为 RQAlpha 带来了通达信公式的方式写策略。 + +启用该 Mod ,会自动将 funcat_ 注入 API 到 RQAlpha 中。 + +该 mod 依赖 funcat_ ,使用前需要安装依赖库: + +.. code-block:: bash + + pip install -i https://pypi.tuna.tsinghua.edu.cn/simple funcat + + +开启或关闭 Mod +=============================== + +.. code-block:: bash + + # 启用 funcat API Mod + $ rqalpha mod enable sys_funcat + + # 关闭 funcat API Mod + $ rqalpha mod disable sys_funcat + + +常用API定义 +=============================== + +行情变量 +------------------ + +* 开盘价::code:`OPEN` :code:`O` +* 收盘价::code:`CLOSE` :code:`C` +* 最高价::code:`HIGH` :code:`H` +* 最低价::code:`LOW` :code:`L` +* 成交量::code:`VOLUME` :code:`V` + + +工具函数 +------------------ + +* n天前的数据:REF + +:code:`REF(C, 10) # 10天前的收盘价` + +* 金叉判断:CROSS + +:code:`CROSS(MA(C, 5), MA(C, 10)) # 5日均线上穿10日均线` + +* 两个序列取最小值:MIN + +:code:`MIN(O, C) # K线实体的最低价` + +* 两个序列取最大值:MAX + +:code:`MAX(O, C) # K线实体的最高价` + +* n天都满足条件:EVERY + +:code:`EVERY(C > MA(C, 5), 10) # 最近10天收盘价都大于5日均线` + +* n天内满足条件的天数:COUNT + +:code:`COUNT(C > O, 10) # 最近10天收阳线的天数` + +* n天内最大值:HHV + +:code:`HHV(MAX(O, C), 60) # 最近60天K线实体的最高价` + +* n天内最小值:LLV + +:code:`LLV(MIN(O, C), 60) # 最近60天K线实体的最低价` + +* 求和n日数据 SUM + +:code:`SUM(C, 10) # 求和10天的收盘价` + +* 求绝对值 ABS + +:code:`ABS(C - O)` + + +API样例策略 +=============================== + +.. code-block:: python + + from rqalpha.api import * + + + def init(context): + context.s1 = "600275.XSHG" + + + def handle_bar(context, bar_dict): + S(context.s1) + # 自己实现 DMA指标(Different of Moving Average) + M1 = 5 + M2 = 89 + M3 = 36 + + DDD = MA(CLOSE, M1) - MA(CLOSE, M2) + AMA = MA(DDD, M3) + + cur_position = context.portfolio.positions[context.s1].quantity + + if DDD < AMA and cur_position > 0: + order_target_percent(context.s1, 0) + + if (HHV(MAX(O, C), 50) / LLV(MIN(O, C), 50) < 2 + and CROSS(DDD, AMA) and cur_position == 0): + order_target_percent(context.s1, 1) + + +更多 API 介绍 +=============================== + +请见 funcat_ 。 + + +.. _funcat: https://github.com/cedricporter/funcat diff --git a/rqalpha/mod/funcat_api/__init__.py b/rqalpha/mod/rqalpha_mod_sys_funcat/__init__.py similarity index 95% rename from rqalpha/mod/funcat_api/__init__.py rename to rqalpha/mod/rqalpha_mod_sys_funcat/__init__.py index e154080de..2863c8f0d 100644 --- a/rqalpha/mod/funcat_api/__init__.py +++ b/rqalpha/mod/rqalpha_mod_sys_funcat/__init__.py @@ -14,8 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .mod import FuncatAPIMod - def load_mod(): + from .mod import FuncatAPIMod return FuncatAPIMod() diff --git a/rqalpha/mod/funcat_api/mod.py b/rqalpha/mod/rqalpha_mod_sys_funcat/mod.py similarity index 82% rename from rqalpha/mod/funcat_api/mod.py rename to rqalpha/mod/rqalpha_mod_sys_funcat/mod.py index 7ce8b4d41..692b6ce78 100644 --- a/rqalpha/mod/funcat_api/mod.py +++ b/rqalpha/mod/rqalpha_mod_sys_funcat/mod.py @@ -14,9 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -import copy - -import numpy as np +import datetime from rqalpha.interface import AbstractMod from rqalpha.environment import Environment @@ -35,6 +33,7 @@ def start_up(self, env, mod_config): from funcat.data.backend import DataBackend from funcat.context import set_current_date + from funcat.utils import get_date_from_int class RQAlphaDataBackend(DataBackend): """ @@ -44,14 +43,11 @@ class RQAlphaDataBackend(DataBackend): def __init__(self): from rqalpha.api import ( - history_bars, all_instruments, instruments, ) self.set_current_date = set_current_date - - self.history_bars = history_bars self.all_instruments = all_instruments self.instruments = instruments self.rqalpha_env = Environment.get_instance() @@ -67,7 +63,7 @@ def _pre_handle_bar(self, *args, **kwargs): calendar_date = self.rqalpha_env.calendar_dt.date() self.set_current_date(calendar_date) - def get_price(self, order_book_id, start, end): + def get_price(self, order_book_id, start, end, freq): """ :param order_book_id: e.g. 000002.XSHE :param start: 20160101 @@ -75,21 +71,22 @@ def get_price(self, order_book_id, start, end): :returns: :rtype: numpy.rec.array """ - # start = get_date_from_int(start) - # end = get_date_from_int(end) - # bar_count = (end - start).days - - # TODO: this is slow, make it run faster - bar_count = 1000 - origin_bars = bars = self.history_bars(order_book_id, bar_count, "1d") - - dtype = copy.deepcopy(bars.dtype) - names = list(dtype.names) - names[0] = "date" - dtype.names = names - bars = np.array(bars, dtype=dtype) - - bars["date"] = origin_bars["datetime"] / 1000000 + start = get_date_from_int(start) + end = get_date_from_int(end) + + scale = 1 + if freq[-1] == "m": + scale *= 240. / int(freq[:-1]) + bar_count = int((end - start).days * scale) + + dt = datetime.datetime.combine(end, datetime.time(23, 59, 59)) + bars = self.rqalpha_env.data_proxy.history_bars( + order_book_id, bar_count, freq, field=None, + dt=dt) + + if bars is None or len(bars) == 0: + raise KeyError("empty bars {}".format(order_book_id)) + bars = bars.copy() return bars diff --git a/rqalpha/mod/rqalpha_mod_sys_progress/README.rst b/rqalpha/mod/rqalpha_mod_sys_progress/README.rst new file mode 100644 index 000000000..79f6d42d5 --- /dev/null +++ b/rqalpha/mod/rqalpha_mod_sys_progress/README.rst @@ -0,0 +1,65 @@ +=============================== +sys_progress Mod +=============================== + +RQAlpha 控制台进度条 Mod + +该 Mod 可以输出当前策略的回测进度。 + +该模块是系统模块,不可删除 + +开启或关闭策略分析 Mod +=============================== + +.. code-block:: bash + + # 关闭策略分析 Mod + $ rqalpha mod disable sys_progress + + # 启用策略分析 Mod + $ rqalpha mod enable sys_progress + +模块配置项 +=============================== + +您可以通过直接修改 `sys_progress` Mod 的配置信息来选择需要启用的功能。 + +默认配置项如下: + +.. code-block:: python + + { + "show": False + } + +您可以直接修改模块配置信息来选择开启/关闭对应功能 + +.. code-block:: python + + from rqalpha import run + config = { + "base": { + "strategy_file": "strategy.py", + "securities": ["stock"], + "start_date": "2015-01-09", + "end_date": "2015-03-09", + "frequency": "1d", + "stock_starting_cash": 100000, + } + "mod": { + "sys_progress": { + "enabled": True, + "show": True + } + } + } + run(config) + +扩展命令 +=============================== + +在启用该 Mod 的情况下,您可以直接通过 :code:`rqalpha run --progress` 的方式来开启进度条的显示。 + +.. code-block:: bash + + $ rqalpha run -f strategy.py --progress diff --git a/rqalpha/mod/rqalpha_mod_sys_progress/__init__.py b/rqalpha/mod/rqalpha_mod_sys_progress/__init__.py new file mode 100644 index 000000000..1027a7846 --- /dev/null +++ b/rqalpha/mod/rqalpha_mod_sys_progress/__init__.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2017 Ricequant, Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click +from rqalpha.__main__ import cli + + +__config__ = { + "show": False +} + + +def load_mod(): + from .mod import ProgressMod + return ProgressMod() + + +""" +注入 --progress option +可以通过 `rqalpha run --progress` 的方式支持回测的时候显示进度条 +""" +cli_prefix = "mod__sys_progress__" +cli.commands['run'].params.append( + click.Option( + ("--progress", cli_prefix + "show"), + is_flag=True, + help="[sys_progress]show progress bar" + ) +) + diff --git a/rqalpha/mod/progress/__init__.py b/rqalpha/mod/rqalpha_mod_sys_progress/mod.py similarity index 60% rename from rqalpha/mod/progress/__init__.py rename to rqalpha/mod/rqalpha_mod_sys_progress/mod.py index ab772cc95..4c8fdb65a 100644 --- a/rqalpha/mod/progress/__init__.py +++ b/rqalpha/mod/rqalpha_mod_sys_progress/mod.py @@ -22,24 +22,26 @@ class ProgressMod(AbstractMod): def __init__(self): + self._show = False + self._progress_bar = None + self._trading_length = 0 self._env = None - self.progress_bar = None def start_up(self, env, mod_config): + self._show = mod_config.show self._env = env - env.event_bus.add_listener(EVENT.POST_AFTER_TRADING, self._tick) - env.event_bus.add_listener(EVENT.POST_SYSTEM_INIT, self._init) + if self._show: + env.event_bus.add_listener(EVENT.POST_SYSTEM_INIT, self._init) + env.event_bus.add_listener(EVENT.POST_AFTER_TRADING, self._tick) - def _init(self): - trading_length = len(self._env.config.base.trading_calendar) - self.progress_bar = click.progressbar(length=trading_length, show_eta=False) + def _init(self, event): + self._trading_length = len(self._env.config.base.trading_calendar) + self.progress_bar = click.progressbar(length=self._trading_length, show_eta=False) - def _tick(self): + def _tick(self, event): self.progress_bar.update(1) def tear_down(self, success, exception=None): - self.progress_bar.render_finish() + if self._show: + self.progress_bar.render_finish() - -def load_mod(): - return ProgressMod() diff --git a/rqalpha/mod/rqalpha_mod_sys_risk/README.rst b/rqalpha/mod/rqalpha_mod_sys_risk/README.rst new file mode 100644 index 000000000..226134ea5 --- /dev/null +++ b/rqalpha/mod/rqalpha_mod_sys_risk/README.rst @@ -0,0 +1,60 @@ +=============================== +sys_risk Mod +=============================== + +RQAlpha 风控 Mod + +启用该 Mod 后会对订单进行事前风控校验。 + +该模块是系统模块,不可删除 + +开启或关闭策略分析 Mod +=============================== + +.. code-block:: bash + + # 关闭策略分析 Mod + $ rqalpha mod disable sys_risk + + # 启用策略分析 Mod + $ rqalpha mod enable sys_risk + +模块配置项 +=============================== + +.. code-block:: python + + { + # 检查限价单价格是否合法 + "validate_price": True, + # 检查标的证券是否可以交易 + "validate_is_trading": True, + # 检查可用资金是否充足 + "validate_cash": True, + # 检查可平仓位是否充足 + "validate_position": True, + } + +您可以通过如下方式来修改模块的配置信息,从而选择开启/关闭风控模块对应的风控项 + +.. code-block:: python + + from rqalpha import run + config = { + "base": { + "strategy_file": "strategy.py", + "securities": ["stock"], + "start_date": "2015-01-09", + "end_date": "2015-03-09", + "frequency": "1d", + "stock_starting_cash": 100000, + } + "mod": { + "sys_risk": { + "enabled": True, + # 关闭仓位是否充足相关的风控判断 + "validate_position": False, + } + } + } + run(config) diff --git a/rqalpha/mod/rqalpha_mod_sys_risk/__init__.py b/rqalpha/mod/rqalpha_mod_sys_risk/__init__.py new file mode 100644 index 000000000..403030634 --- /dev/null +++ b/rqalpha/mod/rqalpha_mod_sys_risk/__init__.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2017 Ricequant, Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click +from rqalpha.__main__ import cli + +__config__ = { + # 检查限价单价格是否合法 + "validate_price": True, + # 检查标的证券是否可以交易 + "validate_is_trading": True, + # 检查可用资金是否充足 + "validate_cash": True, + # 检查可平仓位是否充足 + "validate_position": True, +} + + +def load_mod(): + from .mod import RiskManagerMod + return RiskManagerMod() + + +""" +注入 --short-stock option +可以通过 `rqalpha run --short-stock` 来开启允许卖空 +""" +cli_prefix = "mod__sys_risk__" + +cli.commands['run'].params.append( + click.Option( + ("--no-short-stock/--short-stock", "mod__sys_risk__validate_position"), + is_flag=True, default=True, + help="[sys_risk] enable stock shorting" + ) +) diff --git a/rqalpha/mod/rqalpha_mod_sys_risk/cash_validator.py b/rqalpha/mod/rqalpha_mod_sys_risk/cash_validator.py new file mode 100644 index 000000000..358998dde --- /dev/null +++ b/rqalpha/mod/rqalpha_mod_sys_risk/cash_validator.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2017 Ricequant, Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from rqalpha.interface import AbstractFrontendValidator +from rqalpha.const import SIDE, POSITION_EFFECT, ACCOUNT_TYPE + +from rqalpha.utils.i18n import gettext as _ + + +class CashValidator(AbstractFrontendValidator): + def __init__(self, env): + self._env = env + + def _stock_validator(self, account, order): + if order.side == SIDE.SELL: + return True + # 检查可用资金是否充足 + cost_money = order.frozen_price * order.quantity + if cost_money <= account.cash: + return True + + order.mark_rejected(_( + "Order Rejected: not enough money to buy {order_book_id}, needs {cost_money:.2f}, cash {cash:.2f}").format( + order_book_id=order.order_book_id, + cost_money=cost_money, + cash=account.cash, + )) + return False + + def _future_validator(self, account, order): + if order.position_effect != POSITION_EFFECT.OPEN: + return True + + instrument = self._env.get_instrument(order.order_book_id) + margin_rate = self._env.get_future_margin_rate(order.order_book_id) + margin = order.frozen_price * order.quantity * instrument.contract_multiplier * margin_rate + cost_money = margin * self._env.config.base.margin_multiplier + if cost_money <= account.cash: + return True + + order.mark_rejected(_( + "Order Rejected: not enough money to buy {order_book_id}, needs {cost_money:.2f}, cash {cash:.2f}").format( + order_book_id=order.order_book_id, + cost_money=cost_money, + cash=account.cash, + )) + return False + + def can_submit_order(self, account, order): + if account.type == ACCOUNT_TYPE.STOCK: + return self._stock_validator(account, order) + elif account.type == ACCOUNT_TYPE.FUTURE: + return self._future_validator(account, order) + else: + raise NotImplementedError + + def can_cancel_order(self, account, order): + return True diff --git a/rqalpha/mod/rqalpha_mod_sys_risk/is_trading_validator.py b/rqalpha/mod/rqalpha_mod_sys_risk/is_trading_validator.py new file mode 100644 index 000000000..40aea2d16 --- /dev/null +++ b/rqalpha/mod/rqalpha_mod_sys_risk/is_trading_validator.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2017 Ricequant, Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from rqalpha.interface import AbstractFrontendValidator + +from rqalpha.utils.i18n import gettext as _ + + +class IsTradingValidator(AbstractFrontendValidator): + def __init__(self, env): + self._env = env + + def can_submit_order(self, account, order): + instrument = self._env.data_proxy.instruments(order.order_book_id) + if instrument.listed_date > self._env.trading_dt: + order.mark_rejected(_(u"Order Rejected: {order_book_id} is not listed!").format( + order_book_id=order.order_book_id, + )) + return False + + if instrument.de_listed_date.date() < self._env.trading_dt.date(): + order.mark_rejected(_(u"Order Rejected: {order_book_id} has been delisted!").format( + order_book_id=order.order_book_id, + )) + return False + + if self._env.data_proxy.is_suspended(order.order_book_id, self._env.trading_dt): + order.mark_rejected(_(u"security {order_book_id} is suspended on {date}").format( + order_book_id=order.order_book_id, + date=self._env.trading_dt + )) + return False + + return True + + def can_cancel_order(self, account, order): + return True diff --git a/rqalpha/mod/simulation/mod.py b/rqalpha/mod/rqalpha_mod_sys_risk/mod.py similarity index 54% rename from rqalpha/mod/simulation/mod.py rename to rqalpha/mod/rqalpha_mod_sys_risk/mod.py index 5f88d562d..6f67024d5 100644 --- a/rqalpha/mod/simulation/mod.py +++ b/rqalpha/mod/rqalpha_mod_sys_risk/mod.py @@ -16,20 +16,22 @@ from rqalpha.interface import AbstractMod -from .simulation_broker import SimulationBroker -from .simulation_event_source import SimulationEventSource +from .price_validator import PriceValidator +from .position_validator import PositionValidator +from .cash_validator import CashValidator +from .is_trading_validator import IsTradingValidator -class SimulationMod(AbstractMod): - def __init__(self): - self._env = None - +class RiskManagerMod(AbstractMod): def start_up(self, env, mod_config): - self._env = env - self._env.set_broker(SimulationBroker(self._env)) - - event_source = SimulationEventSource(env, env.config.base.account_list) - env.set_event_source(event_source) + if mod_config.validate_price: + env.add_frontend_validator(PriceValidator(env)) + if mod_config.validate_is_trading: + env.add_frontend_validator(IsTradingValidator(env)) + if mod_config.validate_cash: + env.add_frontend_validator(CashValidator(env)) + if mod_config.validate_position: + env.add_frontend_validator(PositionValidator()) def tear_down(self, code, exception=None): pass diff --git a/rqalpha/mod/rqalpha_mod_sys_risk/position_validator.py b/rqalpha/mod/rqalpha_mod_sys_risk/position_validator.py new file mode 100644 index 000000000..7bda89818 --- /dev/null +++ b/rqalpha/mod/rqalpha_mod_sys_risk/position_validator.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2017 Ricequant, Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from rqalpha.interface import AbstractFrontendValidator +from rqalpha.const import SIDE, POSITION_EFFECT, ACCOUNT_TYPE + +from rqalpha.utils.i18n import gettext as _ + + +class PositionValidator(AbstractFrontendValidator): + @staticmethod + def _stock_validator(account, order): + if order.side != SIDE.SELL: + return True + + position = account.positions[order.order_book_id] + if order.quantity <= position.sellable: + return True + + order.mark_rejected(_( + "Order Rejected: not enough stock {order_book_id} to sell, you want to sell {quantity}," + " sellable {sellable}").format( + order_book_id=order.order_book_id, + quantity=order.quantity, + sellable=position.sellable, + )) + return False + + @staticmethod + def _future_validator(account, order): + if order.position_effect != POSITION_EFFECT.CLOSE: + return True + + position = account.positions[order.order_book_id] + if order.side == SIDE.BUY and order.quantity > position.closable_sell_quantity: + order.mark_rejected(_( + "Order Rejected: not enough securities {order_book_id} to buy close, target" + " sell quantity is {quantity}, sell_closable_quantity {closable}").format( + order_book_id=order.order_book_id, + quantity=order.quantity, + closable=position.closable_sell_quantity, + )) + return False + elif order.side == SIDE.SELL and order.quantity > position.closable_buy_quantity: + order.mark_rejected(_( + "Order Rejected: not enough securities {order_book_id} to sell close, target" + " sell quantity is {quantity}, buy_closable_quantity {closable}").format( + order_book_id=order.order_book_id, + quantity=order.quantity, + closable=position.closable_buy_quantity, + )) + return False + return True + + def can_submit_order(self, account, order): + if account.type == ACCOUNT_TYPE.STOCK: + return self._stock_validator(account, order) + elif account.type == ACCOUNT_TYPE.FUTURE: + return self._future_validator(account, order) + else: + raise NotImplementedError + + def can_cancel_order(self, account, order): + return True diff --git a/rqalpha/mod/rqalpha_mod_sys_risk/price_validator.py b/rqalpha/mod/rqalpha_mod_sys_risk/price_validator.py new file mode 100644 index 000000000..a422cf44f --- /dev/null +++ b/rqalpha/mod/rqalpha_mod_sys_risk/price_validator.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2017 Ricequant, Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from rqalpha.interface import AbstractFrontendValidator +from rqalpha.const import ORDER_TYPE + +from rqalpha.utils.i18n import gettext as _ + + +class PriceValidator(AbstractFrontendValidator): + def __init__(self, env): + self._env = env + + def can_submit_order(self, account, order): + if order.type != ORDER_TYPE.LIMIT: + return True + + limit_up = self._env.price_board.get_limit_up(order.order_book_id) + if order.price > limit_up: + reason = _( + "Order Rejected: limit order price {limit_price} is higher than limit up {limit_up}." + ).format( + limit_price=order.price, + limit_up=limit_up + ) + order.mark_rejected(reason) + return False + + limit_down = self._env.price_board.get_limit_down(order.order_book_id) + if order.price < limit_down: + reason = _( + "Order Rejected: limit order price {limit_price} is lower than limit down {limit_down}." + ).format( + limit_price=order.price, + limit_down=limit_down + ) + order.mark_rejected(reason) + return False + + return True + + def can_cancel_order(self, account, order): + return True diff --git a/rqalpha/mod/rqalpha_mod_sys_simulation/README.rst b/rqalpha/mod/rqalpha_mod_sys_simulation/README.rst new file mode 100644 index 000000000..469a86f8d --- /dev/null +++ b/rqalpha/mod/rqalpha_mod_sys_simulation/README.rst @@ -0,0 +1,77 @@ +=============================== +sys_simulation Mod +=============================== + +RQAlpha 回测 Mod,启用该模块开启回测功能。 + +该模块是系统模块,不可删除 + +开启或关闭策略分析 Mod +=============================== + +.. code-block:: bash + + # 关闭策略分析 Mod + $ rqalpha mod disable sys_simulation + + # 启用策略分析 Mod + $ rqalpha mod enable sys_simulation + +模块配置项 +=============================== + +您可以通过直接修改 `sys_simulation` Mod 的配置信息来更改默认配置项 + +默认配置项如下: + +.. code-block:: python + + { + # 是否开启信号模式 + "signal": False, + # 启用的回测引擎,目前支持 `current_bar` (当前Bar收盘价撮合) 和 `next_bar` (下一个Bar开盘价撮合) + "matching_type": "current_bar", + # 设置滑点 + "slippage": 0, + # 设置手续费乘数,默认为1 + "commission_multiplier": 1, + # price_limit: 在处于涨跌停时,无法买进/卖出,默认开启【在 Signal 模式下,不再禁止买进/卖出,如果开启,则给出警告提示。】 + "price_limit": True, + # 是否有成交量限制 + "volume_limit": True, + # 按照当前成交量的百分比进行撮合 + "volume_percent": 0.25, + } + +您可以通过如下方式来修改模块的配置信息,比如下面的示例中介绍了如何设置滑点 + +.. code-block:: python + + from rqalpha import run + config = { + "base": { + "strategy_file": "strategy.py", + "securities": ["stock"], + "start_date": "2015-01-09", + "end_date": "2015-03-09", + "frequency": "1d", + "stock_starting_cash": 100000, + } + "mod": { + "sys_simulation": { + "enabled": True, + "slippage": 0.01 + } + } + } + run(config) + +扩展命令 +=============================== + +在启用该 Mod 的情况下,您可以使用如下功能: + +* :code:`rqalpha run` 命令增加 :code:`--signal` 选项,您可以指定使用信号方式来直接按照下单价格成交,从而屏蔽订单细节。 +* :code:`rqalpha run` 命令增加 :code:`--slippage` / :code:`-sp` 选项,您可以指定成交所产生的滑点,目前支持按照当前价格的百分比的方式计算滑点。 +* :code:`rqalpha run` 命令增加 :code:`--commission-multiplier` / :code:`--cm` 选项,您可以指定手续费乘数 +* :code:`rqalpha run` 命令增加 :code:`--matching-type` / :code:`--mt` 选项,您可以指定撮合的锚定价格及对应的方式 diff --git a/rqalpha/mod/rqalpha_mod_sys_simulation/__init__.py b/rqalpha/mod/rqalpha_mod_sys_simulation/__init__.py new file mode 100644 index 000000000..b55e54ee4 --- /dev/null +++ b/rqalpha/mod/rqalpha_mod_sys_simulation/__init__.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2017 Ricequant, Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click +from rqalpha.__main__ import cli + + +__config__ = { + # 是否开启信号模式 + "signal": False, + # 启用的回测引擎,目前支持 `current_bar` (当前Bar收盘价撮合) 和 `next_bar` (下一个Bar开盘价撮合) + "matching_type": "current_bar", + # 设置滑点 + "slippage": 0, + # 设置手续费乘数,默认为1 + "commission_multiplier": 1, + # price_limit: 在处于涨跌停时,无法买进/卖出,默认开启【在 Signal 模式下,不再禁止买进/卖出,如果开启,则给出警告提示。】 + "price_limit": True, + # liquidity_limit: 当对手盘没有流动性的时候,无法买进/卖出,默认关闭 + "liquidity_limit": False, + # 是否有成交量限制 + "volume_limit": True, + # 按照当前成交量的百分比进行撮合 + "volume_percent": 0.25, +} + + +def load_mod(): + from .mod import SimulationMod + return SimulationMod() + + +""" +注入 --signal option: 实现信号模式回测 +注入 --slippage option: 实现设置滑点 +注入 --commission-multiplier options: 实现设置手续费乘数 +注入 --matching-type: 实现选择回测引擎 +""" +cli_prefix = "mod__sys_simulation__" + +cli.commands['run'].params.append( + click.Option( + ('--signal', cli_prefix + "signal"), + is_flag=True, + help="[sys_simulation]exclude match engine", + ) +) + +cli.commands['run'].params.append( + click.Option( + ('-sp', '--slippage', cli_prefix + "slippage"), + type=click.FLOAT, + help="[sys_simulation]set slippage" + ) +) + +cli.commands['run'].params.append( + click.Option( + ('-cm', '--commission-multiplier', cli_prefix + "commission_multiplier"), + type=click.FLOAT, + help="[sys_simulation] set commission multiplier" + ) +) + +cli.commands['run'].params.append( + # [Deprecated] using matching type + click.Option( + ('-me', '--match-engine', cli_prefix + "matching_type"), + type=click.Choice(['current_bar', 'next_bar', 'last', 'best_own', 'best_counterparty']), + help="[Deprecated][sys_simulation] set matching type" + ) +) + +cli.commands['run'].params.append( + click.Option( + ('-mt', '--matching-type', cli_prefix + "matching_type"), + type=click.Choice(['current_bar', 'next_bar', 'last', 'best_own', 'best_counterparty']), + help="[sys_simulation] set matching type" + ) +) diff --git a/rqalpha/mod/rqalpha_mod_sys_simulation/decider/__init__.py b/rqalpha/mod/rqalpha_mod_sys_simulation/decider/__init__.py new file mode 100644 index 000000000..eed741d1c --- /dev/null +++ b/rqalpha/mod/rqalpha_mod_sys_simulation/decider/__init__.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2017 Ricequant, Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from rqalpha.const import ACCOUNT_TYPE + +from .commission import StockCommission, FutureCommission +from .slippage import PriceRatioSlippage +from .tax import StockTax, FutureTax + + +class CommissionDecider(object): + def __init__(self, multiplier): + self.deciders = dict() + self.deciders[ACCOUNT_TYPE.STOCK] = StockCommission(multiplier) + self.deciders[ACCOUNT_TYPE.FUTURE] = FutureCommission(multiplier) + + def get_commission(self, account_type, trade): + return self.deciders[account_type].get_commission(trade) + + +class SlippageDecider(object): + def __init__(self, rate): + self.decider = PriceRatioSlippage(rate) + + def get_trade_price(self, side, price): + return self.decider.get_trade_price(side, price) + + +class TaxDecider(object): + def __init__(self, rate=None): + self.deciders = dict() + self.deciders[ACCOUNT_TYPE.STOCK] = StockTax(rate) + self.deciders[ACCOUNT_TYPE.BENCHMARK] = StockTax(rate) + self.deciders[ACCOUNT_TYPE.FUTURE] = FutureTax(rate) + + def get_tax(self, account_type, trade): + return self.deciders[account_type].get_tax(trade) diff --git a/rqalpha/model/commission.py b/rqalpha/mod/rqalpha_mod_sys_simulation/decider/commission.py similarity index 72% rename from rqalpha/model/commission.py rename to rqalpha/mod/rqalpha_mod_sys_simulation/decider/commission.py index 822cbf5b7..050a84b8b 100644 --- a/rqalpha/model/commission.py +++ b/rqalpha/mod/rqalpha_mod_sys_simulation/decider/commission.py @@ -18,19 +18,8 @@ from six import with_metaclass from collections import defaultdict -from ..utils import get_upper_underlying_symbol -from ..utils.default_future_info import DEFAULT_FUTURE_INFO -from ..const import ACCOUNT_TYPE, HEDGE_TYPE, SIDE, COMMISSION_TYPE, POSITION_EFFECT -from ..execution_context import ExecutionContext - - -def init_commission(account_type, multiplier): - if account_type in [ACCOUNT_TYPE.STOCK, ACCOUNT_TYPE.BENCHMARK]: - return StockCommission(multiplier) - elif account_type == ACCOUNT_TYPE.FUTURE: - return FutureCommission(multiplier) - else: - raise NotImplementedError +from rqalpha.const import HEDGE_TYPE, COMMISSION_TYPE, POSITION_EFFECT +from rqalpha.environment import Environment class BaseCommission(with_metaclass(abc.ABCMeta)): @@ -58,7 +47,7 @@ def get_commission(self, trade): 4.1 如果commission 等于 min_commission, 说明是第一笔trade, 此时,返回min_commission(提前把最小手续费收了) 4.2 如果commission 不等于 min_commission, 说明不是第一笔trade, 之前的trade中min_commission已经收过了,所以返回0. """ - order_id = trade.order.order_id + order_id = trade.order_id commission = self.commission_map[order_id] cost_money = trade.last_price * trade.last_quantity * self.rate * self.multiplier if cost_money > commission: @@ -86,23 +75,23 @@ def __init__(self, multiplier, hedge_type=HEDGE_TYPE.SPECULATION): self.hedge_type = hedge_type def get_commission(self, trade): - order = trade.order - order_book_id = order.order_book_id - info = ExecutionContext.get_future_commission_info(order_book_id, self.hedge_type) + order_book_id = trade.order_book_id + env = Environment.get_instance() + info = env.get_future_commission_info(order_book_id, self.hedge_type) commission = 0 if info['commission_type'] == COMMISSION_TYPE.BY_MONEY: - contract_multiplier = ExecutionContext.get_instrument(order.order_book_id).contract_multiplier - if trade.order.position_effect == POSITION_EFFECT.OPEN: + contract_multiplier = env.get_instrument(trade.order_book_id).contract_multiplier + if trade.position_effect == POSITION_EFFECT.OPEN: commission += trade.last_price * trade.last_quantity * contract_multiplier * info['open_commission_ratio'] else: - commission += trade.last_price * (trade.last_quantity - trade._close_today_amount) * contract_multiplier * info[ + commission += trade.last_price * (trade.last_quantity - trade.close_today_amount) * contract_multiplier * info[ 'close_commission_ratio'] - commission += trade.last_price * trade._close_today_amount * contract_multiplier * info[ + commission += trade.last_price * trade.close_today_amount * contract_multiplier * info[ 'close_commission_today_ratio'] else: - if trade.order.position_effect == POSITION_EFFECT.OPEN: + if trade.position_effect == POSITION_EFFECT.OPEN: commission += trade.last_quantity * info['open_commission_ratio'] else: - commission += (trade.last_quantity - trade._close_today_amount) * info['close_commission_ratio'] - commission += trade._close_today_amount * info['close_commission_today_ratio'] + commission += (trade.last_quantity - trade.close_today_amount) * info['close_commission_ratio'] + commission += trade.close_today_amount * info['close_commission_today_ratio'] return commission * self.multiplier diff --git a/rqalpha/model/slippage.py b/rqalpha/mod/rqalpha_mod_sys_simulation/decider/slippage.py similarity index 71% rename from rqalpha/model/slippage.py rename to rqalpha/mod/rqalpha_mod_sys_simulation/decider/slippage.py index f592c36cd..24b0f3183 100644 --- a/rqalpha/model/slippage.py +++ b/rqalpha/mod/rqalpha_mod_sys_simulation/decider/slippage.py @@ -18,14 +18,9 @@ from six import with_metaclass -from ..const import SIDE -from ..utils.exception import patch_user_exc -from ..utils.i18n import gettext as _ - - -def init_slippage(rate=0): - # 未来可能会有多种slippage模型,目前只返回 FixedSlippage - return PriceRatioSlippage(rate) +from rqalpha.const import SIDE +from rqalpha.utils.exception import patch_user_exc +from rqalpha.utils.i18n import gettext as _ class BaseSlippage(with_metaclass(abc.ABCMeta)): @@ -40,10 +35,10 @@ def __init__(self, rate=0.): if 0 <= rate < 1: self.rate = rate else: - raise patch_user_exc(ValueError(_("invalid slippage rate value: value range is [0, 1)"))) + raise patch_user_exc(ValueError(_(u"invalid slippage rate value: value range is [0, 1)"))) - def get_trade_price(self, order, price): - return price + price * self.rate * (1 if order.side == SIDE.BUY else -1) + def get_trade_price(self, side, price): + return price + price * self.rate * (1 if side == SIDE.BUY else -1) # class FixedSlippage(BaseSlippage): diff --git a/rqalpha/model/tax.py b/rqalpha/mod/rqalpha_mod_sys_simulation/decider/tax.py similarity index 61% rename from rqalpha/model/tax.py rename to rqalpha/mod/rqalpha_mod_sys_simulation/decider/tax.py index 34d468146..fdc9857d5 100644 --- a/rqalpha/model/tax.py +++ b/rqalpha/mod/rqalpha_mod_sys_simulation/decider/tax.py @@ -17,21 +17,8 @@ import abc from six import with_metaclass -from ..const import SIDE, ACCOUNT_TYPE -from ..execution_context import ExecutionContext - - -def init_tax(account_type, rate=None): - if account_type in [ACCOUNT_TYPE.STOCK, ACCOUNT_TYPE.BENCHMARK]: - if rate is None: - rate = 0.001 - return StockTax(rate) - elif account_type == ACCOUNT_TYPE.FUTURE: - if rate is None: - rate = 0. - return FutureTax(rate) - else: - raise NotImplementedError +from rqalpha.const import SIDE +from rqalpha.environment import Environment class BaseTax(with_metaclass(abc.ABCMeta)): @@ -41,19 +28,22 @@ def get_tax(self, trade): class StockTax(BaseTax): - def __init__(self, rate): - self.rate = rate + def __init__(self, rate=None): + if rate is None: + self.rate = 0.001 + else: + self.rate = rate def get_tax(self, trade): cost_money = trade.last_price * trade.last_quantity - if ExecutionContext.get_instrument(trade.order.order_book_id).type == 'CS': - return cost_money * self.rate if trade.order.side == SIDE.SELL else 0 + if Environment.get_instance().get_instrument(trade.order_book_id).type == 'CS': + return cost_money * self.rate if trade.side == SIDE.SELL else 0 else: return 0 class FutureTax(BaseTax): - def __init__(self, rate): + def __init__(self, rate=0): self.rate = rate def get_tax(self, trade): diff --git a/rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py b/rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py new file mode 100644 index 000000000..2c599a0d4 --- /dev/null +++ b/rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py @@ -0,0 +1,179 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2017 Ricequant, Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections import defaultdict + +import numpy as np +from rqalpha.const import ORDER_TYPE, SIDE, MATCHING_TYPE +from rqalpha.events import EVENT, Event +from rqalpha.model.trade import Trade +from rqalpha.utils.i18n import gettext as _ + +from .decider import CommissionDecider, SlippageDecider, TaxDecider + + +class Matcher(object): + def __init__(self, env, mod_config): + self._commission_decider = CommissionDecider(mod_config.commission_multiplier) + self._slippage_decider = SlippageDecider(mod_config.slippage) + self._tax_decider = TaxDecider() + self._turnover = defaultdict(int) + self._calendar_dt = None + self._trading_dt = None + self._volume_percent = mod_config.volume_percent + self._price_limit = mod_config.price_limit + self._liquidity_limit = mod_config.liquidity_limit + self._volume_limit = mod_config.volume_limit + self._env = env + self._deal_price_decider = self._create_deal_price_decider(mod_config.matching_type) + + def _create_deal_price_decider(self, matching_type): + decider_dict = { + MATCHING_TYPE.CURRENT_BAR_CLOSE: lambda order_book_id, side: self._env.bar_dict[order_book_id].close, + MATCHING_TYPE.NEXT_BAR_OPEN: lambda order_book_id, side: self._env.bar_dict[order_book_id].open, + MATCHING_TYPE.NEXT_TICK_LAST: lambda order_book_id, side: self._env.price_board.get_last_price(order_book_id), + MATCHING_TYPE.NEXT_TICK_BEST_OWN: lambda order_book_id, side: self._best_own_price_decider(order_book_id, side), + MATCHING_TYPE.NEXT_TICK_BEST_COUNTERPARTY: lambda order_book_id, side: ( + self._env.price_board.get_a1(order_book_id) if side == SIDE.BUY else self._env.price_board.get_b1(order_book_id)) + } + return decider_dict[matching_type] + + def _best_own_price_decider(self, order_book_id, side): + price = self._env.price_board.get_b1(order_book_id) if side == SIDE.BUY else self._env.price_board.get_a1(order_book_id) + if price == 0: + price = self._env.price_board.get_last_price(order_book_id) + return price + + def update(self, calendar_dt, trading_dt): + self._turnover.clear() + self._calendar_dt = calendar_dt + self._trading_dt = trading_dt + + def match(self, open_orders): + price_board = self._env.price_board + for account, order in open_orders: + order_book_id = order.order_book_id + instrument = self._env.get_instrument(order_book_id) + + if np.isnan(price_board.get_last_price(order_book_id)): + listed_date = instrument.listed_date.date() + if listed_date == self._trading_dt.date(): + reason = _(u"Order Cancelled: current security [{order_book_id}] can not be traded in listed date [{listed_date}]").format( + order_book_id=order.order_book_id, + listed_date=listed_date, + ) + else: + reason = _(u"Order Cancelled: current bar [{order_book_id}] miss market data.").format( + order_book_id=order.order_book_id) + order.mark_rejected(reason) + continue + + deal_price = self._deal_price_decider(order_book_id, order.side) + if order.type == ORDER_TYPE.LIMIT: + if order.side == SIDE.BUY and order.price < deal_price: + continue + if order.side == SIDE.SELL and order.price > deal_price: + continue + # 是否限制涨跌停不成交 + if self._price_limit: + if order.side == SIDE.BUY and deal_price >= price_board.get_limit_up(order_book_id): + continue + if order.side == SIDE.SELL and deal_price <= price_board.get_limit_down(order_book_id): + continue + if self._liquidity_limit: + if order.side == SIDE.BUY and price_board.get_a1(order_book_id) == 0: + continue + if order.side == SIDE.SELL and price_board.get_b1(order_book_id) == 0: + continue + else: + if self._price_limit: + if order.side == SIDE.BUY and deal_price >= price_board.get_limit_up(order_book_id): + reason = _( + "Order Cancelled: current bar [{order_book_id}] reach the limit_up price." + ).format(order_book_id=order.order_book_id) + order.mark_rejected(reason) + continue + if order.side == SIDE.SELL and deal_price <= price_board.get_limit_down(order_book_id): + reason = _( + "Order Cancelled: current bar [{order_book_id}] reach the limit_down price." + ).format(order_book_id=order.order_book_id) + order.mark_rejected(reason) + continue + if self._liquidity_limit: + if order.side == SIDE.BUY and price_board.get_a1(order_book_id) == 0: + reason = _( + "Order Cancelled: [{order_book_id}] has no liquidity." + ).format(order_book_id=order.order_book_id) + order.mark_rejected(reason) + continue + if order.side == SIDE.SELL and price_board.get_b1(order_book_id) == 0: + reason = _( + "Order Cancelled: [{order_book_id}] has no liquidity." + ).format(order_book_id=order.order_book_id) + order.mark_rejected(reason) + continue + + if self._volume_limit: + bar = self._env.bar_dict[order_book_id] + volume_limit = round(bar.volume * self._volume_percent) - self._turnover[order.order_book_id] + round_lot = instrument.round_lot + volume_limit = (volume_limit // round_lot) * round_lot + if volume_limit <= 0: + if order.type == ORDER_TYPE.MARKET: + reason = _(u"Order Cancelled: market order {order_book_id} volume {order_volume}" + u" due to volume limit").format( + order_book_id=order.order_book_id, + order_volume=order.quantity + ) + order.mark_cancelled(reason) + continue + + unfilled = order.unfilled_quantity + fill = min(unfilled, volume_limit) + else: + fill = order.unfilled_quantity + + ct_amount = account.positions.get_or_create(order.order_book_id).cal_close_today_amount(fill, order.side) + price = self._slippage_decider.get_trade_price(order.side, deal_price) + trade = Trade.__from_create__( + order_id=order.order_id, + calendar_dt=self._calendar_dt, + trading_dt=self._trading_dt, + price=price, + amount=fill, + side=order.side, + position_effect=order.position_effect, + order_book_id=order.order_book_id, + frozen_price=order.frozen_price, + close_today_amount=ct_amount + ) + trade._commission = self._commission_decider.get_commission(account.type, trade) + trade._tax = self._tax_decider.get_tax(account.type, trade) + order.fill(trade) + self._turnover[order.order_book_id] += fill + + self._env.event_bus.publish_event(Event(EVENT.TRADE, account=account, trade=trade)) + + if order.type == ORDER_TYPE.MARKET and order.unfilled_quantity != 0: + reason = _( + u"Order Cancelled: market order {order_book_id} volume {order_volume} is" + u" larger than 25 percent of current bar volume, fill {filled_volume} actually" + ).format( + order_book_id=order.order_book_id, + order_volume=order.quantity, + filled_volume=order.filled_quantity + ) + order.mark_cancelled(reason) diff --git a/rqalpha/mod/rqalpha_mod_sys_simulation/mod.py b/rqalpha/mod/rqalpha_mod_sys_simulation/mod.py new file mode 100644 index 000000000..079ebe4a1 --- /dev/null +++ b/rqalpha/mod/rqalpha_mod_sys_simulation/mod.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2017 Ricequant, Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import six + +from rqalpha.interface import AbstractMod +from rqalpha.utils.i18n import gettext as _ +from rqalpha.utils.exception import patch_user_exc +from rqalpha.const import MATCHING_TYPE + +from .simulation_broker import SimulationBroker +from .signal_broker import SignalBroker +from .simulation_event_source import SimulationEventSource + + +class SimulationMod(AbstractMod): + def __init__(self): + pass + + def start_up(self, env, mod_config): + mod_config.matching_type = self.parse_matching_type(mod_config.matching_type) + if mod_config.commission_multiplier < 0: + raise patch_user_exc(ValueError(_(u"invalid commission multiplier value: value range is [0, +∞)"))) + if env.config.base.margin_multiplier <= 0: + raise patch_user_exc(ValueError(_(u"invalid margin multiplier value: value range is (0, +∞]"))) + + if env.config.base.frequency == "tick": + mod_config.volume_limit = False + if mod_config.matching_type not in [ + MATCHING_TYPE.NEXT_TICK_LAST, + MATCHING_TYPE.NEXT_TICK_BEST_OWN, + MATCHING_TYPE.NEXT_TICK_BEST_COUNTERPARTY, + ]: + raise RuntimeError(_("Not supported matching type {}").format(mod_config.matching_type)) + else: + if mod_config.matching_type not in [ + MATCHING_TYPE.NEXT_BAR_OPEN, + MATCHING_TYPE.CURRENT_BAR_CLOSE, + ]: + raise RuntimeError(_("Not supported matching type {}").format(mod_config.matching_type)) + + if mod_config.signal: + env.set_broker(SignalBroker(env, mod_config)) + else: + env.set_broker(SimulationBroker(env, mod_config)) + + event_source = SimulationEventSource(env, env.config.base.account_list) + env.set_event_source(event_source) + + def tear_down(self, code, exception=None): + pass + + @staticmethod + def parse_matching_type(me_str): + assert isinstance(me_str, six.string_types) + if me_str == "current_bar": + return MATCHING_TYPE.CURRENT_BAR_CLOSE + elif me_str == "next_bar": + return MATCHING_TYPE.NEXT_BAR_OPEN + elif me_str == "last": + return MATCHING_TYPE.NEXT_TICK_LAST + elif me_str == "best_own": + return MATCHING_TYPE.NEXT_TICK_BEST_OWN + elif me_str == "best_counterparty": + return MATCHING_TYPE.NEXT_TICK_BEST_COUNTERPARTY + else: + raise NotImplementedError diff --git a/rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py b/rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py new file mode 100644 index 000000000..5bf82bc34 --- /dev/null +++ b/rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py @@ -0,0 +1,121 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2017 Ricequant, Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np + +from rqalpha.interface import AbstractBroker +from rqalpha.utils.logger import user_system_log +from rqalpha.utils.i18n import gettext as _ +from rqalpha.events import EVENT, Event +from rqalpha.model.trade import Trade +from rqalpha.const import BAR_STATUS, SIDE, ORDER_TYPE + +from .decider import CommissionDecider, SlippageDecider, TaxDecider +from .utils import init_portfolio + + +class SignalBroker(AbstractBroker): + def __init__(self, env, mod_config): + self._env = env + self._commission_decider = CommissionDecider(mod_config.commission_multiplier) + self._slippage_decider = SlippageDecider(mod_config.slippage) + self._tax_decider = TaxDecider() + self._price_limit = mod_config.price_limit + + def get_portfolio(self): + return init_portfolio(self._env) + + def get_open_orders(self, order_book_id=None): + return [] + + def submit_order(self, order): + account = self._env.get_account(order.order_book_id) + self._env.event_bus.publish_event(Event(EVENT.ORDER_PENDING_NEW, account=account, order=order)) + if order.is_final(): + return + order.active() + self._env.event_bus.publish_event(Event(EVENT.ORDER_CREATION_PASS, account=account, order=order)) + self._match(account, order) + + def cancel_order(self, order): + user_system_log.error(_(u"cancel_order function is not supported in signal mode")) + return None + + def _match(self, account, order): + order_book_id = order.order_book_id + price_board = self._env.price_board + + last_price = price_board.get_last_price(order_book_id) + + if np.isnan(last_price): + instrument = self._env.get_instrument(order_book_id) + listed_date = instrument.listed_date.date() + if listed_date == self._env.trading_dt.date(): + reason = _( + "Order Cancelled: current security [{order_book_id}] can not be traded in listed date [{listed_date}]").format( + order_book_id=order_book_id, + listed_date=listed_date, + ) + else: + reason = _(u"Order Cancelled: current bar [{order_book_id}] miss market data.").format( + order_book_id=order_book_id) + order.mark_rejected(reason) + self._env.event_bus.publish_event(Event(EVENT.ORDER_UNSOLICITED_UPDATE, account=account, order=order)) + return + + if order.type == ORDER_TYPE.LIMIT: + deal_price = order.frozen_price + else: + deal_price = last_price + + if self._price_limit: + """ + 在 Signal 模式下,不再阻止涨跌停是否买进,price_limit 参数表示是否给出警告提示。 + """ + if order.side == SIDE.BUY and deal_price >= price_board.get_limit_up(order_book_id): + user_system_log.warning(_(u"You have traded {order_book_id} with {quantity} lots in {bar_status}").format( + order_book_id=order_book_id, + quantity=order.quantity, + bar_status=BAR_STATUS.LIMIT_UP + )) + return + if order.side == SIDE.SELL and deal_price <= price_board.get_limit_down(order_book_id): + user_system_log.warning(_(u"You have traded {order_book_id} with {quantity} lots in {bar_status}").format( + order_book_id=order_book_id, + quantity=order.quantity, + bar_status=BAR_STATUS.LIMIT_DOWN + )) + return + + ct_amount = account.positions.get_or_create(order_book_id).cal_close_today_amount(order.quantity, order.side) + trade_price = self._slippage_decider.get_trade_price(order.side, deal_price) + trade = Trade.__from_create__( + order_id=order.order_id, + calendar_dt=self._env.calendar_dt, + trading_dt=self._env.trading_dt, + price=trade_price, + amount=order.quantity, + side=order.side, + position_effect=order.position_effect, + order_book_id=order_book_id, + frozen_price=order.frozen_price, + close_today_amount=ct_amount + ) + trade._commission = self._commission_decider.get_commission(account.type, trade) + trade._tax = self._tax_decider.get_tax(account.type, trade) + order.fill(trade) + + self._env.event_bus.publish_event(Event(EVENT.TRADE, account=account, trade=trade)) diff --git a/rqalpha/mod/rqalpha_mod_sys_simulation/simulation_broker.py b/rqalpha/mod/rqalpha_mod_sys_simulation/simulation_broker.py new file mode 100644 index 000000000..6b48de677 --- /dev/null +++ b/rqalpha/mod/rqalpha_mod_sys_simulation/simulation_broker.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2017 Ricequant, Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import jsonpickle + +from rqalpha.interface import AbstractBroker, Persistable +from rqalpha.utils.i18n import gettext as _ +from rqalpha.events import EVENT, Event +from rqalpha.const import MATCHING_TYPE, ORDER_STATUS +from rqalpha.model.order import Order + +from .matcher import Matcher +from .utils import init_portfolio + + +class SimulationBroker(AbstractBroker, Persistable): + def __init__(self, env, mod_config): + self._env = env + self._mod_config = mod_config + + self._matcher = Matcher(env, mod_config) + self._match_immediately = mod_config.matching_type == MATCHING_TYPE.CURRENT_BAR_CLOSE + + self._open_orders = [] + self._delayed_orders = [] + self._frontend_validator = {} + + # 该事件会触发策略的before_trading函数 + self._env.event_bus.add_listener(EVENT.BEFORE_TRADING, self.before_trading) + # 该事件会触发策略的handle_bar函数 + self._env.event_bus.add_listener(EVENT.BAR, self.on_bar) + # 该事件会触发策略的handel_tick函数 + self._env.event_bus.add_listener(EVENT.TICK, self.on_tick) + # 该事件会触发策略的after_trading函数 + self._env.event_bus.add_listener(EVENT.AFTER_TRADING, self.after_trading) + + def get_portfolio(self): + return init_portfolio(self._env) + + def get_open_orders(self, order_book_id=None): + if order_book_id is None: + return [order for account, order in self._open_orders] + else: + return [order for account, order in self._open_orders if order.order_book_id == order_book_id] + + def get_state(self): + return jsonpickle.dumps({ + 'open_orders': [o.get_state() for account, o in self._open_orders], + 'delayed_orders': [o.get_state() for account, o in self._delayed_orders] + }).encode('utf-8') + + def set_state(self, state): + value = jsonpickle.loads(state.decode('utf-8')) + for v in value['open_orders']: + o = Order() + o.set_state(v) + account = self._env.get_account(o.order_book_id) + self._open_orders.append((account, o)) + for v in value['delayed_orders']: + o = Order() + o.set_state(v) + account = self._env.get_account(o.order_book_id) + self._delayed_orders.append((account, o)) + + def submit_order(self, order): + account = self._env.get_account(order.order_book_id) + self._env.event_bus.publish_event(Event(EVENT.ORDER_PENDING_NEW, account=account, order=order)) + if order.is_final(): + return + if self._env.config.base.frequency == '1d' and not self._match_immediately: + self._delayed_orders.append((account, order)) + return + self._open_orders.append((account, order)) + order.active() + self._env.event_bus.publish_event(Event(EVENT.ORDER_CREATION_PASS, account=account, order=order)) + if self._match_immediately: + self._match() + + def cancel_order(self, order): + account = self._env.get_account(order.order_book_id) + + self._env.event_bus.publish_event(Event(EVENT.ORDER_PENDING_CANCEL, account=account, order=order)) + + order.mark_cancelled(_(u"{order_id} order has been cancelled by user.").format(order_id=order.order_id)) + + self._env.event_bus.publish_event(Event(EVENT.ORDER_CANCELLATION_PASS, account=account, order=order)) + + try: + self._open_orders.remove((account, order)) + except ValueError: + try: + self._delayed_orders.remove((account, order)) + except ValueError: + pass + + def before_trading(self, event): + for account, order in self._open_orders: + order.active() + self._env.event_bus.publish_event(Event(EVENT.ORDER_CREATION_PASS, account=account, order=order)) + + def after_trading(self, event): + for account, order in self._open_orders: + order.mark_rejected(_(u"Order Rejected: {order_book_id} can not match. Market close.").format( + order_book_id=order.order_book_id + )) + self._env.event_bus.publish_event(Event(EVENT.ORDER_UNSOLICITED_UPDATE, account=account, order=order)) + self._open_orders = self._delayed_orders + self._delayed_orders = [] + + def on_bar(self, event): + self._matcher.update(self._env.calendar_dt, self._env.trading_dt) + self._match() + + def on_tick(self, event): + tick = event.tick + self._matcher.update(self._env.calendar_dt, self._env.trading_dt) + self._match(tick.order_book_id) + + def _match(self, order_book_id=None): + open_orders = self._open_orders + if order_book_id is not None: + open_orders = [(a, o) for (a, o) in self._open_orders if o.order_book_id == order_book_id] + self._matcher.match(open_orders) + final_orders = [(a, o) for a, o in self._open_orders if o.is_final()] + self._open_orders = [(a, o) for a, o in self._open_orders if not o.is_final()] + + for account, order in final_orders: + if order.status == ORDER_STATUS.REJECTED or order.status == ORDER_STATUS.CANCELLED: + self._env.event_bus.publish_event(Event(EVENT.ORDER_UNSOLICITED_UPDATE, account=account, order=order)) diff --git a/rqalpha/mod/simulation/simulation_event_source.py b/rqalpha/mod/rqalpha_mod_sys_simulation/simulation_event_source.py similarity index 64% rename from rqalpha/mod/simulation/simulation_event_source.py rename to rqalpha/mod/rqalpha_mod_sys_simulation/simulation_event_source.py index cafdd15e3..da6494d15 100644 --- a/rqalpha/mod/simulation/simulation_event_source.py +++ b/rqalpha/mod/rqalpha_mod_sys_simulation/simulation_event_source.py @@ -18,11 +18,12 @@ from rqalpha.interface import AbstractEventSource from rqalpha.events import Event, EVENT -from rqalpha.environment import Environment from rqalpha.utils import get_account_type from rqalpha.utils.exception import CustomException, CustomError, patch_user_exc from rqalpha.utils.datetime_func import convert_int_to_datetime from rqalpha.const import ACCOUNT_TYPE +from rqalpha.utils.i18n import gettext as _ + ONE_MINUTE = datetime.timedelta(minutes=1) @@ -32,19 +33,20 @@ def __init__(self, env, account_list): self._env = env self._account_list = account_list self._universe_changed = False - Environment.get_instance().event_bus.add_listener(EVENT.POST_UNIVERSE_CHANGED, self._on_universe_changed) + self._env.event_bus.add_listener(EVENT.POST_UNIVERSE_CHANGED, self._on_universe_changed) - def _on_universe_changed(self, universe): + def _on_universe_changed(self, event): self._universe_changed = True def _get_universe(self): - universe = Environment.get_instance().universe + universe = self._env.get_universe() if len(universe) == 0 and ACCOUNT_TYPE.STOCK not in self._account_list: error = CustomError() error.set_msg("Current universe is empty. Please use subscribe function before trade") raise patch_user_exc(CustomException(error)) return universe + # [BEGIN] minute event helper @staticmethod def _get_stock_trading_minutes(trading_date): trading_minutes = set() @@ -80,6 +82,7 @@ def _get_trading_minutes(self, trading_date): elif account_type == ACCOUNT_TYPE.FUTURE: trading_minutes = trading_minutes.union(self._get_future_trading_minutes(trading_date)) return sorted(list(trading_minutes)) + # [END] minute event helper def events(self, start_date, end_date, frequency): if frequency == "1d": @@ -90,12 +93,12 @@ def events(self, start_date, end_date, frequency): dt_bar = date.replace(hour=15, minute=0) dt_after_trading = date.replace(hour=15, minute=30) dt_settlement = date.replace(hour=17, minute=0) - yield Event(EVENT.BEFORE_TRADING, dt_before_trading, dt_before_trading) - yield Event(EVENT.BAR, dt_bar, dt_bar) + yield Event(EVENT.BEFORE_TRADING, calendar_dt=dt_before_trading, trading_dt=dt_before_trading) + yield Event(EVENT.BAR, calendar_dt=dt_bar, trading_dt=dt_bar) - yield Event(EVENT.AFTER_TRADING, dt_after_trading, dt_after_trading) - yield Event(EVENT.SETTLEMENT, dt_settlement, dt_settlement) - else: + yield Event(EVENT.AFTER_TRADING, calendar_dt=dt_after_trading, trading_dt=dt_after_trading) + yield Event(EVENT.SETTLEMENT, calendar_dt=dt_settlement, trading_dt=dt_settlement) + elif frequency == '1m': for day in self._env.data_proxy.get_trading_dates(start_date, end_date): before_trading_flag = True date = day.to_pydatetime() @@ -122,19 +125,60 @@ def events(self, start_date, end_date, frequency): if before_trading_flag: before_trading_flag = False before_trading_dt = trading_dt - datetime.timedelta(minutes=30) - yield Event(EVENT.BEFORE_TRADING, before_trading_dt, before_trading_dt) + yield Event(EVENT.BEFORE_TRADING, calendar_dt=before_trading_dt, + trading_dt=before_trading_dt) if self._universe_changed: self._universe_changed = False last_dt = calendar_dt exit_loop = False break # yield handle bar - yield Event(EVENT.BAR, calendar_dt, trading_dt) + yield Event(EVENT.BAR, calendar_dt=calendar_dt, trading_dt=trading_dt) if exit_loop: done = True dt = date.replace(hour=15, minute=30) - yield Event(EVENT.AFTER_TRADING, dt, dt) + yield Event(EVENT.AFTER_TRADING, calendar_dt=dt, trading_dt=dt) + + dt = date.replace(hour=17, minute=0) + yield Event(EVENT.SETTLEMENT, calendar_dt=dt, trading_dt=dt) + elif frequency == "tick": + data_proxy = self._env.data_proxy + for day in data_proxy.get_trading_dates(start_date, end_date): + date = day.to_pydatetime() + last_tick = None + last_dt = None + dt_before_day_trading = date.replace(hour=8, minute=30) + while True: + for tick in data_proxy.get_merge_ticks(self._get_universe(), date, last_dt): + # find before trading time + if last_tick is None: + last_tick = tick + dt = tick.datetime + before_trading_dt = dt - datetime.timedelta(minutes=30) + yield Event(EVENT.BEFORE_TRADING, calendar_dt=before_trading_dt, + trading_dt=before_trading_dt) + + dt = tick.datetime + + if dt < dt_before_day_trading: + trading_dt = dt.replace(year=date.year, month=date.month, day=date.day) + else: + trading_dt = dt + + yield Event(EVENT.TICK, calendar_dt=dt, trading_dt=trading_dt, tick=tick) + + if self._universe_changed: + self._universe_changed = False + last_dt = dt + break + else: + break + + dt = date.replace(hour=15, minute=30) + yield Event(EVENT.AFTER_TRADING, calendar_dt=dt, trading_dt=dt) dt = date.replace(hour=17, minute=0) - yield Event(EVENT.SETTLEMENT, dt, dt) + yield Event(EVENT.SETTLEMENT, calendar_dt=dt, trading_dt=dt) + else: + raise NotImplementedError(_("Frequency {} is not support.").format(frequency)) diff --git a/rqalpha/mod/rqalpha_mod_sys_simulation/utils.py b/rqalpha/mod/rqalpha_mod_sys_simulation/utils.py new file mode 100644 index 000000000..630b2c761 --- /dev/null +++ b/rqalpha/mod/rqalpha_mod_sys_simulation/utils.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2017 Ricequant, Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from rqalpha.const import ACCOUNT_TYPE +from rqalpha.model.account import StockAccount, FutureAccount +from rqalpha.model.position import Positions, StockPosition, FuturePosition +from rqalpha.model.portfolio import Portfolio + + +def init_portfolio(env): + accounts = {} + config = env.config + start_date = config.base.start_date + total_cash = 0 + for account_type in config.base.account_list: + if account_type == ACCOUNT_TYPE.STOCK: + stock_starting_cash = config.base.stock_starting_cash + accounts[ACCOUNT_TYPE.STOCK] = StockAccount(stock_starting_cash, Positions(StockPosition)) + total_cash += stock_starting_cash + elif account_type == ACCOUNT_TYPE.FUTURE: + future_starting_cash = config.base.future_starting_cash + accounts[ACCOUNT_TYPE.FUTURE] = FutureAccount(future_starting_cash, Positions(FuturePosition)) + total_cash += future_starting_cash + else: + raise NotImplementedError + return Portfolio(start_date, 1, total_cash, accounts) diff --git a/rqalpha/mod/rqalpha_mod_sys_stock_realtime/README.rst b/rqalpha/mod/rqalpha_mod_sys_stock_realtime/README.rst new file mode 100644 index 000000000..548ad1676 --- /dev/null +++ b/rqalpha/mod/rqalpha_mod_sys_stock_realtime/README.rst @@ -0,0 +1,32 @@ +=============================== +sys_stock_realtime Mod +=============================== + +RQAlpha 接受实时行情并触发事件 Mod + +该模块目前只是一个初级的 Demo,用于展示如何接入自有行情进行回测/模拟/实盘 + +该模块是系统模块,不可删除 + +开启或关闭 Mod + +=============================== + +.. code-block:: bash + + # 关闭策略分析 Mod + $ rqalpha mod disable sys_stock_realtime + + # 启用策略分析 Mod + $ rqalpha mod enable sys_stock_realtime + +使用方式 +=============================== + +在启动该 Mod 的情况下, + +使用 :code:`--run-type` 或者 :code:`-rt` 为 :code:`p` (PaperTrading),就可以激活改 mod。 + +.. code-block:: bash + + rqalpha run -fq 1m -rt p -f ~/tmp/test_a.py -sc 100000 -l verbose -mc sys_stock_realtime.enabled True diff --git a/rqalpha/mod/simple_stock_realtime_trade/__init__.py b/rqalpha/mod/rqalpha_mod_sys_stock_realtime/__init__.py similarity index 82% rename from rqalpha/mod/simple_stock_realtime_trade/__init__.py rename to rqalpha/mod/rqalpha_mod_sys_stock_realtime/__init__.py index e8f8d5887..bd1b5f7a5 100644 --- a/rqalpha/mod/simple_stock_realtime_trade/__init__.py +++ b/rqalpha/mod/rqalpha_mod_sys_stock_realtime/__init__.py @@ -14,8 +14,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .mod import RealtimeTradeMod + +__config__ = { + "priority": 200, + "persist_path": "./persist/strategy/", + "fps": 3, +} def load_mod(): + from .mod import RealtimeTradeMod return RealtimeTradeMod() diff --git a/rqalpha/mod/analyser/__init__.py b/rqalpha/mod/rqalpha_mod_sys_stock_realtime/data_board.py similarity index 89% rename from rqalpha/mod/analyser/__init__.py rename to rqalpha/mod/rqalpha_mod_sys_stock_realtime/data_board.py index 59809a41e..dacc25766 100644 --- a/rqalpha/mod/analyser/__init__.py +++ b/rqalpha/mod/rqalpha_mod_sys_stock_realtime/data_board.py @@ -14,8 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .mod import AnalyserMod +import pandas as pd -def load_mod(): - return AnalyserMod() +realtime_quotes_df = pd.DataFrame() diff --git a/rqalpha/mod/simple_stock_realtime_trade/data_source.py b/rqalpha/mod/rqalpha_mod_sys_stock_realtime/data_source.py similarity index 71% rename from rqalpha/mod/simple_stock_realtime_trade/data_source.py rename to rqalpha/mod/rqalpha_mod_sys_stock_realtime/data_source.py index 629ab4639..371ab9a5a 100644 --- a/rqalpha/mod/simple_stock_realtime_trade/data_source.py +++ b/rqalpha/mod/rqalpha_mod_sys_stock_realtime/data_source.py @@ -16,31 +16,27 @@ import datetime -import pandas as pd - from rqalpha.data.base_data_source import BaseDataSource from rqalpha.environment import Environment from rqalpha.model.snapshot import SnapshotObject +from . import data_board + class DataSource(BaseDataSource): def __init__(self, path): super(DataSource, self).__init__(path) self._env = Environment.get_instance() - self.realtime_quotes_df = pd.DataFrame() def get_bar(self, instrument, dt, frequency): - # if frequency == '1d': - # return super(DataSource, self).get_bar(instrument, dt, frequency) - - # FIXME: 目前这样仅仅给撮合引擎用,不是定义的bar - # FIXME: self.realtime_quotes_df 是从 event_source.py 那边「飞线」设置过来的 - bar = self.realtime_quotes_df.loc[instrument.order_book_id].to_dict() - + bar = data_board.realtime_quotes_df.loc[instrument.order_book_id].to_dict() return bar + def get_last_price(self, instrument, dt): + return data_board.realtime_quotes_df.loc[instrument.order_book_id]['last'] + def current_snapshot(self, instrument, frequency, dt): - snapshot_dict = self.realtime_quotes_df.loc[instrument.order_book_id].to_dict() + snapshot_dict = data_board.realtime_quotes_df.loc[instrument.order_book_id].to_dict() snapshot_dict["last"] = snapshot_dict["price"] return SnapshotObject(instrument, snapshot_dict) diff --git a/rqalpha/mod/simple_stock_realtime_trade/event_source.py b/rqalpha/mod/rqalpha_mod_sys_stock_realtime/event_source.py similarity index 77% rename from rqalpha/mod/simple_stock_realtime_trade/event_source.py rename to rqalpha/mod/rqalpha_mod_sys_stock_realtime/event_source.py index 6f0834088..dd63f866b 100644 --- a/rqalpha/mod/simple_stock_realtime_trade/event_source.py +++ b/rqalpha/mod/rqalpha_mod_sys_stock_realtime/event_source.py @@ -24,12 +24,13 @@ from rqalpha.environment import Environment from rqalpha.utils.logger import system_log from rqalpha.events import Event, EVENT -from rqalpha.execution_context import ExecutionContext -from rqalpha.utils import json as json_utils +from rqalpha.utils import rq_json from .utils import get_realtime_quotes, order_book_id_2_tushare_code, is_holiday_today, is_tradetime_now +from . import data_board class RealtimeEventSource(AbstractEventSource): + MARKET_DATA_EVENT = "RealtimeEventSource.MARKET_DATA_EVENT" def __init__(self, fps): self._env = Environment.get_instance() @@ -38,32 +39,35 @@ def __init__(self, fps): self.before_trading_fire_date = datetime.date(2000, 1, 1) self.after_trading_fire_date = datetime.date(2000, 1, 1) - - self.clock_engine_thread = Thread(target=self.clock_worker) - self.clock_engine_thread.daemon = True + self.settlement_fire_date = datetime.date(2000, 1, 1) self.quotation_engine_thread = Thread(target=self.quotation_worker) self.quotation_engine_thread.daemon = True + self.clock_engine_thread = Thread(target=self.clock_worker) + self.clock_engine_thread.daemon = True + def set_state(self, state): - persist_dict = json_utils.convert_json_to_dict(state.decode('utf-8')) + persist_dict = rq_json.convert_json_to_dict(state.decode('utf-8')) self.before_trading_fire_date = persist_dict['before_trading_fire_date'] self.after_trading_fire_date = persist_dict['after_trading_fire_date'] + self.settlement_fire_date = persist_dict['settlement_fire_date'] def get_state(self): - return json_utils.convert_dict_to_json({ + return rq_json.convert_dict_to_json({ "before_trading_fire_date": self.before_trading_fire_date, "after_trading_fire_date": self.after_trading_fire_date, + "settlement_fire_date": self.settlement_fire_date, }).encode('utf-8') def quotation_worker(self): while True: if not is_holiday_today() and is_tradetime_now(): - order_book_id_list = sorted(ExecutionContext.data_proxy.all_instruments("CS").order_book_id.tolist()) + order_book_id_list = sorted(Environment.get_instance().data_proxy.all_instruments("CS").order_book_id.tolist()) code_list = [order_book_id_2_tushare_code(code) for code in order_book_id_list] try: - self._env.data_source.realtime_quotes_df = get_realtime_quotes(code_list) + data_board.realtime_quotes_df = get_realtime_quotes(code_list) except Exception as e: system_log.exception("get_realtime_quotes fail") continue @@ -73,7 +77,7 @@ def quotation_worker(self): def clock_worker(self): while True: # wait for the first data ready - if not self._env.data_source.realtime_quotes_df.empty: + if not data_board.realtime_quotes_df.empty: break time.sleep(0.1) @@ -92,6 +96,10 @@ def clock_worker(self): elif dt.strftime("%H:%M:%S") >= "15:10:00" and dt.date() > self.after_trading_fire_date: self.event_queue.put((dt, EVENT.AFTER_TRADING)) self.after_trading_fire_date = dt.date() + elif dt.strftime("%H:%M:%S") >= "15:10:00" and dt.date() > self.settlement_fire_date: + #or (dt.date()-self.settlement_fire_date).days >= 2: + self.event_queue.put((dt, EVENT.SETTLEMENT)) + self.settlement_fire_date = dt.date() if is_tradetime_now(): self.event_queue.put((dt, EVENT.BAR)) @@ -112,4 +120,4 @@ def events(self, start_date, end_date, frequency): continue system_log.debug("real_dt {}, dt {}, event {}", real_dt, dt, event_type) - yield Event(event_type, real_dt, dt) + yield Event(event_type, calendar_dt=real_dt, trading_dt=dt) diff --git a/rqalpha/mod/simple_stock_realtime_trade/mod.py b/rqalpha/mod/rqalpha_mod_sys_stock_realtime/mod.py similarity index 98% rename from rqalpha/mod/simple_stock_realtime_trade/mod.py rename to rqalpha/mod/rqalpha_mod_sys_stock_realtime/mod.py index 81b6acc84..8794d86e0 100644 --- a/rqalpha/mod/simple_stock_realtime_trade/mod.py +++ b/rqalpha/mod/rqalpha_mod_sys_stock_realtime/mod.py @@ -30,6 +30,7 @@ def start_up(self, env, mod_config): env.set_data_source(DataSource(env.config.base.data_bundle_path)) env.set_event_source(RealtimeEventSource(mod_config.fps)) + # add persist persist_provider = DiskPersistProvider(mod_config.persist_path) env.set_persist_provider(persist_provider) diff --git a/rqalpha/mod/simple_stock_realtime_trade/utils.py b/rqalpha/mod/rqalpha_mod_sys_stock_realtime/utils.py similarity index 84% rename from rqalpha/mod/simple_stock_realtime_trade/utils.py rename to rqalpha/mod/rqalpha_mod_sys_stock_realtime/utils.py index 46e470195..d2295aa1c 100644 --- a/rqalpha/mod/simple_stock_realtime_trade/utils.py +++ b/rqalpha/mod/rqalpha_mod_sys_stock_realtime/utils.py @@ -17,22 +17,15 @@ import math import time import datetime -try: - from functools import lru_cache -except Exception as e: - from fastcache import lru_cache - from six.moves import reduce -from rqalpha.environment import Environment from rqalpha.utils.datetime_func import convert_dt_to_int def is_holiday_today(): today = datetime.date.today() - df = Environment.get_instance().data_proxy.get_trading_dates(today, today) - - return len(df) == 0 + from rqalpha.environment import Environment + return not Environment.get_instance().data_proxy.is_trading_date(today) def is_tradetime_now(): @@ -86,9 +79,8 @@ def get_realtime_quotes(code_list, open_only=False): index_df["code"] = index_symbol index_df["is_index"] = True total_df = total_df.append(index_df) - total_df = total_df.set_index("code").sort_index() - columns = set(total_df.columns) - set(["name", "time", "date"]) + columns = set(total_df.columns) - set(["name", "time", "date", "code"]) # columns = filter(lambda x: "_v" not in x, columns) for label in columns: total_df[label] = total_df[label].map(lambda x: 0 if str(x).strip() == "" else x) @@ -96,13 +88,19 @@ def get_realtime_quotes(code_list, open_only=False): total_df["chg"] = total_df["price"] / total_df["pre_close"] - 1 - total_df["order_book_id"] = total_df.index + total_df["order_book_id"] = total_df["code"] total_df["order_book_id"] = total_df["order_book_id"].apply(tushare_code_2_order_book_id) + total_df = total_df.set_index("order_book_id").sort_index() + total_df["datetime"] = total_df["date"] + " " + total_df["time"] total_df["datetime"] = total_df["datetime"].apply(lambda x: convert_dt_to_int(datetime.datetime.strptime(x, "%Y-%m-%d %H:%M:%S"))) total_df["close"] = total_df["price"] + total_df["last"] = total_df["price"] + + total_df["limit_up"] = total_df.apply(lambda row: row.pre_close * (1.1 if "ST" not in row["name"] else 1.05), axis=1).round(2) + total_df["limit_down"] = total_df.apply(lambda row: row.pre_close * (0.9 if "ST" not in row["name"] else 0.95), axis=1).round(2) if open_only: total_df = total_df[total_df.open > 0] diff --git a/rqalpha/mod/simple_stock_realtime_trade/README.md b/rqalpha/mod/simple_stock_realtime_trade/README.md deleted file mode 100644 index cd49854b5..000000000 --- a/rqalpha/mod/simple_stock_realtime_trade/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# Simple A Stock Realtime Trade Mod -使用该Mod可以接收实时行情进行触发。用于 RQAlpha 实时模拟交易,实盘交易。 - -这个是一个初级的DEMO。 - -使用`--run-type`或者`-rt`为`p`(PaperTrading),就可以激活改 mod。 - -``` -rqalpha run -fq 1m -rt p -f ~/tmp/test_a.py -sc 100000 -l verbose -``` diff --git a/rqalpha/mod/simulation/matcher.py b/rqalpha/mod/simulation/matcher.py deleted file mode 100644 index 6aefffab7..000000000 --- a/rqalpha/mod/simulation/matcher.py +++ /dev/null @@ -1,148 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2017 Ricequant, Inc -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections import defaultdict - -from rqalpha.utils.i18n import gettext as _ -from rqalpha.const import ORDER_TYPE, SIDE, BAR_STATUS -from rqalpha.model.trade import Trade -from rqalpha.environment import Environment -from rqalpha.events import EVENT - - -class Matcher(object): - def __init__(self, - deal_price_decider, - bar_limit=True, - volume_percent=0.25): - self._board = None - self._turnover = defaultdict(int) - self._calendar_dt = None - self._trading_dt = None - self._deal_price_decider = deal_price_decider - self._volume_percent = volume_percent - self._bar_limit = bar_limit - - def update(self, calendar_dt, trading_dt, bar_dict): - self._board = bar_dict - self._turnover.clear() - self._calendar_dt = calendar_dt - self._trading_dt = trading_dt - - def match(self, open_orders): - for account, order in open_orders: - slippage_decider = account.slippage_decider - commission_decider = account.commission_decider - tax_decider = account.tax_decider - - bar = self._board[order.order_book_id] - bar_status = bar._bar_status - - if bar_status == BAR_STATUS.ERROR: - listed_date = bar.instrument.listed_date.date() - if listed_date == self._trading_dt.date(): - reason = _("Order Cancelled: current security [{order_book_id}] can not be traded in listed date [{listed_date}]").format( - order_book_id=order.order_book_id, - listed_date=listed_date, - ) - else: - reason = _("Order Cancelled: current bar [{order_book_id}] miss market data.").format( - order_book_id=order.order_book_id) - order._mark_rejected(reason) - continue - - deal_price = self._deal_price_decider(bar) - if order.type == ORDER_TYPE.LIMIT: - if order.price > bar.limit_up: - reason = _( - "Order Rejected: limit order price {limit_price} is higher than limit up {limit_up}." - ).format( - limit_price=order.price, - limit_up=bar.limit_up - ) - order._mark_rejected(reason) - continue - - if order.price < bar.limit_down: - reason = _( - "Order Rejected: limit order price {limit_price} is lower than limit down {limit_down}." - ).format( - limit_price=order.price, - limit_down=bar.limit_down - ) - order._mark_rejected(reason) - continue - - if order.side == SIDE.BUY and order.price < deal_price: - continue - if order.side == SIDE.SELL and order.price > deal_price: - continue - else: - if self._bar_limit and order.side == SIDE.BUY and bar_status == BAR_STATUS.LIMIT_UP: - reason = _( - "Order Cancelled: current bar [{order_book_id}] reach the limit_up price." - ).format(order_book_id=order.order_book_id) - order._mark_rejected(reason) - continue - elif self._bar_limit and order.side == SIDE.SELL and bar_status == BAR_STATUS.LIMIT_DOWN: - reason = _( - "Order Cancelled: current bar [{order_book_id}] reach the limit_down price." - ).format(order_book_id=order.order_book_id) - order._mark_rejected(reason) - continue - - if self._bar_limit: - if order.side == SIDE.BUY and bar_status == BAR_STATUS.LIMIT_UP: - continue - if order.side == SIDE.SELL and bar_status == BAR_STATUS.LIMIT_DOWN: - continue - - volume_limit = round(bar.volume * self._volume_percent) - self._turnover[order.order_book_id] - round_lot = bar.instrument.round_lot - volume_limit = (volume_limit // round_lot) * round_lot - if volume_limit <= 0: - if order.type == ORDER_TYPE.MARKET: - reason = _('Order Cancelled: market order {order_book_id} volume {order_volume}' - ' due to volume limit').format( - order_book_id=order.order_book_id, - order_volume=order.quantity - ) - order._mark_cancelled(reason) - continue - - unfilled = order.unfilled_quantity - fill = min(unfilled, volume_limit) - ct_amount = account.portfolio.positions[order.order_book_id]._cal_close_today_amount(fill, order.side) - price = slippage_decider.get_trade_price(order, deal_price) - trade = Trade.__from_create__(order=order, calendar_dt=self._calendar_dt, trading_dt=self._trading_dt, - price=price, amount=fill, close_today_amount=ct_amount) - trade._commission = commission_decider.get_commission(trade) - trade._tax = tax_decider.get_tax(trade) - order._fill(trade) - self._turnover[order.order_book_id] += fill - - Environment.get_instance().event_bus.publish_event(EVENT.TRADE, account, trade) - - if order.type == ORDER_TYPE.MARKET and order.unfilled_quantity != 0: - reason = _( - "Order Cancelled: market order {order_book_id} volume {order_volume} is" - " larger than 25 percent of current bar volume, fill {filled_volume} actually" - ).format( - order_book_id=order.order_book_id, - order_volume=order.quantity, - filled_volume=order.filled_quantity - ) - order._mark_cancelled(reason) diff --git a/rqalpha/mod/simulation/simulation_broker.py b/rqalpha/mod/simulation/simulation_broker.py deleted file mode 100644 index 76256a8bc..000000000 --- a/rqalpha/mod/simulation/simulation_broker.py +++ /dev/null @@ -1,176 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2017 Ricequant, Inc -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import jsonpickle - -from rqalpha.interface import AbstractBroker, Persistable -from rqalpha.utils import get_account_type -from rqalpha.utils.i18n import gettext as _ -from rqalpha.events import EVENT -from rqalpha.const import MATCHING_TYPE, ORDER_STATUS -from rqalpha.const import ACCOUNT_TYPE -from rqalpha.environment import Environment -from rqalpha.model.account import BenchmarkAccount, StockAccount, FutureAccount - -from .matcher import Matcher - - -def init_accounts(env): - accounts = {} - config = env.config - start_date = config.base.start_date - total_cash = 0 - for account_type in config.base.account_list: - if account_type == ACCOUNT_TYPE.STOCK: - stock_starting_cash = config.base.stock_starting_cash - accounts[ACCOUNT_TYPE.STOCK] = StockAccount(env, stock_starting_cash, start_date) - total_cash += stock_starting_cash - elif account_type == ACCOUNT_TYPE.FUTURE: - future_starting_cash = config.base.future_starting_cash - accounts[ACCOUNT_TYPE.FUTURE] = FutureAccount(env, future_starting_cash, start_date) - total_cash += future_starting_cash - else: - raise NotImplementedError - if config.base.benchmark is not None: - accounts[ACCOUNT_TYPE.BENCHMARK] = BenchmarkAccount(env, total_cash, start_date) - - return accounts - - -class SimulationBroker(AbstractBroker, Persistable): - def __init__(self, env): - self._env = env - if env.config.base.matching_type == MATCHING_TYPE.CURRENT_BAR_CLOSE: - self._matcher = Matcher(lambda bar: bar.close, env.config.validator.bar_limit) - self._match_immediately = True - else: - self._matcher = Matcher(lambda bar: bar.open, env.config.validator.bar_limit) - self._match_immediately = False - - self._accounts = None - self._open_orders = [] - self._board = None - self._turnover = {} - self._delayed_orders = [] - self._frontend_validator = {} - - # 该事件会触发策略的before_trading函数 - self._env.event_bus.add_listener(EVENT.BEFORE_TRADING, self.before_trading) - # 该事件会触发策略的handle_bar函数 - self._env.event_bus.add_listener(EVENT.BAR, self.bar) - # 该事件会触发策略的handel_tick函数 - self._env.event_bus.add_listener(EVENT.TICK, self.tick) - # 该事件会触发策略的after_trading函数 - self._env.event_bus.add_listener(EVENT.AFTER_TRADING, self.after_trading) - - def get_accounts(self): - if self._accounts is None: - self._accounts = init_accounts(self._env) - return self._accounts - - def get_open_orders(self): - return self._open_orders - - def get_state(self): - return jsonpickle.dumps([o.order_id for _, o in self._delayed_orders]).encode('utf-8') - - def set_state(self, state): - delayed_orders = jsonpickle.loads(state.decode('utf-8')) - for account in self._accounts.values(): - for o in account.daily_orders.values(): - if not o._is_final(): - if o.order_id in delayed_orders: - self._delayed_orders.append((account, o)) - else: - self._open_orders.append((account, o)) - - def _get_account_for(self, order_book_id): - account_type = get_account_type(order_book_id) - return self._accounts[account_type] - - def submit_order(self, order): - account = self._get_account_for(order.order_book_id) - - self._env.event_bus.publish_event(EVENT.ORDER_PENDING_NEW, account, order) - - account.append_order(order) - if order._is_final(): - return - - # account.on_order_creating(order) - if self._env.config.base.frequency == '1d' and not self._match_immediately: - self._delayed_orders.append((account, order)) - return - - self._open_orders.append((account, order)) - order._active() - self._env.event_bus.publish_event(EVENT.ORDER_CREATION_PASS, account, order) - if self._match_immediately: - self._match() - - def cancel_order(self, order): - account = self._get_account_for(order.order_book_id) - - self._env.event_bus.publish_event(EVENT.ORDER_PENDING_CANCEL, account, order) - - # account.on_order_cancelling(order) - order._mark_cancelled(_("{order_id} order has been cancelled by user.").format(order_id=order.order_id)) - - self._env.event_bus.publish_event(EVENT.ORDER_CANCELLATION_PASS, account, order) - - # account.on_order_cancellation_pass(order) - try: - self._open_orders.remove((account, order)) - except ValueError: - try: - self._delayed_orders.remove((account, order)) - except ValueError: - pass - - def before_trading(self): - for account, order in self._open_orders: - order._active() - self._env.event_bus.publish_event(EVENT.ORDER_CREATION_PASS, account, order) - - def after_trading(self): - for account, order in self._open_orders: - order._mark_rejected(_("Order Rejected: {order_book_id} can not match. Market close.").format( - order_book_id=order.order_book_id - )) - self._env.event_bus.publish_event(EVENT.ORDER_UNSOLICITED_UPDATE, account, order) - self._open_orders = self._delayed_orders - self._delayed_orders = [] - - def bar(self, bar_dict): - env = Environment.get_instance() - self._matcher.update(env.calendar_dt, env.trading_dt, bar_dict) - self._match() - - def tick(self, tick): - # TODO support tick matching - pass - # env = Environment.get_instance() - # self._matcher.update(env.calendar_dt, env.trading_dt, tick) - # self._match() - - def _match(self): - self._matcher.match(self._open_orders) - final_orders = [(a, o) for a, o in self._open_orders if o._is_final()] - self._open_orders = [(a, o) for a, o in self._open_orders if not o._is_final()] - - for account, order in final_orders: - if order.status == ORDER_STATUS.REJECTED or order.status == ORDER_STATUS.CANCELLED: - self._env.event_bus.publish_event(EVENT.ORDER_UNSOLICITED_UPDATE, account, order) diff --git a/rqalpha/mod_config_template.yml b/rqalpha/mod_config_template.yml new file mode 100644 index 000000000..5c27f3dd9 --- /dev/null +++ b/rqalpha/mod_config_template.yml @@ -0,0 +1,19 @@ +# WARNING: DO NOT EDIT + +mod: + # 回测 / 模拟交易 支持 Mod + sys_simulation: + enabled: true + # 开启该选项,可以在命令行查看回测进度 + sys_progress: + enabled: true + sys_risk: + enabled: true + sys_analyser: + enabled: true + # 技术分析API + sys_funcat: + enabled: false + # 接收实时行情运行 + sys_stock_realtime: + enabled: false diff --git a/rqalpha/model/account/__init__.py b/rqalpha/model/account/__init__.py index 6d2e5f9d5..4335d2797 100644 --- a/rqalpha/model/account/__init__.py +++ b/rqalpha/model/account/__init__.py @@ -15,6 +15,6 @@ # limitations under the License. from .benchmark_account import BenchmarkAccount -from .mixed_account import MixedAccount -from .stock_account import StockAccount from .future_account import FutureAccount +from .stock_account import StockAccount + diff --git a/rqalpha/model/account/base_account.py b/rqalpha/model/account/base_account.py index 6b0e516fb..9e6d77bc5 100644 --- a/rqalpha/model/account/base_account.py +++ b/rqalpha/model/account/base_account.py @@ -15,156 +15,127 @@ # limitations under the License. import six -from collections import OrderedDict - -from ..portfolio import init_portfolio -from ..slippage import init_slippage -from ..tax import init_tax -from ..trade import Trade -from ..order import Order -from ...execution_context import ExecutionContext -from ...interface import Persistable -from ...utils import json as json_utils -from ...events import EVENT - - -class BaseAccount(Persistable): - def __init__(self, env, init_cash, start_date, account_type): - self._account_type = account_type - self._env = env - self.config = env.config - - self.portfolio = init_portfolio(init_cash, start_date, account_type) - self.slippage_decider = init_slippage(env.config.base.slippage) - commission_initializer = env._commission_initializer - self.commission_decider = commission_initializer(self._account_type, env.config.base.commission_multiplier) - self.tax_decider = init_tax(self._account_type) - - self.all_portfolios = OrderedDict() - self.daily_orders = {} - self.daily_trades = [] - self._last_trade_id = 0 - - # 该事件会触发策略的before_trading函数 - self._env.event_bus.add_listener(EVENT.BEFORE_TRADING, self.before_trading) - # 该事件会触发策略的handle_bar函数 - self._env.event_bus.add_listener(EVENT.BAR, self.bar) - # 该事件会触发策略的handel_tick函数 - self._env.event_bus.add_listener(EVENT.TICK, self.tick) - # 该事件会触发策略的after_trading函数 - self._env.event_bus.add_listener(EVENT.AFTER_TRADING, self.after_trading) - # 触发结算事件 - self._env.event_bus.add_listener(EVENT.SETTLEMENT, self.settlement) - - # 创建订单 - self._env.event_bus.add_listener(EVENT.ORDER_PENDING_NEW, self.order_pending_new) - # 创建订单成功 - self._env.event_bus.add_listener(EVENT.ORDER_CREATION_PASS, self.order_creation_pass) - # 创建订单失败 - self._env.event_bus.add_listener(EVENT.ORDER_CREATION_REJECT, self.order_creation_reject) - # 创建撤单 - self._env.event_bus.add_listener(EVENT.ORDER_PENDING_CANCEL, self.order_pending_cancel) - # 撤销订单成功 - self._env.event_bus.add_listener(EVENT.ORDER_CANCELLATION_PASS, self.order_cancellation_pass) - # 撤销订单失败 - self._env.event_bus.add_listener(EVENT.ORDER_CANCELLATION_REJECT, self.order_cancellation_reject) - # 订单状态更新 - self._env.event_bus.add_listener(EVENT.ORDER_UNSOLICITED_UPDATE, self.order_unsolicited_update) - # 成交 - self._env.event_bus.add_listener(EVENT.TRADE, self.trade) - - def set_state(self, state): - persist_dict = json_utils.convert_json_to_dict(state.decode('utf-8')) - self.portfolio.restore_from_dict_(persist_dict['portfolio']) - - del self.daily_trades[:] - self.daily_orders.clear() - - for order_id, order_dict in six.iteritems(persist_dict["daily_orders"]): - self.daily_orders[order_id] = Order.__from_dict__(order_dict) - - for trade_dict in persist_dict["daily_trades"]: - trade = Trade.__from_dict__(trade_dict, self.daily_orders[str(trade_dict["_order_id"])]) - self.daily_trades.append(trade) - - if 'last_trade_id' in persist_dict: - self._last_trade_id = persist_dict['last_trade_id'] +from ...utils.i18n import gettext as _ +from ...utils.logger import user_system_log + + +class BaseAccount(object): + def __init__(self, total_cash, positions, backward_trade_set=set(), register_event=True): + self._positions = positions + self._frozen_cash = 0 + self._total_cash = total_cash + self._backward_trade_set = backward_trade_set + self._transaction_cost = 0 + if register_event: + self.register_event() + + def register_event(self): + """ + 注册事件 + """ + raise NotImplementedError + + def fast_forward(self, orders, trades=list()): + """ + 同步账户信息至最新状态 + :param orders: 订单列表,主要用来计算frozen_cash,如果为None则不计算frozen_cash + :param trades: 交易列表,基于Trades 将当前Positions ==> 最新Positions + """ + raise NotImplementedError + + @property + def type(self): + """ + [enum] 账户类型 + """ + raise NotImplementedError + + @property + def total_value(self): + """ + [float]总权益 + """ + raise NotImplementedError def get_state(self): - return json_utils.convert_dict_to_json(self.__to_dict__()).encode('utf-8') - - def __to_dict__(self): - account_dict = { - "portfolio": self.portfolio.__to_dict__(), - 'last_trade_id': self._last_trade_id, - "daily_orders": {order_id: order.__to_dict__() for order_id, order in six.iteritems(self.daily_orders)}, - "daily_trades": [trade.__to_dict__() for trade in self.daily_trades], - } - return account_dict - - def portfolio_persist(self): - trading_date = ExecutionContext.get_current_trading_dt().date() - self.all_portfolios[trading_date] = self.portfolio._clone() - - def get_portfolio(self, trading_date): - return self.all_portfolios[trading_date] - - def append_order(self, order, is_active=True): - self.daily_orders[order.order_id] = order - - def append_trade(self, trade): - self.daily_trades.append(trade) - - def get_open_orders(self): - return {order.order_id: order for order in self.daily_orders.values() if order._is_active()} - - def get_order(self, order_id): - return self.daily_orders.get(order_id, None) - - def before_trading(self): - open_orders = {} - for k, order in six.iteritems(self.daily_orders): - if not order._is_final(): - open_orders[k] = order + raise NotImplementedError - self.daily_orders = open_orders - self.daily_trades = [] - - def last_trade_id(self): - return self._last_trade_id - - def bar(self, bar_dict): - pass - - def tick(self, tick): - pass - - def after_trading(self): - pass - - def settlement(self): - pass - - def order_pending_new(self, account, order): - pass - - def order_creation_pass(self, account, order): - pass - - def order_creation_reject(self, account, order): - pass - - def order_pending_cancel(self, account, order): - pass - - def order_cancellation_pass(self, account, order): - pass - - def order_cancellation_reject(self, account, order): - pass - - def order_unsolicited_update(self, account, order): - pass - - def trade(self, account, trade): - pass + def set_state(self, state): + raise NotImplementedError + + @property + def positions(self): + """ + [dict] 持仓 + """ + return self._positions + + @property + def frozen_cash(self): + """ + [float] 冻结资金 + """ + return self._frozen_cash + + @property + def cash(self): + """ + [float] 可用资金 + """ + return self._total_cash - self._frozen_cash + + @property + def market_value(self): + """ + [float] 市值 + """ + return sum(position.market_value for position in six.itervalues(self._positions)) + + @property + def transaction_cost(self): + """ + [float] 总费用 + """ + return self._transaction_cost + + # ------------------------------------ Abandon Property ------------------------------------ + + @property + def portfolio_value(self): + """ + [已弃用] 请使用 total_value + """ + user_system_log.warn(_(u"[abandon] {} is no longer used.").format('account.portfolio_value')) + return self.total_value + + @property + def starting_cash(self): + """ + [已弃用] 请使用 total_value + """ + user_system_log.warn(_(u"[abandon] {} is no longer used.").format('account.starting_cash')) + return 0 + + @property + def daily_returns(self): + """ + [已弃用] 请使用 total_value + """ + user_system_log.warn(_(u"[abandon] {} is no longer used.").format('account.daily_returns')) + return 0 + + @property + def total_returns(self): + """ + [已弃用] 请使用 total_value + """ + user_system_log.warn(_(u"[abandon] {} is no longer used.").format('account.total_returns')) + return 0 + + @property + def pnl(self): + """ + [已弃用] 请使用 total_value + """ + user_system_log.warn(_(u"[abandon] {} is no longer used.").format('account.pnl')) + return 0 diff --git a/rqalpha/model/account/benchmark_account.py b/rqalpha/model/account/benchmark_account.py index 3bb703170..23c0bac59 100644 --- a/rqalpha/model/account/benchmark_account.py +++ b/rqalpha/model/account/benchmark_account.py @@ -14,89 +14,51 @@ # See the License for the specific language governing permissions and # limitations under the License. -import six -import pandas as pd import numpy as np -from .base_account import BaseAccount -from ..dividend import Dividend -from ...execution_context import ExecutionContext -from ...const import ACCOUNT_TYPE +from .stock_account import StockAccount +from ...environment import Environment +from ...events import EVENT -class BenchmarkAccount(BaseAccount): - def __init__(self, env, init_cash, start_date): - super(BenchmarkAccount, self).__init__(env, init_cash, start_date, ACCOUNT_TYPE.BENCHMARK) - self.benchmark = env.config.base.benchmark +class BenchmarkAccount(StockAccount): + def __init__(self, total_cash, positions, backward_trade_set=set(), dividend_receivable=None): + super(BenchmarkAccount, self).__init__(total_cash, positions, backward_trade_set, dividend_receivable) + self.benchmark = Environment.get_instance().config.base.benchmark - def before_trading(self): - portfolio = self.portfolio - portfolio._yesterday_portfolio_value = portfolio.portfolio_value - trading_date = ExecutionContext.get_current_trading_dt().date() - self._handle_dividend_payable(trading_date) + def register_event(self): + event_bus = Environment.get_instance().event_bus + event_bus.add_listener(EVENT.SETTLEMENT, self._on_settlement) + event_bus.add_listener(EVENT.PRE_BAR, self._on_bar) + event_bus.add_listener(EVENT.PRE_TICK, self._on_tick) - def bar(self, bar_dict): - price = bar_dict[self.config.base.benchmark].close - if np.isnan(price): - return - portfolio = self.portfolio - portfolio._portfolio_value = None - position = portfolio.positions[self.benchmark] + def _on_bar(self, event): + # run once + if len(self._positions) == 0: + price = event.bar_dict[self.benchmark].close + if np.isnan(price): + return + position = self._positions.get_or_create(self.benchmark) + quantity = int(self._total_cash / price) + position._quantity = quantity + position._avg_price = price + self._total_cash -= quantity * price - if portfolio.market_value == 0: - trade_quantity = int(portfolio.cash / price) - delta_value = trade_quantity * price - commission = 0.0008 * trade_quantity * price - position._total_commission = commission - position._buy_trade_quantity = trade_quantity - position._buy_trade_value = delta_value - position._market_value = delta_value - portfolio._cash = portfolio._cash - delta_value - commission - else: - position._market_value = position._buy_trade_quantity * price + def _on_tick(self, event): + # run once + if len(self._positions) == 0: + tick = event.tick + if tick.order_book_id != self.benchmark: + return + price = tick.last + position = self._positions.get_or_create(self.benchmark) + quantity = int(self._total_cash / price) + position._quantity = quantity + position._avg_price = price + self._total_cash -= quantity * price - def after_trading(self): - trading_date = ExecutionContext.get_current_trading_dt().date() - self.portfolio_persist() - self._handle_dividend_ex_dividend(trading_date) - - def _handle_dividend_payable(self, trading_date): - """handle dividend payable before trading + def _on_settlement(self, event): """ - to_delete_dividend = [] - for order_book_id, dividend_info in six.iteritems(self.portfolio._dividend_info): - dividend_series_dict = dividend_info.dividend_series_dict - - if pd.Timestamp(trading_date) == pd.Timestamp(dividend_series_dict['payable_date']): - dividend_per_share = dividend_series_dict["dividend_cash_before_tax"] / dividend_series_dict["round_lot"] - if dividend_per_share > 0 and dividend_info.quantity > 0: - dividend_cash = dividend_per_share * dividend_info.quantity - self.portfolio._dividend_receivable -= dividend_cash - self.portfolio._cash += dividend_cash - # user_log.info(_("get dividend {dividend} for {order_book_id}").format( - # dividend=dividend_cash, - # order_book_id=order_book_id, - # )) - to_delete_dividend.append(order_book_id) - - for order_book_id in to_delete_dividend: - self.portfolio._dividend_info.pop(order_book_id, None) - - def _handle_dividend_ex_dividend(self, trading_date): - data_proxy = ExecutionContext.get_data_proxy() - for order_book_id, position in six.iteritems(self.portfolio.positions): - dividend_series = data_proxy.get_dividend_by_book_date(order_book_id, trading_date) - if dividend_series is None: - continue - - dividend_series_dict = { - 'book_closure_date': dividend_series['book_closure_date'], - 'ex_dividend_date': dividend_series['ex_dividend_date'], - 'payable_date': dividend_series['payable_date'], - 'dividend_cash_before_tax': dividend_series['dividend_cash_before_tax'], - 'round_lot': dividend_series['round_lot'] - } - - dividend_per_share = dividend_series_dict["dividend_cash_before_tax"] / dividend_series_dict["round_lot"] - self.portfolio._dividend_info[order_book_id] = Dividend(order_book_id, position._quantity, dividend_series_dict) - self.portfolio._dividend_receivable += dividend_per_share * position._quantity + 回测中的Benchmark 不处理退市相关的内容。如果退市则不再更新last_price + """ + self._handle_dividend_book_closure(event.trading_dt.date()) diff --git a/rqalpha/model/account/future_account.py b/rqalpha/model/account/future_account.py index a6c3e0ed8..e57de9e2b 100644 --- a/rqalpha/model/account/future_account.py +++ b/rqalpha/model/account/future_account.py @@ -16,336 +16,200 @@ import six -from ..margin import Margin -from ...const import SIDE, POSITION_EFFECT, ACCOUNT_TYPE +from .base_account import BaseAccount +from ...environment import Environment +from ...events import EVENT +from ...const import ACCOUNT_TYPE, POSITION_EFFECT from ...utils.i18n import gettext as _ from ...utils.logger import user_system_log -from ...execution_context import ExecutionContext -from .base_account import BaseAccount + +def margin_of(order_book_id, quantity, price): + env = Environment.get_instance() + contract_multiplier = env.get_instrument(order_book_id).contract_multiplier + margin_rate = env.get_future_margin_rate(order_book_id) + margin_multiplier = env.config.base.margin_multiplier + return quantity * price * margin_multiplier * margin_rate * contract_multiplier class FutureAccount(BaseAccount): - def __init__(self, env, init_cash, start_date): - super(FutureAccount, self).__init__(env, init_cash, start_date, ACCOUNT_TYPE.FUTURE) - self._margin_decider = Margin(env.config.base.margin_multiplier) + def register_event(self): + event_bus = Environment.get_instance().event_bus + event_bus.add_listener(EVENT.SETTLEMENT, self._settlement) + event_bus.add_listener(EVENT.ORDER_PENDING_NEW, self._on_order_pending_new) + event_bus.add_listener(EVENT.ORDER_CREATION_REJECT, self._on_order_creation_reject) + event_bus.add_listener(EVENT.ORDER_CANCELLATION_PASS, self._on_order_unsolicited_update) + event_bus.add_listener(EVENT.ORDER_UNSOLICITED_UPDATE, self._on_order_unsolicited_update) + event_bus.add_listener(EVENT.TRADE, self._on_trade) + + def fast_forward(self, orders, trades=list()): + # 计算 Positions + for trade in trades: + if trade.exec_id in self._backward_trade_set: + continue + self._apply_trade(trade) + # 计算 Frozen Cash + self._frozen_cash = sum(self._frozen_cash_of_order(order) for order in orders if order.is_active()) + + def get_state(self): + return { + 'positions': { + order_book_id: position.get_state() + for order_book_id, position in six.iteritems(self._positions) + }, + 'frozen_cash': self._frozen_cash, + 'total_cash': self._total_cash, + 'backward_trade_set': list(self._backward_trade_set), + 'transaction_cost': self._transaction_cost, + } + + def set_state(self, state): + self._frozen_cash = state['frozen_cash'] + self._total_cash = state['total_cash'] + self._backward_trade_set = set(state['backward_trade_set']) + self._transaction_cost = state['transaction_cost'] + self._positions.clear() + for order_book_id, v in six.iteritems(state['positions']): + position = self._positions.get_or_create(order_book_id) + position.set_state(v) @property - def margin_decider(self): - return self._margin_decider + def type(self): + return ACCOUNT_TYPE.FUTURE - def before_trading(self): - super(FutureAccount, self).before_trading() - positions = self.portfolio.positions - removing_ids = [] - for order_book_id in positions.keys(): - position = positions[order_book_id] - if position._quantity == 0 and position._buy_open_order_quantity == 0 \ - and position._sell_open_order_quantity == 0: - removing_ids.append(order_book_id) - for order_book_id in removing_ids: - positions.pop(order_book_id, None) + @staticmethod + def _frozen_cash_of_order(order): + if order.position_effect == POSITION_EFFECT.OPEN: + return margin_of(order.order_book_id, order.unfilled_quantity, order.frozen_price) + else: + return 0 - def after_trading(self): - pass + @staticmethod + def _frozen_cash_of_trade(trade): + if trade.position_effect == POSITION_EFFECT.OPEN: + return margin_of(trade.order_book_id, trade.last_quantity, trade.frozen_price) + else: + return 0 - def settlement(self): - portfolio = self.portfolio - portfolio._portfolio_value = None - positions = portfolio.positions - data_proxy = ExecutionContext.get_data_proxy() - trading_date = ExecutionContext.get_current_trading_dt().date() + @property + def total_value(self): + return self._total_cash + self.margin + self.holding_pnl - for order_book_id, position in six.iteritems(positions): - settle_price = data_proxy.get_settle_price(order_book_id, trading_date) - position._last_price = settle_price - self._update_market_value(position, settle_price) + # -- Margin 相关 + @property + def margin(self): + """ + [float] 总保证金 + """ + return sum(position.margin for position in six.itervalues(self._positions)) - self.portfolio_persist() + @property + def buy_margin(self): + """ + [float] 买方向保证金 + """ + return sum(position.buy_margin for position in six.itervalues(self._positions)) - portfolio._yesterday_portfolio_value = portfolio.portfolio_value + @property + def sell_margin(self): + """ + [float] 卖方向保证金 + """ + return sum(position.sell_margin for position in six.itervalues(self._positions)) - de_listed_id_list = [] - for order_book_id, position in six.iteritems(positions): - # 检查合约是否到期,如果到期,则按照结算价来进行平仓操作 - if position._de_listed_date is not None and trading_date >= position._de_listed_date.date(): - de_listed_id_list.append(order_book_id) - else: - settle_price = data_proxy.get_settle_price(order_book_id, trading_date) - self._update_holding_by_settle(position, settle_price) - position._daily_realized_pnl = 0 - position._buy_daily_realized_pnl = 0 - position._sell_daily_realized_pnl = 0 - for de_listed_id in de_listed_id_list: - if positions[de_listed_id]._quantity != 0: - user_system_log.warn( - _("{order_book_id} is expired, close all positions by system").format(order_book_id=de_listed_id)) - del positions[de_listed_id] + # -- PNL 相关 + @property + def daily_pnl(self): + """ + [float] 当日盈亏 + """ + return self.realized_pnl + self.holding_pnl - self.transaction_cost - portfolio._daily_transaction_cost = 0 + @property + def holding_pnl(self): + """ + [float] 浮动盈亏 + """ + return sum(position.holding_pnl for position in six.itervalues(self._positions)) - def bar(self, bar_dict): - portfolio = self.portfolio - portfolio._portfolio_value = None - positions = portfolio.positions + @property + def realized_pnl(self): + """ + [float] 平仓盈亏 + """ + return sum(position.realized_pnl for position in six.itervalues(self._positions)) + + def _settlement(self, event): + old_margin = self.margin + old_holding_pnl = self.holding_pnl + for position in list(self._positions.values()): + order_book_id = position.order_book_id + if position.is_de_listed() and position.buy_quantity + position.sell_quantity != 0: + self._total_cash += position.market_value * position.margin_rate + user_system_log.warn( + _(u"{order_book_id} is expired, close all positions by system").format(order_book_id=order_book_id)) + del self._positions[order_book_id] + elif position.buy_quantity == 0 and position.sell_quantity == 0: + del self._positions[order_book_id] + else: + position.apply_settlement() + self._total_cash = self._total_cash + (old_margin - self.margin) + old_holding_pnl + self._transaction_cost = 0 - for order_book_id, position in six.iteritems(positions): - bar = bar_dict[order_book_id] - if not bar.isnan: - position._last_price = bar.close - self._update_market_value(position, bar.close) + # 如果 total_value <= 0 则认为已爆仓,清空仓位,资金归0 + if self.total_value <= 0: + self._positions.clear() + self._total_cash = 0 - def tick(self, tick): - portfolio = self.portfolio - portfolio._portfolio_value = None - position = portfolio.positions[tick.order_book_id] - position._last_price = tick.last - self._update_market_value(position, tick.last) + self._backward_trade_set.clear() - def order_pending_new(self, account, order): - if self != account: + def _on_order_pending_new(self, event): + if self != event.account: return - if order._is_final(): - return - order_book_id = order.order_book_id - position = self.portfolio.positions[order_book_id] - position._total_orders += 1 - created_quantity = order.quantity - created_value = order._frozen_price * created_quantity * position._contract_multiplier - frozen_margin = self.margin_decider.cal_margin(order_book_id, order.side, created_value) - self._update_order_data(position, order, created_quantity, created_value) - self._update_frozen_cash(order, frozen_margin) - - def on_order_creation_pass(self, account, order): - pass + self._frozen_cash += self._frozen_cash_of_order(event.order) - def order_creation_reject(self, account, order): - if self != account: + def _on_order_creation_reject(self, event): + if self != event.account: return - order_book_id = order.order_book_id - position = self.portfolio.positions[order_book_id] - position._total_orders -= 1 - cancel_quantity = order.unfilled_quantity - cancel_value = -order._frozen_price * cancel_quantity * position._contract_multiplier - frozen_margin = self.margin_decider.cal_margin(order_book_id, order.side, cancel_value) - self._update_order_data(position, order, -cancel_quantity, cancel_value) - self._update_frozen_cash(order, frozen_margin) - - def order_pending_cancel(self, account, order): - pass - - def order_cancellation_pass(self, account, order): - self._cancel_order_cal(account, order) + self._frozen_cash -= self._frozen_cash_of_order(event.order) - def order_cancellation_reject(self, account, order): - pass - - def order_unsolicited_update(self, account, order): - self._cancel_order_cal(account, order) - - def _cancel_order_cal(self, account, order): - if self != account: + def _on_order_unsolicited_update(self, event): + if self != event.account: return - order_book_id = order.order_book_id - position = self.portfolio.positions[order.order_book_id] - rejected_quantity = order.unfilled_quantity - rejected_value = -order._frozen_price * rejected_quantity * position._contract_multiplier - frozen_margin = self.margin_decider.cal_margin(order_book_id, order.side, rejected_value) - self._update_order_data(position, order, -rejected_quantity, rejected_value) - self._update_frozen_cash(order, frozen_margin) + self._frozen_cash -= self._frozen_cash_of_order(event.order) - def trade(self, account, trade): - if self != account: + def _on_trade(self, event): + if self != event.account: return - order = trade.order - order_book_id = order.order_book_id - bar_dict = ExecutionContext.get_current_bar_dict() - portfolio = self.portfolio - portfolio._portfolio_value = None - position = portfolio.positions[order_book_id] - position._is_traded = True - position._total_trades += 1 - trade_quantity = trade.last_quantity - - if order.position_effect == POSITION_EFFECT.OPEN: - if order.side == SIDE.BUY: - position._buy_avg_open_price = (position._buy_avg_open_price * position.buy_quantity + trade_quantity * - trade.last_price) / (position.buy_quantity + trade_quantity) - elif order.side == SIDE.SELL: - position._sell_avg_open_price = (position._sell_avg_open_price * position.sell_quantity + - trade_quantity * trade.last_price) / (position.sell_quantity + - trade_quantity) + self._apply_trade(event.trade) - minus_value_by_trade = -order._frozen_price * trade_quantity * position._contract_multiplier - trade_value = trade.last_price * trade_quantity * position._contract_multiplier - frozen_margin = self.margin_decider.cal_margin(order_book_id, order.side, minus_value_by_trade) - - portfolio._total_tax += trade.tax - portfolio._total_commission += trade.commission - portfolio._daily_transaction_cost = portfolio._daily_transaction_cost + trade.tax + trade.commission - - self._update_frozen_cash(order, frozen_margin) - self._update_order_data(position, order, -trade_quantity, minus_value_by_trade) - self._update_trade_data(position, trade, trade_quantity, trade_value) + def _apply_trade(self, trade): + if trade.exec_id in self._backward_trade_set: + return + order_book_id = trade.order_book_id + position = self._positions.get_or_create(order_book_id) + delta_cash = position.apply_trade(trade) - self._update_market_value(position, bar_dict[order_book_id].close) - self._last_trade_id = trade.exec_id + self._transaction_cost += trade.transaction_cost + self._total_cash -= trade.transaction_cost + self._total_cash += delta_cash + self._frozen_cash -= self._frozen_cash_of_trade(trade) + self._backward_trade_set.add(trade.exec_id) - @staticmethod - def _update_holding_by_settle(position, settle_price): - position._prev_settle_price = settle_price - position._buy_old_holding_list += position._buy_today_holding_list - position._sell_old_holding_list += position._sell_today_holding_list - position._buy_today_holding_list = [] - position._sell_today_holding_list = [] + # ------------------------------------ Abandon Property ------------------------------------ - def _update_trade_data(self, position, trade, trade_quantity, trade_value): + @property + def daily_holding_pnl(self): """ - 计算 [buy|sell]_trade_[value|quantity] - 计算 [buy|sell]_[open|close]_trade_quantity - 计算 [buy|sell]_settle_holding - 计算 [buy|sell]_today_holding_list - 计算 [buy|sell]_holding_cost - 计算 [bar|sell]_margin - 计算 daily_realized_pnl + [已弃用] 请使用 holding_pnl """ - order = trade.order - - if order.side == SIDE.BUY: - if order.position_effect == POSITION_EFFECT.OPEN: - position._buy_open_trade_quantity += trade_quantity - position._buy_open_trade_value += trade_value - position._buy_open_transaction_cost += trade.commission - position._buy_today_holding_list.insert(0, (trade.last_price, trade_quantity)) - else: - position._buy_close_trade_quantity += trade_quantity - position._buy_close_trade_value += trade_value - position._buy_close_transaction_cost += trade.commission - delta_daily_realized_pnl = self._update_holding_by_close_action(trade) - position._daily_realized_pnl += delta_daily_realized_pnl - position._sell_daily_realized_pnl += delta_daily_realized_pnl - position._buy_trade_quantity += trade_quantity - position._buy_trade_value += trade_value - else: - if order.position_effect == POSITION_EFFECT.OPEN: - position._sell_open_trade_quantity += trade_quantity - position._sell_open_trade_value += trade_value - position._sell_open_transaction_cost += trade.commission - position._sell_today_holding_list.insert(0, (trade.last_price, trade_quantity)) - else: - position._sell_close_trade_quantity += trade_quantity - position._sell_close_trade_value += trade_value - position._sell_close_transaction_cost += trade.commission - delta_daily_realized_pnl = self._update_holding_by_close_action(trade) - position._daily_realized_pnl += delta_daily_realized_pnl - position._buy_daily_realized_pnl += delta_daily_realized_pnl - position._sell_trade_quantity += trade_quantity - position._sell_trade_value += trade_value - - @staticmethod - def _update_order_data(position, order, inc_order_quantity, inc_order_value): - if order.side == SIDE.BUY: - if order.position_effect == POSITION_EFFECT.OPEN: - position._buy_open_order_quantity += inc_order_quantity - position._buy_open_order_value += inc_order_value - else: - position._buy_close_order_quantity += inc_order_quantity - position._buy_close_order_value += inc_order_value - else: - if order.position_effect == POSITION_EFFECT.OPEN: - position._sell_open_order_quantity += inc_order_quantity - position._sell_open_order_value += inc_order_value - else: - position._sell_close_order_quantity += inc_order_quantity - position._sell_close_order_value += inc_order_value + user_system_log.warn(_(u"[abandon] {} is no longer used.").format('future_account.daily_holding_pnl')) + return self.holding_pnl - def _update_frozen_cash(self, order, inc_order_value): - if order.position_effect == POSITION_EFFECT.OPEN: - self.portfolio._frozen_cash += inc_order_value - - def _update_holding_by_close_action(self, trade): - order = trade.order - order_book_id = order.order_book_id - position = self.portfolio.positions[order_book_id] - settle_price = position._prev_settle_price - left_quantity = trade.last_quantity - delta_daily_realized_pnl = 0 - if order.side == SIDE.BUY: - # 先平昨仓 - while True: - if left_quantity == 0: - break - if len(position._sell_old_holding_list) == 0: - break - oldest_price, oldest_quantity = position._sell_old_holding_list.pop() - if oldest_quantity > left_quantity: - consumed_quantity = left_quantity - position._sell_old_holding_list.append((oldest_price, oldest_quantity - left_quantity)) - else: - consumed_quantity = oldest_quantity - left_quantity -= consumed_quantity - delta_daily_realized_pnl += self._cal_daily_realized_pnl(trade, settle_price, consumed_quantity) - # 再平今仓 - while True: - if left_quantity <= 0: - break - oldest_price, oldest_quantity = position._sell_today_holding_list.pop() - if oldest_quantity > left_quantity: - consumed_quantity = left_quantity - position._sell_today_holding_list.append((oldest_price, oldest_quantity - left_quantity)) - else: - consumed_quantity = oldest_quantity - left_quantity -= consumed_quantity - delta_daily_realized_pnl += self._cal_daily_realized_pnl(trade, oldest_price, consumed_quantity) - else: - # 先平昨仓 - while True: - if left_quantity == 0: - break - if len(position._buy_old_holding_list) == 0: - break - oldest_price, oldest_quantity = position._buy_old_holding_list.pop() - if oldest_quantity > left_quantity: - consumed_quantity = left_quantity - position._buy_old_holding_list.append((oldest_price, oldest_quantity - left_quantity)) - else: - consumed_quantity = oldest_quantity - left_quantity -= consumed_quantity - delta_daily_realized_pnl += self._cal_daily_realized_pnl(trade, settle_price, consumed_quantity) - # 再平今仓 - while True: - if left_quantity <= 0: - break - oldest_price, oldest_quantity = position._buy_today_holding_list.pop() - if oldest_quantity > left_quantity: - consumed_quantity = left_quantity - position._buy_today_holding_list.append((oldest_price, oldest_quantity - left_quantity)) - left_quantity = 0 - else: - consumed_quantity = oldest_quantity - left_quantity -= consumed_quantity - delta_daily_realized_pnl += self._cal_daily_realized_pnl(trade, oldest_price, consumed_quantity) - return delta_daily_realized_pnl - - @staticmethod - def _update_market_value(position, price): + @property + def daily_realized_pnl(self): """ - 计算 market_value - 计算 pnl - 计算 daily_holding_pnl + [已弃用] 请使用 realized_pnl """ - botq = position._buy_open_trade_quantity - sctq = position._sell_close_trade_quantity - bctq = position._buy_close_trade_quantity - sotq = position._sell_open_trade_quantity - position._buy_market_value = (botq - sctq) * price * position._contract_multiplier - position._sell_market_value = (bctq - sotq) * price * position._contract_multiplier - position._market_value = position._buy_market_value + position._sell_market_value - - def _cal_daily_realized_pnl(self, trade, cost_price, consumed_quantity): - order = trade.order - position = self.portfolio.positions[order.order_book_id] - if order.side == SIDE.BUY: - return (cost_price - trade.last_price) * consumed_quantity * position._contract_multiplier - else: - return (trade.last_price - cost_price) * consumed_quantity * position._contract_multiplier + user_system_log.warn(_(u"[abandon] {} is no longer used.").format('future_account.daily_realized_pnl')) + return self.realized_pnl diff --git a/rqalpha/model/account/mixed_account.py b/rqalpha/model/account/mixed_account.py deleted file mode 100644 index ec5a2009d..000000000 --- a/rqalpha/model/account/mixed_account.py +++ /dev/null @@ -1,197 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2017 Ricequant, Inc -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from ...const import DAYS_CNT, ACCOUNT_TYPE -from ...utils import merge_dicts, exclude_benchmark_generator, get_account_type -from ...utils.repr import dict_repr - - -class MixedAccount(object): - def __init__(self, accounts): - self._accounts = exclude_benchmark_generator(accounts) - self._portfolio = MixedPortfolio([account.portfolio for account in self._accounts.values()]) - - def get_portfolio(self, trading_date): - return MixedPortfolio([account.get_portfolio(trading_date) for account in self._accounts.values()]) - - def get_order(self, order_id): - for account in self._accounts.values(): - order = account.get_order(order_id) - if order is not None: - return order - - def get_open_orders(self): - result = {} - for account in self._accounts.values(): - result.update(account.get_open_orders()) - return result - - @property - def portfolio(self): - return self._portfolio - - -class MixedPortfolio(object): - __repr__ = dict_repr - - def __init__(self, portfolio_list): - self._portfolio_list = portfolio_list - - @property - def _type(self): - return ACCOUNT_TYPE.TOTAL - - @property - def _pid(self): - return ACCOUNT_TYPE.TOTAL.value - - @property - def _yesterday_portfolio_value(self): - return sum(portfolio._yesterday_portfolio_value for portfolio in self._portfolio_list) - - @property - def daily_returns(self): - """ - 【float】投资组合每日收益率 - """ - return self.daily_pnl / self._yesterday_portfolio_value - - @property - def starting_cash(self): - """ - 【float】初始资金,为子组合初始资金的加总 - """ - return sum(portfolio.starting_cash for portfolio in self._portfolio_list) - - @property - def start_date(self): - """ - 【datetime.datetime】策略投资组合的回测/实时模拟交易的开始日期 - """ - for portfolio in self._portfolio_list: - return portfolio.start_date - - @property - def frozen_cash(self): - """ - 【float】冻结资金 - """ - return sum(portfolio.frozen_cash for portfolio in self._portfolio_list) - - @property - def cash(self): - """ - 【float】可用资金,为子组合可用资金的加总 - """ - return sum(portfolio.cash for portfolio in self._portfolio_list) - - @property - def portfolio_value(self): - """ - 【float】总权益,为子组合总权益加总 - """ - return sum(portfolio.portfolio_value for portfolio in self._portfolio_list) - - @property - def positions(self): - """ - 【dict】一个包含所有仓位的字典,以order_book_id作为键,position对象作为值,关于position的更多的信息可以在下面的部分找到。 - """ - return MixedPositions(self._portfolio_list) - - @property - def daily_pnl(self): - """ - 【float】当日盈亏,子组合当日盈亏的加总 - """ - return sum(portfolio.daily_pnl for portfolio in self._portfolio_list) - - @property - def market_value(self): - """ - 【float】投资组合当前的市场价值,为子组合市场价值的加总 - """ - return sum(portfolio.market_value for portfolio in self._portfolio_list) - - @property - def pnl(self): - """ - 【float】当前投资组合的累计盈亏 - """ - return self.portfolio_value - self.starting_cash - - @property - def total_returns(self): - """ - 【float】投资组合至今的累积收益率。计算方法是现在的投资组合价值/投资组合的初始资金 - """ - return self.pnl / self.starting_cash - - @property - def annualized_returns(self): - """ - 【float】投资组合的年化收益率 - """ - current_date = self._portfolio_list[0]._current_date - return (1 + self.total_returns) ** ( - DAYS_CNT.DAYS_A_YEAR / float((current_date - self.start_date).days + 1)) - 1 - - @property - def transaction_cost(self): - """ - 【float】总费用 - """ - return sum(portfolio.transaction_cost for portfolio in self._portfolio_list) - - -class MixedPositions(dict): - def __init__(self, portfolio_list): - super(MixedPositions, self).__init__() - self.portfolio_list = portfolio_list - - def __missing__(self, order_book_id): - account_type = get_account_type(order_book_id) - for portfolio in self.portfolio_list: - if portfolio._type == account_type: - return portfolio.positions[order_book_id] - return None - - def __repr__(self): - keys = [] - for portfolio in self.portfolio_list: - keys += portfolio.positions.keys() - return str(sorted(keys)) - - def __len__(self): - return sum(len(portfolio.positions) for portfolio in self.portfolio_list) - - def __iter__(self): - keys = [] - for portfolio in self.portfolio_list: - keys += portfolio.positions.keys() - for key in sorted(keys): - yield key - - def items(self): - items = merge_dicts(*[portfolio.positions.items() for portfolio in self.portfolio_list]) - for k in sorted(items.keys()): - yield k, items[k] - - def keys(self): - keys = [] - for portfolio in self.portfolio_list: - keys += list(portfolio.positions.keys()) - return iter(sorted(keys)) diff --git a/rqalpha/model/account/stock_account.py b/rqalpha/model/account/stock_account.py index bed06a4d8..01bf7d91a 100644 --- a/rqalpha/model/account/stock_account.py +++ b/rqalpha/model/account/stock_account.py @@ -15,272 +15,182 @@ # limitations under the License. import six -import pandas as pd +from collections import defaultdict from .base_account import BaseAccount -from ..dividend import Dividend -from ...const import SIDE, ACCOUNT_TYPE +from ...events import EVENT +from ...environment import Environment +from ...utils.logger import user_system_log from ...utils.i18n import gettext as _ -from ...utils.logger import user_system_log, system_log -from ...execution_context import ExecutionContext +from ...const import SIDE, ACCOUNT_TYPE class StockAccount(BaseAccount): - def __init__(self, env, init_cash, start_date): - super(StockAccount, self).__init__(env, init_cash, start_date, ACCOUNT_TYPE.STOCK) - - def before_trading(self): - super(StockAccount, self).before_trading() - positions = self.portfolio.positions - removing_ids = [] - for order_book_id in positions.keys(): - position = positions[order_book_id] - if position._quantity == 0: - removing_ids.append(order_book_id) - for order_book_id in removing_ids: - positions.pop(order_book_id, None) - trading_date = ExecutionContext.get_current_trading_dt().date() - self._handle_dividend_payable(trading_date) - if self.config.base.handle_split: - self._handle_split(trading_date) - - def after_trading(self): - trading_date = ExecutionContext.get_current_trading_dt().date() - portfolio = self.portfolio - # de_listed may occur - portfolio._portfolio_value = None - - positions = portfolio.positions - - de_listed_id_list = [] - # update buy_today_holding_quantity to zero for T+1 - for order_book_id in positions: - position = positions[order_book_id] - position._buy_today_holding_quantity = 0 - - # 检查股票今天是否退市,如果退市,则按照close_price卖出,并warning - if position._de_listed_date is not None and trading_date >= position._de_listed_date.date(): - de_listed_id_list.append(order_book_id) - - for de_listed_id in de_listed_id_list: - position = positions[de_listed_id] - if self.config.validator.cash_return_by_stock_delisted: - portfolio._cash += position.market_value - if position._quantity != 0: - user_system_log.warn( - _("{order_book_id} is expired, close all positions by system").format(order_book_id=de_listed_id)) - del positions[de_listed_id] - - def settlement(self): - portfolio = self.portfolio - trading_date = ExecutionContext.get_current_trading_dt().date() - - self.portfolio_persist() - - portfolio._yesterday_portfolio_value = portfolio.portfolio_value - - self._handle_dividend_ex_dividend(trading_date) - - def bar(self, bar_dict): - portfolio = self.portfolio - # invalidate cache - portfolio._portfolio_value = None - positions = portfolio.positions - - for order_book_id, position in six.iteritems(positions): - bar = bar_dict[order_book_id] - if not bar.isnan: - position._market_value = position._quantity * bar.close - position._last_price = bar.close - - def tick(self, tick): - portfolio = self.portfolio - # invalidate cache - portfolio._portfolio_value = None - position = portfolio.positions[tick.order_book_id] - - position._market_value = position._quantity * tick.last_price - position._last_price = tick.last_price - - def order_pending_new(self, account, order): - if self != account: - return - if order._is_final(): + def __init__(self, total_cash, positions, backward_trade_set=set(), dividend_receivable=None, register_event=True): + super(StockAccount, self).__init__(total_cash, positions, backward_trade_set, register_event) + self._dividend_receivable = dividend_receivable if dividend_receivable else {} + + def register_event(self): + event_bus = Environment.get_instance().event_bus + event_bus.add_listener(EVENT.TRADE, self._on_trade) + event_bus.add_listener(EVENT.ORDER_PENDING_NEW, self._on_order_pending_new) + event_bus.add_listener(EVENT.ORDER_CREATION_REJECT, self._on_order_unsolicited_update) + event_bus.add_listener(EVENT.ORDER_UNSOLICITED_UPDATE, self._on_order_unsolicited_update) + event_bus.add_listener(EVENT.ORDER_CANCELLATION_PASS, self._on_order_unsolicited_update) + event_bus.add_listener(EVENT.PRE_BEFORE_TRADING, self._before_trading) + event_bus.add_listener(EVENT.SETTLEMENT, self._on_settlement) + + def get_state(self): + return { + 'positions': { + order_book_id: position.get_state() + for order_book_id, position in six.iteritems(self._positions) + }, + 'frozen_cash': self._frozen_cash, + 'total_cash': self._total_cash, + 'backward_trade_set': list(self._backward_trade_set), + 'dividend_receivable': self._dividend_receivable, + 'transaction_cost': self._transaction_cost, + } + + def set_state(self, state): + self._frozen_cash = state['frozen_cash'] + self._total_cash = state['total_cash'] + self._backward_trade_set = set(state['backward_trade_set']) + self._dividend_receivable = state['dividend_receivable'] + self._transaction_cost = state['transaction_cost'] + self._positions.clear() + for order_book_id, v in six.iteritems(state['positions']): + position = self._positions.get_or_create(order_book_id) + position.set_state(v) + + def fast_forward(self, orders, trades=list()): + # 计算 Positions + for trade in trades: + if trade.exec_id in self._backward_trade_set: + continue + self._apply_trade(trade) + # 计算 Frozen Cash + self._frozen_cash = 0 + frozen_quantity = defaultdict(int) + for o in orders: + if o.is_final(): + continue + if o.side == SIDE.BUY: + self._frozen_cash += o.frozen_price * o.unfilled_quantity + else: + frozen_quantity[o.order_book_id] += o.unfilled_quantity + for order_book_id, position in six.iteritems(self._positions): + position.reset_frozen(frozen_quantity[order_book_id]) + + def _on_trade(self, event): + if event.account != self: return - order_book_id = order.order_book_id - position = self.portfolio.positions[order_book_id] - position._total_orders += 1 - create_quantity = order.quantity - create_value = order._frozen_price * create_quantity + self._apply_trade(event.trade) - self._update_order_data(order, create_quantity, create_value) - self._update_frozen_cash(order, create_value) - - def order_creation_pass(self, account, order): - pass - - def order_creation_reject(self, account, order): - if self != account: + def _apply_trade(self, trade): + if trade.exec_id in self._backward_trade_set: return - order_book_id = order.order_book_id - position = self.portfolio.positions[order_book_id] - position._total_orders -= 1 - cancel_quantity = order.unfilled_quantity - cancel_value = order._frozen_price * cancel_quantity - self._update_order_data(order, cancel_quantity, cancel_value) - self._update_frozen_cash(order, -cancel_value) - - def order_pending_cancel(self, account, order): - pass - - def order_cancellation_pass(self, account, order): - self._cancel_order_cal(account, order) - def order_cancellation_reject(self, account, order): - pass - - def order_unsolicited_update(self, account, order): - self._cancel_order_cal(account, order) - - def _cancel_order_cal(self, account, order): - if self != account: - return - rejected_quantity = order.unfilled_quantity - rejected_value = order._frozen_price * rejected_quantity - self._update_order_data(order, -rejected_quantity, -rejected_value) - self._update_frozen_cash(order, -rejected_value) + position = self._positions.get_or_create(trade.order_book_id) + position.apply_trade(trade) + self._transaction_cost += trade.transaction_cost + self._total_cash -= trade.transaction_cost + if trade.side == SIDE.BUY: + self._total_cash -= trade.last_quantity * trade.last_price + self._frozen_cash -= trade.frozen_price * trade.last_quantity + else: + self._total_cash += trade.last_price * trade.last_quantity + self._backward_trade_set.add(trade.exec_id) - def trade(self, account, trade): - if self != account: + def _on_order_pending_new(self, event): + if event.account != self: return - portfolio = self.portfolio - portfolio._portfolio_value = None - order = trade.order - bar_dict = ExecutionContext.get_current_bar_dict() - order_book_id = order.order_book_id - position = portfolio.positions[order.order_book_id] - position._is_traded = True - trade_quantity = trade.last_quantity - minus_value_by_trade = order._frozen_price * trade_quantity - trade_value = trade.last_price * trade_quantity - + order = event.order + position = self._positions.get(order.order_book_id, None) + if position is not None: + position.on_order_pending_new_(order) if order.side == SIDE.BUY: - position._avg_price = (position._avg_price * position._quantity + - trade_quantity * trade.last_price) / (position._quantity + trade_quantity) + order_value = order.frozen_price * order.quantity + self._frozen_cash += order_value - self._update_order_data(order, -trade_quantity, -minus_value_by_trade) - self._update_trade_data(order, trade, trade_quantity, trade_value) - self._update_frozen_cash(order, -minus_value_by_trade) - price = bar_dict[order_book_id].close - if order.side == SIDE.BUY and order.order_book_id not in \ - {'510900.XSHG', '513030.XSHG', '513100.XSHG', '513500.XSHG'}: - position._buy_today_holding_quantity += trade_quantity - position._market_value = (position._buy_trade_quantity - position._sell_trade_quantity) * price - position._last_price = price - position._total_trades += 1 + def _on_order_unsolicited_update(self, event): + if event.account != self: + return - portfolio._total_tax += trade.tax - portfolio._total_commission += trade.commission - portfolio._cash = portfolio._cash - trade.tax - trade.commission + order = event.order + position = self._positions.get_or_create(order.order_book_id) + position.on_order_cancel_(order) if order.side == SIDE.BUY: - portfolio._cash -= trade_value - else: - portfolio._cash += trade_value + unfilled_value = order.unfilled_quantity * order.frozen_price + self._frozen_cash -= unfilled_value - self._last_trade_id = trade.exec_id + def _before_trading(self, event): + trading_date = Environment.get_instance().trading_dt.date() + self._handle_dividend_payable(trading_date) + if Environment.get_instance().config.base.handle_split: + self._handle_split(trading_date) - def _update_order_data(self, order, inc_order_quantity, inc_order_value): - position = self.portfolio.positions[order.order_book_id] - if order.side == SIDE.BUY: - position._buy_order_quantity += inc_order_quantity - position._buy_order_value += inc_order_value - else: - position._sell_order_quantity += inc_order_quantity - position._sell_order_value += inc_order_value + def _on_settlement(self, event): + for position in list(self._positions.values()): + order_book_id = position.order_book_id + if position.is_de_listed() and position.quantity != 0: + if Environment.get_instance().config.validator.cash_return_by_stock_delisted: + self._total_cash += position.market_value + user_system_log.warn( + _(u"{order_book_id} is expired, close all positions by system").format(order_book_id=order_book_id) + ) + self._positions.pop(order_book_id, None) + elif position.quantity == 0: + self._positions.pop(order_book_id, None) + else: + position.apply_settlement() + + self._transaction_cost = 0 + self._backward_trade_set.clear() + self._handle_dividend_book_closure(event.trading_dt.date()) + + @property + def type(self): + return ACCOUNT_TYPE.STOCK - def _update_trade_data(self, order, trade, trade_quantity, trade_value): - position = self.portfolio.positions[order.order_book_id] - position._transaction_cost = position._transaction_cost + trade.commission + trade.tax - if order.side == SIDE.BUY: - position._buy_trade_quantity += trade_quantity - position._buy_trade_value += trade_value - else: - position._sell_trade_quantity += trade_quantity - position._sell_trade_value += trade_value + def _handle_dividend_payable(self, trading_date): + to_be_removed = [] + for order_book_id, dividend in six.iteritems(self._dividend_receivable): + if dividend['payable_date'] == trading_date: + to_be_removed.append(order_book_id) + self._total_cash += dividend['quantity'] * dividend['dividend_per_share'] + for order_book_id in to_be_removed: + del self._dividend_receivable[order_book_id] + + def _handle_dividend_book_closure(self, trading_date): + for order_book_id, position in six.iteritems(self._positions): + dividend = Environment.get_instance().data_proxy.get_dividend_by_book_date(order_book_id, trading_date) + if dividend is None: + continue - def _update_frozen_cash(self, order, inc_order_value): - portfolio = self.portfolio - if order.side == SIDE.BUY: - portfolio._frozen_cash += inc_order_value - portfolio._cash -= inc_order_value + dividend_per_share = dividend['dividend_cash_before_tax'] / dividend['round_lot'] + self._dividend_receivable[order_book_id] = { + 'quantity': position.quantity, + 'dividend_per_share': dividend_per_share, + 'payable_date': dividend['payable_date'].date() + } def _handle_split(self, trading_date): - import rqdatac - for order_book_id, position in six.iteritems(self.portfolio.positions): - split_df = rqdatac.get_split(order_book_id, start_date="2005-01-01", end_date="2099-01-01") - if split_df is None: - system_log.warn(_("no split data {}").foramt(order_book_id)) + for order_book_id, position in six.iteritems(self._positions): + split = Environment.get_instance().data_proxy.get_split_by_ex_date(order_book_id, trading_date) + if split is None: continue - try: - series = split_df.loc[trading_date] - except KeyError: - continue - - # 处理拆股 - - user_system_log.info(_("split {order_book_id}, {position}").format( - order_book_id=order_book_id, - position=position, - )) - - ratio = series.split_coefficient_to / series.split_coefficient_from - for key in ["_buy_order_quantity", "_sell_order_quantity", "_buy_trade_quantity", "_sell_trade_quantity"]: - setattr(position, key, getattr(position, key) * ratio) + ratio = split['split_coefficient_to'] / split['split_coefficient_from'] + position.split_(ratio) - user_system_log.info(_("split {order_book_id}, {position}").format( - order_book_id=order_book_id, - position=position, - )) + @property + def total_value(self): + return self.market_value + self._total_cash - user_system_log.info(_("split {order_book_id}, {series}").format( - order_book_id=order_book_id, - series=series, - )) - - def _handle_dividend_payable(self, trading_date): - """handle dividend payable before trading + @property + def dividend_receivable(self): """ - to_delete_dividend = [] - for order_book_id, dividend_info in six.iteritems(self.portfolio._dividend_info): - dividend_series_dict = dividend_info.dividend_series_dict - - if pd.Timestamp(trading_date) == pd.Timestamp(dividend_series_dict['payable_date']): - dividend_per_share = dividend_series_dict["dividend_cash_before_tax"] / dividend_series_dict["round_lot"] - if dividend_per_share > 0 and dividend_info.quantity > 0: - dividend_cash = dividend_per_share * dividend_info.quantity - self.portfolio._dividend_receivable -= dividend_cash - self.portfolio._cash += dividend_cash - to_delete_dividend.append(order_book_id) - - for order_book_id in to_delete_dividend: - self.portfolio._dividend_info.pop(order_book_id, None) - - def _handle_dividend_ex_dividend(self, trading_date): - data_proxy = ExecutionContext.get_data_proxy() - for order_book_id, position in six.iteritems(self.portfolio.positions): - dividend_series = data_proxy.get_dividend_by_book_date(order_book_id, trading_date) - if dividend_series is None: - continue - - dividend_series_dict = { - 'book_closure_date': dividend_series['book_closure_date'].to_pydatetime(), - 'ex_dividend_date': dividend_series['ex_dividend_date'].to_pydatetime(), - 'payable_date': dividend_series['payable_date'].to_pydatetime(), - 'dividend_cash_before_tax': float(dividend_series['dividend_cash_before_tax']), - 'round_lot': int(dividend_series['round_lot']) - } - - dividend_per_share = dividend_series_dict["dividend_cash_before_tax"] / dividend_series_dict["round_lot"] - - self.portfolio._dividend_info[order_book_id] = Dividend(order_book_id, position._quantity, dividend_series_dict) - self.portfolio._dividend_receivable += dividend_per_share * position._quantity + [float] 投资组合在分红现金收到账面之前的应收分红部分。具体细节在分红部分 + """ + return sum(d['quantity'] * d['dividend_per_share'] for d in six.itervalues(self._dividend_receivable)) diff --git a/rqalpha/model/bar.py b/rqalpha/model/bar.py index 8b3606102..9f2c6e4a6 100644 --- a/rqalpha/model/bar.py +++ b/rqalpha/model/bar.py @@ -17,13 +17,13 @@ import six import numpy as np +from ..execution_context import ExecutionContext from ..environment import Environment from ..const import RUN_TYPE from ..utils.datetime_func import convert_int_to_datetime from ..utils.i18n import gettext as _ from ..utils.logger import system_log from ..utils.exception import patch_user_exc -from ..execution_context import ExecutionContext from ..const import EXECUTION_PHASE, BAR_STATUS @@ -49,7 +49,7 @@ def __init__(self, instrument, data, dt=None): @property def open(self): """ - 【float】当日开盘价 + [float] 当日开盘价 """ return self._data["open"] @@ -60,14 +60,14 @@ def close(self): @property def low(self): """ - 【float】截止到当前的最低价 + [float] 截止到当前的最低价 """ return self._data["low"] @property def high(self): """ - 【float】截止到当前的最高价 + [float] 截止到当前的最高价 """ return self._data["high"] @@ -76,65 +76,21 @@ def limit_up(self): try: v = self._data['limit_up'] return v if v != 0 else np.nan - except (ValueError, KeyError): - if self._limit_up is None: - data_proxy = ExecutionContext.data_proxy - dt = ExecutionContext.get_current_trading_dt() - if data_proxy.is_st_stock(self._instrument.order_book_id, dt): - self._limit_up = np.floor(self.prev_close * 10500) / 10000 - else: - self._limit_up = np.floor(self.prev_close * 11000) / 10000 - return self._limit_up + except (KeyError, ValueError): + return np.nan @property def limit_down(self): try: v = self._data['limit_down'] return v if v != 0 else np.nan - except (ValueError, KeyError): - if self._limit_down is None: - data_proxy = ExecutionContext.data_proxy - dt = ExecutionContext.get_current_trading_dt() - if data_proxy.is_st_stock(self._instrument.order_book_id, dt): - self._limit_down = np.ceil(self.prev_close * 9500) / 10000 - else: - self._limit_down = np.ceil(self.prev_close * 9000) / 10000 - return self._limit_down - - @property - def _internal_limit_up(self): - try: - v = self._data['limit_up'] - return v if v != 0 else np.nan - except (ValueError, KeyError): - if self.__internal_limit_up is None: - data_proxy = ExecutionContext.data_proxy - dt = ExecutionContext.get_current_trading_dt() - if data_proxy.is_st_stock(self._instrument.order_book_id, dt): - self.__internal_limit_up = np.floor(self.prev_close * 10490) / 10000 - else: - self.__internal_limit_up = np.floor(self.prev_close * 10990) / 10000 - return self.__internal_limit_up - - @property - def _internal_limit_down(self): - try: - v = self._data['limit_down'] - return v if v != 0 else np.nan - except (ValueError, KeyError): - if self.__internal_limit_down is None: - data_proxy = ExecutionContext.data_proxy - dt = ExecutionContext.get_current_trading_dt() - if data_proxy.is_st_stock(self._instrument.order_book_id, dt): - self.__internal_limit_down = np.ceil(self.prev_close * 9510) / 10000 - else: - self.__internal_limit_down = np.ceil(self.prev_close * 9010) / 10000 - return self.__internal_limit_down + except (KeyError, ValueError): + return np.nan @property def prev_close(self): """ - 【float】截止到当前的最低价 + [float] 截止到当前的最低价 """ try: return self._data['prev_close'] @@ -142,9 +98,9 @@ def prev_close(self): pass if self._prev_close is None: - data_proxy = ExecutionContext.data_proxy - dt = ExecutionContext.get_current_trading_dt() - self._prev_close = data_proxy.get_prev_close(self._instrument.order_book_id, dt) + trading_dt = Environment.get_instance().trading_dt + data_proxy = Environment.get_instance().data_proxy + self._prev_close = data_proxy.get_prev_close(self._instrument.order_book_id, trading_dt) return self._prev_close @property @@ -154,30 +110,30 @@ def _bar_status(self): """ if self.isnan or np.isnan(self.limit_up): return BAR_STATUS.ERROR - if self.close >= self._internal_limit_up: + if self.close >= self.limit_up: return BAR_STATUS.LIMIT_UP - if self.close <= self._internal_limit_down: + if self.close <= self.limit_down: return BAR_STATUS.LIMIT_DOWN return BAR_STATUS.NORMAL @property def last(self): """ - 【float】当前最新价 + [float] 当前最新价 """ return self.close @property def volume(self): """ - 【float】截止到当前的成交量 + [float] 截止到当前的成交量 """ return self._data["volume"] @property def total_turnover(self): """ - 【float】截止到当前的成交额 + [float] 截止到当前的成交额 """ return self._data['total_turnover'] @@ -204,13 +160,13 @@ def basis_spread(self): try: return self._data['basis_spread'] except (ValueError, KeyError): - if self._instrument.type != 'Future' or ExecutionContext.config.base.run_type != RUN_TYPE.PAPER_TRADING: + if self._instrument.type != 'Future' or Environment.get_instance().config.base.run_type != RUN_TYPE.PAPER_TRADING: raise if self._basis_spread is None: if self._instrument.underlying_symbol in ['IH', 'IC', 'IF']: order_book_id = self.INDEX_MAP[self._instrument.underlying_symbol] - bar = ExecutionContext.data_proxy.get_bar(order_book_id, None, '1m') + bar = Environment.get_instance().data_proxy.get_bar(order_book_id, None, '1m') self._basis_spread = self.close - bar.close else: self._basis_spread = np.nan @@ -223,7 +179,7 @@ def settlement(self): @property def prev_settlement(self): """ - 【float】昨日结算价(期货专用) + [float] 昨日结算价(期货专用) """ try: return self._data['prev_settlement'] @@ -231,15 +187,15 @@ def prev_settlement(self): pass if self._prev_settlement is None: - data_proxy = ExecutionContext.data_proxy - dt = ExecutionContext.get_current_trading_dt().date() - self._prev_settlement = data_proxy.get_prev_settlement(self._instrument.order_book_id, dt) + trading_dt = Environment.get_instance().trading_dt + data_proxy = Environment.get_instance().data_proxy + self._prev_settlement = data_proxy.get_prev_settlement(self._instrument.order_book_id, trading_dt) return self._prev_settlement @property def open_interest(self): """ - 【float】截止到当前的持仓量(期货专用) + [float] 截止到当前的持仓量(期货专用) """ return self._data['open_interest'] @@ -256,21 +212,21 @@ def instrument(self): @property def order_book_id(self): """ - 【str】交易标的代码 + [str] 交易标的代码 """ return self._instrument.order_book_id @property def symbol(self): """ - 【str】合约简称 + [str] 合约简称 """ return self._instrument.symbol @property def is_trading(self): """ - 【datetime.datetime】 时间戳 + [datetime.datetime] 时间戳 """ return self._data['volume'] > 0 @@ -283,8 +239,7 @@ def suspended(self): if self.isnan: return True - data_proxy = ExecutionContext.data_proxy - return data_proxy.is_suspended(self._instrument.order_book_id, int(self._data['datetime'] // 1000000)) + return Environment.get_instance().data_proxy.is_suspended(self._instrument.order_book_id, int(self._data['datetime'] // 1000000)) def mavg(self, intervals, frequency='1d'): if frequency == 'day': @@ -293,14 +248,13 @@ def mavg(self, intervals, frequency='1d'): frequency = '1m' # copy form history - dt = ExecutionContext.get_current_calendar_dt() - data_proxy = ExecutionContext.data_proxy + env = Environment.get_instance() + dt = env.calendar_dt - if (Environment.get_instance().config.base.frequency == '1m' and frequency == '1d') or \ - ExecutionContext.get_active().phase == EXECUTION_PHASE.BEFORE_TRADING: + if (env.config.base.frequency == '1m' and frequency == '1d') or ExecutionContext.phase() == EXECUTION_PHASE.BEFORE_TRADING: # 在分钟回测获取日线数据, 应该推前一天 - dt = data_proxy.get_previous_trading_date(dt.date()) - bars = data_proxy.fast_history(self._instrument.order_book_id, intervals, frequency, 'close', dt) + dt = env.data_proxy.get_previous_trading_date(env.calendar_dt.date()) + bars = env.data_proxy.fast_history(self._instrument.order_book_id, intervals, frequency, 'close', dt) return bars.mean() def vwap(self, intervals, frequency='1d'): @@ -310,14 +264,13 @@ def vwap(self, intervals, frequency='1d'): frequency = '1m' # copy form history - dt = ExecutionContext.get_current_calendar_dt() - data_proxy = ExecutionContext.data_proxy + env = Environment.get_instance() + dt = env.calendar_dt - if (Environment.get_instance().config.base.frequency == '1m' and frequency == '1d') or \ - ExecutionContext.get_active().phase == EXECUTION_PHASE.BEFORE_TRADING: + if (env.config.base.frequency == '1m' and frequency == '1d') or ExecutionContext.phase() == EXECUTION_PHASE.BEFORE_TRADING: # 在分钟回测获取日线数据, 应该推前一天 - dt = data_proxy.get_previous_trading_date(dt.date()) - bars = data_proxy.fast_history(self._instrument.order_book_id, intervals, frequency, ['close', 'volume'], dt) + dt = env.data_proxy.get_previous_trading_date(env.calendar_dt.date()) + bars = env.data_proxy.fast_history(self._instrument.order_book_id, intervals, frequency, ['close', 'volume'], dt) sum = bars['volume'].sum() if sum == 0: # 全部停牌 @@ -359,19 +312,19 @@ def update_dt(self, dt): self._cache.clear() def items(self): - return ((o, self.__getitem__(o)) for o in Environment.get_instance().universe) + return ((o, self.__getitem__(o)) for o in Environment.get_instance().get_universe()) def keys(self): - return (o for o in Environment.get_instance().universe) + return (o for o in Environment.get_instance().get_universe()) def values(self): - return (self.__getitem__(o) for o in Environment.get_instance().universe) + return (self.__getitem__(o) for o in Environment.get_instance().get_universe()) def __contains__(self, o): - return o in Environment.get_instance().universe + return o in Environment.get_instance().get_universe() def __len__(self): - return len(Environment.get_instance().universe) + return len(Environment.get_instance().get_universe()) def __getitem__(self, key): if not isinstance(key, six.string_types): @@ -389,7 +342,7 @@ def __getitem__(self, key): bar = self._data_proxy.get_bar(order_book_id, self._dt, self._frequency) except Exception as e: system_log.exception(e) - raise patch_user_exc(KeyError(_("id_or_symbols {} does not exist").format(key))) + raise patch_user_exc(KeyError(_(u"id_or_symbols {} does not exist").format(key))) if bar is None: return BarObject(instrument, NANDict, self._dt) else: diff --git a/rqalpha/model/dividend.py b/rqalpha/model/dividend.py deleted file mode 100644 index 15784a36c..000000000 --- a/rqalpha/model/dividend.py +++ /dev/null @@ -1,40 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2017 Ricequant, Inc -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import six - -DividendPersistMap = { - "order_book_id": "order_book_id", - "quantity": "quantity", - "dividend_series_dict": "dividend_series_dict", -} - - -class Dividend(object): - def __init__(self, order_book_id, quantity, dividend_series_dict): - self.order_book_id = order_book_id - self.quantity = quantity - self.dividend_series_dict = dividend_series_dict - - @classmethod - def __from_dict__(cls, dividend_dict): - return cls(dividend_dict['order_book_id'], dividend_dict["quantity"], dividend_dict["dividend_series_dict"]) - - def __to_dict__(self): - dividend_dict = {} - for persist_key, origin_key in six.iteritems(DividendPersistMap): - dividend_dict[persist_key] = getattr(self, origin_key) - return dividend_dict diff --git a/rqalpha/model/instrument.py b/rqalpha/model/instrument.py index e29aade95..759c9731e 100644 --- a/rqalpha/model/instrument.py +++ b/rqalpha/model/instrument.py @@ -17,7 +17,7 @@ import six import datetime -from ..execution_context import ExecutionContext +from ..environment import Environment from ..utils import instrument_type_str2enum @@ -48,14 +48,14 @@ def __repr__(self): @property def listing(self): - now = ExecutionContext.get_current_calendar_dt() + now = Environment.get_instance().calendar_dt return self.listed_date <= now <= self.de_listed_date def days_from_listed(self): if self.listed_date == self.DEFAULT_LISTED_DATE: return -1 - date = ExecutionContext.get_current_trading_dt().date() + date = Environment.get_instance().trading_dt.date() if self.de_listed_date.date() < date: return -1 @@ -69,8 +69,8 @@ def enum_type(self): def days_to_expire(self): if self.type != 'Future' or self.order_book_id[-2:] == '88' or self.order_book_id[-2:] == '99': return -1 - - date = ExecutionContext.get_current_trading_dt().date() + + date = Environment.get_instance().trading_dt.date() days = (self.maturity_date.date() - date).days return -1 if days < 0 else days @@ -124,7 +124,7 @@ def name(self): return self.__name def __repr__(self): - return "{0}:{1}".format(self.__code,self.__name) + return "{0}:{1}".format(self.__code, self.__name) class IndustryCode(object): diff --git a/rqalpha/model/margin.py b/rqalpha/model/margin.py deleted file mode 100644 index d9690f593..000000000 --- a/rqalpha/model/margin.py +++ /dev/null @@ -1,26 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2017 Ricequant, Inc -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from ..execution_context import ExecutionContext - - -class Margin(object): - def __init__(self, multiplier): - self.multiplier = multiplier - - def cal_margin(self, order_book_id, side, value): - margin_rate = ExecutionContext.get_future_margin(order_book_id) - return self.multiplier * value * margin_rate diff --git a/rqalpha/model/order.py b/rqalpha/model/order.py index 92ba6c0ad..595d546f0 100644 --- a/rqalpha/model/order.py +++ b/rqalpha/model/order.py @@ -14,33 +14,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -import six import time -from ..const import ORDER_STATUS, SIDE, POSITION_EFFECT, ORDER_TYPE +from ..const import ORDER_STATUS, ORDER_TYPE, SIDE, POSITION_EFFECT from ..utils import id_gen from ..utils.repr import property_repr, properties from ..utils.logger import user_system_log -OrderPersistMap = { - "_order_id": "_order_id", - "_calendar_dt": "_calendar_dt", - "_trading_dt": "_trading_dt", - "_quantity": "_quantity", - "_order_book_id": "_order_book_id", - "_side": "_side", - "_position_effect": "_position_effect", - "_message": "_message", - "_filled_quantity": "_filled_quantity", - "_status": "_status", - "_frozen_price": "_frozen_price", - "_type": "_type", - "_avg_price": "_avg_price", - "_transaction_cost": "_transaction_cost", -} - - class Order(object): order_id_gen = id_gen(int(time.time())) @@ -63,6 +44,47 @@ def __init__(self): self._avg_price = None self._transaction_cost = None + @staticmethod + def _enum_to_str(v): + return v.name + + @staticmethod + def _str_to_enum(enum_class, s): + return enum_class.__members__[s] + + def get_state(self): + return { + 'order_id': self._order_id, + 'calendar_dt': self._calendar_dt, + 'trading_dt': self._trading_dt, + 'order_book_id': self._order_book_id, + 'quantity': self._quantity, + 'side': self._enum_to_str(self._side), + 'position_effect': self._enum_to_str(self._position_effect) if self._position_effect is not None else None, + 'message': self._message, + 'filled_quantity': self._filled_quantity, + 'status': self._enum_to_str(self._status), + 'frozen_price': self._frozen_price, + 'type': self._enum_to_str(self._type), + } + + def set_state(self, d): + self._order_id = d['order_id'] + self._calendar_dt = d['calendar_dt'] + self._trading_dt = d['trading_dt'] + self._order_book_id = d['order_book_id'] + self._quantity = d['quantity'] + self._side = self._str_to_enum(SIDE, d['side']) + if d['position_effect'] is None: + self._position_effect = None + else: + self._position_effect = self._str_to_enum(POSITION_EFFECT, d['position_effect']) + self._message = d['messages'] + self._filled_quantity = d['filled_quantity'] + self._status = self._str_to_enum(ORDER_STATUS, d['status']) + self._frozen_price = d['frozen_price'] + self._type = self._str_to_enum(ORDER_TYPE, d['type']) + @classmethod def __from_create__(cls, calendar_dt, trading_dt, order_book_id, quantity, side, style, position_effect): order = cls() @@ -86,164 +108,166 @@ def __from_create__(cls, calendar_dt, trading_dt, order_book_id, quantity, side, order._transaction_cost = 0 return order - @classmethod - def __from_dict__(cls, order_dict): - order = cls() - for persist_key, origin_key in six.iteritems(OrderPersistMap): - setattr(order, origin_key, order_dict[persist_key]) - return order - - def __to_dict__(self): - order_dict = {} - for persist_key, origin_key in six.iteritems(OrderPersistMap): - order_dict[persist_key] = getattr(self, origin_key) - return order_dict - @property def order_id(self): """ - 【int】唯一标识订单的id + [int] 唯一标识订单的id """ return self._order_id @property def trading_datetime(self): """ - 【datetime.datetime】订单的交易日期(对应期货夜盘) + [datetime.datetime] 订单的交易日期(对应期货夜盘) """ return self._trading_dt @property def datetime(self): """ - 【datetime.datetime】订单创建时间 + [datetime.datetime] 订单创建时间 """ return self._calendar_dt @property def quantity(self): """ - 【int】订单数量 + [int] 订单数量 """ return self._quantity @property def unfilled_quantity(self): """ - 【int】订单未成交数量 + [int] 订单未成交数量 """ return self._quantity - self._filled_quantity @property def order_book_id(self): """ - 【str】合约代码 + [str] 合约代码 """ return self._order_book_id @property def side(self): """ - 【SIDE】订单方向 + [SIDE] 订单方向 """ return self._side @property def position_effect(self): """ - 【POSITION_EFFECT】订单开平(期货专用) + [POSITION_EFFECT] 订单开平(期货专用) """ return self._position_effect @property def message(self): """ - 【str】信息。比如拒单时候此处会提示拒单原因 + [str] 信息。比如拒单时候此处会提示拒单原因 """ return self._message @property def filled_quantity(self): """ - 【int】订单已成交数量 + [int] 订单已成交数量 """ return self._filled_quantity @property def status(self): """ - 【ORDER_STATUS】订单状态 + [ORDER_STATUS] 订单状态 """ return self._status @property def price(self): """ - 【float】订单价格,只有在订单类型为'限价单'的时候才有意义 + [float] 订单价格,只有在订单类型为'限价单'的时候才有意义 """ return 0 if self.type == ORDER_TYPE.MARKET else self._frozen_price @property def type(self): """ - 【ORDER_TYPE】订单类型 + [ORDER_TYPE] 订单类型 """ return self._type @property def avg_price(self): """ - 【float】成交均价 + [float] 成交均价 """ return self._avg_price @property def transaction_cost(self): """ - 【float】费用 + [float] 费用 """ return self._transaction_cost - def _is_final(self): - if self.status == ORDER_STATUS.PENDING_NEW or self.status == ORDER_STATUS.ACTIVE: - return False - else: - return True + @property + def frozen_price(self): + """ + [float] 冻结价格 + """ + return self._frozen_price + + def is_final(self): + return self._status not in { + ORDER_STATUS.PENDING_NEW, + ORDER_STATUS.ACTIVE, + ORDER_STATUS.PENDING_CANCEL + } - def _is_active(self): + def is_active(self): return self.status == ORDER_STATUS.ACTIVE - def _active(self): + def active(self): self._status = ORDER_STATUS.ACTIVE - def _fill(self, trade): - amount = trade.last_quantity - assert self.filled_quantity + amount <= self.quantity - new_quantity = self._filled_quantity + amount - self._avg_price = (self._avg_price * self._filled_quantity + trade.last_price * amount) / new_quantity + def set_pending_cancel(self): + if not self.is_final(): + self._status = ORDER_STATUS.PENDING_CANCEL + + def fill(self, trade): + quantity = trade.last_quantity + assert self.filled_quantity + quantity <= self.quantity + new_quantity = self._filled_quantity + quantity + self._avg_price = (self._avg_price * self._filled_quantity + trade.last_price * quantity) / new_quantity self._transaction_cost += trade.commission + trade.tax self._filled_quantity = new_quantity if self.unfilled_quantity == 0: self._status = ORDER_STATUS.FILLED - def _mark_rejected(self, reject_reason): - if not self._is_final(): + def mark_rejected(self, reject_reason): + if not self.is_final(): self._message = reject_reason self._status = ORDER_STATUS.REJECTED user_system_log.warn(reject_reason) - def _mark_cancelled(self, cancelled_reason, user_warn=True): - if not self._is_final(): + def mark_cancelled(self, cancelled_reason, user_warn=True): + if not self.is_final(): self._message = cancelled_reason self._status = ORDER_STATUS.CANCELLED if user_warn: user_system_log.warn(cancelled_reason) + def set_frozen_price(self, value): + self._frozen_price = value + def __simple_object__(self): return properties(self) -class OrderStyle: +class OrderStyle(object): def get_limit_price(self): raise NotImplementedError diff --git a/rqalpha/model/portfolio.py b/rqalpha/model/portfolio.py new file mode 100644 index 000000000..8372dde38 --- /dev/null +++ b/rqalpha/model/portfolio.py @@ -0,0 +1,249 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2017 Ricequant, Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import six +import jsonpickle + +from ..environment import Environment +from ..const import DAYS_CNT, ACCOUNT_TYPE +from ..utils import get_account_type, merge_dicts +from ..utils.repr import property_repr +from ..events import EVENT + + +class Portfolio(object): + __repr__ = property_repr + + def __init__(self, start_date, static_unit_net_value, units, accounts, register_event=True): + self._start_date = start_date + self._static_unit_net_value = static_unit_net_value + self._units = units + self._accounts = accounts + self._mixed_positions = None + if register_event: + self.register_event() + + def register_event(self): + """ + 注册事件 + """ + event_bus = Environment.get_instance().event_bus + event_bus.prepend_listener(EVENT.PRE_BEFORE_TRADING, self._pre_before_trading) + + def get_state(self): + return jsonpickle.encode({ + 'start_date': self._start_date, + 'static_unit_net_value': self._static_unit_net_value, + 'units': self._units, + 'accounts': { + name: account.get_state() for name, account in six.iteritems(self._accounts) + } + }).encode('utf-8') + + def set_state(self, state): + state = state.decode('utf-8') + value = jsonpickle.decode(state) + self._start_date = value['start_date'] + self._static_unit_net_value = value['static_unit_net_value'] + self._units = value['units'] + for k, v in six.iteritems(value['accounts']): + if k == 'ACCOUNT_TYPE.STOCK': + self._accounts[ACCOUNT_TYPE.STOCK].set_state(v) + elif k == 'ACCOUNT_TYPE.FUTURE': + self._accounts[ACCOUNT_TYPE.FUTURE].set_state(v) + elif k == 'ACCOUNT_TYPE.BENCHMARK': + self._accounts[ACCOUNT_TYPE.BENCHMARK].set_state(v) + else: + raise NotImplementedError + + def _pre_before_trading(self, event): + self._static_unit_net_value = self.unit_net_value + + @property + def accounts(self): + """ + [dict] 账户字典 + """ + return self._accounts + + @property + def stock_account(self): + """ + [StockAccount] 股票账户 + """ + return self._accounts.get(ACCOUNT_TYPE.STOCK, None) + + @property + def future_account(self): + """ + [FutureAccount] 期货账户 + """ + return self._accounts.get(ACCOUNT_TYPE.FUTURE, None) + + @property + def start_date(self): + """ + [datetime.datetime] 策略投资组合的开始日期 + """ + return self._start_date + + @property + def units(self): + """ + [float] 份额 + """ + return self._units + + @property + def unit_net_value(self): + """ + [float] 实时净值 + """ + return self.total_value / self._units + + @property + def static_unit_net_value(self): + return self._static_unit_net_value + + @property + def daily_pnl(self): + """ + [float] 当日盈亏 + """ + return self.total_value - self._static_unit_net_value * self.units + + @property + def daily_returns(self): + """ + [float] 当前最新一天的日收益 + """ + return 0 if self._static_unit_net_value == 0 else self.unit_net_value / self._static_unit_net_value - 1 + + @property + def total_returns(self): + """ + [float] 累计收益率 + """ + return self.unit_net_value - 1 + + @property + def annualized_returns(self): + """ + [float] 累计年化收益率 + """ + current_date = Environment.get_instance().trading_dt.date() + return self.unit_net_value ** (DAYS_CNT.DAYS_A_YEAR / float((current_date - self.start_date).days + 1)) - 1 + + @property + def total_value(self): + """ + [float]总权益 + """ + return sum(account.total_value for account in six.itervalues(self._accounts)) + + @property + def portfolio_value(self): + """ + [Deprecated] 总权益 + """ + return self.total_value + + @property + def positions(self): + """ + [dict] 持仓 + """ + if self._mixed_positions is None: + self._mixed_positions = MixedPositions(self._accounts) + return self._mixed_positions + + @property + def cash(self): + """ + [float] 可用资金 + """ + return sum(account.cash for account in six.itervalues(self._accounts)) + + @property + def dividend_receivable(self): + return sum(getattr(account, 'dividend_receivable', 0) for account in six.itervalues(self._accounts)) + + @property + def transaction_cost(self): + return sum(account.transaction_cost for account in six.itervalues(self._accounts)) + + @property + def market_value(self): + """ + [float] 市值 + """ + return sum(account.market_value for account in six.itervalues(self._accounts)) + + @property + def pnl(self): + return (self.unit_net_value - 1) * self.units + + @property + def starting_cash(self): + return self.units + + @property + def frozen_cash(self): + return sum(account.frozen_cash for account in six.itervalues(self._accounts)) + + +class MixedPositions(dict): + + def __init__(self, accounts): + super(MixedPositions, self).__init__() + self._accounts = accounts + + def __missing__(self, key): + account_type = get_account_type(key) + for a_type in self._accounts: + if a_type == account_type: + return self._accounts[a_type].positions[key] + return None + + def __contains__(self, item): + return item in self.keys() + + def __repr__(self): + keys = [] + for account in six.itervalues(self._accounts): + keys += account.positions.keys() + return str(sorted(keys)) + + def __len__(self): + return sum(len(account.positions) for account in six.itervalues(self._accounts)) + + def __iter__(self): + keys = [] + for account in six.itervalues(self._accounts): + keys += account.positions.keys() + for key in sorted(keys): + yield key + + def items(self): + items = merge_dicts(*[account.positions.items() for account in six.itervalues(self._accounts)]) + for k in sorted(items.keys()): + yield k, items[k] + + def keys(self): + keys = [] + for account in six.itervalues(self._accounts): + keys += list(account.positions.keys()) + return sorted(keys) diff --git a/rqalpha/model/portfolio/__init__.py b/rqalpha/model/portfolio/__init__.py deleted file mode 100644 index 2b0c73755..000000000 --- a/rqalpha/model/portfolio/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2017 Ricequant, Inc -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from .future_portfolio import FuturePortfolio -from .stock_portfolio import StockPortfolio -from .base_portfolio import BasePortfolio -from ...const import ACCOUNT_TYPE - - -def init_portfolio(init_cash, start_date, account_type): - if account_type == ACCOUNT_TYPE.STOCK: - return StockPortfolio(init_cash, start_date, ACCOUNT_TYPE.STOCK) - elif account_type == ACCOUNT_TYPE.FUTURE: - return FuturePortfolio(init_cash, start_date, ACCOUNT_TYPE.FUTURE) - elif account_type == ACCOUNT_TYPE.BENCHMARK: - return StockPortfolio(init_cash, start_date, ACCOUNT_TYPE.BENCHMARK) \ No newline at end of file diff --git a/rqalpha/model/portfolio/base_portfolio.py b/rqalpha/model/portfolio/base_portfolio.py deleted file mode 100644 index 76e84af99..000000000 --- a/rqalpha/model/portfolio/base_portfolio.py +++ /dev/null @@ -1,160 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2017 Ricequant, Inc -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from six import itervalues - -from ...const import DAYS_CNT -from ...utils.repr import property_repr - - -class BasePortfolio(object): - - __repr__ = property_repr - - def __init__(self, cash, start_date, account_type): - self._account_type = account_type - self._yesterday_portfolio_value = cash - self._yesterday_units = cash - self._cash = cash - self._starting_cash = cash - self._units = cash - self._start_date = start_date - self._current_date = start_date - - self._frozen_cash = 0. - self._total_commission = 0. - self._total_tax = 0. - - self._dividend_receivable = 0. - self._dividend_info = {} - - @property - def _type(self): - return self._account_type - - @property - def _pid(self): - return self._account_type.value - - @property - def daily_returns(self): - """ - 【float】当前最新一天的每日收益 - """ - return 0 if self.yesterday_unit_net_value == 0 else self.unit_net_value / self.yesterday_unit_net_value - 1 - - @property - def yesterday_unit_net_value(self): - return 0 if self._yesterday_units == 0 else self._yesterday_portfolio_value / self._yesterday_units - - @property - def unit_net_value(self): - return 0 if self._units == 0 else self.portfolio_value / self._units - - @property - def yesterday_units(self): - return self._yesterday_units - - @property - def units(self): - return self._units - - @property - def starting_cash(self): - """ - 【float】回测或实盘交易给算法策略设置的初始资金 - """ - return self._starting_cash - - @property - def start_date(self): - """ - 【datetime.datetime】策略投资组合的回测/实时模拟交易的开始日期 - """ - return self._start_date - - @property - def frozen_cash(self): - """ - 【float】冻结资金 - """ - return self._frozen_cash - - @property - def cash(self): - # 可用资金 - raise NotImplementedError - - @property - def portfolio_value(self): - """ - 【float】总权益,包含市场价值和剩余现金 - """ - # 投资组合总值 - raise NotImplementedError - - @property - def positions(self): - # 持仓 - raise NotImplementedError - - @property - def daily_pnl(self): - """ - 【float】当日盈亏,当日投资组合总权益-昨日投资组合总权益 - """ - # 当日盈亏 - raise NotImplementedError - - @property - def market_value(self): - """ - 【float】投资组合当前所有证券仓位的市值的加总 - """ - return sum(position.market_value for position in itervalues(self.positions)) - - @property - def pnl(self): - """ - 【float】当前投资组合的累计盈亏 - """ - # 总盈亏 - return self.portfolio_value - self.starting_cash - - @property - def total_returns(self): - """ - 【float】投资组合至今的累积收益率。计算方法是现在的投资组合价值/投资组合的初始资金 - """ - # 总收益率 - return 0 if self.starting_cash == 0 else self.pnl / self.starting_cash - - @property - def annualized_returns(self): - """ - 【float】投资组合的年化收益率 - """ - # 年化收益率 - return (1 + self.total_returns) ** ( - DAYS_CNT.DAYS_A_YEAR / float((self._current_date - self.start_date).days + 1)) - 1 - - @property - def transaction_cost(self): - """ - 【float】总费用 - """ - # 总费用 - return self._total_commission + self._total_tax diff --git a/rqalpha/model/portfolio/future_portfolio.py b/rqalpha/model/portfolio/future_portfolio.py deleted file mode 100644 index fbb4146fd..000000000 --- a/rqalpha/model/portfolio/future_portfolio.py +++ /dev/null @@ -1,179 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2017 Ricequant, Inc -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import six - -from .base_portfolio import BasePortfolio -from ..dividend import Dividend -from ..position import Positions, PositionsClone, FuturePosition -from ...utils.repr import dict_repr - - -FuturePersistMap = { - "_yesterday_portfolio_value": "_yesterday_portfolio_value", - "_yesterday_units": "_yesterday_units", - "_cash": "_cash", - "_starting_cash": "_starting_cash", - "_units": "_units", - "_start_date": "_start_date", - "_current_date": "_current_date", - "_frozen_cash": "_frozen_cash", - "_total_commission": "_total_commission", - "_total_tax": "_total_tax", - "_dividend_receivable": "_dividend_receivable", - "_dividend_info": "_dividend_info", - "_daily_transaction_cost": "_daily_transaction_cost", - "_positions": "_positions", -} - - -class FuturePortfolioClone(object): - __repr__ = dict_repr - - -class FuturePortfolio(BasePortfolio): - def __init__(self, cash, start_date, account_type): - super(FuturePortfolio, self).__init__(cash, start_date, account_type) - self._daily_transaction_cost = 0 - self._positions = Positions(FuturePosition) - self._portfolio_value = None - - def restore_from_dict_(self, portfolio_dict): - self._cash = portfolio_dict['_cash'] - self._start_date = portfolio_dict['_start_date'] - self._positions.clear() - self._dividend_info.clear() - for persist_key, origin_key in six.iteritems(FuturePersistMap): - if persist_key == "_dividend_info": - for order_book_id, dividend_dict in six.iteritems(portfolio_dict[persist_key]): - self._dividend_info[order_book_id] = Dividend.__from_dict__(dividend_dict) - elif persist_key == "_positions": - for order_book_id, position_dict in six.iteritems(portfolio_dict[persist_key]): - self._positions[order_book_id] = FuturePosition.__from_dict__(position_dict) - else: - try: - setattr(self, origin_key, portfolio_dict[persist_key]) - except KeyError as e: - if persist_key in ["_yesterday_units", "_units"]: - # FIXME 对于已有 persist_key 做暂时error handling 处理。 - setattr(self, origin_key, portfolio_dict["_starting_cash"]) - else: - raise e - - def __to_dict__(self): - p_dict = {} - for persist_key, origin_key in six.iteritems(FuturePersistMap): - if persist_key == "_dividend_info": - p_dict[persist_key] = {oid: dividend.__to_dict__() for oid, dividend in six.iteritems(getattr(self, origin_key))} - elif persist_key == "_positions": - p_dict[persist_key] = {oid: position.__to_dict__() for oid, position in six.iteritems(getattr(self, origin_key))} - else: - p_dict[persist_key] = getattr(self, origin_key) - return p_dict - - @property - def positions(self): - """ - 【dict】一个包含期货子组合仓位的字典,以order_book_id作为键,position对象作为值 - """ - return self._positions - - @property - def cash(self): - """ - 【float】可用资金 - """ - return self.portfolio_value - self.margin - self.daily_holding_pnl - self.frozen_cash - - @property - def portfolio_value(self): - """ - 【float】总权益,昨日总权益+当日盈亏 - """ - if self._portfolio_value is None: - self._portfolio_value = self._yesterday_portfolio_value + self.daily_pnl - - return self._portfolio_value - - # @property - # def _risk_cash(self): - # # 风控资金 - # risk_cash = self.cash if self.daily_holding_pnl > 0 else self.cash + self.daily_holding_pnl - # return risk_cash - - @property - def buy_margin(self): - """ - 【float】多头保证金 - """ - # 买保证金 - # TODO 这里需要考虑 T TF 这种跨合约单向大边的情况 - # TODO 这里需要考虑 同一个合约跨期单向大边的情况 - return sum(position.buy_margin for position in six.itervalues(self.positions)) - - @property - def sell_margin(self): - """ - 【float】空头保证金 - """ - # 卖保证金 - return sum(position.sell_margin for position in six.itervalues(self.positions)) - - @property - def margin(self): - """ - 【float】已占用保证金 - """ - # 总保证金 - return sum(position.margin for position in six.itervalues(self.positions)) - - @property - def daily_holding_pnl(self): - """ - 【float】当日浮动盈亏 - """ - # 当日持仓盈亏 - return sum(position.daily_holding_pnl for position in six.itervalues(self.positions)) - - @property - def daily_realized_pnl(self): - """ - 【float】当日平仓盈亏 - """ - # 当日平仓盈亏 - return sum(position.daily_realized_pnl for position in six.itervalues(self.positions)) - - @property - def daily_pnl(self): - """ - 【float】当日盈亏,当日浮动盈亏 + 当日平仓盈亏 - 当日费用 - """ - # 当日盈亏 - return self.daily_realized_pnl + self.daily_holding_pnl - self._daily_transaction_cost - - def _clone(self): - p = FuturePortfolioClone() - for key in dir(self): - if "__" in key: - continue - if key == "positions": - ps = PositionsClone(FuturePosition) - for order_book_id in self.positions: - ps[order_book_id] = self.positions[order_book_id]._clone() - setattr(p, key, ps) - else: - setattr(p, key, getattr(self, key)) - return p diff --git a/rqalpha/model/portfolio/stock_portfolio.py b/rqalpha/model/portfolio/stock_portfolio.py deleted file mode 100644 index dd487eee2..000000000 --- a/rqalpha/model/portfolio/stock_portfolio.py +++ /dev/null @@ -1,141 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2017 Ricequant, Inc -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import six - -from .base_portfolio import BasePortfolio -from ..dividend import Dividend -from ..position import Positions, PositionsClone, StockPosition -from ...utils.repr import dict_repr - - -StockPersistMap = { - "_yesterday_portfolio_value": "_yesterday_portfolio_value", - "_yesterday_units": "_yesterday_units", - "_cash": "_cash", - "_starting_cash": "_starting_cash", - "_units": "_units", - "_start_date": "_start_date", - "_current_date": "_current_date", - "_frozen_cash": "_frozen_cash", - "_total_commission": "_total_commission", - "_total_tax": "_total_tax", - "_dividend_receivable": "_dividend_receivable", - "_dividend_info": "_dividend_info", - "_positions": "_positions", -} - - -# ET:如果这里修改的话,记得提醒我修改pickle那里,多谢 -class StockPortfolioClone(object): - __repr__ = dict_repr - - -class StockPortfolio(BasePortfolio): - - def __init__(self, cash, start_date, account_type): - super(StockPortfolio, self).__init__(cash, start_date, account_type) - self._positions = Positions(StockPosition) - self._portfolio_value = None - - def restore_from_dict_(self, portfolio_dict): - self._cash = portfolio_dict['_cash'] - self._start_date = portfolio_dict['_start_date'] - self._positions.clear() - self._dividend_info.clear() - for persist_key, origin_key in six.iteritems(StockPersistMap): - if persist_key == "_dividend_info": - tmp = {} - for order_book_id, dividend_dict in six.iteritems(portfolio_dict[persist_key]): - tmp[order_book_id] = Dividend.__from_dict__(dividend_dict) - setattr(self, origin_key, tmp) - elif persist_key == "_positions": - for order_book_id, position_dict in six.iteritems(portfolio_dict[persist_key]): - self._positions[order_book_id] = StockPosition.__from_dict__(position_dict) - else: - try: - setattr(self, origin_key, portfolio_dict[persist_key]) - except KeyError as e: - if persist_key in ["_yesterday_units", "_units"]: - # FIXME 对于已有 persist_key 做暂时error handling 处理。 - setattr(self, origin_key, portfolio_dict["_starting_cash"]) - else: - raise e - - def __to_dict__(self): - p_dict = {} - for persist_key, origin_key in six.iteritems(StockPersistMap): - if persist_key == "_dividend_info": - p_dict[persist_key] = {oid: dividend.__to_dict__() for oid, dividend in six.iteritems(getattr(self, origin_key))} - elif persist_key == "_positions": - p_dict[persist_key] = {oid: position.__to_dict__() for oid, position in six.iteritems(getattr(self, origin_key))} - else: - p_dict[persist_key] = getattr(self, origin_key) - return p_dict - - @property - def cash(self): - """ - 【float】可用资金 - """ - return self._cash - - @property - def positions(self): - """ - 【dict】一个包含股票子组合仓位的字典,以order_book_id作为键,position对象作为值,关于position的更多的信息可以在下面的部分找到。 - """ - return self._positions - - @property - def daily_pnl(self): - """ - 【float】当日盈亏,当日投资组合总权益-昨日投资组合总权益 - """ - return self.portfolio_value - self._yesterday_portfolio_value - - @property - def portfolio_value(self): - """ - 【float】总权益,包含市场价值和剩余现金 - """ - if self._portfolio_value is None: - # 总资金 + Sum(position._position_value) - self._portfolio_value = self.cash + self.frozen_cash + sum( - position._position_value for position in six.itervalues(self.positions)) - - return self._portfolio_value - - @property - def dividend_receivable(self): - """ - 【float】投资组合在分红现金收到账面之前的应收分红部分。具体细节在分红部分 - """ - return self._dividend_receivable - - def _clone(self): - p = StockPortfolioClone() - for key in dir(self): - if "__" in key: - continue - if key == "positions": - ps = PositionsClone(StockPosition) - for order_book_id in self.positions: - ps[order_book_id] = self.positions[order_book_id]._clone() - setattr(p, key, ps) - else: - setattr(p, key, getattr(self, key)) - return p diff --git a/rqalpha/model/position/__init__.py b/rqalpha/model/position/__init__.py index 093a0415f..d74053aea 100644 --- a/rqalpha/model/position/__init__.py +++ b/rqalpha/model/position/__init__.py @@ -14,53 +14,23 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .base_position import BasePosition, PositionClone +from .base_position import BasePosition from .future_position import FuturePosition from .stock_position import StockPosition class Positions(dict): - def __init__(self, position_type): + def __init__(self, position_cls): super(Positions, self).__init__() - self._position_type = position_type + self._position_cls = position_cls + self._cached_positions = {} def __missing__(self, key): - p = self._position_type(key) - self[key] = p - return p - - def _clone(self): - ps = {} - for order_book_id in self: - ps[order_book_id] = self[order_book_id]._clone() - return ps - - -class PositionsClone(dict): - def __init__(self, position_type): - super(PositionsClone, self).__init__() - self._position_type = position_type - - def __missing__(self, key): - p = PositionClone() - position = self._position_type(key) - for key in dir(position): - if "__" in key: - continue - setattr(p, key, getattr(position, key)) - return p - - def __repr__(self): - return str([order_book_id for order_book_id in self]) - - def __iter__(self): - for key in sorted(super(PositionsClone, self).keys()): - yield key - - def items(self): - for key in self.keys(): - yield key, self[key] - - def keys(self): - for key in sorted(super(PositionsClone, self).keys()): - yield key + if key not in self._cached_positions: + self._cached_positions[key] = self._position_cls(key) + return self._cached_positions[key] + + def get_or_create(self, key): + if key not in self: + self[key] = self._position_cls(key) + return self[key] diff --git a/rqalpha/model/position/base_position.py b/rqalpha/model/position/base_position.py index 7dc65df9f..154799bca 100644 --- a/rqalpha/model/position/base_position.py +++ b/rqalpha/model/position/base_position.py @@ -14,14 +14,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -from ...utils.repr import property_repr, dict_repr - - -class PositionClone(object): - __repr__ = dict_repr - - def __simple_object__(self): - return self.__dict__ +from ...utils.repr import property_repr +from ...environment import Environment +from ...utils.i18n import gettext as _ +from ...utils.logger import user_system_log class BasePosition(object): @@ -30,67 +26,62 @@ class BasePosition(object): def __init__(self, order_book_id): self._order_book_id = order_book_id - self._last_price = 0 - self._market_value = 0. - self._buy_trade_value = 0. - self._sell_trade_value = 0 - self._buy_order_value = 0. - self._sell_order_value = 0. - self._buy_order_quantity = 0 - self._sell_order_quantity = 0 - self._buy_trade_quantity = 0 - self._sell_trade_quantity = 0 + def get_state(self): + raise NotImplementedError - self._total_orders = 0 - self._total_trades = 0 + def set_state(self, state): + raise NotImplementedError - self._is_traded = False + @property + def order_book_id(self): + return self._order_book_id @property def market_value(self): """ - 【float】投资组合当前所有证券仓位的市值的加总 + [float] 当前仓位市值 """ - return self._market_value + raise NotImplementedError @property - def order_book_id(self): - """ - 【str】合约代码 - """ - return self._order_book_id + def transaction_cost(self): + raise NotImplementedError @property - def total_orders(self): - """ - 【int】该仓位的总订单的次数 - """ - return self._total_orders + def type(self): + raise NotImplementedError @property - def total_trades(self): + def last_price(self): + return Environment.get_instance().get_last_price(self._order_book_id) + + # -- Function + def is_de_listed(self): """ - 【int】该仓位的总成交的次数 + 判断合约是否过期 """ - return self._total_trades + instrument = Environment.get_instance().get_instrument(self._order_book_id) + current_date = Environment.get_instance().trading_dt + if instrument.de_listed_date is not None and current_date >= instrument.de_listed_date: + return True + return False - @property - def _position_value(self): + def apply_settlement(self): + raise NotImplementedError + + def apply_trade(self, trade): raise NotImplementedError @property - def pnl(self): - """ - 【float】持仓累计盈亏 - """ - return self._market_value + self._sell_trade_value - self._buy_trade_value - - def _clone(self): - p = PositionClone() - for key in dir(self): - if "__" in key: - continue - setattr(p, key, getattr(self, key)) - return p + def total_orders(self): + """abandon""" + user_system_log.warn(_(u"[abandon] {} is no longer valid.").format('position.total_orders')) + return 0 + + @property + def total_trades(self): + """abandon""" + user_system_log.warn(_(u"[abandon] {} is no longer valid.").format('position.total_trades')) + return 0 diff --git a/rqalpha/model/position/future_position.py b/rqalpha/model/position/future_position.py index 0aca9b0fa..1e3ef41ca 100644 --- a/rqalpha/model/position/future_position.py +++ b/rqalpha/model/position/future_position.py @@ -14,461 +14,446 @@ # See the License for the specific language governing permissions and # limitations under the License. -import six - from .base_position import BasePosition -from ...execution_context import ExecutionContext from ...environment import Environment -from ...const import ACCOUNT_TYPE, SIDE - - -FuturePersistMap = { - "_order_book_id": "_order_book_id", - "_last_price": "_last_price", - "_market_value": "_market_value", - "_buy_market_value": "_buy_market_value", - "_sell_market_value": "_sell_market_value", - "_buy_trade_value": "_buy_trade_value", - "_sell_trade_value": "_sell_trade_value", - "_buy_order_value": "_buy_order_value", - "_sell_order_value": "_sell_order_value", - "_buy_order_quantity": "_buy_order_quantity", - "_sell_order_quantity": "_sell_order_quantity", - "_buy_trade_quantity": "_buy_trade_quantity", - "_sell_trade_quantity": "_sell_trade_quantity", - "_total_orders": "_total_orders", - "_total_trades": "_total_trades", - "_is_traded": "_is_traded", - "_buy_open_order_value": "_buy_open_order_value", - "_sell_open_order_value": "_sell_open_order_value", - "_buy_close_order_value": "_buy_close_order_value", - "_sell_close_order_value": "_sell_close_order_value", - "_buy_open_order_quantity": "_buy_open_order_quantity", - "_sell_open_order_quantity": "_sell_open_order_quantity", - "_buy_close_order_quantity": "_buy_close_order_quantity", - "_sell_close_order_quantity": "_sell_close_order_quantity", - "_buy_open_trade_quantity": "_buy_open_trade_quantity", - "_sell_open_trade_quantity": "_sell_open_trade_quantity", - "_buy_close_trade_quantity": "_buy_close_trade_quantity", - "_sell_close_trade_quantity": "_sell_close_trade_quantity", - "_daily_realized_pnl": "_daily_realized_pnl", - "_prev_settle_price": "_prev_settle_price", - "_buy_old_holding_list": "_buy_old_holding_list", - "_sell_old_holding_list": "_sell_old_holding_list", - "_buy_today_holding_list": "_buy_today_holding_list", - "_sell_today_holding_list": "_sell_today_holding_list", - "_contract_multiplier": "_contract_multiplier", - "_de_listed_date": "_de_listed_date", - "_buy_open_transaction_cost": "_buy_open_transaction_cost", - "_buy_close_transaction_cost": "_buy_close_transaction_cost", - "_sell_open_transaction_cost": "_sell_open_transaction_cost", - "_sell_close_transaction_cost": "_sell_close_transaction_cost", - "_buy_daily_realized_pnl": "_buy_daily_realized_pnl", - "_sell_daily_realized_pnl": "_sell_daily_realized_pnl", - "_buy_avg_open_price": "_buy_avg_open_price", - "_sell_avg_open_price": "_sell_avg_open_price", -} +from ...const import SIDE, POSITION_EFFECT, ACCOUNT_TYPE -class FuturePosition(BasePosition): +def margin_of(order_book_id, quantity, price): + env = Environment.get_instance() + contract_multiplier = env.get_instrument(order_book_id).contract_multiplier + margin_rate = env.get_future_margin_rate(order_book_id) + margin_multiplier = env.config.base.margin_multiplier + return quantity * price * margin_multiplier * margin_rate * contract_multiplier - # buy_open_order_value: 买开挂单总值 - # sell_open_order_value: 卖开挂单总值 - # buy_close_order_value: 买平挂单总值 - # sell_close_order_value: 卖平挂单总值 - # buy_open_order_quantity: 买开挂单量 - # sell_open_order_quantity: 卖开挂单量 - # buy_close_order_quantity: 买平挂单量 - # sell_close_order_quantity: 卖平挂单量 - # buy_open_trade_quantity: 买开成交量 - # sell_open_trade_quantity: 卖开成交量 - # buy_close_trade_quantity: 买平成交量 - # sell_close_trade_quantity: 买平成交量 - # daily_realized_pnl: 当日平仓盈亏 - # buy_settle_holding: 买结算后昨仓 - # sell_settle_holding: 卖结算后昨仓 - # buy_today_holding_list: > 买当日持仓队列 - # sell_today_holding_list: > 卖当日持仓队列 +class FuturePosition(BasePosition): def __init__(self, order_book_id): super(FuturePosition, self).__init__(order_book_id) - self._buy_open_order_value = 0. - self._sell_open_order_value = 0. - self._buy_close_order_value = 0. - self._sell_close_order_value = 0. - self._buy_open_order_quantity = 0 - self._sell_open_order_quantity = 0 - self._buy_close_order_quantity = 0 - self._sell_close_order_quantity = 0 - self._buy_open_trade_quantity = 0 - self._buy_open_trade_value = 0 - self._sell_open_trade_quantity = 0 - self._sell_open_trade_value = 0 - self._buy_close_trade_quantity = 0 - self._buy_close_trade_value = 0 - self._sell_close_trade_quantity = 0 - self._sell_close_trade_value = 0 - self._daily_realized_pnl = 0. - self._prev_settle_price = 0. - self._buy_old_holding_list = [] # [(price, amount)] - self._sell_old_holding_list = [] # [(price, amount)] - self._buy_today_holding_list = [] # [(price, amount)] - self._sell_today_holding_list = [] # [(price, amount)] - instrument = ExecutionContext.get_instrument(self.order_book_id) - if instrument is None: - self._contract_multiplier = None - self._de_listed_date = None - else: - self._contract_multiplier = instrument.contract_multiplier - self._de_listed_date = instrument.de_listed_date - - self._buy_open_transaction_cost = 0. - self._buy_close_transaction_cost = 0. - self._sell_open_transaction_cost = 0. - self._sell_close_transaction_cost = 0. - self._buy_daily_realized_pnl = 0. - self._sell_daily_realized_pnl = 0. + + self._buy_old_holding_list = [] + self._sell_old_holding_list = [] + self._buy_today_holding_list = [] + self._sell_today_holding_list = [] + + self._buy_transaction_cost = 0. + self._sell_transaction_cost = 0. + self._buy_realized_pnl = 0. + self._sell_realized_pnl = 0. + self._buy_avg_open_price = 0. self._sell_avg_open_price = 0. - self._buy_market_value = 0. - self._sell_market_value = 0. - - @classmethod - def __from_dict__(cls, position_dict): - position = cls(position_dict["_order_book_id"]) - for persist_key, origin_key in six.iteritems(FuturePersistMap): - try: - setattr(position, origin_key, position_dict[persist_key]) - except KeyError as e: - if persist_key in ["_buy_avg_open_price", "_sell_avg_open_price"]: - # FIXME 对于已有 persist_key 做暂时error handling 处理。 - setattr(position, origin_key, 0.) - else: - raise e - return position - def __to_dict__(self): - p_dict = {} - for persist_key, origin_key in six.iteritems(FuturePersistMap): - p_dict[persist_key] = getattr(self, origin_key) - return p_dict + def get_state(self): + return { + 'order_book_id': self._order_book_id, + 'buy_old_holding_list': self._buy_old_holding_list, + 'sell_old_holding_list': self._sell_old_holding_list, + 'buy_today_holding_list': self._buy_today_holding_list, + 'sell_today_holding_list': self._sell_today_holding_list, + 'buy_transaction_cost': self._buy_transaction_cost, + 'sell_transaction_cost': self._sell_transaction_cost, + 'buy_realized_pnl': self._buy_realized_pnl, + 'sell_realized_pnl': self._sell_realized_pnl, + 'buy_avg_open_price': self._buy_avg_open_price, + 'sell_avg_open_price': self._sell_avg_open_price, + } + + def set_state(self, state): + assert self._order_book_id == state['order_book_id'] + self._buy_old_holding_list = state['buy_old_holding_list'] + self._sell_old_holding_list = state['sell_old_holding_list'] + self._buy_today_holding_list = state['buy_today_holding_list'] + self._sell_today_holding_list = state['sell_today_holding_list'] + self._buy_transaction_cost = state['buy_transaction_cost'] + self._sell_transaction_cost = state['sell_transaction_cost'] + self._buy_avg_open_price = state['buy_avg_open_price'] + self._sell_avg_open_price = state['sell_avg_open_price'] @property - def buy_open_order_quantity(self): - """ - 【int】买开挂单量 - """ - return self._buy_open_order_quantity + def type(self): + return ACCOUNT_TYPE.FUTURE @property - def sell_open_order_quantity(self): - """ - 【int】卖开挂单量 - """ - return self._sell_open_order_quantity + def margin_rate(self): + margin_rate = Environment.get_instance().get_future_margin_rate(self.order_book_id) + margin_multiplier = Environment.get_instance().config.base.margin_multiplier + return margin_rate * margin_multiplier @property - def buy_close_order_quantity(self): - """ - 【int】买平挂单量 - """ - return self._buy_close_order_quantity + def market_value(self): + return (self.buy_quantity - self.sell_quantity) * self.last_price * self.contract_multiplier @property - def sell_close_order_quantity(self): + def buy_market_value(self): + return self.buy_quantity * self.last_price * self.contract_multiplier + + @property + def sell_market_value(self): + return self.sell_quantity * self.last_price * self.contract_multiplier + + # -- PNL 相关 + @property + def contract_multiplier(self): + return Environment.get_instance().get_instrument(self.order_book_id).contract_multiplier + + @property + def open_orders(self): + return Environment.get_instance().broker.get_open_orders(self.order_book_id) + + @property + def buy_holding_pnl(self): """ - 【int】卖平挂单量 + [float] 买方向当日持仓盈亏 """ - return self._sell_close_order_quantity + return (self.last_price - self.buy_avg_holding_price) * self.buy_quantity * self.contract_multiplier @property - def daily_realized_pnl(self): + def sell_holding_pnl(self): """ - 【float】当日平仓盈亏 + [float] 卖方向当日持仓盈亏 """ - return self._daily_realized_pnl + return (self.sell_avg_holding_price - self.last_price) * self.sell_quantity * self.contract_multiplier @property - def daily_pnl(self): + def buy_realized_pnl(self): """ - 【float】当日盈亏,当日浮动盈亏+当日平仓盈亏 + [float] 买方向平仓盈亏 """ - return self.daily_realized_pnl + self.daily_holding_pnl + return self._buy_realized_pnl @property - def daily_holding_pnl(self): + def sell_realized_pnl(self): """ - 【float】当日持仓盈亏 + [float] 卖方向平仓盈亏 """ - # daily_holding_pnl: < float > 当日持仓盈亏 - return self._market_value + self._sell_holding_cost - self._buy_holding_cost + return self._sell_realized_pnl @property - def _buy_daily_holding_pnl(self): - return (self._last_price - self.buy_avg_holding_price) * self.buy_quantity * self._contract_multiplier + def holding_pnl(self): + """ + [float] 当日持仓盈亏 + """ + return self.buy_holding_pnl + self.sell_holding_pnl @property - def _sell_daily_holding_pnl(self): - return (self.sell_avg_holding_price - self._last_price) * self.sell_quantity * self._contract_multiplier + def realized_pnl(self): + """ + [float] 当日平仓盈亏 + """ + return self.buy_realized_pnl + self.sell_realized_pnl @property def buy_daily_pnl(self): """ - 【float】多头仓位当日盈亏 + [float] 当日买方向盈亏 """ - return self._buy_daily_holding_pnl + self._buy_daily_realized_pnl + return self.buy_holding_pnl + self.buy_realized_pnl @property def sell_daily_pnl(self): """ - 【float】空头仓位当日盈亏 + [float] 当日卖方向盈亏 """ - return self._sell_daily_holding_pnl + self._sell_daily_realized_pnl + return self.sell_holding_pnl + self.sell_realized_pnl @property - def margin(self): + def daily_pnl(self): """ - 【float】仓位总保证金 + [float] 当日盈亏 """ - # 总保证金 - # TODO 这里之后需要进行修改,需要考虑单向大边的情况 - return self.buy_margin + self.sell_margin + return self.holding_pnl + self.realized_pnl @property - def buy_margin(self): + def buy_pnl(self): """ - 【float】多头持仓占用保证金 + [float] 买方向累计盈亏 """ - # buy_margin: < float > 买保证金 - margin_decider = Environment.get_instance().accounts[ACCOUNT_TYPE.FUTURE].margin_decider - return margin_decider.cal_margin(self.order_book_id, SIDE.BUY, self._buy_holding_cost) + return (self.last_price - self._buy_avg_open_price) * self.buy_quantity * self.contract_multiplier @property - def sell_margin(self): + def sell_pnl(self): """ - 【float】空头持仓占用保证金 + [float] 卖方向累计盈亏 """ - # sell_margin: < float > 卖保证金 - margin_decider = Environment.get_instance().accounts[ACCOUNT_TYPE.FUTURE].margin_decider - return margin_decider.cal_margin(self.order_book_id, SIDE.SELL, self._sell_holding_cost) - - @property - def _buy_old_holding_quantity(self): - return sum(amount for price, amount in self._buy_old_holding_list) - - @property - def _sell_old_holding_quantity(self): - return sum(amount for price, amount in self._sell_old_holding_list) - - @property - def _buy_today_holding_quantity(self): - return sum(amount for price, amount in self._buy_today_holding_list) + return (self._sell_avg_open_price - self.last_price) * self.sell_quantity * self.contract_multiplier @property - def _sell_today_holding_quantity(self): - return sum(amount for price, amount in self._sell_today_holding_list) - - @property - def buy_quantity(self): + def pnl(self): """ - 【int】多头持仓 + [float] 累计盈亏 """ - # 买方向总持仓 - return self._buy_old_holding_quantity + self._buy_today_holding_quantity + return self.buy_pnl + self.sell_pnl + # -- Quantity 相关 @property - def sell_quantity(self): + def buy_open_order_quantity(self): """ - 【int】空头持仓 + [int] 买方向挂单量 """ - # 卖方向总持仓 - return self._sell_old_holding_quantity + self._sell_today_holding_quantity + return sum(order.unfilled_quantity for order in self.open_orders if + order.side == SIDE.BUY and order.position_effect == POSITION_EFFECT.OPEN) @property - def buy_avg_holding_price(self): + def sell_open_order_quantity(self): """ - 【float】多头持仓均价 + [int] 卖方向挂单量 """ - return 0 if self.buy_quantity == 0 else self._buy_holding_cost / self.buy_quantity / self._contract_multiplier + return sum(order.unfilled_quantity for order in self.open_orders if + order.side == SIDE.SELL and order.position_effect == POSITION_EFFECT.OPEN) @property - def sell_avg_holding_price(self): + def buy_close_order_quantity(self): """ - 【float】空头持仓均价 + [int] 买方向挂单量 """ - return 0 if self.sell_quantity == 0 else self._sell_holding_cost / self.sell_quantity / self._contract_multiplier - - @property - def _buy_closable_quantity(self): - # 买方向可平仓量 - return self.buy_quantity - self._sell_close_order_quantity - - @property - def _sell_closable_quantity(self): - # 卖方向可平仓量 - return self.sell_quantity - self._buy_close_order_quantity + return sum(order.unfilled_quantity for order in self.open_orders if order.side == SIDE.BUY and + order.position_effect in [POSITION_EFFECT.CLOSE, POSITION_EFFECT.CLOSE_TODAY]) @property - def closable_buy_quantity(self): + def sell_close_order_quantity(self): """ - 【float】可平多头持仓 + [int] 卖方向挂单量 """ - return self._buy_closable_quantity + return sum(order.unfilled_quantity for order in self.open_orders if order.side == SIDE.SELL and + order.position_effect in [POSITION_EFFECT.CLOSE, POSITION_EFFECT.CLOSE_TODAY]) @property - def closable_sell_quantity(self): + def buy_old_quantity(self): """ - 【int】可平空头持仓 + [int] 买方向昨仓 """ - return self._sell_closable_quantity - - @property - def _buy_old_holding_cost(self): - return self._buy_old_holding_quantity * self._prev_settle_price * self._contract_multiplier - - @property - def _sell_old_holding_cost(self): - return self._sell_old_holding_quantity * self._prev_settle_price * self._contract_multiplier - - @property - def _buy_today_holding_cost(self): - return sum(p * a * self._contract_multiplier for p, a in self._buy_today_holding_list) - - @property - def _sell_today_holding_cost(self): - return sum(p * a * self._contract_multiplier for p, a in self._sell_today_holding_list) + return sum(amount for price, amount in self._buy_old_holding_list) @property - def _buy_holding_cost(self): - return self._buy_old_holding_cost + self._buy_today_holding_cost + def sell_old_quantity(self): + """ + [int] 卖方向昨仓 + """ + return sum(amount for price, amount in self._sell_old_holding_list) @property - def _sell_holding_cost(self): - return self._sell_old_holding_cost + self._sell_today_holding_cost + def buy_today_quantity(self): + """ + [int] 买方向今仓 + """ + return sum(amount for price, amount in self._buy_today_holding_list) @property - def _quantity(self): - return self.buy_quantity + self.sell_quantity + def sell_today_quantity(self): + """ + [int] 卖方向今仓 + """ + return sum(amount for price, amount in self._sell_today_holding_list) @property - def _position_value(self): - # 总保证金 + 当日持仓盈亏 + 当日平仓盈亏 - return self.margin + self.daily_holding_pnl + self.daily_realized_pnl + def buy_quantity(self): + """ + [int] 买方向持仓 + """ + return self.buy_old_quantity + self.buy_today_quantity @property - def buy_today_quantity(self): + def sell_quantity(self): """ - 【int】多头今仓 + [int] 卖方向持仓 """ - # Buy今仓 - return sum(amount for (price, amount) in self._buy_today_holding_list) + return self.sell_old_quantity + self.sell_today_quantity @property - def sell_today_quantity(self): + def closable_buy_quantity(self): """ - 【int】空头今仓 + [float] 可平买方向持仓 """ - # Sell今仓 - return sum(amount for (price, amount) in self._sell_today_holding_list) + return self.buy_quantity - self.sell_close_order_quantity @property - def _closable_buy_today_quantity(self): - return self.buy_today_quantity - self._sell_close_order_quantity + def closable_sell_quantity(self): + """ + [float] 可平卖方向持仓 + """ + return self.sell_quantity - self.buy_close_order_quantity + # -- Margin 相关 @property - def _closable_sell_today_quantity(self): - return self.sell_today_quantity - self._buy_close_order_quantity + def buy_margin(self): + """ + [float] 买方向持仓保证金 + """ + return self._buy_holding_cost * self.margin_rate @property - def buy_pnl(self): + def sell_margin(self): """ - 【float】多头仓位累计盈亏 + [float] 卖方向持仓保证金 """ - return self._sell_close_trade_value - self._buy_open_trade_value + \ - self.buy_quantity * self._last_price * self._contract_multiplier + return self._sell_holding_cost * self.margin_rate @property - def sell_pnl(self): + def margin(self): """ - 【float】空头仓位累计盈亏 + [float] 保证金 """ - return self._sell_open_trade_value - self._buy_close_trade_value - \ - self.sell_quantity * self._last_price * self._contract_multiplier + # TODO 需要添加单向大边相关的处理逻辑 + return self.buy_margin + self.sell_margin @property - def buy_daily_pnl(self): + def buy_avg_holding_price(self): """ - 【float】多头仓位当日盈亏 + [float] 买方向持仓均价 """ - return self._buy_daily_holding_pnl + self._buy_daily_realized_pnl + return 0 if self.buy_quantity == 0 else self._buy_holding_cost / self.buy_quantity / self.contract_multiplier @property - def sell_daily_pnl(self): + def sell_avg_holding_price(self): """ - 【float】空头仓位当日盈亏 + [float] 卖方向持仓均价 """ - return self._sell_daily_holding_pnl + self._sell_daily_realized_pnl + return 0 if self.sell_quantity == 0 else self._sell_holding_cost / self.sell_quantity / self.contract_multiplier + + @property + def _buy_holding_cost(self): + return sum(p * a * self.contract_multiplier for p, a in self.buy_holding_list) + + @property + def _sell_holding_cost(self): + return sum(p * a * self.contract_multiplier for p, a in self.sell_holding_list) @property - def _buy_holding_list(self): + def buy_holding_list(self): return self._buy_old_holding_list + self._buy_today_holding_list @property - def _sell_holding_list(self): + def sell_holding_list(self): return self._sell_old_holding_list + self._sell_today_holding_list @property def buy_avg_open_price(self): - """ - 【float】多头持仓均价 - """ return self._buy_avg_open_price @property def sell_avg_open_price(self): - """ - 【float】空头开仓均价 - """ return self._sell_avg_open_price @property def buy_transaction_cost(self): - """ - 【float】多头费用 - """ - return self._buy_open_transaction_cost + self._sell_close_transaction_cost + return self._buy_transaction_cost @property def sell_transaction_cost(self): - """ - 【float】空头费用 - """ - return self._sell_open_transaction_cost + self._buy_close_transaction_cost + return self._sell_transaction_cost @property def transaction_cost(self): - """ - 【float】仓位交易费用 - """ - return self.buy_transaction_cost + self.sell_transaction_cost + return self._buy_transaction_cost + self._sell_transaction_cost - @property - def buy_market_value(self): - """ - 【float】多头仓位市值加总 - """ - return self._buy_market_value - - @property - def sell_market_value(self): - """ - 【float】空头仓位市值加总 - """ - return self._sell_market_value + # -- Function - def _cal_close_today_amount(self, trade_amount, trade_side): + def cal_close_today_amount(self, trade_amount, trade_side): if trade_side == SIDE.SELL: - close_today_amount = trade_amount - self._buy_old_holding_quantity + close_today_amount = trade_amount - self.buy_old_quantity else: - close_today_amount = trade_amount - self._sell_old_holding_quantity + close_today_amount = trade_amount - self.sell_old_quantity return max(close_today_amount, 0) + + def apply_settlement(self): + data_proxy = Environment.get_instance().data_proxy + trading_date = Environment.get_instance().trading_dt.date() + settle_price = data_proxy.get_settle_price(self.order_book_id, trading_date) + self._buy_old_holding_list = [(settle_price, self.buy_quantity)] + self._sell_old_holding_list = [(settle_price, self.sell_quantity)] + self._buy_today_holding_list = [] + self._sell_today_holding_list = [] + + self._buy_transaction_cost = 0. + self._sell_transaction_cost = 0. + self._buy_realized_pnl = 0. + self._sell_realized_pnl = 0. + + def apply_trade(self, trade): + trade_quantity = trade.last_quantity + if trade.side == SIDE.BUY: + if trade.position_effect == POSITION_EFFECT.OPEN: + self._buy_avg_open_price = (self._buy_avg_open_price * self.buy_quantity + + trade_quantity * trade.last_price) / (self.buy_quantity + trade_quantity) + self._buy_transaction_cost += trade.transaction_cost + self._buy_today_holding_list.insert(0, (trade.last_price, trade_quantity)) + return -1 * margin_of(self.order_book_id, trade_quantity, trade.last_price) + else: + old_margin = self.margin + self._sell_transaction_cost += trade.transaction_cost + delta_realized_pnl = self._close_holding(trade) + self._sell_realized_pnl += delta_realized_pnl + return old_margin - self.margin + delta_realized_pnl + else: + if trade.position_effect == POSITION_EFFECT.OPEN: + self._sell_avg_open_price = (self._sell_avg_open_price * self.sell_quantity + + trade_quantity * trade.last_price) / (self.sell_quantity + trade_quantity) + self._sell_transaction_cost += trade.transaction_cost + self._sell_today_holding_list.insert(0, (trade.last_price, trade_quantity)) + return -1 * margin_of(self.order_book_id, trade_quantity, trade.last_price) + else: + old_margin = self.margin + self._buy_transaction_cost += trade.transaction_cost + delta_realized_pnl = self._close_holding(trade) + self._buy_realized_pnl += delta_realized_pnl + return old_margin - self.margin + delta_realized_pnl + + def _close_holding(self, trade): + left_quantity = trade.last_quantity + delta = 0 + if trade.side == SIDE.BUY: + # 先平昨仓 + if len(self._sell_old_holding_list) != 0: + old_price, old_quantity = self._sell_old_holding_list.pop() + + if old_quantity > left_quantity: + consumed_quantity = left_quantity + self._sell_old_holding_list = [(old_price, old_quantity - left_quantity)] + else: + consumed_quantity = old_quantity + left_quantity -= consumed_quantity + delta += self._cal_realized_pnl(old_price, trade.last_price, trade.side, consumed_quantity) + # 再平进仓 + while True: + if left_quantity <= 0: + break + oldest_price, oldest_quantity = self._sell_today_holding_list.pop() + if oldest_quantity > left_quantity: + consumed_quantity = left_quantity + self._sell_today_holding_list.append((oldest_price, oldest_quantity - left_quantity)) + else: + consumed_quantity = oldest_quantity + left_quantity -= consumed_quantity + delta += self._cal_realized_pnl(oldest_price, trade.last_price, trade.side, consumed_quantity) + else: + # 先平昨仓 + if len(self._buy_old_holding_list) != 0: + old_price, old_quantity = self._buy_old_holding_list.pop() + if old_quantity > left_quantity: + consumed_quantity = left_quantity + self._buy_old_holding_list = [(old_price, old_quantity - left_quantity)] + else: + consumed_quantity = old_quantity + left_quantity -= consumed_quantity + delta += self._cal_realized_pnl(old_price, trade.last_price, trade.side, consumed_quantity) + # 再平今仓 + while True: + if left_quantity <= 0: + break + oldest_price, oldest_quantity = self._buy_today_holding_list.pop() + if oldest_quantity > left_quantity: + consumed_quantity = left_quantity + self._buy_today_holding_list.append((oldest_price, oldest_quantity - left_quantity)) + left_quantity = 0 + else: + consumed_quantity = oldest_quantity + left_quantity -= consumed_quantity + delta += self._cal_realized_pnl(oldest_price, trade.last_price, trade.side, consumed_quantity) + return delta + + def _cal_realized_pnl(self, cost_price, trade_price, side, consumed_quantity): + if side == SIDE.BUY: + return (cost_price - trade_price) * consumed_quantity * self.contract_multiplier + else: + return (trade_price - cost_price) * consumed_quantity * self.contract_multiplier diff --git a/rqalpha/model/position/stock_position.py b/rqalpha/model/position/stock_position.py index 07844cb0c..3e4beacad 100644 --- a/rqalpha/model/position/stock_position.py +++ b/rqalpha/model/position/stock_position.py @@ -14,140 +14,167 @@ # See the License for the specific language governing permissions and # limitations under the License. -import six - from .base_position import BasePosition -from ...execution_context import ExecutionContext -from ...const import ACCOUNT_TYPE - - -StockPersistMap = { - "_order_book_id": "_order_book_id", - "_last_price": "_last_price", - "_market_value": "_market_value", - "_buy_trade_value": "_buy_trade_value", - "_sell_trade_value": "_sell_trade_value", - "_buy_order_value": "_buy_order_value", - "_sell_order_value": "_sell_order_value", - "_buy_order_quantity": "_buy_order_quantity", - "_sell_order_quantity": "_sell_order_quantity", - "_buy_trade_quantity": "_buy_trade_quantity", - "_sell_trade_quantity": "_sell_trade_quantity", - "_total_orders": "_total_orders", - "_total_trades": "_total_trades", - "_is_traded": "_is_traded", - "_buy_today_holding_quantity": "_buy_today_holding_quantity", - "_avg_price": "_avg_price", - "_de_listed_date": "_de_listed_date", - "_transaction_cost": "_transaction_cost", -} +from ...const import ACCOUNT_TYPE, SIDE +from ...environment import Environment +from ...utils.i18n import gettext as _ +from ...utils.logger import user_system_log class StockPosition(BasePosition): - def __init__(self, order_book_id): super(StockPosition, self).__init__(order_book_id) - self._buy_today_holding_quantity = 0 # int T+1,所以记录下来该股票今天的买单量 - self._avg_price = 0. # float 获得该持仓的买入均价,计算方法为每次买入的数量做加权平均。 - instrument = ExecutionContext.get_instrument(self.order_book_id) - self._de_listed_date = None if instrument is None else instrument.de_listed_date - self._transaction_cost = 0. - - @classmethod - def __from_dict__(cls, position_dict): - position = cls(position_dict["_order_book_id"]) - for persist_key, origin_key in six.iteritems(StockPersistMap): - setattr(position, origin_key, position_dict[persist_key]) - return position - - def __to_dict__(self): - p_dict = {} - for persist_key, origin_key in six.iteritems(StockPersistMap): - p_dict[persist_key] = getattr(self, origin_key) - return p_dict + self._quantity = 0 + self._avg_price = 0 + self._non_closable = 0 # 当天买入的不能卖出 + self._frozen = 0 # 冻结量 + self._transaction_cost = 0 # 交易费用 + + def get_state(self): + return { + 'order_book_id': self._order_book_id, + 'quantity': self._quantity, + 'avg_price': self._avg_price, + 'non_closable': self._non_closable, + 'frozen': self._frozen, + 'transaction_cost': self._transaction_cost, + } + + def set_state(self, state): + assert self._order_book_id == state['order_book_id'] + self._quantity = state['quantity'] + self._avg_price = state['avg_price'] + self._non_closable = state['non_closable'] + self._frozen = state['frozen'] + self._transaction_cost = state['transaction_cost'] + + def apply_trade(self, trade): + self._transaction_cost += trade.transaction_cost + if trade.side == SIDE.BUY: + self._avg_price = (self._avg_price * self._quantity + trade.last_quantity * trade.last_price) / ( + self._quantity + trade.last_quantity) + self._quantity += trade.last_quantity + + if self._order_book_id not in {'510900.XSHG', '513030.XSHG', '513100.XSHG', '513500.XSHG'}: + # 除了上述 T+0 基金,其他都是 T+1 + self._non_closable += trade.last_quantity + else: + self._quantity -= trade.last_quantity + self._frozen -= trade.last_quantity + + def apply_settlement(self): + self._non_closable = 0 + + def reset_frozen(self, frozen): + self._frozen = frozen + + def cal_close_today_amount(self, *args): + return 0 @property - def _position_value(self): - return self._market_value + def type(self): + return ACCOUNT_TYPE.STOCK + + def split_(self, ratio): + self._quantity *= ratio + # split 发生时,这两个值理论上应该都是0 + self._frozen *= ratio + self._non_closable *= ratio + + def on_order_pending_new_(self, order): + if order.side == SIDE.SELL: + self._frozen += order.quantity + + def on_order_creation_reject_(self, order): + if order.side == SIDE.SELL: + self._frozen -= order.quantity + + def on_order_cancel_(self, order): + if order.side == SIDE.SELL: + self._frozen -= order.unfilled_quantity @property def quantity(self): """ - 【int】当前持仓股数 + [int] 当前持仓股数 """ return self._quantity @property - def bought_quantity(self): + def avg_price(self): """ - 【int】该证券的总买入股数,例如:如果你的投资组合并没有任何平安银行的成交,那么平安银行这个股票的仓位就是0 + [float] 获得该持仓的买入均价,计算方法为每次买入的数量做加权平均 """ - return self._buy_trade_quantity + return self._avg_price @property - def sold_quantity(self): - """ - 【int】该证券的总卖出股数,例如:如果你的投资组合曾经买入过平安银行股票200股并且卖出过100股,那么这个属性会返回100 - """ - return self._sell_trade_quantity + def pnl(self): + return self._quantity * (self.last_price - self._avg_price) @property - def bought_value(self): + def sellable(self): """ - 【float】该证券的总买入的价值,等于每一个该证券的 买入成交价 * 买入股数 总和 + [int] 该仓位可卖出股数。T+1的市场中sellable = 所有持仓 - 今日买入的仓位 - 已冻结 """ - return self._buy_trade_value + return self._quantity - self._non_closable - self._frozen @property - def sold_value(self): - """ - 【float】该证券的总卖出价值,等于每一个该证券的 卖出成交价 * 卖出股数 总和 - """ - return self._sell_trade_value + def market_value(self): + return self._quantity * self.last_price @property - def average_cost(self): + def transaction_cost(self): + return self._transaction_cost + + @property + def value_percent(self): """ - 【已弃用】请使用 avg_price 获取持仓买入均价 + [float] 获得该持仓的实时市场价值在总投资组合价值中所占比例,取值范围[0, 1] """ - return self._avg_price + accounts = Environment.get_instance().portfolio.accounts + if ACCOUNT_TYPE.STOCK not in accounts: + return 0 + total_value = accounts[ACCOUNT_TYPE.STOCK].total_value + return 0 if total_value == 0 else self.market_value / total_value + + # ------------------------------------ Abandon Property ------------------------------------ @property - def avg_price(self): + def bought_quantity(self): """ - 【float】获得该持仓的买入均价,计算方法为每次买入的数量做加权平均 + [已弃用] """ - return self._avg_price + user_system_log.warn(_(u"[abandon] {} is no longer valid.").format('stock_position.bought_quantity')) + return self._quantity @property - def sellable(self): + def sold_quantity(self): """ - 【int】该仓位可卖出股数。T+1的市场中sellable = 所有持仓-今日买入的仓位 + [已弃用] """ - return self._quantity - self._buy_today_holding_quantity - self._sell_order_quantity + user_system_log.warn(_(u"[abandon] {} is no longer valid.").format('stock_position.sold_quantity')) + return 0 @property - def _quantity(self): - return self._buy_trade_quantity - self._sell_trade_quantity + def bought_value(self): + """ + [已弃用] + """ + user_system_log.warn(_(u"[abandon] {} is no longer valid.").format('stock_position.bought_value')) + return self._quantity * self._avg_price @property - def transaction_cost(self): + def sold_value(self): """ - 【float】总费用 + [已弃用] """ - return self._transaction_cost + user_system_log.warn(_(u"[abandon] {} is no longer valid.").format('stock_position.sold_value')) + return 0 @property - def value_percent(self): + def average_cost(self): """ - 【float】获得该持仓的实时市场价值在总投资组合价值中所占比例,取值范围[0, 1] + [已弃用] 请使用 avg_price 获取持仓买入均价 """ - accounts = ExecutionContext.accounts - if ACCOUNT_TYPE.STOCK not in accounts: - # FIXME 现在无法区分这个position是stock的还是benchmark的,但是benchmark因为没有用到这个字段,所以可以暂时返0处理。 - return 0 - portfolio = accounts[ACCOUNT_TYPE.STOCK].portfolio - return 0 if portfolio.portfolio_value == 0 else self._position_value / portfolio.portfolio_value - - def _cal_close_today_amount(self, trade_amount, side): - return 0 + user_system_log.warn(_(u"[abandon] {} is no longer valid.").format('stock_position.average_cost')) + return self._avg_price diff --git a/rqalpha/model/snapshot.py b/rqalpha/model/snapshot.py index 037c5de49..efa09bf26 100644 --- a/rqalpha/model/snapshot.py +++ b/rqalpha/model/snapshot.py @@ -19,6 +19,7 @@ import numpy as np from ..utils.datetime_func import convert_int_to_datetime +from ..model.tick import Tick class SnapshotObject(object): @@ -28,12 +29,12 @@ class SnapshotObject(object): ('high', np.float64), ('low', np.float64), ('last', np.float64), - ('volume', np.uint32), - ('total_turnover', np.uint64), + ('volume', np.int32), + ('total_turnover', np.int64), ('prev_close', np.float64) ] - _FUTURE_FIELDS = _STOCK_FIELDS + [('open_interest', np.uint32), ('prev_settlement', np.float64)] + _FUTURE_FIELDS = _STOCK_FIELDS + [('open_interest', np.int32), ('prev_settlement', np.float64)] _STOCK_FIELD_NAMES = [_n for _n, _ in _STOCK_FIELDS] _FUTURE_FIELD_NAMES = [_n for _n, _ in _FUTURE_FIELDS] @@ -67,56 +68,56 @@ def dtype_for_(instrument): @property def open(self): """ - 【float】当日开盘价 + [float] 当日开盘价 """ return self._data["open"] @property def last(self): """ - 【float】当前最新价 + [float] 当前最新价 """ return self._data["last"] @property def low(self): """ - 【float】截止到当前的最低价 + [float] 截止到当前的最低价 """ return self._data["low"] @property def high(self): """ - 【float】截止到当前的最高价 + [float] 截止到当前的最高价 """ return self._data["high"] @property def prev_close(self): """ - 【float】昨日收盘价 + [float] 昨日收盘价 """ return self._data['prev_close'] @property def volume(self): """ - 【float】截止到当前的成交量 + [float] 截止到当前的成交量 """ return self._data['volume'] @property def total_turnover(self): """ - 【float】截止到当前的成交额 + [float] 截止到当前的成交额 """ return self._data['total_turnover'] @property def datetime(self): """ - 【datetime.datetime】当前快照数据的时间戳 + [datetime.datetime] 当前快照数据的时间戳 """ if self._dt is not None: return self._dt @@ -131,21 +132,21 @@ def instrument(self): @property def order_book_id(self): """ - 【str】股票代码 + [str] 股票代码 """ return self._instrument.order_book_id @property def prev_settlement(self): """ - 【float】昨日结算价(期货专用) + [float] 昨日结算价(期货专用) """ return self._data['prev_settlement'] @property def open_interest(self): """ - 【float】截止到当前的持仓量(期货专用) + [float] 截止到当前的持仓量(期货专用) """ return self._data['open_interest'] @@ -166,6 +167,8 @@ def __repr__(self): if isinstance(self._data, dict): # in pt base.extend([k, v] for k, v in six.iteritems(self._data) if k != 'datetime') + elif isinstance(self._data, Tick): + return repr(self._data).replace('Tick', 'Snapshot') else: base.extend((n, self._data[n]) for n in self._data.dtype.names if n != 'datetime') return "Snapshot({0})".format(', '.join('{0}: {1}'.format(k, v) for k, v in base)) diff --git a/rqalpha/model/tick.py b/rqalpha/model/tick.py index df437f95d..122b64292 100644 --- a/rqalpha/model/tick.py +++ b/rqalpha/model/tick.py @@ -14,12 +14,13 @@ # See the License for the specific language governing permissions and # limitations under the License. +from ..utils.datetime_func import convert_date_time_ms_int_to_datetime + class Tick(object): - def __init__(self, order_book_id, dt, snapshot): + def __init__(self, order_book_id, tick): self._order_book_id = order_book_id - self._dt = dt - self._snapshot = snapshot + self._tick = tick @property def order_book_id(self): @@ -27,64 +28,141 @@ def order_book_id(self): @property def datetime(self): - return self._dt + dt = convert_date_time_ms_int_to_datetime(self._tick["date"], self._tick["time"]) + return dt @property def open(self): - return self._snapshot['open'] + return self._tick['open'] @property def last(self): - return self._snapshot['last'] + return self._tick['last'] @property def high(self): - return self._snapshot['high'] + return self._tick['high'] @property def low(self): - return self._snapshot['low'] + return self._tick['low'] @property def prev_close(self): - return self._snapshot['prev_close'] + return self._tick['prev_close'] @property def volume(self): - return self._snapshot['volume'] + return self._tick['volume'] @property def total_turnover(self): - return self._snapshot['total_turnover'] + return self._tick['total_turnover'] @property def open_interest(self): - return self._snapshot['open_interest'] + return self._tick['open_interest'] @property def prev_settlement(self): - return self._snapshot['prev_settlement'] + return self._tick['prev_settlement'] + + # FIXME use dynamic creation + @property + def b1(self): + return self._tick['b1'] + + @property + def b2(self): + return self._tick['b2'] + + @property + def b3(self): + return self._tick['b3'] + + @property + def b4(self): + return self._tick['b4'] + + @property + def b5(self): + return self._tick['b5'] + + @property + def b1_v(self): + return self._tick['b1_v'] + + @property + def b2_v(self): + return self._tick['b2_v'] + + @property + def b3_v(self): + return self._tick['b3_v'] + + @property + def b4_v(self): + return self._tick['b4_v'] + + @property + def b5_v(self): + return self._tick['b5_v'] + + @property + def a1(self): + return self._tick['a1'] + + @property + def a2(self): + return self._tick['a2'] + + @property + def a3(self): + return self._tick['a3'] + + @property + def a4(self): + return self._tick['a4'] + + @property + def a5(self): + return self._tick['a5'] + + @property + def a1_v(self): + return self._tick['a1_v'] @property - def bid(self): - return self._snapshot['bid'] + def a2_v(self): + return self._tick['a2_v'] @property - def bid_volume(self): - return self._snapshot['bid_volume'] + def a3_v(self): + return self._tick['a3_v'] @property - def ask(self): - return self._snapshot['ask'] + def a4_v(self): + return self._tick['a4_v'] @property - def ask_volume(self): - return self._snapshot['ask_volume'] + def a5_v(self): + return self._tick['a5_v'] @property def limit_up(self): - return self._snapshot['limit_up'] + return self._tick['limit_up'] @property def limit_down(self): - return self._snapshot['limit_down'] + return self._tick['limit_down'] + + def __repr__(self): + items = [] + for name in dir(self): + if name.startswith("_"): + continue + items.append((name, getattr(self, name))) + return "Tick({0})".format(', '.join('{0}: {1}'.format(k, v) for k, v in items)) + + def __getitem__(self, key): + return getattr(self, key) diff --git a/rqalpha/model/trade.py b/rqalpha/model/trade.py index 1f21c5c81..19ac1f6e5 100644 --- a/rqalpha/model/trade.py +++ b/rqalpha/model/trade.py @@ -14,28 +14,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -import six import time from ..utils import id_gen from ..utils.repr import property_repr, properties -TradePersistMap = { - "_calendar_dt": "_calendar_dt", - "_trading_dt": "_trading_dt", - "_price": "_price", - "_amount": "_amount", - "_order_id": "_order_id", - "_commission": "_commission", - "_tax": "_tax", - "_trade_id": "_trade_id", - "_close_today_amount": "_close_today_amount", -} - class Trade(object): - __slots__ = ["_calendar_dt", "_trading_dt", "_price", "_amount", "_order", "_commission", "_tax", "_trade_id", - "_close_today_amount"] __repr__ = property_repr @@ -46,45 +31,38 @@ def __init__(self): self._trading_dt = None self._price = None self._amount = None - self._order = None + self._order_id = None self._commission = None self._tax = None self._trade_id = None self._close_today_amount = None + self._side = None + self._position_effect = None + self._order_book_id = None + self._frozen_price = None @classmethod - def __from_create__(cls, order, calendar_dt, trading_dt, price, amount, commission=0., tax=0., trade_id=None, - close_today_amount=0): + def __from_create__(cls, order_id, calendar_dt, trading_dt, price, amount, side, position_effect, order_book_id, + commission=0., tax=0., trade_id=None, close_today_amount=0, frozen_price=0): trade = cls() trade._calendar_dt = calendar_dt trade._trading_dt = trading_dt trade._price = price trade._amount = amount - trade._order = order + trade._order_id = order_id trade._commission = commission trade._tax = tax trade._trade_id = trade_id if trade_id is not None else next(trade.trade_id_gen) trade._close_today_amount = close_today_amount + trade._side = side + trade._position_effect = position_effect + trade._order_book_id = order_book_id + trade._frozen_price = frozen_price return trade - @classmethod - def __from_dict__(cls, trade_dict, order): - trade = cls() - for persist_key, origin_key in six.iteritems(TradePersistMap): - if persist_key == "_order_id": - continue - setattr(trade, origin_key, trade_dict[persist_key]) - trade._order = order - return trade - - def __to_dict__(self): - trade_dict = {} - for persist_key, origin_key in six.iteritems(TradePersistMap): - if persist_key == "_order_id": - trade_dict["_order_id"] = self._order.order_id - else: - trade_dict[persist_key] = getattr(self, origin_key) - return trade_dict + @property + def order_book_id(self): + return self._order_book_id @property def trading_datetime(self): @@ -96,7 +74,7 @@ def datetime(self): @property def order_id(self): - return self.order.order_id + return self._order_id @property def last_price(self): @@ -106,10 +84,6 @@ def last_price(self): def last_quantity(self): return self._amount - @property - def order(self): - return self._order - @property def commission(self): return self._commission @@ -122,13 +96,25 @@ def tax(self): def transaction_cost(self): return self._tax + self._commission + @property + def side(self): + return self._side + @property def position_effect(self): - return self.order.position_effect + return self._position_effect @property def exec_id(self): return self._trade_id + @property + def frozen_price(self): + return self._frozen_price + + @property + def close_today_amount(self): + return self._close_today_amount + def __simple_object__(self): return properties(self) diff --git a/rqalpha/utils/__init__.py b/rqalpha/utils/__init__.py index d52e1f7b1..e0d1b322a 100644 --- a/rqalpha/utils/__init__.py +++ b/rqalpha/utils/__init__.py @@ -18,6 +18,7 @@ import pprint import re import six +import collections from contextlib import contextmanager @@ -26,6 +27,7 @@ from ..utils.datetime_func import TimeRange from ..utils.default_future_info import STOCK_TRADING_PERIOD, TRADING_PERIOD_DICT from ..utils.i18n import gettext as _ +from ..utils.py2 import lru_cache def safe_round(value, ndigits=3): @@ -44,9 +46,6 @@ def __call__(cls, *args, **kwargs): class RqAttrDict(object): - ''' - fuck attrdict - ''' def __init__(self, d=None): self.__dict__ = d if d is not None else dict() @@ -59,8 +58,30 @@ def __repr__(self): return pprint.pformat(self.__dict__) def __iter__(self): - for k, v in six.iteritems(self.__dict__): - yield k, v + return self.__dict__.__iter__() + + def update(self, other): + RqAttrDict._update_dict_recursive(self, other) + + def items(self): + return six.iteritems(self.__dict__) + + iteritems = items + + @staticmethod + def _update_dict_recursive(target, other): + if isinstance(other, RqAttrDict): + other = other.__dict__ + if isinstance(target, RqAttrDict): + target = target.__dict__ + + for k, v in six.iteritems(other): + if isinstance(v, collections.Mapping): + r = RqAttrDict._update_dict_recursive(target.get(k, {}), v) + target[k] = r + else: + target[k] = other[k] + return target def dummy_func(*args, **kwargs): @@ -88,7 +109,7 @@ def __getattr__(self, _): def to_sector_name(s): from ..model.instrument import SectorCode, SectorCodeItem - for _, v in six.iteritems(SectorCode.__dict__): + for __, v in six.iteritems(SectorCode.__dict__): if isinstance(v, SectorCodeItem): if v.cn == s or v.en == s or v.name == s: return v.name @@ -99,7 +120,7 @@ def to_sector_name(s): def to_industry_code(s): from ..model.instrument import IndustryCode, IndustryCodeItem - for _, v in six.iteritems(IndustryCode.__dict__): + for __, v in six.iteritems(IndustryCode.__dict__): if isinstance(v, IndustryCodeItem): if v.name == s: return v.code @@ -144,10 +165,10 @@ def run_when_strategy_not_hold(func): from ..utils.logger import system_log def wrapper(*args, **kwargs): - if not Environment.get_instance().is_strategy_hold: + if not Environment.get_instance().config.extra.is_hold: return func(*args, **kwargs) else: - system_log.debug(_("not run {}({}, {}) because strategy is hold").format(func, args, kwargs)) + system_log.debug(_(u"not run {}({}, {}) because strategy is hold").format(func, args, kwargs)) return wrapper @@ -193,9 +214,10 @@ def instrument_type_str2enum(type_str): ] +@lru_cache(None) def get_account_type(order_book_id): - from ..execution_context import ExecutionContext - instrument = ExecutionContext.get_instrument(order_book_id) + from ..environment import Environment + instrument = Environment.get_instance().get_instrument(order_book_id) enum_type = instrument.enum_type if enum_type in INST_TYPE_IN_STOCK_ACCOUNT: return ACCOUNT_TYPE.STOCK @@ -205,10 +227,6 @@ def get_account_type(order_book_id): raise NotImplementedError -def exclude_benchmark_generator(accounts): - return {k: v for k, v in six.iteritems(accounts) if k != ACCOUNT_TYPE.BENCHMARK} - - def get_upper_underlying_symbol(order_book_id): p = re.compile(UNDERLYING_SYMBOL_PATTERN) result = p.findall(order_book_id) diff --git a/rqalpha/utils/arg_checker.py b/rqalpha/utils/arg_checker.py index 26605dfb9..06bbde3b9 100644 --- a/rqalpha/utils/arg_checker.py +++ b/rqalpha/utils/arg_checker.py @@ -24,7 +24,6 @@ from dateutil.parser import parse as parse_date from .exception import RQInvalidArgument, RQTypeError -from ..execution_context import ExecutionContext from ..model.instrument import Instrument from ..environment import Environment from ..const import INSTRUMENT_TYPE, RUN_TYPE @@ -46,7 +45,7 @@ def is_instance_of(self, types): def check_is_instance_of(func_name, value): if not isinstance(value, types): raise RQInvalidArgument( - _('function {}: invalid {} argument, expect a value of type {}, got {} (type: {})').format( + _(u"function {}: invalid {} argument, expect a value of type {}, got {} (type: {})").format( func_name, self._arg_name, types, value, type(value) )) @@ -55,22 +54,22 @@ def check_is_instance_of(func_name, value): def raise_not_valid_instrument_error(self, func_name, arg_name, value): raise RQInvalidArgument( - _('function {}: invalid {} argument, expect a valid instrument/order_book_id/symbol, ' - 'got {} (type: {})').format( + _(u"function {}: invalid {} argument, expect a valid instrument/order_book_id/symbol, " + u"got {} (type: {})").format( func_name, self._arg_name, value, type(value) )) def raise_not_valid_stock_error(self, func_name, arg_name, value): raise RQInvalidArgument( - _('function {}: invalid {} argument, expect a valid stock instrument/order_book_id/symbol, ' - 'got {} (type: {})').format( + _(u"function {}: invalid {} argument, expect a valid stock instrument/order_book_id/symbol, " + u"got {} (type: {})").format( func_name, self._arg_name, value, type(value) )) def raise_not_valid_future_error(self, func_name, arg_name, value): raise RQInvalidArgument( - _('function {}: invalid {} argument, expect a valid future instrument/order_book_id/symbol, ' - 'got {} (type: {})').format( + _(u"function {}: invalid {} argument, expect a valid future instrument/order_book_id/symbol, " + u"got {} (type: {})").format( func_name, self._arg_name, value, type(value) )) @@ -80,21 +79,21 @@ def _is_valid_instrument(self, func_name, value): if isinstance(value, six.string_types): if config.base.run_type == RUN_TYPE.PAPER_TRADING: if "88" in value: - raise RQInvalidArgument(_("Main Future contracts[88] are not supported in paper trading.")) + raise RQInvalidArgument(_(u"Main Future contracts[88] are not supported in paper trading.")) if "99" in value: - raise RQInvalidArgument(_("Index Future contracts[99] are not supported in paper trading.")) + raise RQInvalidArgument(_(u"Index Future contracts[99] are not supported in paper trading.")) else: if "88" in value: global main_contract_warning_flag if main_contract_warning_flag: main_contract_warning_flag = False - user_system_log.warn(_("Main Future contracts[88] are not supported in paper trading.")) + user_system_log.warn(_(u"Main Future contracts[88] are not supported in paper trading.")) if "99" in value: global index_contract_warning_flag if index_contract_warning_flag: index_contract_warning_flag = False - user_system_log.warn(_("Index Future contracts[99] are not supported in paper trading.")) - instrument = ExecutionContext.get_data_proxy().instruments(value) + user_system_log.warn(_(u"Index Future contracts[99] are not supported in paper trading.")) + instrument = Environment.get_instance().get_instrument(value) if instrument is None: self.raise_not_valid_instrument_error(func_name, self._arg_name, value) return @@ -110,7 +109,7 @@ def is_valid_instrument(self): def _is_valid_stock(self, func_name, value): if isinstance(value, six.string_types): - instrument = ExecutionContext.get_data_proxy().instruments(value) + instrument = Environment.get_instance().get_instrument(value) if instrument is None: self.raise_not_valid_instrument_error(func_name, self._arg_name, value) if instrument.enum_type not in INST_TYPE_IN_STOCK_ACCOUNT: @@ -131,7 +130,7 @@ def is_valid_stock(self): def _is_valid_future(self, func_name, value): if isinstance(value, six.string_types): - instrument = ExecutionContext.get_data_proxy().instruments(value) + instrument = Environment.get_instance().get_instrument(value) if instrument is None: self.raise_not_valid_instrument_error(func_name, self._arg_name, value) if instrument.enum_type != INSTRUMENT_TYPE.FUTURE: @@ -153,7 +152,7 @@ def _is_number(self, func_name, value): v = float(value) except ValueError: raise RQInvalidArgument( - _('function {}: invalid {} argument, expect a number, got {} (type: {})').format( + _(u"function {}: invalid {} argument, expect a number, got {} (type: {})").format( func_name, self._arg_name, value, type(value)) ) @@ -168,10 +167,9 @@ def check_is_in(func_name, value): if value not in valid_values: raise RQInvalidArgument( - _('function {}: invalid {} argument, valid: {}, got {} (type: {})').format( + _(u"function {}: invalid {} argument, valid: {}, got {} (type: {})").format( func_name, self._arg_name, repr(valid_values), value, type(value)) ) - return self._rules.append(check_is_in) return self @@ -183,7 +181,7 @@ def check_are_valid_fields(func_name, fields): if isinstance(fields, six.string_types): if fields not in valid_fields: raise RQInvalidArgument( - _('function {}: invalid {} argument, valid fields are {}, got {} (type: {})').format( + _(u"function {}: invalid {} argument, valid fields are {}, got {} (type: {})").format( func_name, self._arg_name, repr(valid_fields), fields, type(fields) )) return @@ -195,14 +193,16 @@ def check_are_valid_fields(func_name, fields): invalid_fields = [field for field in fields if field not in valid_fields] if invalid_fields: raise RQInvalidArgument( - _('function {}: invalid field {}, valid fields are {}, got {} (type: {})').format( + _(u"function {}: invalid field {}, valid fields are {}, got {} (type: {})").format( func_name, invalid_fields, repr(valid_fields), fields, type(fields) )) + return raise RQInvalidArgument( - _('function {}: invalid {} argument, expect a string or a list of string, got {} (type: {})').format( + _(u"function {}: invalid {} argument, expect a string or a list of string, got {} (type: {})").format( func_name, self._arg_name, repr(fields), type(fields) )) + self._rules.append(check_are_valid_fields) return self @@ -217,7 +217,7 @@ def _are_valid_instruments(self, func_name, values): return raise RQInvalidArgument( - _('function {}: invalid {} argument, expect a string or a list of string, got {} (type: {})').format( + _(u"function {}: invalid {} argument, expect a string or a list of string, got {} (type: {})").format( func_name, self._arg_name, repr(values), type(values) )) @@ -234,35 +234,57 @@ def check_is_valid_date(func_name, value): if isinstance(value, six.string_types): try: v = parse_date(value) + return except ValueError: raise RQInvalidArgument( - _('function {}: invalid {} argument, expect a valid date, got {} (type: {})').format( + _(u"function {}: invalid {} argument, expect a valid date, got {} (type: {})").format( func_name, self._arg_name, value, type(value) )) raise RQInvalidArgument( - _('function {}: invalid {} argument, expect a valid date, got {} (type: {})').format( + _(u"function {}: invalid {} argument, expect a valid date, got {} (type: {})").format( func_name, self._arg_name, value, type(value) )) self._rules.append(check_is_valid_date) return self + def is_greater_or_equal_than(self, low): + def check_greater_or_equal_than(func_name, value): + if value < low: + raise RQInvalidArgument( + _(u"function {}: invalid {} argument, expect a value >= {}, got {} (type: {})").format( + func_name, self._arg_name, low, value, type(value) + )) + self._rules.append(check_greater_or_equal_than) + return self + def is_greater_than(self, low): def check_greater_than(func_name, value): if value <= low: raise RQInvalidArgument( - _('function {}: invalid {} argument, expect a value > {}, got {} (type: {})').format( + _(u"function {}: invalid {} argument, expect a value > {}, got {} (type: {})").format( func_name, self._arg_name, low, value, type(value) )) self._rules.append(check_greater_than) return self + def is_less_or_equal_than(self, high): + def check_less_or_equal_than(func_name, value): + if value > high: + raise RQInvalidArgument( + _(u"function {}: invalid {} argument, expect a value <= {}, got {} (type: {})").format( + func_name, self._arg_name, high, value, type(value) + )) + + self._rules.append(check_less_or_equal_than) + return self + def is_less_than(self, high): def check_less_than(func_name, value): if value >= high: raise RQInvalidArgument( - _('function {}: invalid {} argument, expect a value < {}, got {} (type: {})').format( + _(u"function {}: invalid {} argument, expect a value < {}, got {} (type: {})").format( func_name, self._arg_name, high, value, type(value) )) @@ -279,8 +301,8 @@ def _is_valid_interval(self, func_name, value): if not valid: raise RQInvalidArgument( - _("function {}: invalid {} argument, interval should be in form of '1d', '3m', '4q', '2y', " - "got {} (type: {})").format( + _(u"function {}: invalid {} argument, interval should be in form of '1d', '3m', '4q', '2y', " + u"got {} (type: {})").format( func_name, self.arg_name, value, type(value) )) @@ -288,13 +310,35 @@ def is_valid_interval(self): self._rules.append(self._is_valid_interval) return self + def _is_valid_quarter(self, func_name, value): + if value is None: + valid = True + else: + valid = isinstance(value, six.string_types) and value[-2] == 'q' + if valid: + try: + valid = 1990 <= int(value[:-2]) <= 2050 and 1 <= int(value[-1]) <= 4 + except ValueError: + valid = False + + if not valid: + raise RQInvalidArgument( + _(u"function {}: invalid {} argument, quarter should be in form of '2012q3', " + u"got {} (type: {})").format( + func_name, self.arg_name, value, type(value) + )) + + def is_valid_quarter(self): + self._rules.append(self._is_valid_quarter) + return self + def _are_valid_query_entities(self, func_name, entities): from sqlalchemy.orm.attributes import InstrumentedAttribute for e in entities: if not isinstance(e, InstrumentedAttribute): raise RQInvalidArgument( - _("function {}: invalid {} argument, should be entity like " - "Fundamentals.balance_sheet.total_equity, got {} (type: {})").format( + _(u"function {}: invalid {} argument, should be entity like " + u"Fundamentals.balance_sheet.total_equity, got {} (type: {})").format( func_name, self.arg_name, e, type(e) )) @@ -303,7 +347,7 @@ def are_valid_query_entities(self): return self def _is_valid_frequency(self, func_name, value): - valid = isinstance(value, six.string_types) and value[-1] in ('d', 'm') + valid = isinstance(value, six.string_types) and value[-1] in ("d", "m") if valid: try: valid = int(value[:-1]) > 0 @@ -312,8 +356,8 @@ def _is_valid_frequency(self, func_name, value): if not valid: raise RQInvalidArgument( - _("function {}: invalid {} argument, frequency should be in form of " - "'1m', '5m', '1d', got {} (type: {})").format( + _(u"function {}: invalid {} argument, frequency should be in form of " + u"'1m', '5m', '1d', got {} (type: {})").format( func_name, self.arg_name, value, type(value) )) @@ -349,13 +393,15 @@ def api_rule_check_wrapper(*args, **kwargs): try: call_args = inspect.getcallargs(unwrapper(func), *args, **kwargs) except TypeError as e: - raise RQTypeError(*e.args).with_traceback(tb) + six.reraise(RQTypeError, RQTypeError(*e.args), tb) + return try: for r in rules: r.verify(func.__name__, call_args[r.arg_name]) except RQInvalidArgument as e: - raise e.with_traceback(tb) + six.reraise(RQInvalidArgument, e, tb) + return raise diff --git a/rqalpha/utils/cached_property.py b/rqalpha/utils/cached_property.py deleted file mode 100644 index 2eccdd157..000000000 --- a/rqalpha/utils/cached_property.py +++ /dev/null @@ -1,44 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2017 Ricequant, Inc -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import inspect - - -class CachedProperty(object): - - def __init__(self, func): - self.func = func - - def __get__(self, obj, cls): - if obj is None: - return self - if "__cached__" not in obj.__dict__: - obj.__dict__["__cached__"] = [] - value = obj.__dict__[self.func.__name__] = self.func(obj) - obj.__dict__["__cached__"].append(self.func.__name__) - return value - - @property - def annotation(self): - return inspect.signature(self.func).return_annotation - - -def clear_cache(obj): - if "__cached__" not in obj.__dict__: - return - for cached_key in obj.__dict__["__cached__"]: - obj.__dict__.pop(cached_key) - obj.__dict__["__cached__"] = [] \ No newline at end of file diff --git a/rqalpha/utils/config.py b/rqalpha/utils/config.py index ccda28e4e..8a18d3025 100644 --- a/rqalpha/utils/config.py +++ b/rqalpha/utils/config.py @@ -17,65 +17,101 @@ import six import os import ruamel.yaml as yaml +import simplejson as json import datetime import logbook import locale -from pprint import pformat import codecs import shutil +from pprint import pformat from . import RqAttrDict, logger from .exception import patch_user_exc from .logger import user_log, user_system_log, system_log, std_log, user_std_handler -from ..const import ACCOUNT_TYPE, MATCHING_TYPE, RUN_TYPE, PERSIST_MODE +from ..const import RUN_TYPE, PERSIST_MODE, ACCOUNT_TYPE from ..utils.i18n import gettext as _, localization from ..utils.dict_func import deep_update +from ..utils.py2 import to_utf8 from ..mod.utils import mod_config_value_parse -def load_config(config_path, loader=yaml.Loader, verify_version=True): +def load_config(config_path, loader=yaml.Loader): + if config_path is None: + return {} if not os.path.exists(config_path): - system_log.error(_("config.yml not found in {config_path}").format(config_path)) + system_log.error(_(u"config.yml not found in {config_path}").format(config_path)) return False - with codecs.open(config_path, encoding="utf-8") as stream: - config = yaml.load(stream, loader) - if verify_version: - config = config_version_verify(config, config_path) + if ".json" in config_path: + with codecs.open(config_path, encoding="utf-8") as f: + json_config = f.read() + config = json.loads(json_config) + else: + with codecs.open(config_path, encoding="utf-8") as stream: + config = yaml.load(stream, loader) return config def dump_config(config_path, config, dumper=yaml.RoundTripDumper): - with codecs.open(config_path, mode='w', encoding='utf-8') as file: - file.write(yaml.dump(config, Dumper=dumper)) - - -def get_default_config_path(): - config_template_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../config_template.yml") - default_config_path = os.path.abspath(os.path.expanduser("~/.rqalpha/config.yml")) - if not os.path.exists(default_config_path): - dir_path = os.path.dirname(default_config_path) - if not os.path.exists(dir_path): - os.makedirs(dir_path) - shutil.copy(config_template_path, default_config_path) - return default_config_path - - -def config_version_verify(config, config_path): - config_template_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../config_template.yml") - default_config = load_config(config_template_path, verify_version=False) - config_version = config.get("version", None) - if config_version != default_config["version"]: - back_config_file_path = config_path + "." + datetime.datetime.now().date().strftime("%Y%m%d") + ".bak" - shutil.move(config_path, back_config_file_path) - shutil.copy(config_template_path, config_path) - - system_log.warning(_(""" -Your current config file {config_file_path} is too old and may cause RQAlpha running error. -RQAlpha has replaced the config file with the newest one. -the backup config file has been saved in {back_config_file_path}. - """).format(config_file_path=config_path, back_config_file_path=back_config_file_path)) - config = default_config - return config + with codecs.open(config_path, mode='w', encoding='utf-8') as stream: + stream.write(to_utf8(yaml.dump(config, Dumper=dumper))) + + +def load_mod_config(config_path, loader=yaml.Loader): + mod_config = load_config(config_path, loader) + if mod_config is None or "mod" not in mod_config: + import os + os.remove(config_path) + config_path = get_mod_config_path() + return load_mod_config(config_path, loader) + else: + return mod_config + + +def get_mod_config_path(generate=False): + mod_config_path = os.path.abspath(os.path.expanduser("~/.rqalpha/mod_config.yml")) + mod_template_path = os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), "../mod_config_template.yml")) + if not os.path.exists(mod_config_path): + if generate: + dir_path = os.path.dirname(mod_config_path) + if not os.path.exists(dir_path): + os.makedirs(dir_path) + shutil.copy(mod_template_path, mod_config_path) + return mod_config_path + else: + return mod_template_path + return mod_config_path + + +def get_user_config_path(config_path=None): + if config_path is None: + if os.path.exists(os.path.abspath(os.path.join(os.getcwd(), "config.yml"))): + return os.path.abspath(os.path.join(os.getcwd(), "config.yml")) + if os.path.exists(os.path.abspath(os.path.join(os.getcwd(), "config.json"))): + return os.path.abspath(os.path.join(os.getcwd(), "config.json")) + return None + else: + if not os.path.exists(config_path): + system_log.error(_("config path: {config_path} does not exist.").format(config_path=config_path)) + return None + return config_path + + +# def config_version_verify(config, config_path): +# default_config_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../default_config.yml") +# default_config = load_config(default_config_path, verify_version=False) +# config_version = config.get("version", None) +# if config_version != default_config["version"]: +# back_config_file_path = config_path + "." + datetime.datetime.now().date().strftime("%Y%m%d") + ".bak" +# shutil.move(config_path, back_config_file_path) +# shutil.copy(default_config_path, config_path) +# +# system_log.warning(_(u""" +# Your current config file {config_file_path} is too old and may cause RQAlpha running error. +# RQAlpha has replaced the config file with the newest one. +# the backup config file has been saved in {back_config_file_path}. +# """).format(config_file_path=config_path, back_config_file_path=back_config_file_path)) +# config = default_config +# return config def set_locale(lc): @@ -98,14 +134,21 @@ def parse_config(config_args, config_path=None, click_type=True, source_code=Non set_locale(config_args.get("extra__locale", None)) - config_path = get_default_config_path() if config_path is None else os.path.abspath(config_path) + user_config_path = get_user_config_path(config_path) + mod_config_path = get_mod_config_path() - config = load_config(config_path) + # load default config from rqalpha + config = load_config(os.path.join(os.path.dirname(os.path.realpath(__file__)), "../default_config.yml")) + # load mod config + mod_config = load_config(mod_config_path) + deep_update(mod_config, config) + # load user config + user_config = load_config(user_config_path) + deep_update(user_config, config) + # use config_args to extend config if click_type: for key, value in six.iteritems(config_args): - if key in ["config_path"]: - continue if config_args[key] is not None: keys = key.split("__") keys.reverse() @@ -121,8 +164,8 @@ def parse_config(config_args, config_path=None, click_type=True, source_code=Non else: deep_update(config_args, config) - config = parse_user_config(config, source_code) - + # config from user code + config = parse_user_config_from_code(config, source_code) config = RqAttrDict(config) base_config = config.base @@ -138,10 +181,6 @@ def parse_config(config_args, config_path=None, click_type=True, source_code=Non base_config.end_date = datetime.datetime.strptime(base_config.end_date, "%Y-%m-%d") if isinstance(base_config.end_date, datetime.datetime): base_config.end_date = base_config.end_date.date() - if base_config.commission_multiplier < 0: - raise patch_user_exc(ValueError(_("invalid commission multiplier value: value range is [0, +∞)"))) - if base_config.margin_multiplier <= 0: - raise patch_user_exc(ValueError(_("invalid margin multiplier value: value range is (0, +∞]"))) if base_config.data_bundle_path is None: base_config.data_bundle_path = os.path.expanduser("~/.rqalpha") @@ -153,18 +192,17 @@ def parse_config(config_args, config_path=None, click_type=True, source_code=Non if not os.path.exists(base_config.data_bundle_path): system_log.error( - _("data bundle not found in {bundle_path}. Run `rqalpha update_bundle` to download data bundle.").format( + _(u"data bundle not found in {bundle_path}. Run `rqalpha update_bundle` to download data bundle.").format( bundle_path=base_config.data_bundle_path)) return if source_code is None and not os.path.exists(base_config.strategy_file): system_log.error( - _("strategy file not found in {strategy_file}").format(strategy_file=base_config.strategy_file)) + _(u"strategy file not found in {strategy_file}").format(strategy_file=base_config.strategy_file)) return base_config.run_type = parse_run_type(base_config.run_type) - base_config.account_list = gen_account_list(base_config.strategy_type) - base_config.matching_type = parse_matching_type(base_config.matching_type) + base_config.account_list = parse_account_list(base_config.securities) base_config.persist_mode = parse_persist_mode(base_config.persist_mode) if extra_config.log_level.upper() != "NONE": @@ -173,18 +211,17 @@ def parse_config(config_args, config_path=None, click_type=True, source_code=Non user_system_log.handlers.append(user_std_handler) if extra_config.context_vars: - import base64 - import json - extra_config.context_vars = json.loads(base64.b64decode(extra_config.context_vars).decode('utf-8')) + if isinstance(extra_config.context_vars, six.string_types): + extra_config.context_vars = json.loads(to_utf8(extra_config.context_vars)) if base_config.stock_starting_cash < 0: - raise patch_user_exc(ValueError(_('invalid stock starting cash: {}').format(base_config.stock_starting_cash))) + raise patch_user_exc(ValueError(_(u"invalid stock starting cash: {}").format(base_config.stock_starting_cash))) if base_config.future_starting_cash < 0: - raise patch_user_exc(ValueError(_('invalid future starting cash: {}').format(base_config.future_starting_cash))) + raise patch_user_exc(ValueError(_(u"invalid future starting cash: {}").format(base_config.future_starting_cash))) if base_config.stock_starting_cash + base_config.future_starting_cash == 0: - raise patch_user_exc(ValueError(_('stock starting cash and future starting cash can not be both 0.'))) + raise patch_user_exc(ValueError(_(u"stock starting cash and future starting cash can not be both 0."))) system_log.level = getattr(logbook, extra_config.log_level.upper(), logbook.NOTSET) std_log.level = getattr(logbook, extra_config.log_level.upper(), logbook.NOTSET) @@ -199,7 +236,7 @@ def parse_config(config_args, config_path=None, click_type=True, source_code=Non return config -def parse_user_config(config, source_code=None): +def parse_user_config_from_code(config, source_code=None): try: if source_code is None: with codecs.open(config["base"]["strategy_file"], encoding="utf-8") as f: @@ -212,39 +249,22 @@ def parse_user_config(config, source_code=None): __config__ = scope.get("__config__", {}) - deep_update(__config__, config) - for sub_key, sub_dict in six.iteritems(__config__): if sub_key not in config["whitelist"]: continue deep_update(sub_dict, config[sub_key]) except Exception as e: - system_log.error(_('in parse_user_config, exception: {e}').format(e=e)) + system_log.error(_(u"in parse_user_config, exception: {e}").format(e=e)) finally: return config -def gen_account_list(account_list_str): - assert isinstance(account_list_str, six.string_types) - account_list = account_list_str.split("_") - return [parse_account_type(account_str) for account_str in account_list] - - -def parse_account_type(account_type_str): - assert isinstance(account_type_str, six.string_types) - if account_type_str == "stock": - return ACCOUNT_TYPE.STOCK - elif account_type_str == "future": - return ACCOUNT_TYPE.FUTURE - - -def parse_matching_type(me_str): - assert isinstance(me_str, six.string_types) - if me_str == "current_bar": - return MATCHING_TYPE.CURRENT_BAR_CLOSE - elif me_str == "next_bar": - return MATCHING_TYPE.NEXT_BAR_OPEN +def parse_account_list(securities): + if isinstance(securities, (tuple, list)): + return [ACCOUNT_TYPE[security.upper()] for security in securities] + elif isinstance(securities, six.string_types): + return [ACCOUNT_TYPE[securities.upper()]] else: raise NotImplementedError @@ -255,8 +275,8 @@ def parse_run_type(rt_str): return RUN_TYPE.BACKTEST elif rt_str == "p": return RUN_TYPE.PAPER_TRADING - elif rt_str == "CTP": - return RUN_TYPE.CTP + elif rt_str == 'r': + return RUN_TYPE.LIVE_TRADING else: raise NotImplementedError @@ -268,4 +288,4 @@ def parse_persist_mode(persist_mode): elif persist_mode == 'on_crash': return PERSIST_MODE.ON_CRASH else: - raise RuntimeError(_('unknown persist mode: {persist_mode}').format(persist_mode=persist_mode)) + raise RuntimeError(_(u"unknown persist mode: {persist_mode}").format(persist_mode=persist_mode)) diff --git a/rqalpha/utils/datetime_func.py b/rqalpha/utils/datetime_func.py index c207b1f00..a9f3cbdba 100644 --- a/rqalpha/utils/datetime_func.py +++ b/rqalpha/utils/datetime_func.py @@ -14,13 +14,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -from collections import namedtuple -try: - # For Python 2 兼容 - from functools import lru_cache -except Exception as e: - from fastcache import lru_cache import datetime +from collections import namedtuple + +from .py2 import lru_cache TimeRange = namedtuple('TimeRange', ['start', 'end']) @@ -46,6 +43,11 @@ def get_last_date(trading_calendar, dt): return trading_calendar[idx - 1] +def convert_date_to_date_int(dt): + t = dt.year * 10000 + dt.month * 100 + dt.day + return t + + def convert_date_to_int(dt): t = dt.year * 10000 + dt.month * 100 + dt.day t *= 1000000 @@ -82,3 +84,14 @@ def convert_int_to_datetime(dt_int): minute, second = divmod(r, 100) return datetime.datetime(year, month, day, hour, minute, second) + +def convert_date_time_ms_int_to_datetime(date_int, time_int): + date_int, time_int = int(date_int), int(time_int) + dt = _convert_int_to_date(date_int) + + hours, r = divmod(time_int, 10000000) + minutes, r = divmod(r, 100000) + seconds, millisecond = divmod(r, 1000) + + return dt.replace(hour=hours, minute=minutes, second=seconds, + microsecond=millisecond * 1000) diff --git a/rqalpha/utils/default_future_info.py b/rqalpha/utils/default_future_info.py index f8787c195..2e5d2fb61 100644 --- a/rqalpha/utils/default_future_info.py +++ b/rqalpha/utils/default_future_info.py @@ -15,8 +15,9 @@ # limitations under the License. from datetime import time + from .datetime_func import TimeRange -from ..const import COMMISSION_TYPE, MARGIN_TYPE + NIGHT_UNDERLYING_SYMBOL = ["CU", "AL", "ZN", "PB", "SN", "NI", "RB", "HC", "BU", "RU", "AU", "AG", "Y", "M", "A", "B", "P", "J", "JM", "I", "CF", "SR", "OI", "MA", "ZC", "FG", "RM"] @@ -91,1630 +92,3 @@ ] TRADING_PERIOD_DICT.update({underlying_symbol: time_period7 for underlying_symbol in ["T", "TF"]}) -DEFAULT_FUTURE_INFO = { - "SM": { - "hedge": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 3.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 3.0 - }, - "speculation": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 3.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 3.0 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 3.0, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 3.0 - } - }, - "SR": { - "hedge": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 3.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 3.0 - }, - "speculation": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 3.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 3.0 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 3.0, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 3.0 - } - }, - "JD": { - "hedge": { - "short_margin_ratio": 0.08, - "close_commission_today_ratio": 0.00015, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 0.00015, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.08, - "open_commission_ratio": 0.00015 - }, - "speculation": { - "short_margin_ratio": 0.08, - "close_commission_today_ratio": 0.00015, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 0.00015, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.08, - "open_commission_ratio": 0.00015 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 0.00015, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 0.00015, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 0.00015 - } - }, - "T": { - "hedge": { - "short_margin_ratio": 0.02, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 3.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.02, - "open_commission_ratio": 3.0 - }, - "speculation": { - "short_margin_ratio": 0.02, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 3.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.02, - "open_commission_ratio": 3.0 - }, - "arbitrage": { - "short_margin_ratio": 0.02, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 3.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.02, - "open_commission_ratio": 3.0 - } - }, - "P": { - "hedge": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 2.5, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 2.5 - }, - "speculation": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 2.5, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 2.5 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 2.5, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 2.5 - } - }, - "BB": { - "hedge": { - "short_margin_ratio": 0.2, - "close_commission_today_ratio": 5e-05, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 0.0001, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.2, - "open_commission_ratio": 0.0001 - }, - "speculation": { - "short_margin_ratio": 0.2, - "close_commission_today_ratio": 5e-05, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 0.0001, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.2, - "open_commission_ratio": 0.0001 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 5e-05, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 0.0001, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 0.0001 - } - }, - "RM": { - "hedge": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 1.5, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 1.5 - }, - "speculation": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 1.5, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 1.5 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 1.5, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 1.5 - } - }, - "RS": { - "hedge": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 2.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 2.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 2.0 - }, - "speculation": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 2.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 2.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 2.0 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 2.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 2.0, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 2.0 - } - }, - "J": { - "hedge": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 3e-05, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 6e-05, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 6e-05 - }, - "speculation": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 3e-05, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 6e-05, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 6e-05 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 3e-05, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 6e-05, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 6e-05 - } - }, - "RI": { - "hedge": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 2.5, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 2.5, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 2.5 - }, - "speculation": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 2.5, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 2.5, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 2.5 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 2.5, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 2.5, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 2.5 - } - }, - "ER": { - "hedge": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 2.5, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 2.5, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 2.5 - }, - "speculation": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 2.5, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 2.5, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 2.5 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 2.5, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 2.5, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 2.5 - } - }, - "L": { - "hedge": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 2.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 2.0 - }, - "speculation": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 2.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 2.0 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 2.0, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 2.0 - } - }, - "PP": { - "hedge": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 2.5e-05, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 5e-05, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 5e-05 - }, - "speculation": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 2.5e-05, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 5e-05, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 5e-05 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 2.5e-05, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 5e-05, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 5e-05 - } - }, - "SN": { - "hedge": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 3.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 3.0 - }, - "speculation": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 3.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 3.0 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 3.0, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 3.0 - } - }, - "I": { - "hedge": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 3e-05, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 6e-05, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 6e-05 - }, - "speculation": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 3e-05, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 6e-05, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 6e-05 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 3e-05, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 6e-05, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 6e-05 - } - }, - "TA": { - "hedge": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 3.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 3.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 3.0 - }, - "speculation": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 3.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 3.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 3.0 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 3.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 3.0, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 3.0 - } - }, - "AL": { - "hedge": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 3.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 3.0 - }, - "speculation": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 3.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 3.0 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 3.0, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 3.0 - } - }, - "ZC": { - "hedge": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 4.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 4.0 - }, - "speculation": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 4.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 4.0 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 4.0, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 4.0 - } - }, - "TC": { - "hedge": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 4.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 4.0 - }, - "speculation": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 4.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 4.0 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 4.0, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 4.0 - } - }, - "LR": { - "hedge": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 3.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 3.0 - }, - "speculation": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 3.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 3.0 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 3.0, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 3.0 - } - }, - "RU": { - "hedge": { - "short_margin_ratio": 0.08, - "close_commission_today_ratio": 4.5e-05, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 4.5e-05, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.08, - "open_commission_ratio": 4.5e-05 - }, - "speculation": { - "short_margin_ratio": 0.08, - "close_commission_today_ratio": 4.5e-05, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 4.5e-05, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.08, - "open_commission_ratio": 4.5e-05 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 4.5e-05, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 4.5e-05, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 4.5e-05 - } - }, - "M": { - "hedge": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 1.5, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 1.5 - }, - "speculation": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 1.5, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 1.5 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 1.5, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 1.5 - } - }, - "TF": { - "hedge": { - "short_margin_ratio": 0.012, - "close_commission_today_ratio": 3.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 3.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.012, - "open_commission_ratio": 3.0 - }, - "speculation": { - "short_margin_ratio": 0.012, - "close_commission_today_ratio": 3.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 3.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.012, - "open_commission_ratio": 3.0 - }, - "arbitrage": { - "short_margin_ratio": 0.012, - "close_commission_today_ratio": 3.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 3.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.012, - "open_commission_ratio": 3.0 - } - }, - "MA": { - "hedge": { - "short_margin_ratio": 0.07, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 1.4, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.07, - "open_commission_ratio": 1.4 - }, - "speculation": { - "short_margin_ratio": 0.07, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 1.4, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.07, - "open_commission_ratio": 1.4 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 1.4, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 1.4 - } - }, - "ME": { - "hedge": { - "short_margin_ratio": 0.07, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 1.4, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.07, - "open_commission_ratio": 1.4 - }, - "speculation": { - "short_margin_ratio": 0.07, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 1.4, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.07, - "open_commission_ratio": 1.4 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 1.4, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 1.4 - } - }, - "WR": { - "hedge": { - "short_margin_ratio": 0.2, - "close_commission_today_ratio": 4e-05, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 4e-05, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.2, - "open_commission_ratio": 4e-05 - }, - "speculation": { - "short_margin_ratio": 0.2, - "close_commission_today_ratio": 4e-05, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 4e-05, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.2, - "open_commission_ratio": 4e-05 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 4e-05, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 4e-05, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 4e-05 - } - }, - "RB": { - "hedge": { - "short_margin_ratio": 0.06, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 4.5e-05, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.06, - "open_commission_ratio": 4.5e-05 - }, - "speculation": { - "short_margin_ratio": 0.06, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 4.5e-05, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.06, - "open_commission_ratio": 4.5e-05 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 4.5e-05, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 4.5e-05 - } - }, - "C": { - "hedge": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 1.2, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 1.2 - }, - "speculation": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 1.2, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 1.2 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 1.2, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 1.2 - } - }, - "JR": { - "hedge": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 3.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 3.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 3.0 - }, - "speculation": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 3.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 3.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 3.0 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 3.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 3.0, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 3.0 - } - }, - "SF": { - "hedge": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 3.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 3.0 - }, - "speculation": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 3.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 3.0 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 3.0, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 3.0 - } - }, - "OI": { - "hedge": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 2.5, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 2.5 - }, - "speculation": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 2.5, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 2.5 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 2.5, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 2.5 - } - }, - "RO": { - "hedge": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 2.5, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 2.5 - }, - "speculation": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 2.5, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 2.5 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 2.5, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 2.5 - } - }, - "CF": { - "hedge": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 4.3, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 4.3 - }, - "speculation": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 4.3, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 4.3 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 4.3, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 4.3 - } - }, - "BU": { - "hedge": { - "short_margin_ratio": 0.07, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 3e-05, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.07, - "open_commission_ratio": 3e-05 - }, - "speculation": { - "short_margin_ratio": 0.07, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 3e-05, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.07, - "open_commission_ratio": 3e-05 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 3e-05, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 3e-05 - } - }, - "JM": { - "hedge": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 3e-05, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 6e-05, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 6e-05 - }, - "speculation": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 3e-05, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 6e-05, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 6e-05 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 3e-05, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 6e-05, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 6e-05 - } - }, - "IH": { - "hedge": { - "short_margin_ratio": 0.2, - "close_commission_today_ratio": 0.000115, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 2.5e-05, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.2, - "open_commission_ratio": 2.5e-05 - }, - "speculation": { - "short_margin_ratio": 0.4, - "close_commission_today_ratio": 0.0023, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 2.3e-05, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.4, - "open_commission_ratio": 2.3e-05 - }, - "arbitrage": { - "short_margin_ratio": 0.2, - "close_commission_today_ratio": 0.000115, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 2.5e-05, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.2, - "open_commission_ratio": 2.5e-05 - } - }, - "FG": { - "hedge": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 3.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 3.0 - }, - "speculation": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 3.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 3.0 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 3.0, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 3.0 - } - }, - "PM": { - "hedge": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 5.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 5.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 5.0 - }, - "speculation": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 5.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 5.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 5.0 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 5.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 5.0, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 5.0 - } - }, - "FB": { - "hedge": { - "short_margin_ratio": 0.2, - "close_commission_today_ratio": 5e-05, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 0.0001, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.2, - "open_commission_ratio": 0.0001 - }, - "speculation": { - "short_margin_ratio": 0.2, - "close_commission_today_ratio": 5e-05, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 0.0001, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.2, - "open_commission_ratio": 0.0001 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 5e-05, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 0.0001, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 0.0001 - } - }, - "CS": { - "hedge": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 1.5, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 1.5 - }, - "speculation": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 1.5, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 1.5 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 1.5, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 1.5 - } - }, - "B": { - "hedge": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 2.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 2.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 2.0 - }, - "speculation": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 2.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 2.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 2.0 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 2.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 2.0, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 2.0 - } - }, - "IC": { - "hedge": { - "short_margin_ratio": 0.2, - "close_commission_today_ratio": 0.000115, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 2.5e-05, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.2, - "open_commission_ratio": 2.5e-05 - }, - "speculation": { - "short_margin_ratio": 0.4, - "close_commission_today_ratio": 0.0023, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 2.3e-05, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.4, - "open_commission_ratio": 2.3e-05 - }, - "arbitrage": { - "short_margin_ratio": 0.2, - "close_commission_today_ratio": 0.000115, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 2.5e-05, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.2, - "open_commission_ratio": 2.5e-05 - } - }, - "WH": { - "hedge": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 2.5, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 2.5 - }, - "speculation": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 2.5, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 2.5 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 2.5, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 2.5 - } - }, - "WS": { - "hedge": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 2.5, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 2.5 - }, - "speculation": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 2.5, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 2.5 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 2.5, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 2.5 - } - }, - "FU": { - "hedge": { - "short_margin_ratio": 0.2, - "close_commission_today_ratio": 2e-05, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 2e-05, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.2, - "open_commission_ratio": 2e-05 - }, - "speculation": { - "short_margin_ratio": 0.2, - "close_commission_today_ratio": 2e-05, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 2e-05, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.2, - "open_commission_ratio": 2e-05 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 2e-05, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 2e-05, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 2e-05 - } - }, - "AU": { - "hedge": { - "short_margin_ratio": 0.06, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 10.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.06, - "open_commission_ratio": 10.0 - }, - "speculation": { - "short_margin_ratio": 0.06, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 10.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.06, - "open_commission_ratio": 10.0 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 10.0, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 10.0 - } - }, - "CU": { - "hedge": { - "short_margin_ratio": 0.08, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 2.5e-05, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.08, - "open_commission_ratio": 2.5e-05 - }, - "speculation": { - "short_margin_ratio": 0.08, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 2.5e-05, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.08, - "open_commission_ratio": 2.5e-05 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 2.5e-05, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 2.5e-05 - } - }, - "V": { - "hedge": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 2.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 2.0 - }, - "speculation": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 2.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 2.0 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 2.0, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 2.0 - } - }, - "Y": { - "hedge": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 2.5, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 2.5 - }, - "speculation": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 2.5, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 2.5 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 2.5, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 2.5 - } - }, - "AG": { - "hedge": { - "short_margin_ratio": 0.08, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 5e-05, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.08, - "open_commission_ratio": 5e-05 - }, - "speculation": { - "short_margin_ratio": 0.08, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 5e-05, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.08, - "open_commission_ratio": 5e-05 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 5e-05, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 5e-05 - } - }, - "PB": { - "hedge": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 4e-05, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 4e-05 - }, - "speculation": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 4e-05, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 4e-05 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 4e-05, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 4e-05 - } - }, - "IF": { - "hedge": { - "short_margin_ratio": 0.2, - "close_commission_today_ratio": 0.000115, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 2.5e-05, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.2, - "open_commission_ratio": 2.5e-05 - }, - "speculation": { - "short_margin_ratio": 0.4, - "close_commission_today_ratio": 0.0023, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 2.3e-05, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.4, - "open_commission_ratio": 2.3e-05 - }, - "arbitrage": { - "short_margin_ratio": 0.2, - "close_commission_today_ratio": 0.000115, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 2.5e-05, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.2, - "open_commission_ratio": 2.5e-05 - } - }, - "A": { - "hedge": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 2.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 2.0 - }, - "speculation": { - "short_margin_ratio": 0.05, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 2.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.05, - "open_commission_ratio": 2.0 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 2.0, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 2.0 - } - }, - "NI": { - "hedge": { - "short_margin_ratio": 0.07, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 6.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.07, - "open_commission_ratio": 6.0 - }, - "speculation": { - "short_margin_ratio": 0.07, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 6.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.07, - "open_commission_ratio": 6.0 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 6.0, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 6.0 - } - }, - "HC": { - "hedge": { - "short_margin_ratio": 0.06, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 4e-05, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.06, - "open_commission_ratio": 4e-05 - }, - "speculation": { - "short_margin_ratio": 0.06, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 4e-05, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.06, - "open_commission_ratio": 4e-05 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_MONEY, - "close_commission_ratio": 4e-05, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 4e-05 - } - }, - "ZN": { - "hedge": { - "short_margin_ratio": 0.06, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 3.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.06, - "open_commission_ratio": 3.0 - }, - "speculation": { - "short_margin_ratio": 0.06, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 3.0, - "margin_type": MARGIN_TYPE.BY_MONEY, - "long_margin_ratio": 0.06, - "open_commission_ratio": 3.0 - }, - "arbitrage": { - "short_margin_ratio": 0.0, - "close_commission_today_ratio": 0.0, - "commission_type": COMMISSION_TYPE.BY_VOLUME, - "close_commission_ratio": 3.0, - "margin_type": None, - "long_margin_ratio": 0.0, - "open_commission_ratio": 3.0 - } - } -} - diff --git a/rqalpha/utils/dict_func.py b/rqalpha/utils/dict_func.py index 18a014b09..b569635bb 100644 --- a/rqalpha/utils/dict_func.py +++ b/rqalpha/utils/dict_func.py @@ -14,12 +14,14 @@ # See the License for the specific language governing permissions and # limitations under the License. +import collections + def deep_update(from_dict, to_dict): for (key, value) in from_dict.items(): - if key in to_dict.keys() and \ - isinstance(to_dict[key], dict) and \ - isinstance(value, dict): + if (key in to_dict.keys() and + isinstance(to_dict[key], collections.Mapping) and + isinstance(value, collections.Mapping)): deep_update(value, to_dict[key]) else: to_dict[key] = value diff --git a/rqalpha/utils/exception.py b/rqalpha/utils/exception.py index cf877e615..6876c6ded 100644 --- a/rqalpha/utils/exception.py +++ b/rqalpha/utils/exception.py @@ -27,6 +27,7 @@ def __init__(self): self.exc_val = None self.exc_tb = None self.error_type = const.EXC_TYPE.NOTSET + self.max_exc_var_len = 160 def set_exc(self, exc_type, exc_val, exc_tb): self.exc_type = exc_type @@ -47,12 +48,21 @@ def __repr__(self): if len(self.stacks) == 0: return self.msg + def _repr(v): + try: + var_str = repr(v) + if len(var_str) > self.max_exc_var_len: + var_str = var_str[:self.max_exc_var_len] + " ..." + return var_str + except Exception: + return 'UNREPRESENTABLE VALUE' + content = ["Traceback (most recent call last):"] for filename, lineno, func_name, code, local_variables in self.stacks: content.append(' File %s, line %s in %s' % (filename, lineno, func_name)) content.append(' %s' % (code, )) for k, v in six.iteritems(local_variables): - content.append(' --> %s = %s' % (k, repr(v))) + content.append(' --> %s = %s' % (k, _repr(v))) content.append('') content.append("%s: %s" % (self.exc_type.__name__, self.msg)) diff --git a/rqalpha/utils/i18n.py b/rqalpha/utils/i18n.py index 89c72d6e1..d38eb1b7a 100644 --- a/rqalpha/utils/i18n.py +++ b/rqalpha/utils/i18n.py @@ -17,6 +17,7 @@ import os.path from gettext import NullTranslations, translation from .logger import system_log +from .py2 import to_utf8 class Localization(object): @@ -55,7 +56,4 @@ def set_locale(self, locales, trans_dir=None): def gettext(message): trans_txt = localization.trans.gettext(message) - try: - return trans_txt.decode('utf-8') - except AttributeError: - return trans_txt + return to_utf8(trans_txt) diff --git a/rqalpha/utils/logger.py b/rqalpha/utils/logger.py index c08ff9be5..84409d228 100644 --- a/rqalpha/utils/logger.py +++ b/rqalpha/utils/logger.py @@ -14,10 +14,13 @@ # See the License for the specific language governing permissions and # limitations under the License. +import traceback import logbook +import better_exceptions from logbook import Logger from logbook.more import ColorizedStderrHandler +from .py2 import to_utf8, from_utf8 logbook.set_datetime_format("local") @@ -26,6 +29,21 @@ logbook.base._level_names[logbook.base.WARNING] = 'WARN' +# better_exceptions hot patch +def format_exception(exc, value, tb): + formatted, colored_source = better_exceptions.format_traceback(tb) + + if not str(value) and exc is AssertionError: + value.args = (colored_source,) + title = traceback.format_exception_only(exc, value) + title = from_utf8(title[0].strip()) + full_trace = u'Traceback (most recent call last):\n{}{}\n'.format(formatted, title) + + return full_trace + +better_exceptions.format_exception = format_exception + + __all__ = [ "user_log", "system_log", @@ -36,17 +54,16 @@ def user_std_handler_log_formatter(record, handler): - from ..execution_context import ExecutionContext - + from ..environment import Environment try: - dt = ExecutionContext.get_current_calendar_dt().strftime(DATETIME_FORMAT) + dt = Environment.get_instance().calendar_dt.strftime(DATETIME_FORMAT) except Exception: dt = "0000-00-00" log = "{dt} {level} {msg}".format( dt=dt, level=record.level_name, - msg=record.message, + msg=to_utf8(record.message), ) return log @@ -57,10 +74,11 @@ def user_std_handler_log_formatter(record, handler): def formatter_builder(tag): def formatter(record, handler): + log = "[{formatter_tag}] [{time}] {level}: {msg}".format( formatter_tag=tag, level=record.level_name, - msg=record.message, + msg=to_utf8(record.message), time=record.time, ) @@ -78,7 +96,7 @@ def formatter(record, handler): # 用于用户异常的详细日志打印 user_detail_log = Logger("user_detail_log") -user_detail_log.handlers.append(ColorizedStderrHandler(bubble=True)) +# user_detail_log.handlers.append(ColorizedStderrHandler(bubble=True)) # 系统日志 system_log = Logger("system_log") @@ -96,3 +114,4 @@ def user_print(*args, **kwargs): message = sep.join(map(str, args)) + end user_log.info(message) + diff --git a/rqalpha/mod/progressive_output_csv/__init__.py b/rqalpha/utils/package_helper.py similarity index 64% rename from rqalpha/mod/progressive_output_csv/__init__.py rename to rqalpha/utils/package_helper.py index 21d93a5ae..6ac605c38 100644 --- a/rqalpha/mod/progressive_output_csv/__init__.py +++ b/rqalpha/utils/package_helper.py @@ -14,8 +14,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .mod import ProgressiveOutputCSVMod +from .logger import system_log -def load_mod(): - return ProgressiveOutputCSVMod() +def import_mod(mod_name): + try: + from importlib import import_module + return import_module(mod_name) + except Exception as e: + system_log.error("*" * 10) + system_log.error("Mod Import Error: ") + system_log.error(e) + system_log.error("*" * 10) + return None diff --git a/rqalpha/utils/persisit_helper.py b/rqalpha/utils/persisit_helper.py index f3fc5ced2..7e532678a 100644 --- a/rqalpha/utils/persisit_helper.py +++ b/rqalpha/utils/persisit_helper.py @@ -58,7 +58,7 @@ def __init__(self, persist_provider, event_bus, persist_mode): event_bus.add_listener(EVENT.POST_BAR, self.persist) event_bus.add_listener(EVENT.POST_SETTLEMENT, self.persist) - def persist(self, *args, **kwargs): + def persist(self, *args): for key, obj in six.iteritems(self._objects): state = obj.get_state() if not state: diff --git a/rqalpha/utils/proxy.py b/rqalpha/utils/proxy.py deleted file mode 100644 index 127d98886..000000000 --- a/rqalpha/utils/proxy.py +++ /dev/null @@ -1,65 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2017 Ricequant, Inc -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import six - - -class PositionsProxy(object): - def __init__(self, postions): - self._postions = postions - self.__class__.__repr__ = postions.__repr__ - - def __repr__(self): - return str([order_book_id for order_book_id in self.keys()]) - - def __getitem__(self, name): - position = self._postions[name] - return position._clone() - - def __iter__(self): - for key in self.keys(): - yield key - - def __len__(self): - return len(self._postions) - - def items(self): - for key, value in sorted(six.iteritems(self._postions)): - yield key, value._clone() - - def keys(self): - return sorted(self._postions.keys()) - - def __setattr__(self, name, value): - if name not in ["_postions"]: - raise AttributeError("{} can not modify to {}".format(name, value)) - super(PositionsProxy, self).__setattr__(name, value) - - -class PortfolioProxy(object): - def __init__(self, portfolio): - self._portfolio = portfolio - self.__class__.__repr__ = portfolio.__repr__ - - self.positions = PositionsProxy(portfolio.positions) - - def __getattr__(self, name): - return getattr(self._portfolio, name) - - def __setattr__(self, name, value): - if name not in ["_portfolio", "_positions_proxy", "positions"]: - raise AttributeError("{} can not modify to {}".format(name, value)) - super(PortfolioProxy, self).__setattr__(name, value) diff --git a/rqalpha/utils/py2.py b/rqalpha/utils/py2.py new file mode 100644 index 000000000..755ac4c1e --- /dev/null +++ b/rqalpha/utils/py2.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2017 Ricequant, Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import six + + +if six.PY2: + import sys + reload(sys) + sys.setdefaultencoding("utf-8") + + +def to_utf8(string): + try: + if six.PY2: + return string.encode('utf-8') + else: + return string + except AttributeError: + return to_utf8(str(string)) + except UnicodeDecodeError: + return to_utf8(unicode(string, 'utf-8')) + + +def from_utf8(string): + try: + return string.decode('utf-8') + except AttributeError: + return string + +try: + from functools import lru_cache +except ImportError: + from fastcache import lru_cache + +try: + from inspect import signature +except ImportError: + from funcsigs import signature diff --git a/rqalpha/utils/repr.py b/rqalpha/utils/repr.py index 4e37effbf..b986fdad5 100644 --- a/rqalpha/utils/repr.py +++ b/rqalpha/utils/repr.py @@ -16,8 +16,6 @@ import six -from .cached_property import CachedProperty - def property_repr(inst): # return pformat(properties(inst)) @@ -62,5 +60,5 @@ def slots(inst): def iter_properties_of_class(cls): for varname in vars(cls): value = getattr(cls, varname) - if isinstance(value, (property, CachedProperty)): + if isinstance(value, property): yield varname diff --git a/rqalpha/utils/risk.py b/rqalpha/utils/risk.py index 7591c4736..65aa55543 100644 --- a/rqalpha/utils/risk.py +++ b/rqalpha/utils/risk.py @@ -52,16 +52,15 @@ def __init__(self, daily_returns, benchmark_daily_returns, risk_free_rate, days, self._benchmark = benchmark_daily_returns self._risk_free_rate = risk_free_rate self._annual_factor = _annual_factor(period) + self._daily_risk_free_rate = self._risk_free_rate / self._annual_factor self._alpha = None self._beta = None self._sharpe = None self._return = np.expm1(np.log1p(self._portfolio).sum()) self._annual_return = (1 + self._return) ** (365 / days) - 1 - # self._annual_return = (1 + self._return) ** (self._annual_factor / len(self._portfolio)) - 1 self._benchmark_return = np.expm1(np.log1p(self._benchmark).sum()) self._benchmark_annual_return = (1 + self._benchmark_return) ** (365 / days) - 1 - # self._benchmark_annual_return = (1 + self._benchmark_return) ** (self._annual_factor / len(self._portfolio)) - 1 self._max_drawdown = None self._volatility = None self._annual_volatility = None @@ -74,6 +73,7 @@ def __init__(self, daily_returns, benchmark_daily_returns, risk_free_rate, days, self._downside_risk = None self._annual_downside_risk = None self._calmar = None + self._avg_excess_return = None @property def return_rate(self): @@ -101,7 +101,9 @@ def alpha(self): self._beta = np.nan return np.nan - self._alpha = self._annual_return - self._risk_free_rate - self.beta * (self._benchmark_annual_return - self._risk_free_rate) + self._alpha = np.mean(self._portfolio - self._daily_risk_free_rate - self.beta * ( + self._benchmark - self._daily_risk_free_rate + )) * self._annual_factor return self._alpha @property @@ -120,14 +122,22 @@ def beta(self): self._beta = cov[0][1] / cov[1][1] return self._beta + @property + def avg_excess_return(self): + if self._avg_excess_return is not None: + return self._avg_excess_return + # self._avg_excess_return = 1.0 / len(self._portfolio) * (self._portfolio - self._daily_risk_free_rate).sum() + self._avg_excess_return = np.mean(self._portfolio - self._daily_risk_free_rate) + return self._avg_excess_return + def _calc_volatility(self): if len(self._portfolio) < 2: - self._volatility = 0 - self._annual_volatility = 0 + self._volatility = 0. + self._annual_volatility = 0. else: - std = self._portfolio.std() - self._volatility = std * (len(self._portfolio) ** 0.5) - self._annual_volatility = std * (self._annual_factor ** 0.5) + # std = self._portfolio.std(ddof=1) + self._volatility = self._portfolio.std(ddof=1) + self._annual_volatility = self._volatility * (self._annual_factor ** 0.5) @property def volatility(self): @@ -147,12 +157,12 @@ def annual_volatility(self): def _calc_benchmark_volatility(self): if len(self._benchmark) < 2: - self._benchmark_volatility = 0 - self._benchmark_annual_volatility = 0 + self._benchmark_volatility = 0. + self._benchmark_annual_volatility = 0. else: - std = self._benchmark.std() - self._benchmark_volatility = std * (len(self._benchmark) ** 0.5) - self._benchmark_annual_volatility = std * (self._annual_factor ** 0.5) + # std = self._benchmark.std(ddof=1) + self._benchmark_volatility = self._benchmark.std(ddof=1) + self._benchmark_annual_volatility = self._benchmark_volatility * (self._annual_factor ** 0.5) @property def benchmark_volatility(self): @@ -186,13 +196,18 @@ def max_drawdown(self): def _calc_tracking_error(self): if len(self._portfolio) < 2: - self._tracking_error = np.nan - return np.nan + self._tracking_error = 0. + self._annual_tracking_error = 0. + return 0 active_return = self._portfolio - self._benchmark - mean_squars = np.mean(np.square(active_return)) - self._tracking_error = (mean_squars ** 0.5) * (len(active_return) ** 0.5) - self._annual_tracking_error = (mean_squars ** 0.5) * (self._annual_factor ** 0.5) + # sum_mean_squares = np.sum(np.square(active_return)) + # self._avg_tracking_return = np.mean(np.sum(active_return)) + self._avg_tracking_return = np.mean(active_return) + # self._tracking_error = (sum_mean_squares ** 0.5) * ((self._annual_factor / (len(active_return) - 1)) ** 0.5) + self._tracking_error = active_return.std(ddof=1) + # self._annual_tracking_error = (sum_mean_squares ** 0.5) / (self._annual_factor ** 0.5) + self._annual_tracking_error = self._tracking_error * (self._annual_factor ** 0.5) @property def tracking_error(self): @@ -208,7 +223,7 @@ def annual_tracking_error(self): return self._annual_tracking_error self._calc_tracking_error() - return self._tracking_error + return self._annual_tracking_error @property def information_ratio(self): @@ -219,15 +234,11 @@ def information_ratio(self): self._information_ratio = np.nan return np.nan - if np.isnan(self.tracking_error): - self._information_ratio = 0.0 - return 0 - if self.tracking_error == 0: self._information_ratio = np.nan return np.nan - self._information_ratio = np.mean(self._portfolio - self._benchmark) / self.tracking_error + self._information_ratio = np.sqrt(self._annual_factor) * self._avg_tracking_return / self.tracking_error return self._information_ratio @property @@ -239,15 +250,24 @@ def sharpe(self): self._sharpe = np.nan return np.nan - self._sharpe = (self._annual_return - self._risk_free_rate) / self.annual_volatility + std_excess_return = np.sqrt((1 / (len(self._portfolio) - 1)) * np.sum( + (self._portfolio - self._daily_risk_free_rate - self.avg_excess_return) ** 2 + )) + self._sharpe = np.sqrt(self._annual_factor) * self.avg_excess_return / std_excess_return return self._sharpe def _calc_downside_risk(self): + if len(self._portfolio) < 2: + self._annual_downside_risk = 0. + self._downside_risk = 0. + return 0 diff = self._portfolio - self._benchmark - diff[diff > 0] = 0 - mean_squares = np.mean(np.square(diff)) - self._annual_downside_risk = (mean_squares ** 0.5) * (self._annual_factor ** 0.5) - self._downside_risk = (mean_squares ** 0.5) * (len(diff) ** 0.5) + diff[diff > 0] = 0. + sum_mean_squares = np.sum(np.square(diff)) + # self._annual_downside_risk = (sum_mean_squares ** 0.5) * \ + # ((self._annual_factor / (len(self._portfolio) - 1)) ** 0.5) + self._downside_risk = (sum_mean_squares / (len(diff) - 1)) ** 0.5 + self._annual_downside_risk = self._downside_risk * (self._annual_factor ** 0.5) @property def downside_risk(self): @@ -274,7 +294,7 @@ def sortino(self): self._sortino = np.nan return np.nan - self._sortino = (self._annual_return - self._risk_free_rate) / self.downside_risk + self._sortino = self._annual_factor * self.avg_excess_return / self.annual_downside_risk return self._sortino @property diff --git a/rqalpha/utils/json.py b/rqalpha/utils/rq_json.py similarity index 99% rename from rqalpha/utils/json.py rename to rqalpha/utils/rq_json.py index e8e9c0065..8abe67862 100644 --- a/rqalpha/utils/json.py +++ b/rqalpha/utils/rq_json.py @@ -16,6 +16,7 @@ import simplejson as json import datetime + from .. import const diff --git a/rqalpha/utils/scheduler.py b/rqalpha/utils/scheduler.py index 3a167adef..32b00cdbe 100644 --- a/rqalpha/utils/scheduler.py +++ b/rqalpha/utils/scheduler.py @@ -20,15 +20,11 @@ from dateutil.parser import parse from ..execution_context import ExecutionContext -from ..utils.exception import patch_user_exc, ModifyExceptionFromType -from ..const import EXC_TYPE, EXECUTION_PHASE from ..environment import Environment +from ..const import EXC_TYPE, EXECUTION_PHASE from ..events import EVENT - -try: - from inspect import signature -except ImportError: - from funcsigs import signature +from ..utils.py2 import signature +from ..utils.exception import patch_user_exc, ModifyExceptionFromType def market_close(hour=0, minute=0): @@ -66,8 +62,8 @@ def run_monthly(func, tradingday=None, time_rule=None, **kwargs): def _verify_function(name, func): if not callable(func): raise patch_user_exc(ValueError('scheduler.{}: func should be callable'.format(name))) - signature = signature(func) - if len(signature.parameters) != 2: + sig = signature(func) + if len(sig.parameters) != 2: raise patch_user_exc(TypeError( 'scheduler.{}: func should take exactly 2 arguments (context, bar_dict)'.format(name))) @@ -180,7 +176,7 @@ def run_monthly(self, func, tradingday=None, time_rule=None, **kwargs): self._registry.append((lambda: self._is_nth_trading_day_in_month(tradingday), time_checker, func)) - def next_day_(self): + def next_day_(self, event): if len(self._registry) == 0: return @@ -196,8 +192,9 @@ def next_day_(self): def _minutes_since_midnight(hour, minute): return hour * 60 + minute - def next_bar_(self, bars): - with ExecutionContext(EXECUTION_PHASE.SCHEDULED, bars): + def next_bar_(self, event): + bars = event.bar_dict + with ExecutionContext(EXECUTION_PHASE.SCHEDULED): self._current_minute = self._minutes_since_midnight(self._ucontext.now.hour, self._ucontext.now.minute) for day_rule, time_rule, func in self._registry: if day_rule() and time_rule(): @@ -205,7 +202,7 @@ def next_bar_(self, bars): func(self._ucontext, bars) self._last_minute = self._current_minute - def before_trading_(self): + def before_trading_(self, event): with ExecutionContext(EXECUTION_PHASE.BEFORE_TRADING): self._stage = 'before_trading' for day_rule, time_rule, func in self._registry: @@ -216,7 +213,7 @@ def before_trading_(self): def _fill_week(self): weekday = self._today.isoweekday() - weekend = self._today + datetime.timedelta(days=7-weekday) + weekend = self._today + datetime.timedelta(days=7 - weekday) week_start = weekend - datetime.timedelta(days=6) left = self._TRADING_DATES.searchsorted(week_start) @@ -225,9 +222,9 @@ def _fill_week(self): def _fill_month(self): try: - month_end = self._today.replace(month=self._today.month+1, day=1) + month_end = self._today.replace(month=self._today.month + 1, day=1) except ValueError: - month_end = self._today.replace(year=self._today.year+1, month=1, day=1) + month_end = self._today.replace(year=self._today.year + 1, month=1, day=1) month_begin = self._today.replace(day=1) left, right = self._TRADING_DATES.searchsorted(month_begin), self._TRADING_DATES.searchsorted(month_end) diff --git a/rqalpha/utils/translations/zh_Hans_CN/LC_MESSAGES/messages.mo b/rqalpha/utils/translations/zh_Hans_CN/LC_MESSAGES/messages.mo index 6680d9d24..dc843215c 100644 Binary files a/rqalpha/utils/translations/zh_Hans_CN/LC_MESSAGES/messages.mo and b/rqalpha/utils/translations/zh_Hans_CN/LC_MESSAGES/messages.mo differ diff --git a/rqalpha/utils/translations/zh_Hans_CN/LC_MESSAGES/messages.po b/rqalpha/utils/translations/zh_Hans_CN/LC_MESSAGES/messages.po index 4f40417f7..b69a2e126 100644 --- a/rqalpha/utils/translations/zh_Hans_CN/LC_MESSAGES/messages.po +++ b/rqalpha/utils/translations/zh_Hans_CN/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2017-02-28 15:22+0800\n" +"POT-Creation-Date: 2017-04-26 15:15+0800\n" "PO-Revision-Date: 2016-10-24 21:20+0800\n" "Last-Translator: FULL NAME \n" "Language: zh_Hans_CN\n" @@ -18,7 +18,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.3.4\n" -#: rqalpha/execution_context.py:141 +#: rqalpha/execution_context.py:105 #, python-format msgid "You cannot call %s when executing %s" msgstr "%s 不能在 %s 中调用。" @@ -41,7 +41,7 @@ msgstr "benchmark {benchmark} 在 {start_date} 还未上市。请重新选择起 msgid "benchmark {benchmark} has been de_listed on {end_date}" msgstr "benchmark {benchmark} 在 {end_date} 已退市。请重新选择结束日期或benchmark。" -#: rqalpha/main.py:119 +#: rqalpha/main.py:143 msgid "" "\n" "[WARNING]\n" @@ -55,428 +55,478 @@ msgstr "" "更新操作首先会清空该文件件下所有内容\n" "您确定要继续吗?\n" -#: rqalpha/main.py:131 +#: rqalpha/main.py:155 msgid "try {} ..." msgstr "尝试 {} ..." -#: rqalpha/main.py:140 +#: rqalpha/main.py:164 msgid "downloading ..." msgstr "下载中 ..." -#: rqalpha/main.py:154 +#: rqalpha/main.py:178 msgid "Data bundle download successfully in {bundle_path}" msgstr "数据下载成功: {bundle_path}" -#: rqalpha/main.py:294 -msgid "unknown event from event source: {}" -msgstr "从 event source 中获取的无效事件: {}" - -#: rqalpha/main.py:300 +#: rqalpha/main.py:295 msgid "strategy run successfully, normal exit" msgstr "策略运行成功,正常退出" -#: rqalpha/main.py:309 rqalpha/main.py:322 rqalpha/main.py:325 +#: rqalpha/main.py:301 rqalpha/main.py:318 rqalpha/main.py:321 msgid "strategy execute exception" msgstr "策略运行产生异常" -#: rqalpha/plot.py:116 +#: rqalpha/api/api_base.py:132 rqalpha/api/api_base.py:232 +#: rqalpha/api/api_base.py:263 rqalpha/api/api_future.py:185 +#: rqalpha/api/api_stock.py:420 +msgid "unsupported order_book_id type" +msgstr "不支持该 order_book_id 类型" + +#: rqalpha/api/api_base.py:176 +msgid "Cancel order fail: invalid order id" +msgstr "撤单失败: 无效的order id" + +#: rqalpha/api/api_base.py:705 +msgid "in get_dividend, start_date {} is later than the previous test day {}" +msgstr "在 get_dividend 函数中,start_date {} 晚于当前回测时间的上一个交易日 {}。" + +#: rqalpha/api/api_future.py:63 rqalpha/api/api_stock.py:99 +#: rqalpha/api/api_stock.py:211 +msgid "Limit order price should be positive" +msgstr "Limit order 价格应该为正,请检查您的下单价格。" + +#: rqalpha/api/api_future.py:75 rqalpha/api/api_future.py:77 +#: rqalpha/api/api_stock.py:122 rqalpha/api/api_stock.py:124 +msgid "Order Creation Failed: [{order_book_id}] No market data" +msgstr "订单创建失败: 当前合约[{order_book_id}]没有市场数据。" + +#: rqalpha/api/api_future.py:178 rqalpha/api/api_stock.py:415 +msgid "{order_book_id} is not supported in current strategy type" +msgstr "当前策略类型不支持 {order_book_id} 合约。" + +#: rqalpha/api/api_stock.py:96 rqalpha/api/api_stock.py:208 +msgid "style should be OrderStyle" +msgstr "style 应该为 OrderStyle 类型,比如MarketOrder()" + +#: rqalpha/api/api_stock.py:130 +msgid "Order Creation Failed: 0 order quantity" +msgstr "订单创建失败: 下单量为0。" + +#: rqalpha/api/api_stock.py:269 +msgid "percent should between -1 and 1" +msgstr "percent 取值应该在 -1 和 1 之间。" + +#: rqalpha/api/api_stock.py:348 +msgid "percent should between 0 and 1" +msgstr "percent 取值应该在 0 和 1 之间。" + +#: rqalpha/core/strategy.py:38 +msgid "deprecated parameter[bar_dict] in before_trading function." +msgstr "[Deprecated]在before_trading函数中,第二个参数bar_dict已经不再使用了。" + +#: rqalpha/core/strategy.py:55 +msgid "" +"[deprecated] before_day_trading is no longer used. use before_trading " +"instead." +msgstr "[deprecated] before_day_trading 已经不再使用,请使用 before_trading 代替。" + +#: rqalpha/core/strategy.py:57 +msgid "" +"[deprecated] before_night_trading is no longer used. use before_trading " +"instead." +msgstr "[deprecated] before_night_trading 已经不再使用,请使用 before_trading 代替。" + +#: rqalpha/core/strategy_context.py:244 rqalpha/core/strategy_context.py:249 +#: rqalpha/core/strategy_context.py:254 rqalpha/core/strategy_context.py:258 +#: rqalpha/core/strategy_context.py:262 rqalpha/core/strategy_context.py:266 +#: rqalpha/core/strategy_context.py:270 +#: rqalpha/model/account/base_account.py:108 +#: rqalpha/model/account/base_account.py:116 +#: rqalpha/model/account/base_account.py:124 +#: rqalpha/model/account/base_account.py:132 +#: rqalpha/model/account/base_account.py:140 +#: rqalpha/model/account/future_account.py:204 +#: rqalpha/model/account/future_account.py:212 +msgid "[abandon] {} is no longer used." +msgstr "[abandon] {} 已经不再使用" + +#: rqalpha/mod/__init__.py:50 +msgid "loading mod {}" +msgstr "载入 Mod {}" + +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:114 msgid "Total Returns" msgstr "回测收益" -#: rqalpha/plot.py:117 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:115 msgid "Annual Returns" msgstr "回测年化收益" -#: rqalpha/plot.py:118 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:116 msgid "Benchmark Returns" msgstr "基准收益" -#: rqalpha/plot.py:120 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:118 msgid "Benchmark Annual" msgstr "基准年化收益" -#: rqalpha/plot.py:123 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:121 msgid "Alpha" msgstr "" -#: rqalpha/plot.py:124 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:122 msgid "Beta" msgstr "" -#: rqalpha/plot.py:125 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:123 msgid "Sharpe" msgstr "" -#: rqalpha/plot.py:126 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:124 msgid "Sortino" msgstr "" -#: rqalpha/plot.py:127 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:125 msgid "Information Ratio" msgstr "" -#: rqalpha/plot.py:129 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:127 msgid "Volatility" msgstr "" -#: rqalpha/plot.py:130 rqalpha/plot.py:160 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:128 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:158 msgid "MaxDrawdown" msgstr "" -#: rqalpha/plot.py:131 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:129 msgid "Tracking Error" msgstr "" -#: rqalpha/plot.py:132 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:130 msgid "Downside Risk" msgstr "" -#: rqalpha/plot.py:141 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:139 msgid "MaxDD/MaxDDD" msgstr "最大回撤/最大回撤持续期" -#: rqalpha/plot.py:154 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:152 msgid "strategy" msgstr "策略" -#: rqalpha/plot.py:156 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:154 msgid "benchmark" msgstr "基准" -#: rqalpha/plot.py:162 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:160 msgid "MaxDDD" msgstr "最大回撤持续期" -#: rqalpha/api/api_base.py:123 rqalpha/api/api_base.py:224 -#: rqalpha/api/api_base.py:254 rqalpha/api/api_future.py:184 -#: rqalpha/api/api_stock.py:470 -msgid "unsupported order_book_id type" -msgstr "不支持该 order_book_id 类型" - -#: rqalpha/api/api_base.py:171 -msgid "Cancel order fail: invalid order id" -msgstr "撤单失败: 无效的order id" - -#: rqalpha/api/api_base.py:685 -msgid "in get_dividend, start_date {} is later than the previous test day {}" -msgstr "在 get_dividend 函数中,start_date {} 晚于当前回测时间的上一个交易日 {}。" - -#: rqalpha/api/api_future.py:62 rqalpha/api/api_stock.py:106 -#: rqalpha/api/api_stock.py:245 -msgid "Limit order price should be positive" -msgstr "Limit order 价格应该为正,请检查您的下单价格。" - -#: rqalpha/api/api_future.py:76 rqalpha/api/api_future.py:77 -#: rqalpha/api/api_stock.py:131 rqalpha/api/api_stock.py:132 -msgid "Order Creation Failed: [{order_book_id}] No market data" -msgstr "订单创建失败: 当前合约[{order_book_id}]没有市场数据。" - -#: rqalpha/api/api_future.py:177 rqalpha/api/api_stock.py:465 -msgid "{order_book_id} is not supported in current strategy type" -msgstr "当前策略类型不支持 {order_book_id} 合约。" - -#: rqalpha/api/api_stock.py:103 rqalpha/api/api_stock.py:242 -msgid "style should be OrderStyle" -msgstr "style 应该为 OrderStyle 类型,比如MarketOrder()" - -#: rqalpha/api/api_stock.py:138 -msgid "Order Creation Failed: 0 order quantity" -msgstr "订单创建失败: 下单量为0。" - -#: rqalpha/api/api_stock.py:320 -msgid "percent should between -1 and 1" -msgstr "percent 取值应该在 -1 和 1 之间。" - -#: rqalpha/api/api_stock.py:439 -msgid "percent should between 0 and 1" -msgstr "percent 取值应该在 0 和 1 之间。" - -#: rqalpha/core/strategy.py:38 -msgid "deprecated parameter[bar_dict] in before_trading function." -msgstr "[Deprecated]在before_trading函数中,第二个参数bar_dict已经不再使用了。" - -#: rqalpha/core/strategy.py:55 +#: rqalpha/mod/rqalpha_mod_sys_risk/cash_validator.py:35 +#: rqalpha/mod/rqalpha_mod_sys_risk/cash_validator.py:54 msgid "" -"[deprecated] before_day_trading is no longer used. use before_trading " -"instead." -msgstr "[deprecated] before_day_trading 已经不再使用,请使用 before_trading 代替。" - -#: rqalpha/core/strategy.py:57 -msgid "" -"[deprecated] before_night_trading is no longer used. use before_trading " -"instead." -msgstr "[deprecated] before_night_trading 已经不再使用,请使用 before_trading 代替。" - -#: rqalpha/core/strategy_context.py:312 rqalpha/core/strategy_context.py:316 -#: rqalpha/core/strategy_context.py:320 rqalpha/core/strategy_context.py:324 -#: rqalpha/core/strategy_context.py:328 -msgid "[deprecated] {} is no longer used." -msgstr "[deprecated] {} 已经不再使用。" - -#: rqalpha/mod/mod_handler.py:43 -msgid "loading mod {}" -msgstr "载入 Mod {}" +"Order Rejected: not enough money to buy {order_book_id}, needs " +"{cost_money:.2f}, cash {cash:.2f}" +msgstr "订单被拒单: 可用资金不足。当前资金: {cash:.2f},{order_book_id} 下单所需资金: {cost_money:.2f}。" -#: rqalpha/mod/risk_manager/frontend_validator.py:51 +#: rqalpha/mod/rqalpha_mod_sys_risk/is_trading_validator.py:29 msgid "Order Rejected: {order_book_id} is not listed!" msgstr "订单被拒单: {order_book_id} 未上市。" -#: rqalpha/mod/risk_manager/frontend_validator.py:55 +#: rqalpha/mod/rqalpha_mod_sys_risk/is_trading_validator.py:35 msgid "Order Rejected: {order_book_id} has been delisted!" msgstr "订单被拒单: {order_book_id} 已退市。" -#: rqalpha/mod/risk_manager/frontend_validator.py:59 -msgid "Order Rejected: {order_book_id} is not trading!" -msgstr "订单被拒单: {order_book_id} 无市场数据。" - -#: rqalpha/mod/risk_manager/frontend_validator.py:68 -msgid "Order Rejected: {order_book_id} is suspended!" -msgstr "订单被拒单: {order_book_id} 已停牌。" +#: rqalpha/mod/rqalpha_mod_sys_risk/is_trading_validator.py:41 +msgid "security {order_book_id} is suspended on {date}" +msgstr "{order_book_id} 在 {date} 时停牌。" -#: rqalpha/mod/risk_manager/frontend_validator.py:90 -#: rqalpha/mod/risk_manager/frontend_validator.py:124 -msgid "" -"Order Rejected: not enough money to buy {order_book_id}, needs " -"{cost_money:.2f}, cash {cash:.2f}" -msgstr "订单被拒单: 可用资金不足。当前资金: {cash:.2f},{order_book_id} 下单所需资金: {cost_money:.2f}。" - -#: rqalpha/mod/risk_manager/frontend_validator.py:105 +#: rqalpha/mod/rqalpha_mod_sys_risk/position_validator.py:33 msgid "" "Order Rejected: not enough stock {order_book_id} to sell, you want to " "sell {quantity}, sellable {sellable}" msgstr "订单被拒单: 仓位不足。平仓量: {quantity},可平数量: {sellable}。" -#: rqalpha/mod/risk_manager/frontend_validator.py:139 +#: rqalpha/mod/rqalpha_mod_sys_risk/position_validator.py:49 msgid "" "Order Rejected: not enough securities {order_book_id} to buy close, " "target sell quantity is {quantity}, sell_closable_quantity {closable}" msgstr "订单被拒单: 卖方向可平仓位不足。平仓手数: {quantity},可平手数: {closable}。" -#: rqalpha/mod/risk_manager/frontend_validator.py:147 +#: rqalpha/mod/rqalpha_mod_sys_risk/position_validator.py:58 msgid "" "Order Rejected: not enough securities {order_book_id} to sell close, " "target sell quantity is {quantity}, buy_closable_quantity {closable}" msgstr "订单被拒单: 买方向可平仓位不足。平仓手数: {quantity},可平手数: {closable}。" -#: rqalpha/mod/simulation/matcher.py:57 -msgid "" -"Order Cancelled: current security [{order_book_id}] can not be traded in " -"listed date [{listed_date}]" -msgstr "订单被撤销: [{order_book_id}] 上市首日无法交易。" - -#: rqalpha/mod/simulation/matcher.py:62 -msgid "Order Cancelled: current bar [{order_book_id}] miss market data." -msgstr "订单被拒单: [{order_book_id}] 当前缺失市场数据。" - -#: rqalpha/mod/simulation/matcher.py:70 +#: rqalpha/mod/rqalpha_mod_sys_risk/price_validator.py:33 msgid "" "Order Rejected: limit order price {limit_price} is higher than limit up " "{limit_up}." msgstr "订单被拒单: 订单价格 {limit_price} 超过当天涨停板价格 {limit_up}。" -#: rqalpha/mod/simulation/matcher.py:80 +#: rqalpha/mod/rqalpha_mod_sys_risk/price_validator.py:44 msgid "" "Order Rejected: limit order price {limit_price} is lower than limit down " "{limit_down}." msgstr "订单被拒单: 订单价格 {limit_price} 低于当天跌停板价格 {limit_down}。" -#: rqalpha/mod/simulation/matcher.py:95 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:74 +#: rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py:67 +msgid "" +"Order Cancelled: current security [{order_book_id}] can not be traded in " +"listed date [{listed_date}]" +msgstr "订单被撤销: [{order_book_id}] 上市首日无法交易。" + +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:79 +#: rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py:73 +msgid "Order Cancelled: current bar [{order_book_id}] miss market data." +msgstr "订单被拒单: [{order_book_id}] 当前缺失市场数据。" + +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:104 msgid "Order Cancelled: current bar [{order_book_id}] reach the limit_up price." msgstr "订单被拒单: [{order_book_id}] 已涨停。" -#: rqalpha/mod/simulation/matcher.py:101 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:110 msgid "Order Cancelled: current bar [{order_book_id}] reach the limit_down price." msgstr "订单被拒单: [{order_book_id}] 已跌停。" -#: rqalpha/mod/simulation/matcher.py:118 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:117 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:123 +msgid "Order Cancelled: [{order_book_id}] has no liquidity." +msgstr "合约 [{order_book_id}] 流动性不足,拒单。" + +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:136 msgid "" "Order Cancelled: market order {order_book_id} volume {order_volume} due " "to volume limit" msgstr "" -#: rqalpha/mod/simulation/matcher.py:140 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:171 msgid "" "Order Cancelled: market order {order_book_id} volume {order_volume} is " "larger than 25 percent of current bar volume, fill {filled_volume} " "actually" msgstr "{order_book_id} 下单量 {order_volume} 超过当前 Bar 成交量的25%,实际成交 {filled_volume}。" -#: rqalpha/mod/simulation/simulation_broker.py:130 +#: rqalpha/mod/rqalpha_mod_sys_simulation/mod.py:36 +msgid "invalid commission multiplier value: value range is [0, +∞)" +msgstr "无效的 手续费乘数 设置: 其值范围为 [0, +∞)" + +#: rqalpha/mod/rqalpha_mod_sys_simulation/mod.py:38 +msgid "invalid margin multiplier value: value range is (0, +∞]" +msgstr "无效的 保证金乘数 设置: 其值范围为 (0, +∞]" + +#: rqalpha/mod/rqalpha_mod_sys_simulation/mod.py:47 +#: rqalpha/mod/rqalpha_mod_sys_simulation/mod.py:53 +msgid "Not supported matching type {}" +msgstr "不支持撮合类型: {}" + +#: rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py:54 +msgid "cancel_order function is not supported in signal mode" +msgstr "在 Signal 模式下,不支持 cancel_order 函数。" + +#: rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py:89 +#: rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py:96 +msgid "You have traded {order_book_id} with {quantity} lots in {bar_status}" +msgstr "您在 {bar_status} 下成交 {quantity} 手 {order_book_id} (请尽量避免涨跌停时下单)" + +#: rqalpha/mod/rqalpha_mod_sys_simulation/simulation_broker.py:97 msgid "{order_id} order has been cancelled by user." msgstr "订单 {order_id} 被手动取消。" -#: rqalpha/mod/simulation/simulation_broker.py:150 +#: rqalpha/mod/rqalpha_mod_sys_simulation/simulation_broker.py:116 msgid "Order Rejected: {order_book_id} can not match. Market close." msgstr "订单被拒单: {order_book_id} 当天交易结束,订单无法成交。" -#: rqalpha/model/bar.py:392 -msgid "id_or_symbols {} does not exist" -msgstr "您选择的证券[{}]不存在。" +#: rqalpha/mod/rqalpha_mod_sys_simulation/simulation_event_source.py:184 +msgid "Frequency {} is not support." +msgstr "不支持回测频率: {}" -#: rqalpha/model/slippage.py:43 +#: rqalpha/mod/rqalpha_mod_sys_simulation/decider/slippage.py:38 msgid "invalid slippage rate value: value range is [0, 1)" msgstr "无效的 滑点 设置: 其值范围为 [0, 1]" -#: rqalpha/model/account/future_account.py:82 -#: rqalpha/model/account/stock_account.py:71 +#: rqalpha/model/bar.py:345 +msgid "id_or_symbols {} does not exist" +msgstr "您选择的证券[{}]不存在。" + +#: rqalpha/model/account/future_account.py:144 +#: rqalpha/model/account/stock_account.py:141 msgid "{order_book_id} is expired, close all positions by system" msgstr "{order_book_id} 已退市/交割,系统自动平仓。" -#: rqalpha/model/account/stock_account.py:222 -msgid "no split data {}" -msgstr "没有拆股数据 {}" - -#: rqalpha/model/account/stock_account.py:231 -#: rqalpha/model/account/stock_account.py:240 -msgid "split {order_book_id}, {position}" -msgstr "拆股 {order_book_id}, {position}" - -#: rqalpha/model/account/stock_account.py:245 -msgid "split {order_book_id}, {series}" -msgstr "拆股 {order_book_id}, {series}" - -#: rqalpha/utils/__init__.py:150 +#: rqalpha/model/position/base_position.py:79 +#: rqalpha/model/position/base_position.py:85 +#: rqalpha/model/position/stock_position.py:147 +#: rqalpha/model/position/stock_position.py:155 +#: rqalpha/model/position/stock_position.py:163 +#: rqalpha/model/position/stock_position.py:171 +#: rqalpha/model/position/stock_position.py:179 +msgid "[abandon] {} is no longer valid." +msgstr "[abandon] {} 已经不再有效" + +#: rqalpha/utils/__init__.py:171 msgid "not run {}({}, {}) because strategy is hold" msgstr "因为策略上一次运行还没接触,取消执行 {}({}, {})" -#: rqalpha/utils/arg_checker.py:58 +#: rqalpha/utils/arg_checker.py:48 msgid "" "function {}: invalid {} argument, expect a value of type {}, got {} " "(type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:67 +#: rqalpha/utils/arg_checker.py:57 msgid "" "function {}: invalid {} argument, expect a valid " "instrument/order_book_id/symbol, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:74 +#: rqalpha/utils/arg_checker.py:64 msgid "" "function {}: invalid {} argument, expect a valid stock " "instrument/order_book_id/symbol, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:81 +#: rqalpha/utils/arg_checker.py:71 msgid "" "function {}: invalid {} argument, expect a valid future " "instrument/order_book_id/symbol, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:92 rqalpha/utils/arg_checker.py:100 +#: rqalpha/utils/arg_checker.py:82 rqalpha/utils/arg_checker.py:90 msgid "Main Future contracts[88] are not supported in paper trading." msgstr "期货主力连续合约[88] 在实盘模拟中暂不支持。" -#: rqalpha/utils/arg_checker.py:94 rqalpha/utils/arg_checker.py:105 +#: rqalpha/utils/arg_checker.py:84 rqalpha/utils/arg_checker.py:95 msgid "Index Future contracts[99] are not supported in paper trading." msgstr "期货指数连续合约[99] 在实盘模拟中暂不支持。" -#: rqalpha/utils/arg_checker.py:165 +#: rqalpha/utils/arg_checker.py:155 msgid "function {}: invalid {} argument, expect a number, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:180 +#: rqalpha/utils/arg_checker.py:170 msgid "function {}: invalid {} argument, valid: {}, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:195 +#: rqalpha/utils/arg_checker.py:184 msgid "function {}: invalid {} argument, valid fields are {}, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:207 +#: rqalpha/utils/arg_checker.py:196 msgid "function {}: invalid field {}, valid fields are {}, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:212 rqalpha/utils/arg_checker.py:229 +#: rqalpha/utils/arg_checker.py:202 rqalpha/utils/arg_checker.py:220 msgid "" "function {}: invalid {} argument, expect a string or a list of string, " "got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:248 rqalpha/utils/arg_checker.py:253 +#: rqalpha/utils/arg_checker.py:240 rqalpha/utils/arg_checker.py:245 msgid "function {}: invalid {} argument, expect a valid date, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:264 +#: rqalpha/utils/arg_checker.py:256 +msgid "function {}: invalid {} argument, expect a value >= {}, got {} (type: {})" +msgstr "" + +#: rqalpha/utils/arg_checker.py:266 msgid "function {}: invalid {} argument, expect a value > {}, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:274 +#: rqalpha/utils/arg_checker.py:276 +msgid "function {}: invalid {} argument, expect a value <= {}, got {} (type: {})" +msgstr "" + +#: rqalpha/utils/arg_checker.py:287 msgid "function {}: invalid {} argument, expect a value < {}, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:291 +#: rqalpha/utils/arg_checker.py:304 msgid "" "function {}: invalid {} argument, interval should be in form of '1d', " "'3m', '4q', '2y', got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:304 +#: rqalpha/utils/arg_checker.py:326 +msgid "" +"function {}: invalid {} argument, quarter should be in form of '2012q3', " +"got {} (type: {})" +msgstr "" + +#: rqalpha/utils/arg_checker.py:340 msgid "" "function {}: invalid {} argument, should be entity like " "Fundamentals.balance_sheet.total_equity, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:323 +#: rqalpha/utils/arg_checker.py:359 msgid "" "function {}: invalid {} argument, frequency should be in form of '1m', " "'5m', '1d', got {} (type: {})" msgstr "" -#: rqalpha/utils/config.py:71 +#: rqalpha/utils/config.py:42 msgid "config.yml not found in {config_path}" msgstr "没有在 {config_path} 找到 config.yml 文件" -#: rqalpha/utils/config.py:92 -msgid "" -"\n" -"Your current config file {config_file_path} is too old and may cause " -"RQAlpha running error.\n" -"RQAlpha has replaced the config file with the newest one.\n" -"the backup config file has been saved in {back_config_file_path}.\n" -" " -msgstr "" -"\n" -"您使用的配置文件 {config_file_path} \n" -"版本过久,可能会导致RQAlpha运行错误。\n" -"已为您替换为新的配置文件,旧的配置文件备份存储于 {back_config_file_path}\n" -" " - -#: rqalpha/utils/config.py:136 -msgid "invalid commission multiplier value: value range is [0, +∞)" -msgstr "无效的 手续费乘数 设置: 其值范围为 [0, +∞)" - -#: rqalpha/utils/config.py:138 -msgid "invalid margin multiplier value: value range is (0, +∞]" -msgstr "无效的 保证金乘数 设置: 其值范围为 (0, +∞]" +#: rqalpha/utils/config.py:94 +msgid "config path: {config_path} does not exist." +msgstr "配置文件路径: {config_path} 不存在" -#: rqalpha/utils/config.py:150 +#: rqalpha/utils/config.py:195 msgid "" "data bundle not found in {bundle_path}. Run `rqalpha update_bundle` to " "download data bundle." msgstr "没有在 {bundle_path} 找到数据源相关文件,请运行 `rqalpha update_bundle` 下载最新数据。" -#: rqalpha/utils/config.py:156 +#: rqalpha/utils/config.py:201 msgid "strategy file not found in {strategy_file}" msgstr "没有在 {strategy_file} 找到 策略文件。" -#: rqalpha/utils/config.py:175 +#: rqalpha/utils/config.py:218 msgid "invalid stock starting cash: {}" msgstr "股票账户初始资金不能为负,当前为: {}" -#: rqalpha/utils/config.py:178 +#: rqalpha/utils/config.py:221 msgid "invalid future starting cash: {}" msgstr "期货账户初始资金不能为负,当前为: {}" -#: rqalpha/utils/config.py:181 +#: rqalpha/utils/config.py:224 msgid "stock starting cash and future starting cash can not be both 0." msgstr "初始资金需要大于0。" -#: rqalpha/utils/config.py:218 +#: rqalpha/utils/config.py:258 msgid "in parse_user_config, exception: {e}" msgstr "在执行 parse_user_config 时,产生异常: {e}" -#: rqalpha/utils/config.py:266 +#: rqalpha/utils/config.py:291 msgid "unknown persist mode: {persist_mode}" msgstr "无效的 persist mode: {persist_mode}" +#~ msgid "unknown event from event source: {}" +#~ msgstr "从 event source 中获取的无效事件: {}" + +#~ msgid "Order Rejected: {order_book_id} is not trading!" +#~ msgstr "订单被拒单: {order_book_id} 无市场数据。" + +#~ msgid "Order Rejected: {order_book_id} is suspended!" +#~ msgstr "订单被拒单: {order_book_id} 已停牌。" + +#~ msgid "no split data {}" +#~ msgstr "没有拆股数据 {}" + +#~ msgid "split {order_book_id}, {position}" +#~ msgstr "拆股 {order_book_id}, {position}" + +#~ msgid "split {order_book_id}, {series}" +#~ msgstr "拆股 {order_book_id}, {series}" + +#~ msgid "" +#~ msgstr "" + +#~ msgid "[deprecated] {} is no longer used." +#~ msgstr "[deprecated] {} 已经不再使用。" + diff --git a/setup.py b/setup.py index 7ef7a06c0..9870e0c04 100644 --- a/setup.py +++ b/setup.py @@ -17,6 +17,7 @@ import sys from os.path import dirname, join +from pip.req import parse_requirements from setuptools import ( find_packages, @@ -27,32 +28,10 @@ with open(join(dirname(__file__), 'rqalpha/VERSION.txt'), 'rb') as f: version = f.read().decode('ascii').strip() -install_requirements = [ - 'numpy>=1.11.1', - 'pandas>=0.18.1', - 'python-dateutil>=2.5.3', - 'pytz>=2016.4', - 'six>=1.10.0', - 'pytest>=2.9.2', - 'logbook==1.0.0', - 'click>=6.6', - 'bcolz>=1.1.0', - 'matplotlib>=1.5.1', - 'jsonpickle==0.9.4', - 'simplejson>=3.10.0', - 'dill==0.2.5', - 'XlsxWriter>=0.9.6', - 'line-profiler>=2.0', - 'ruamel.yaml>=0.13.14', -] - -if sys.version_info.major < 3: - install_requirements.extend([ - 'enum34>=1.1.6', - 'fastcache>=1.0.2', - 'funcsigs' - ]) +requirements = [str(ir.req) for ir in parse_requirements("requirements.txt", session=False)] +if sys.version_info.major == 2: + requirements += [str(ir.req) for ir in parse_requirements("requirements-py2.txt", session=False)] setup( name='rqalpha', @@ -64,11 +43,12 @@ license='Apache License v2', package_data={'': ['*.*']}, url='https://github.com/ricequant/rqalpha', - install_requires=install_requirements, + install_requires=requirements, zip_safe=False, entry_points={ "console_scripts": [ "rqalpha = rqalpha.__main__:entry_point", + "rqalpha-cmd = rqalpha.cmd:entry_point", ] }, classifiers=[ diff --git a/test.py b/test.py index 2ce6a9ca3..b0c80ccb0 100644 --- a/test.py +++ b/test.py @@ -29,17 +29,14 @@ from rqalpha import run from rqalpha.utils.logger import system_log -from rqalpha.utils.config import parse_config - TEST_DIR = os.path.abspath("./tests/") TEST_OUT = os.path.abspath("./tests/outs/") - pd.set_option("display.width", 160) -def get_test_files(file_path=None): +def run_tests(file_path=None): if file_path is not None: files = [file_path] else: @@ -54,7 +51,7 @@ def get_test_files(file_path=None): system_log.exception() error_map[filename.replace(".py", "")] = e for filename, result_data in iteritems(error_map): - print("*" * 20, "【{}】did not pass!".format(filename), "*" * 20) + print(u"*" * 20, u"[{}]did not pass!".format(filename), u"*" * 20) if isinstance(result_data, Exception): system_log.error(result_data) else: @@ -64,8 +61,8 @@ def get_test_files(file_path=None): # print("+" * 10, "new test Dataframe: ", "+" * 10) # print(df.drop(result.columns[result.all()], axis=1)) print(result.all()) - print("=" * 40) - print("[{}|{}] strategies has been passed!".format(len(files) - len(error_map), len(files))) + print(u"=" * 40) + print(u"[{}|{}] strategies has been passed!".format(len(files) - len(error_map), len(files))) return len(error_map) @@ -75,8 +72,8 @@ def run_test(filename): "strategy_file": os.path.join(TEST_DIR, filename) } } - print("Start test: " + str(config["base"]["strategy_file"])) - result_dict = run(config) + print(u"Start test: " + str(config["base"]["strategy_file"])) + result_dict = run(config)['sys_analyser'] df = result_dict["total_portfolios"] # del df['positions'] @@ -104,8 +101,8 @@ def run_test(filename): except: pass - df = df.round(4) - old_df = old_df.round(4) + df = df.round(0) + old_df = old_df.round(0) result = df.eq(old_df) if not result.all().all(): @@ -141,27 +138,48 @@ def is_enable_coverage(): def test_api(): - print("Testing API......") - from rqalpha import run + # FIXME Error msg is hard to understand @zjuguxi + # return - from tests.api.test_api_base import test_get_order_code_new, test_get_open_order_code_new, \ - test_cancel_order_code_new, \ - test_update_universe_code_new, test_subscribe_code_new, test_unsubscribe_code_new, \ - test_get_yield_curve_code_new, \ - test_history_bars_code_new, test_all_instruments_code_new, test_instruments_code_new, test_sector_code_new, \ - test_concept_code_new, test_industry_code_new, test_get_trading_dates_code_new, \ - test_get_previous_trading_date_code_new, test_get_next_trading_date_code_new, test_get_dividend_code_new - - from tests.api.test_api_stock import test_order_shares_code_new, test_order_lots_code_new, \ - test_order_value_code_new, \ - test_order_percent_code_new, test_order_target_value_code_new + print(u"Testing API......") + from rqalpha import run - from tests.api.test_api_future import test_buy_open_code_new, test_sell_open_code_new, test_buy_close_code_new, \ - test_sell_close_code_new + from tests.api.test_api_base import ( + test_get_order_code_new, + test_get_open_order_code_new, + test_cancel_order_code_new, + test_update_universe_code_new, + test_subscribe_code_new, + test_unsubscribe_code_new, + test_get_yield_curve_code_new, + test_history_bars_code_new, + test_all_instruments_code_new, + test_instruments_code_new, + test_sector_code_new, + test_concept_code_new, + test_industry_code_new, + test_get_trading_dates_code_new, + test_get_previous_trading_date_code_new, + test_get_next_trading_date_code_new, + test_get_dividend_code_new, + ) + + from tests.api.test_api_stock import ( + test_order_shares_code_new, test_order_lots_code_new, + test_order_value_code_new, + test_order_percent_code_new, test_order_target_value_code_new, + ) + + from tests.api.test_api_future import ( + test_buy_open_code_new, + test_sell_open_code_new, + test_buy_close_code_new, + test_sell_close_code_new, + ) base_api_config = { "base": { - "strategy_type": "stock", + "securities": "stock", "start_date": "2016-12-01", "end_date": "2016-12-31", "frequency": "1d", @@ -173,16 +191,16 @@ def test_api(): "log_level": "error", }, "mod": { - "progress": { - "enabled": False, - "priority": 400, + "sys_progress": { + "enabled": True, + "show": True, }, }, } stock_api_config = { "base": { - "strategy_type": "stock", + "securities": "stock", "start_date": "2016-03-07", "end_date": "2016-03-08", "frequency": "1d", @@ -194,16 +212,16 @@ def test_api(): "log_level": "error", }, "mod": { - "progress": { - "enabled": False, - "priority": 400, + "sys_progress": { + "enabled": True, + "show": True, }, }, } future_api_config = { "base": { - "strategy_type": "future", + "securities": "future", "start_date": "2016-03-07", "end_date": "2016-03-08", "frequency": "1d", @@ -215,50 +233,93 @@ def test_api(): "log_level": "error", }, "mod": { - "progress": { - "enabled": False, - "priority": 400, + "sys_progress": { + "enabled": True, + "show": True, }, }, } # =================== Test Base API =================== - run(base_api_config, test_get_order_code_new) - run(base_api_config, test_get_open_order_code_new) - run(base_api_config, test_cancel_order_code_new) - run(base_api_config, test_update_universe_code_new) - run(base_api_config, test_subscribe_code_new) - run(base_api_config, test_unsubscribe_code_new) - run(base_api_config, test_get_yield_curve_code_new) - run(base_api_config, test_history_bars_code_new) - run(base_api_config, test_all_instruments_code_new) - run(base_api_config, test_instruments_code_new) - run(base_api_config, test_sector_code_new) - run(base_api_config, test_industry_code_new) - run(base_api_config, test_concept_code_new) - run(base_api_config, test_get_trading_dates_code_new) - run(base_api_config, test_get_previous_trading_date_code_new) - run(base_api_config, test_get_next_trading_date_code_new) - run(base_api_config, test_get_dividend_code_new) + tasks = [] + + tasks.append((base_api_config, test_get_order_code_new, "test_get_order_code_new")) + tasks.append((base_api_config, test_get_open_order_code_new, "test_get_open_order_code_new")) + tasks.append((base_api_config, test_cancel_order_code_new, "test_cancel_order_code_new")) + tasks.append((base_api_config, test_update_universe_code_new, "test_update_universe_code_new")) + tasks.append((base_api_config, test_subscribe_code_new, "test_subscribe_code_new")) + tasks.append((base_api_config, test_unsubscribe_code_new, "test_unsubscribe_code_new")) + tasks.append((base_api_config, test_get_yield_curve_code_new, "test_get_yield_curve_code_new")) + tasks.append((base_api_config, test_history_bars_code_new, "test_history_bars_code_new")) + tasks.append((base_api_config, test_all_instruments_code_new, "test_all_instruments_code_new")) + tasks.append((base_api_config, test_instruments_code_new, "test_instruments_code_new")) + tasks.append((base_api_config, test_sector_code_new, "test_sector_code_new")) + tasks.append((base_api_config, test_industry_code_new, "test_industry_code_new")) + tasks.append((base_api_config, test_concept_code_new, "test_concept_code_new")) + tasks.append((base_api_config, test_get_trading_dates_code_new, "test_get_trading_dates_code_new")) + tasks.append((base_api_config, test_get_previous_trading_date_code_new, "test_get_previous_trading_date_code_new")) + tasks.append((base_api_config, test_get_next_trading_date_code_new, "test_get_next_trading_date_code_new")) + tasks.append((base_api_config, test_get_dividend_code_new, "test_get_dividend_code_new")) # =================== Test Stock API =================== - run(stock_api_config, test_order_shares_code_new) - run(stock_api_config, test_order_lots_code_new) - run(stock_api_config, test_order_value_code_new) - run(stock_api_config, test_order_percent_code_new) - run(stock_api_config, test_order_target_value_code_new) + tasks.append((stock_api_config, test_order_shares_code_new, "test_order_shares_code_new")) + tasks.append((stock_api_config, test_order_lots_code_new, "test_order_lots_code_new")) + tasks.append((stock_api_config, test_order_value_code_new, "test_order_value_code_new")) + tasks.append((stock_api_config, test_order_percent_code_new, "test_order_percent_code_new")) + tasks.append((stock_api_config, test_order_target_value_code_new, "test_order_target_value_code_new")) # =================== Test Future API =================== - run(future_api_config, test_buy_open_code_new) - run(future_api_config, test_sell_open_code_new) - run(future_api_config, test_buy_close_code_new) - run(future_api_config, test_sell_close_code_new) + tasks.append((future_api_config, test_buy_open_code_new, "test_buy_open_code_new")) + tasks.append((future_api_config, test_sell_open_code_new, "test_sell_open_code_new")) + tasks.append((future_api_config, test_buy_close_code_new, "test_buy_close_code_new")) + tasks.append((future_api_config, test_sell_close_code_new, "test_sell_close_code_new")) - print("API test ends.") + for cfg, source_code, name in tasks: + print("running", name) + run(cfg, source_code) + + print(u"API test ends.") def test_strategy(): - get_test_files() + run_tests() + + +def write_csv(path, fields): + old_test_times = [] + if not os.path.exists(path): + with open(path, 'w') as csv_file: + writer = csv.DictWriter(csv_file, fieldnames=fields) + writer.writeheader() + with open(path) as csv_file: + reader = csv.DictReader(csv_file) + for row in reader: + old_test_times.append(row) + + if performance_path is None: + if len(old_test_times) != 0 and time_spend > float(old_test_times[-1]["time_spend"]) * 1.1: + system_log.error("代码咋写的,太慢了!") + system_log.error("上次测试用例执行的总时长为:" + old_test_times[-1]["time_spend"]) + system_log.error("本次测试用例执行的总时长增长为: " + str(time_spend)) + else: + with open(path, 'a') as csv_file: + writer = csv.DictWriter(csv_file, fieldnames=fields) + writer.writerow({'date_time': end_time, 'time_spend': time_spend}) + else: + if 0 < len(old_test_times) < 5 and time_spend > float(sum(float(i['time_spend']) for i in old_test_times)) / len(old_test_times) * 1.1: + print('Average time of last 5 runs:', float(sum(float(i['time_spend']) for i in old_test_times))/len(old_test_times)) + print('Now time spend:', time_spend) + raise RuntimeError('Performance regresses!') + elif len(old_test_times) >= 5 and time_spend > float(sum(float(i['time_spend']) for i in old_test_times[-5:])) / 5 * 1.1: + print('Average time of last 5 runs:', + float(sum(float(i['time_spend']) for i in old_test_times[-5:])) / 5) + print('Now time spend:', time_spend) + raise RuntimeError('Performance regresses!') + else: + with open(path, 'a') as csv_file: + writer = csv.DictWriter(csv_file, fieldnames=fields) + writer.writerow({'date_time': end_time, 'time_spend': time_spend}) + if __name__ == '__main__': if is_enable_coverage(): @@ -266,9 +327,13 @@ def test_strategy(): cov = coverage.Coverage() cov.start() + performance_path = None + field_names = ['date_time', 'time_spend'] + start_time = datetime.now() + if len(sys.argv) >= 2: - if sys.argv[1] == 'mod': + if sys.argv[1] == 'api': test_api() end_time = datetime.now() @@ -276,36 +341,27 @@ def test_strategy(): test_strategy() end_time = datetime.now() + elif sys.argv[1] == 'performance': + test_api() + test_strategy() + end_time = datetime.now() + performance_path = sys.argv[2] + time_spend = (end_time - start_time).total_seconds() + write_csv(performance_path, field_names) + else: target_file = sys.argv[1] - get_test_files(target_file) + run_tests(target_file) end_time = datetime.now() else: test_api() - error_count = get_test_files() + error_count = run_tests() end_time = datetime.now() if error_count == 0: time_csv_file_path = os.path.join(TEST_OUT, "time.csv") - field_names = ['date_time', 'time_spend'] - old_test_times = [] time_spend = (end_time - start_time).total_seconds() - if not os.path.exists(time_csv_file_path): - with open(time_csv_file_path, 'w') as csv_file: - writer = csv.DictWriter(csv_file, fieldnames=field_names) - writer.writeheader() - with open(time_csv_file_path) as csv_file: - reader = csv.DictReader(csv_file) - for row in reader: - old_test_times.append(row) - if len(old_test_times) != 0 and time_spend > float(old_test_times[-1]["time_spend"]) * 1.1: - system_log.error("代码咋写的,太慢了!") - system_log.error("上次测试用例执行的总时长为:" + old_test_times[-1]["time_spend"]) - system_log.error("本次测试用例执行的总时长增长为: " + str(time_spend)) - else: - with open(time_csv_file_path, 'a') as csv_file: - writer = csv.DictWriter(csv_file, fieldnames=field_names) - writer.writerow({'date_time': end_time, 'time_spend': time_spend}) + write_csv(time_csv_file_path, field_names) else: print('Failed!') diff --git a/tests/api/__init__.py b/tests/api/__init__.py index 64dea0dba..c6d24b7bd 100644 --- a/tests/api/__init__.py +++ b/tests/api/__init__.py @@ -1,19 +1,16 @@ #!/usr/bin/env python -# encoding: utf-8 - - -""" -@author: Gu Xi -@contact: guxi@ricequant.com -@site: http://www.ricequant.com -@file: __init__.py.py -@time: 2017/2/17 下午1:35 -""" - - -def func(): - pass - - -if __name__ == '__main__': - pass \ No newline at end of file +# -*- coding: utf-8 -*- +# +# Copyright 2017 Ricequant, Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/api/test_api_base.py b/tests/api/test_api_base.py index 45429eb24..f7bb7cab5 100644 --- a/tests/api/test_api_base.py +++ b/tests/api/test_api_base.py @@ -1,9 +1,33 @@ #!/usr/bin/env python -# encoding: utf-8 +# -*- coding: utf-8 -*- +# +# Copyright 2017 Ricequant, Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import inspect +import re + + + +def get_code_block(func): + lines = inspect.getsourcelines(func)[0][1:] + return "".join([re.sub(r'^ ', '', line) for line in lines]) + def test_get_order(): from rqalpha.api import order_shares, get_order + def init(context): context.s1 = '000001.XSHE' context.amount = 100 @@ -14,11 +38,12 @@ def handle_bar(context, bar_dict): assert order.order_book_id == context.s1 assert order.quantity == context.amount assert order.unfilled_quantity + order.filled_quantity == order.quantity -test_get_order_code_new = "".join(inspect.getsourcelines(test_get_order)[0]) +test_get_order_code_new = get_code_block(test_get_order) def test_get_open_order(): from rqalpha.api import order_shares, get_open_orders, get_order + def init(context): context.s1 = '000001.XSHE' context.limitprice = 8.9 @@ -34,17 +59,18 @@ def handle_bar(context, bar_dict): print('cash: ', context.portfolio.cash) print('check_get_open_orders done') print(order.order_id) - print(get_open_orders().keys()) + # print(get_open_orders()) print(get_open_orders()) print(get_order(order.order_id)) if context.counter == 2: assert order.order_id in get_open_orders() context.counter = 0 -test_get_open_order_code_new = "".join(inspect.getsourcelines(test_get_open_order)[0]) +test_get_open_order_code_new = get_code_block(test_get_open_order) def test_cancel_order(): from rqalpha.api import order_shares, cancel_order, get_order + def init(context): context.s1 = '000001.XSHE' context.limitprice = 8.59 @@ -58,11 +84,12 @@ def handle_bar(context, bar_dict): assert order.filled_quantity == 0 return order_id assert order.price == context.limitprice -test_cancel_order_code_new = "".join(inspect.getsourcelines(test_cancel_order)[0]) +test_cancel_order_code_new = get_code_block(test_cancel_order) def test_update_universe(): from rqalpha.api import update_universe, history_bars + def init(context): context.s1 = '000001.XSHE' context.s2 = '600340.XSHG' @@ -77,11 +104,12 @@ def handle_bar(context, bar_dict): print(sorted(his.tolist())) print(sorted([24.1, 23.71, 23.82, 23.93, 23.66])) assert sorted(his.tolist()) == sorted([26.06, 26.13, 26.54, 26.6, 26.86]) -test_update_universe_code_new = "".join(inspect.getsourcelines(test_update_universe)[0]) +test_update_universe_code_new = get_code_block(test_update_universe) def test_subscribe(): from rqalpha.api import subscribe + def init(context): context.f1 = 'AU88' context.amount = 1 @@ -89,11 +117,12 @@ def init(context): def handle_bar(context, bar_dict): assert context.f1 in context.universe -test_subscribe_code_new = "".join(inspect.getsourcelines(test_subscribe)[0]) +test_subscribe_code_new = get_code_block(test_subscribe) def test_unsubscribe(): from rqalpha.api import subscribe, unsubscribe + def init(context): context.f1 = 'AU88' context.amount = 1 @@ -102,11 +131,12 @@ def init(context): def handle_bar(context, bar_dict): assert context.f1 not in context.universe -test_unsubscribe_code_new = "".join(inspect.getsourcelines(test_unsubscribe)[0]) +test_unsubscribe_code_new = get_code_block(test_unsubscribe) def test_get_yield_curve(): from rqalpha.api import get_yield_curve + def init(context): pass @@ -114,11 +144,12 @@ def handle_bar(context, bar_dict): df = get_yield_curve('20161101') assert df.iloc[0, 0] == 0.019923 assert df.iloc[0, 6] == 0.021741 -test_get_yield_curve_code_new = "".join(inspect.getsourcelines(test_get_yield_curve)[0]) +test_get_yield_curve_code_new = get_code_block(test_get_yield_curve) def test_history_bars(): from rqalpha.api import history_bars + def init(context): context.s1 = '000001.XSHE' pass @@ -127,11 +158,12 @@ def handle_bar(context, bar_dict): return_list = history_bars(context.s1, 5, '1d', 'close') if str(context.now.date()) == '2016-12-29': assert return_list.tolist() == [9.08, 9.1199, 9.08, 9.06, 9.08] -test_history_bars_code_new = "".join(inspect.getsourcelines(test_history_bars)[0]) +test_history_bars_code_new = get_code_block(test_history_bars) def test_all_instruments(): from rqalpha.api import all_instruments + def init(context): pass @@ -149,37 +181,39 @@ def handle_bar(context, bar_dict): assert all_instruments('FenjiB').shape >= (140, 9) assert all_instruments('INDX').shape >= (500, 8) assert all_instruments('Future').shape >= (3500, 16) -test_all_instruments_code_new = "".join(inspect.getsourcelines(test_all_instruments)[0]) +test_all_instruments_code_new = get_code_block(test_all_instruments) + def test_instruments_code(): from rqalpha.api import instruments + def init(context): context.s1 = '000001.XSHE' pass def handle_bar(context, bar_dict): - print('hello') ins = instruments(context.s1) assert ins.sector_code_name == '金融' assert ins.symbol == '平安银行' assert ins.order_book_id == context.s1 assert ins.type == 'CS' - print('world') -test_instruments_code_new = "".join(inspect.getsourcelines(test_instruments_code)[0]) +test_instruments_code_new = get_code_block(test_instruments_code) def test_sector(): from rqalpha.api import sector + def init(context): pass def handle_bar(context, bar_dict): assert len(sector('金融')) >= 180 -test_sector_code_new = "".join(inspect.getsourcelines(test_sector)[0]) +test_sector_code_new = get_code_block(test_sector) def test_industry(): from rqalpha.api import industry, instruments + def init(context): context.s1 = '000001.XSHE' context.s2 = '600340.XSHG' @@ -191,25 +225,27 @@ def handle_bar(context, bar_dict): industry_list_2 = industry(ins_2.industry_name) assert context.s1 in industry_list_1 assert context.s2 in industry_list_2 -test_industry_code_new = "".join(inspect.getsourcelines(test_industry)[0]) +test_industry_code_new = get_code_block(test_industry) def test_concept(): from rqalpha.api import concept, instruments + def init(context): context.s1 = '000002.XSHE' def handle_bar(context, bar_dict): ins = instruments(context.s1) concept_list = concept(ins.concept_names[:4]) # 取 concept_names 的前4个字,即 context.s1 的第一个概念 - assert context.s1 in concept_list - assert len(concept_list) >= 90 -test_concept_code_new = "".join(inspect.getsourcelines(test_concept)[0]) + assert context.s1 in concept_list, 'context.s1 not in concept_list' + assert len(concept('深圳本地')) >= 55, 'len(concept_list) < 55, length: {}, concept_list: {}'.format(len(concept_list), ins.concept_names[:4]) +test_concept_code_new = get_code_block(test_concept) def test_get_trading_dates(): from rqalpha.api import get_trading_dates import datetime + def init(context): pass @@ -223,11 +259,12 @@ def handle_bar(context, bar_dict): assert sorted([item.strftime("%Y%m%d") for item in correct_dates_list]) == sorted( [item.strftime("%Y%m%d") for item in trading_dates_list]) -test_get_trading_dates_code_new = "".join(inspect.getsourcelines(test_get_trading_dates)[0]) +test_get_trading_dates_code_new = get_code_block(test_get_trading_dates) def test_get_previous_trading_date(): from rqalpha.api import get_previous_trading_date + def init(context): pass @@ -239,23 +276,24 @@ def handle_bar(context, bar_dict): assert str(get_previous_trading_date('2010-01-03').date()) == '2009-12-31' assert str(get_previous_trading_date('2009-01-03').date()) == '2008-12-31' assert str(get_previous_trading_date('2005-01-05').date()) == '2005-01-04' -test_get_previous_trading_date_code_new = "".join(inspect.getsourcelines(test_get_previous_trading_date)[0]) +test_get_previous_trading_date_code_new = get_code_block(test_get_previous_trading_date) def test_get_next_trading_date(): from rqalpha.api import get_next_trading_date + def init(context): pass def handle_bar(context, bar_dict): assert str(get_next_trading_date('2017-01-03').date()) == '2017-01-04' assert str(get_next_trading_date('2007-01-03').date()) == '2007-01-04' -test_get_next_trading_date_code_new = "".join(inspect.getsourcelines(test_get_next_trading_date)[0]) +test_get_next_trading_date_code_new = get_code_block(test_get_next_trading_date) def test_get_dividend(): from rqalpha.api import get_dividend - import pandas + def init(context): pass @@ -263,9 +301,9 @@ def handle_bar(context, bar_dict): df = get_dividend('000001.XSHE', start_date='20130104') df_to_assert = df.loc[df['book_closure_date'] == ' 2013-06-19'] assert df.shape >= (4, 5) - assert df_to_assert.iloc[0, 1] == 0.9838 - assert df_to_assert.iloc[0, 3] == pandas.tslib.Timestamp('2013-06-20 00:00:00') -test_get_dividend_code_new = "".join(inspect.getsourcelines(test_get_dividend)[0]) + assert df_to_assert.iloc[0, 1] == 0.6149 + assert str(df_to_assert.iloc[0, 3]) == '2013-06-20 00:00:00' +test_get_dividend_code_new = get_code_block(test_get_dividend) # =================== 以下把代码写为纯字符串 =================== @@ -302,7 +340,7 @@ def handle_bar(context, bar_dict): print('cash: ', context.portfolio.cash) print('check_get_open_orders done') print(order.order_id) - print(get_open_orders().keys()) + # print(get_open_orders()) print(get_open_orders()) print(get_order(order.order_id)) if context.counter == 2: diff --git a/tests/api/test_api_future.py b/tests/api/test_api_future.py index 76c71b826..c81a85aaf 100644 --- a/tests/api/test_api_future.py +++ b/tests/api/test_api_future.py @@ -1,9 +1,26 @@ #!/usr/bin/env python -# encoding: utf-8 -import inspect +# -*- coding: utf-8 -*- +# +# Copyright 2017 Ricequant, Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .test_api_base import get_code_block + def test_buy_open(): from rqalpha.api import buy_open, subscribe, get_order, ORDER_STATUS, POSITION_EFFECT, SIDE + def init(context): context.f1 = 'P88' context.amount = 1 @@ -15,18 +32,19 @@ def init(context): def handle_bar(context, bar_dict): order_id = buy_open(context.f1, 1) order = get_order(order_id) - assert order.order_book_id == context.f1 - assert order.quantity == 1 - assert order.status == ORDER_STATUS.ACTIVE - assert order.unfilled_quantity == 1 - assert order.unfilled_quantity + order.filled_quantity == order.quantity - assert order.side == SIDE.BUY - assert order.position_effect == POSITION_EFFECT.OPEN -test_buy_open_code_new = "".join(inspect.getsourcelines(test_buy_open)[0]) + assert order.order_book_id == context.f1, 'Order_book_id is wrong' + assert order.quantity == 1, 'order.quantity is wrong' + assert order.status == ORDER_STATUS.FILLED, 'order.status is wrong' + assert order.unfilled_quantity == 0, 'order.unfilled_quantity is wrong' + assert order.unfilled_quantity + order.filled_quantity == order.quantity, 'order.unfilled_quantity is wrong' + assert order.side == SIDE.BUY, 'order.side is wrong' + assert order.position_effect == POSITION_EFFECT.OPEN, 'order.position_effect is wrong' +test_buy_open_code_new = get_code_block(test_buy_open) def test_sell_open(): from rqalpha.api import sell_open, subscribe, get_order, ORDER_STATUS, POSITION_EFFECT, SIDE + def init(context): context.f1 = 'P88' context.amount = 1 @@ -38,18 +56,19 @@ def init(context): def handle_bar(context, bar_dict): order_id = sell_open(context.f1, 1) order = get_order(order_id) - assert order.order_book_id == context.f1 - assert order.quantity == 1 - assert order.status == ORDER_STATUS.ACTIVE - assert order.unfilled_quantity == 1 - assert order.unfilled_quantity + order.filled_quantity == order.quantity - assert order.side == SIDE.SELL - assert order.position_effect == POSITION_EFFECT.OPEN -test_sell_open_code_new = "".join(inspect.getsourcelines(test_sell_open)[0]) + assert order.order_book_id == context.f1, 'Order_book_id is wrong' + assert order.quantity == 1, 'order.quantity is wrong' + assert order.status == ORDER_STATUS.FILLED, 'order.status is wrong' + assert order.unfilled_quantity == 0, 'order.unfilled_quantity is wrong' + assert order.unfilled_quantity + order.filled_quantity == order.quantity, 'order.unfilled_quantity is wrong' + assert order.side == SIDE.SELL, 'order.side is wrong' + assert order.position_effect == POSITION_EFFECT.OPEN, 'order.position_effect is wrong' +test_sell_open_code_new = get_code_block(test_sell_open) def test_buy_close(): from rqalpha.api import buy_close, subscribe, get_order, ORDER_STATUS, POSITION_EFFECT, SIDE + def init(context): context.f1 = 'P88' context.amount = 1 @@ -61,18 +80,19 @@ def init(context): def handle_bar(context, bar_dict): order_id = buy_close(context.f1, 1) order = get_order(order_id) - assert order.order_book_id == context.f1 - assert order.quantity == 1 - assert order.status == ORDER_STATUS.ACTIVE - assert order.unfilled_quantity == 1 - assert order.unfilled_quantity + order.filled_quantity == order.quantity - assert order.side == SIDE.BUY - assert order.position_effect == POSITION_EFFECT.CLOSE -test_buy_close_code_new = "".join(inspect.getsourcelines(test_buy_close)[0]) + assert order.order_book_id == context.f1, 'Order_book_id is wrong' + assert order.quantity == 1, 'order.quantity is wrong' + assert order.status == ORDER_STATUS.REJECTED, 'order.status is wrong' + assert order.unfilled_quantity == 1, 'order.unfilled_quantity is wrong' + assert order.unfilled_quantity + order.filled_quantity == order.quantity, 'order.unfilled_quantity is wrong' + assert order.side == SIDE.BUY, 'order.side is wrong' + assert order.position_effect == POSITION_EFFECT.CLOSE, 'order.position_effect is wrong' +test_buy_close_code_new = get_code_block(test_buy_close) def test_sell_close(): from rqalpha.api import sell_close, subscribe, get_order, ORDER_STATUS, POSITION_EFFECT, SIDE + def init(context): context.f1 = 'P88' context.amount = 1 @@ -86,9 +106,9 @@ def handle_bar(context, bar_dict): order = get_order(order_id) assert order.order_book_id == context.f1 assert order.quantity == 1 - assert order.status == ORDER_STATUS.ACTIVE + assert order.status == ORDER_STATUS.REJECTED assert order.unfilled_quantity == 1 assert order.unfilled_quantity + order.filled_quantity == order.quantity assert order.side == SIDE.SELL assert order.position_effect == POSITION_EFFECT.CLOSE -test_sell_close_code_new = "".join(inspect.getsourcelines(test_sell_close)[0]) +test_sell_close_code_new = get_code_block(test_sell_close) diff --git a/tests/api/test_api_stock.py b/tests/api/test_api_stock.py index ebf3f1352..73b36dbae 100644 --- a/tests/api/test_api_stock.py +++ b/tests/api/test_api_stock.py @@ -1,19 +1,26 @@ #!/usr/bin/env python -# encoding: utf-8 -import inspect -import datetime - - -def test_order_shares(): - def init(context): - pass - - def handle_bar(context, bar_dict): - pass +# -*- coding: utf-8 -*- +# +# Copyright 2017 Ricequant, Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .test_api_base import get_code_block def test_order_shares(): from rqalpha.api import order_shares, get_order, SIDE, LimitOrder + def init(context): context.order_count = 0 context.s1 = "000001.XSHE" @@ -26,16 +33,17 @@ def handle_bar(context, bar_dict): order_id = order_shares(context.s1, context.amount, style=LimitOrder(context.limitprice)) order = get_order(order_id) order_side = SIDE.BUY if context.amount > 0 else SIDE.SELL - assert order.side == order_side - assert order.order_book_id == context.s1 - assert order.quantity == context.amount - assert order.unfilled_quantity + order.filled_quantity == order.quantity - assert order.price == context.limitprice -test_order_shares_code_new = "".join(inspect.getsourcelines(test_order_shares)[0]) + assert order.side == order_side, 'order.side is wrong' + assert order.order_book_id == context.s1, 'Order_book_id is wrong' + assert order.quantity == context.amount, 'order.quantity is wrong' + assert order.unfilled_quantity + order.filled_quantity == order.quantity, 'order.unfilled_quantity is wrong' + assert order.price == context.limitprice, 'order.price is wrong' +test_order_shares_code_new = get_code_block(test_order_shares) def test_order_lots(): from rqalpha.api import order_lots, get_order, SIDE, LimitOrder + def init(context): context.order_count = 0 context.s1 = "000001.XSHE" @@ -48,16 +56,17 @@ def handle_bar(context, bar_dict): order_id = order_lots(context.s1, 1, style=LimitOrder(context.limitprice)) order = get_order(order_id) order_side = SIDE.BUY if context.amount > 0 else SIDE.SELL - assert order.side == order_side - assert order.order_book_id == context.s1 - assert order.quantity == 100 - assert order.unfilled_quantity + order.filled_quantity == order.quantity - assert order.price == context.limitprice -test_order_lots_code_new = "".join(inspect.getsourcelines(test_order_lots)[0]) + assert order.side == order_side, 'order.side is wrong' + assert order.order_book_id == context.s1, 'Order_book_id is wrong' + assert order.quantity == 100, 'order.quantity is wrong' + assert order.unfilled_quantity + order.filled_quantity == order.quantity, 'order.unfilled_quantity is wrong' + assert order.price == context.limitprice, 'order.price is wrong' +test_order_lots_code_new = get_code_block(test_order_lots) def test_order_value(): from rqalpha.api import order_value, get_order, SIDE, LimitOrder + def init(context): context.order_count = 0 context.s1 = "000001.XSHE" @@ -68,16 +77,17 @@ def handle_bar(context, bar_dict): order_id = order_value(context.s1, 1000, style=LimitOrder(context.limitprice)) order = get_order(order_id) order_side = SIDE.BUY if order.quantity > 0 else SIDE.SELL - assert order.side == order_side - assert order.order_book_id == context.s1 - assert order.quantity == 100 - assert order.unfilled_quantity + order.filled_quantity == order.quantity - assert order.price == context.limitprice -test_order_value_code_new = "".join(inspect.getsourcelines(test_order_value)[0]) + assert order.side == order_side, 'order.side is wrong' + assert order.order_book_id == context.s1, 'Order_book_id is wrong' + assert order.quantity == 100, 'order.quantity is wrong' + assert order.unfilled_quantity + order.filled_quantity == order.quantity, 'order.unfilled_quantity is wrong' + assert order.price == context.limitprice, 'order.price is wrong' +test_order_value_code_new = get_code_block(test_order_value) def test_order_percent(): from rqalpha.api import order_percent, get_order, SIDE, LimitOrder + def init(context): context.order_count = 0 context.s1 = "000001.XSHE" @@ -88,15 +98,16 @@ def handle_bar(context, bar_dict): order_id = order_percent(context.s1, 0.0001, style=LimitOrder(context.limitprice)) order = get_order(order_id) order_side = SIDE.BUY if order.quantity > 0 else SIDE.SELL - assert order.side == order_side - assert order.order_book_id == context.s1 - assert order.unfilled_quantity + order.filled_quantity == order.quantity - assert order.price == context.limitprice -test_order_percent_code_new = "".join(inspect.getsourcelines(test_order_percent)[0]) + assert order.side == order_side, 'order.side is wrong' + assert order.order_book_id == context.s1, 'Order_book_id is wrong' + assert order.unfilled_quantity + order.filled_quantity == order.quantity, 'order.unfilled_quantity is wrong' + assert order.price == context.limitprice, 'order.price is wrong' +test_order_percent_code_new = get_code_block(test_order_percent) def test_order_target_value(): from rqalpha.api import order_target_percent, get_order, SIDE, LimitOrder + def init(context): context.order_count = 0 context.s1 = "000001.XSHE" @@ -108,7 +119,7 @@ def handle_bar(context, bar_dict): print("after: ", context.portfolio.cash) order = get_order(order_id) order_side = SIDE.BUY if order.quantity > 0 else SIDE.SELL - assert order.side == order_side - assert order.order_book_id == context.s1 - assert order.price == context.limitprice -test_order_target_value_code_new = "".join(inspect.getsourcelines(test_order_target_value)[0]) + assert order.side == order_side, 'order.side is wrong' + assert order.order_book_id == context.s1, 'Order_book_id is wrong' + assert order.price == context.limitprice, 'order.price is wrong' +test_order_target_value_code_new = get_code_block(test_order_target_value) diff --git a/tests/outs/test_f_buy_and_hold.pkl b/tests/outs/test_f_buy_and_hold.pkl index 0649fd251..7f3511e6c 100644 Binary files a/tests/outs/test_f_buy_and_hold.pkl and b/tests/outs/test_f_buy_and_hold.pkl differ diff --git a/tests/outs/test_f_macd.pkl b/tests/outs/test_f_macd.pkl index 2d15acccf..dc4b0ae4f 100644 Binary files a/tests/outs/test_f_macd.pkl and b/tests/outs/test_f_macd.pkl differ diff --git a/tests/outs/test_f_macd_signal.pkl b/tests/outs/test_f_macd_signal.pkl new file mode 100644 index 000000000..c83bf144f Binary files /dev/null and b/tests/outs/test_f_macd_signal.pkl differ diff --git a/tests/outs/test_f_mean_reverting.pkl b/tests/outs/test_f_mean_reverting.pkl index 5a26fefae..107c8424b 100644 Binary files a/tests/outs/test_f_mean_reverting.pkl and b/tests/outs/test_f_mean_reverting.pkl differ diff --git a/tests/outs/test_s_buy_and_hold.pkl b/tests/outs/test_s_buy_and_hold.pkl new file mode 100644 index 000000000..8d1af62b4 Binary files /dev/null and b/tests/outs/test_s_buy_and_hold.pkl differ diff --git a/tests/outs/test_s_dma.pkl b/tests/outs/test_s_dma.pkl index e6fadc79b..b93b99804 100644 Binary files a/tests/outs/test_s_dma.pkl and b/tests/outs/test_s_dma.pkl differ diff --git a/tests/outs/test_s_dual_thrust.pkl b/tests/outs/test_s_dual_thrust.pkl index f7f997149..8765d169a 100644 Binary files a/tests/outs/test_s_dual_thrust.pkl and b/tests/outs/test_s_dual_thrust.pkl differ diff --git a/tests/outs/test_s_golden_cross.pkl b/tests/outs/test_s_golden_cross.pkl index 8f1c550c9..0d85e355a 100644 Binary files a/tests/outs/test_s_golden_cross.pkl and b/tests/outs/test_s_golden_cross.pkl differ diff --git a/tests/outs/test_s_scheduler.pkl b/tests/outs/test_s_scheduler.pkl new file mode 100644 index 000000000..141557414 Binary files /dev/null and b/tests/outs/test_s_scheduler.pkl differ diff --git a/tests/outs/test_s_turtle.pkl b/tests/outs/test_s_turtle.pkl index 6c2db8980..69cef4721 100644 Binary files a/tests/outs/test_s_turtle.pkl and b/tests/outs/test_s_turtle.pkl differ diff --git a/tests/outs/test_s_turtle_signal.pkl b/tests/outs/test_s_turtle_signal.pkl new file mode 100644 index 000000000..a45162081 Binary files /dev/null and b/tests/outs/test_s_turtle_signal.pkl differ diff --git a/tests/outs/test_sf_buy_and_hold.pkl b/tests/outs/test_sf_buy_and_hold.pkl new file mode 100644 index 000000000..b8c553449 Binary files /dev/null and b/tests/outs/test_sf_buy_and_hold.pkl differ diff --git a/tests/outs/time.csv b/tests/outs/time.csv index e68c2809e..71047a492 100644 --- a/tests/outs/time.csv +++ b/tests/outs/time.csv @@ -1,3 +1,5 @@ date_time,time_spend -2017-03-01 20:44:14.427070,24.485413 -2017-03-06 10:43:55.971036,25.256511 +2017-04-25 21:55:14.575241,44.742035 +2017-04-26 11:36:53.780488,43.712649 +2017-04-26 15:06:39.224211,44.557727 +2017-04-26 16:26:06.669304,44.146478 diff --git a/tests/test_f_buy_and_hold.py b/tests/test_f_buy_and_hold.py index c3a967d55..fff5c5318 100644 --- a/tests/test_f_buy_and_hold.py +++ b/tests/test_f_buy_and_hold.py @@ -10,23 +10,21 @@ def handle_bar(context, bar_dict): __config__ = { "base": { - "strategy_type": "future", + "securities": "future", "start_date": "2015-01-09", "end_date": "2015-03-09", "frequency": "1d", - "matching_type": "next_bar", + "matching_type": "current_bar", "future_starting_cash": 1000000, - "commission_multiplier": 0.01, "benchmark": None, }, "extra": { "log_level": "error", - # "plot": True, }, "mod": { - "progress": { + "sys_progress": { "enabled": True, - "priority": 400, + "show": True, }, }, } diff --git a/tests/test_f_macd.py b/tests/test_f_macd.py index d12d90339..5a72558d8 100644 --- a/tests/test_f_macd.py +++ b/tests/test_f_macd.py @@ -47,11 +47,11 @@ def handle_bar(context, bar_dict): __config__ = { "base": { - "strategy_type": "future", + "securities": "future", "start_date": "2014-09-01", "end_date": "2016-09-05", "frequency": "1d", - "matching_type": "next_bar", + "matching_type": "current_bar", "future_starting_cash": 1000000, "benchmark": None, }, @@ -59,12 +59,9 @@ def handle_bar(context, bar_dict): "log_level": "error", }, "mod": { - "progress": { + "sys_progress": { "enabled": True, - "priority": 400, + "show": True, }, }, - "validator": { - "bar_limit": False, - }, } diff --git a/tests/test_f_macd_signal.py b/tests/test_f_macd_signal.py new file mode 100644 index 000000000..fb024e210 --- /dev/null +++ b/tests/test_f_macd_signal.py @@ -0,0 +1,70 @@ +# 可以自己import我们平台支持的第三方python模块,比如pandas、numpy等 +import talib +import numpy as np + + +# 在这个方法中编写任何的初始化逻辑。context对象将会在你的算法策略的任何方法之间做传递 +def init(context): + # context内引入全局变量s1,存储目标合约信息 + context.s1 = "IF88" + + # 使用MACD需要设置长短均线和macd平均线的参数 + context.SHORTPERIOD = 12 + context.LONGPERIOD = 26 + context.SMOOTHPERIOD = 9 + context.OBSERVATION = 50 + + # 初始化时订阅合约行情。订阅之后的合约行情会在handle_bar中进行更新 + subscribe(context.s1) + + +# 你选择的期货数据更新将会触发此段逻辑,例如日线或分钟线更新 +def handle_bar(context, bar_dict): + # 开始编写你的主要的算法逻辑 + # 获取历史收盘价序列,history_bars函数直接返回ndarray,方便之后的有关指标计算 + prices = history_bars(context.s1, context.OBSERVATION, '1d', 'close') + + # 用Talib计算MACD取值,得到三个时间序列数组,分别为macd,signal 和 hist + macd, signal, hist = talib.MACD(prices, context.SHORTPERIOD, context.LONGPERIOD, context.SMOOTHPERIOD) + + # macd 是长短均线的差值,signal是macd的均线,如果短均线从下往上突破长均线,为入场信号,进行买入开仓操作 + if (macd[-1] - signal[-1] > 0 and macd[-2] - signal[-2] < 0): + sell_qty = context.portfolio.positions[context.s1].sell_quantity + # 先判断当前卖方仓位,如果有,则进行平仓操作 + if (sell_qty > 0): + buy_close(context.s1, 1) + # 买入开仓 + buy_open(context.s1, 1) + + if (macd[-1] - signal[-1] < 0 and macd[-2] - signal[-2] > 0): + buy_qty = context.portfolio.positions[context.s1].buy_quantity + # 先判断当前买方仓位,如果有,则进行平仓操作 + if (buy_qty > 0): + sell_close(context.s1, 1) + # 卖出开仓 + sell_open(context.s1, 1) + + +__config__ = { + "base": { + "securities": "future", + "start_date": "2014-09-01", + "end_date": "2016-09-05", + "frequency": "1d", + "matching_type": "current_bar", + "future_starting_cash": 1000000, + "benchmark": None, + }, + "extra": { + "log_level": "error", + }, + "mod": { + "sys_progress": { + "enabled": True, + "show": True, + }, + "sys_simulation": { + "signal": True, + } + }, +} diff --git a/tests/test_f_mean_reverting.py b/tests/test_f_mean_reverting.py index b1c4da38d..e9b1a7c35 100644 --- a/tests/test_f_mean_reverting.py +++ b/tests/test_f_mean_reverting.py @@ -122,7 +122,7 @@ def handle_bar(context, bar_dict): __config__ = { "base": { - "strategy_type": "future", + "securities": "future", "start_date": "2014-06-01", "end_date": "2015-08-01", "frequency": "1d", @@ -134,12 +134,9 @@ def handle_bar(context, bar_dict): "log_level": "error", }, "mod": { - "progress": { + "sys_progress": { "enabled": True, - "priority": 400, + "show": True, }, }, - "validator": { - "bar_limit": True, - } } diff --git a/tests/test_s_buy_and_hold.py b/tests/test_s_buy_and_hold.py new file mode 100644 index 000000000..b3d2241dc --- /dev/null +++ b/tests/test_s_buy_and_hold.py @@ -0,0 +1,36 @@ +def init(context): + context.s1 = "000001.XSHE" + + +def before_trading(context): + pass + + +def handle_bar(context, bar_dict): + order_shares(context.s1, 1000) + + +def after_trading(context): + pass + +__config__ = { + "base": { + "securities": "stock", + "start_date": "2015-01-09", + "end_date": "2016-01-12", + "frequency": "1d", + "matching_type": "current_bar", + "stock_starting_cash": 1000000, + "benchmark": "000300.XSHG", + }, + "extra": { + "log_level": "error", + "show": True, + }, + "mod": { + "sys_progress": { + "enabled": True, + "show": True, + }, + }, +} diff --git a/tests/test_s_dma.py b/tests/test_s_dma.py index e633754a2..7557f52d4 100644 --- a/tests/test_s_dma.py +++ b/tests/test_s_dma.py @@ -24,14 +24,13 @@ def handle_bar(context, bar_dict): if DDD < AMA and cur_position > 0: order_target_percent(context.s1, 0) - if (HHV(MAX(O, C), 50) / LLV(MIN(O, C), 50) < 2 - and CROSS(DDD, AMA) and cur_position == 0): + if (HHV(MAX(O, C), 50) / LLV(MIN(O, C), 50) < 2 and CROSS(DDD, AMA) and cur_position == 0): order_target_percent(context.s1, 1) __config__ = { "base": { - "strategy_type": "stock", + "securities": "stock", "start_date": "2008-07-01", "end_date": "2017-01-01", "frequency": "1d", @@ -44,12 +43,13 @@ def handle_bar(context, bar_dict): "log_level": "error", }, "mod": { - "progress": { + "sys_progress": { "enabled": True, - "priority": 400, + "show": True, }, - "funcat_api": { + "sys_funcat": { "enabled": True, + "show": True, }, }, } diff --git a/tests/test_s_dual_thrust.py b/tests/test_s_dual_thrust.py index ba654279f..33a46e92d 100644 --- a/tests/test_s_dual_thrust.py +++ b/tests/test_s_dual_thrust.py @@ -48,7 +48,6 @@ def handle_bar(context, bar_dict): # 使用第n-1日的收盘价作为当前价 current_price = Close[2] - Range = max((HH - LC), (HC - LL)) K1 = 0.9 BuyLine = Openprice + K1 * Range @@ -65,7 +64,7 @@ def handle_bar(context, bar_dict): __config__ = { "base": { - "strategy_type": "stock", + "securities": "stock", "start_date": "2013-01-01", "end_date": "2015-12-29", "frequency": "1d", @@ -78,9 +77,9 @@ def handle_bar(context, bar_dict): "log_level": "error", }, "mod": { - "progress": { + "sys_progress": { "enabled": True, - "priority": 400, + "show": True, }, }, } diff --git a/tests/test_s_golden_cross.py b/tests/test_s_golden_cross.py index f3fdb17a2..6c7f7e4f7 100644 --- a/tests/test_s_golden_cross.py +++ b/tests/test_s_golden_cross.py @@ -38,7 +38,7 @@ def handle_bar(context, bar_dict): __config__ = { "base": { - "strategy_type": "stock", + "securities": "stock", "start_date": "2008-07-01", "end_date": "2017-01-01", "frequency": "1d", @@ -51,12 +51,13 @@ def handle_bar(context, bar_dict): "log_level": "error", }, "mod": { - "progress": { + "sys_progress": { "enabled": True, - "priority": 400, + "show": True, }, - "funcat_api": { + "sys_funcat": { "enabled": True, + "show": True, }, }, } diff --git a/tests/test_s_scheduler.py b/tests/test_s_scheduler.py new file mode 100644 index 000000000..5046749ed --- /dev/null +++ b/tests/test_s_scheduler.py @@ -0,0 +1,35 @@ +from rqalpha.api import * + + +def init(context): + scheduler.run_weekly(rebalance, 1, time_rule=market_open(0, 0)) + + +def rebalance(context, bar_dict): + stock = "000001.XSHE" + if context.portfolio.positions[stock].quantity == 0: + order_target_percent(stock, 1) + else: + order_target_percent(stock, 0) + + +__config__ = { + "base": { + "securities": "stock", + "start_date": "2008-07-01", + "end_date": "2017-01-01", + "frequency": "1d", + "matching_type": "current_bar", + "stock_starting_cash": 100000, + "benchmark": "000001.XSHE", + }, + "extra": { + "log_level": "error", + }, + "mod": { + "sys_progress": { + "enabled": True, + "show": True, + }, + }, +} diff --git a/tests/test_s_turtle.py b/tests/test_s_turtle.py index 67f774556..b8a43c780 100644 --- a/tests/test_s_turtle.py +++ b/tests/test_s_turtle.py @@ -11,15 +11,14 @@ def get_extreme(array_high_price_result, array_low_price_result): return [max_result, min_result] -def get_atr_and_unit( atr_array_result, atr_length_result, portfolio_value_result): - atr = atr_array_result[ atr_length_result-1] +def get_atr_and_unit(atr_array_result, atr_length_result, portfolio_value_result): + atr = atr_array_result[atr_length_result - 1] unit = math.floor(portfolio_value_result * .01 / atr) return [atr, unit] def get_stop_price(first_open_price_result, units_hold_result, atr_result): - stop_price = first_open_price_result - 2 * atr_result \ - + (units_hold_result - 1) * 0.5 * atr_result + stop_price = first_open_price_result - 2 * atr_result + (units_hold_result - 1) * 0.5 * atr_result return stop_price @@ -42,9 +41,9 @@ def init(context): def handle_bar(context, bar_dict): portfolio_value = context.portfolio.portfolio_value - high_price = history_bars(context.s, context.open_observe_time+1, '1d', 'high') - low_price_for_atr = history_bars(context.s, context.open_observe_time+1, '1d', 'low') - low_price_for_extreme = history_bars(context.s, context.close_observe_time+1, '1d', 'low') + high_price = history_bars(context.s, context.open_observe_time + 1, '1d', 'high') + low_price_for_atr = history_bars(context.s, context.open_observe_time + 1, '1d', 'low') + low_price_for_extreme = history_bars(context.s, context.close_observe_time + 1, '1d', 'low') close_price = history_bars(context.s, context.open_observe_time+2, '1d', 'close') close_price_for_atr = close_price[:-1] @@ -113,27 +112,24 @@ def handle_bar(context, bar_dict): context.pre_trading_signal = context.trading_signal + __config__ = { "base": { - "strategy_type": "stock", + "securities": "stock", "start_date": "2008-07-01", "end_date": "2014-09-01", "frequency": "1d", "matching_type": "current_bar", "stock_starting_cash": 1000000, "benchmark": "000300.XSHG", - "slippage": 0.00123, }, "extra": { "log_level": "error", }, "mod": { - "progress": { + "sys_progress": { "enabled": True, - "priority": 400, + "show": True, }, }, - "validator": { - "bar_limit": False, - }, } diff --git a/tests/test_s_turtle_signal.py b/tests/test_s_turtle_signal.py new file mode 100644 index 000000000..6a3657c05 --- /dev/null +++ b/tests/test_s_turtle_signal.py @@ -0,0 +1,138 @@ +import numpy as np +import talib +import math + + +def get_extreme(array_high_price_result, array_low_price_result): + np_array_high_price_result = np.array(array_high_price_result[:-1]) + np_array_low_price_result = np.array(array_low_price_result[:-1]) + max_result = np_array_high_price_result.max() + min_result = np_array_low_price_result.min() + return [max_result, min_result] + + +def get_atr_and_unit(atr_array_result, atr_length_result, portfolio_value_result): + atr = atr_array_result[atr_length_result - 1] + unit = math.floor(portfolio_value_result * .01 / atr) + return [atr, unit] + + +def get_stop_price(first_open_price_result, units_hold_result, atr_result): + stop_price = first_open_price_result - 2 * atr_result + (units_hold_result - 1) * 0.5 * atr_result + return stop_price + + +def init(context): + context.trade_day_num = 0 + context.unit = 0 + context.atr = 0 + context.trading_signal = 'start' + context.pre_trading_signal = '' + context.units_hold_max = 4 + context.units_hold = 0 + context.quantity = 0 + context.max_add = 0 + context.first_open_price = 0 + context.s = '000300.XSHG' + context.open_observe_time = 55 + context.close_observe_time = 20 + context.atr_time = 20 + + +def handle_bar(context, bar_dict): + portfolio_value = context.portfolio.portfolio_value + high_price = history_bars(context.s, context.open_observe_time + 1, '1d', 'high') + low_price_for_atr = history_bars(context.s, context.open_observe_time + 1, '1d', 'low') + low_price_for_extreme = history_bars(context.s, context.close_observe_time + 1, '1d', 'low') + close_price = history_bars(context.s, context.open_observe_time+2, '1d', 'close') + close_price_for_atr = close_price[:-1] + + atr_array = talib.ATR(high_price, low_price_for_atr, close_price_for_atr, timeperiod=context.atr_time) + + maxx = get_extreme(high_price, low_price_for_extreme)[0] + minn = get_extreme(high_price, low_price_for_extreme)[1] + atr = atr_array[-2] + + if context.trading_signal != 'start': + if context.units_hold != 0: + context.max_add += 0.5 * get_atr_and_unit(atr_array, atr_array.size, portfolio_value)[0] + else: + context.max_add = bar_dict[context.s].last + + cur_position = context.portfolio.positions[context.s].quantity + available_cash = context.portfolio.cash + market_value = context.portfolio.market_value + + if (cur_position > 0 and + bar_dict[context.s].last < get_stop_price(context.first_open_price, context.units_hold, atr)): + context.trading_signal = 'stop' + else: + if cur_position > 0 and bar_dict[context.s].last < minn: + context.trading_signal = 'exit' + else: + if (bar_dict[context.s].last > context.max_add and context.units_hold != 0 and + context.units_hold < context.units_hold_max and + available_cash > bar_dict[context.s].last*context.unit): + context.trading_signal = 'entry_add' + else: + if bar_dict[context.s].last > maxx and context.units_hold == 0: + context.max_add = bar_dict[context.s].last + context.trading_signal = 'entry' + + atr = get_atr_and_unit(atr_array, atr_array.size, portfolio_value)[0] + if context.trade_day_num % 5 == 0: + context.unit = get_atr_and_unit(atr_array, atr_array.size, portfolio_value)[1] + context.trade_day_num += 1 + context.quantity = context.unit + + if (context.trading_signal != context.pre_trading_signal or + (context.units_hold < context.units_hold_max and context.units_hold > 1) or + context.trading_signal == 'stop'): + if context.trading_signal == 'entry': + context.quantity = context.unit + if available_cash > bar_dict[context.s].last*context.quantity: + order_shares(context.s, context.quantity) + context.first_open_price = bar_dict[context.s].last + context.units_hold = 1 + + if context.trading_signal == 'entry_add': + context.quantity = context.unit + order_shares(context.s, context.quantity) + context.units_hold += 1 + + if context.trading_signal == 'stop': + if context.units_hold > 0: + order_shares(context.s, -context.quantity) + context.units_hold -= 1 + + if context.trading_signal == 'exit': + if cur_position > 0: + order_shares(context.s, -cur_position) + context.units_hold = 0 + + context.pre_trading_signal = context.trading_signal + + +__config__ = { + "base": { + "securities": "stock", + "start_date": "2008-07-01", + "end_date": "2014-09-01", + "frequency": "1d", + "matching_type": "current_bar", + "stock_starting_cash": 1000000, + "benchmark": "000300.XSHG", + }, + "extra": { + "log_level": "error", + }, + "mod": { + "sys_progress": { + "enabled": True, + "show": True, + }, + "sys_simulation": { + "signal": True, + } + }, +} diff --git a/tests/test_sf_buy_and_hold.py b/tests/test_sf_buy_and_hold.py new file mode 100644 index 000000000..8f19d6ec3 --- /dev/null +++ b/tests/test_sf_buy_and_hold.py @@ -0,0 +1,63 @@ +# 可以自己import我们平台支持的第三方python模块,比如pandas、numpy等。 + + +# 在这个方法中编写任何的初始化逻辑。context对象将会在你的算法策略的任何方法之间做传递。 +def init(context): + # context内引入全局变量counter + context.counter = 0 + subscribe('IH88') + + # 实时打印日志 + logger.info("RunInfo: {}".format(context.run_info)) + + +# before_trading此函数会在每天交易开始前被调用,当天只会被调用一次 +def before_trading(context): + pass + + +# 你选择的证券的数据更新将会触发此段逻辑,例如日或分钟历史数据切片或者是实时数据切片更新 +def handle_bar(context, bar_dict): + # 开始编写你的主要的算法逻辑 + + # bar_dict[order_book_id] 可以拿到某个证券的bar信息 + # context.portfolio 可以拿到现在总的投资组合信息 + # context.stock_portfolio 可以拿到当前股票子组合的信息 + # context.future_portfolio 可以拿到当前期货子组合的信息 + # 使用order_shares(id_or_ins, amount)方法进行落单 + + # TODO: 开始编写你的算法吧! + context.counter += 1 + if context.counter == 1: + # 买入50ETF + order_shares('510050.XSHG', 330000) + # 卖出开仓50股指期货一手 + sell_open('IH88', 1) + + +# after_trading函数会在每天交易结束后被调用,当天只会被调用一次 +def after_trading(context): + pass + + +__config__ = { + "base": { + "securities": ["stock", "future"], + "start_date": "2016-06-01", + "end_date": "2016-10-05", + "frequency": "1d", + "matching_type": "current_bar", + "future_starting_cash": 1000000, + "future_starting_cash": 1000000, + "benchmark": None, + }, + "extra": { + "log_level": "error", + }, + "mod": { + "sys_progress": { + "enabled": True, + "show": True, + }, + }, +}