├── .gitignore ├── LICENSE.txt ├── README.md ├── docs ├── asset.md ├── changelog.md ├── configure │ ├── README.md │ └── config.json ├── faq.md ├── images │ ├── framework.png │ ├── login.png │ ├── rabbitmq_permission.png │ ├── rabbitmq_permission2.png │ ├── struct.png │ ├── userpage.png │ └── wx_qrcode.png ├── market.md ├── others │ ├── locker.md │ ├── logger.md │ ├── rabbitmq_deploy.md │ └── tasks.md ├── requirements.txt └── trade.md ├── example ├── binance │ ├── README.md │ ├── config.json │ └── main.py ├── huobi │ ├── README.md │ ├── config.json │ └── main.py └── okex │ ├── README.md │ ├── config.json │ └── main.py ├── quant ├── __init__.py ├── asset.py ├── config.py ├── const.py ├── error.py ├── event.py ├── heartbeat.py ├── market.py ├── order.py ├── platform │ ├── __init__.py │ ├── binance.py │ ├── huobi.py │ └── okex.py ├── position.py ├── quant.py ├── tasks.py ├── trade.py └── utils │ ├── __init__.py │ ├── decorator.py │ ├── logger.py │ ├── tools.py │ └── web.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.pyc 3 | .DS_Store 4 | build 5 | dist 6 | MANIFEST 7 | test 8 | scripts -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 The Python Packaging Authority (PyPA) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## TheNextQuant 3 | 4 | 异步事件驱动的量化交易/做市系统。 5 | 6 | ![](docs/images/framework.png) 7 | 8 | ![](docs/images/struct.png) 9 | 10 | 11 | ### 框架依赖 12 | 13 | - 运行环境 14 | - python 3.5.3 或以上版本 15 | 16 | - 依赖python三方包 17 | - aiohttp>=3.2.1 18 | - aioamqp>=0.13.0 19 | - motor>=2.0.0 (可选) 20 | 21 | - RabbitMQ服务器 22 | - 事件发布、订阅 23 | 24 | - MongoDB数据库(可选) 25 | - 数据存储 26 | 27 | 28 | ### 安装 29 | 使用 `pip` 可以简单方便安装: 30 | ```text 31 | pip install thenextquant 32 | ``` 33 | 34 | 35 | ### Demo使用示例 36 | 37 | - 推荐创建如下结构的文件及文件夹: 38 | ```text 39 | ProjectName 40 | |----- docs 41 | | |----- README.md 42 | |----- scripts 43 | | |----- run.sh 44 | |----- config.json 45 | |----- src 46 | | |----- main.py 47 | | |----- strategy 48 | | |----- strategy1.py 49 | | |----- strategy2.py 50 | | |----- ... 51 | |----- .gitignore 52 | |----- README.md 53 | ``` 54 | 55 | - 快速体验示例 56 | [Demo](example/demo) 57 | 58 | 59 | - 运行 60 | ```text 61 | python src/main.py config.json 62 | ``` 63 | 64 | 65 | ### 使用文档 66 | 67 | 本框架使用的是Python原生异步库(asyncio)实现异步事件驱动,所以在使用之前,需要先了解 [Python Asyncio](https://docs.python.org/3/library/asyncio.html)。 68 | 69 | - [服务配置](docs/configure/README.md) 70 | - [行情](docs/market.md) 71 | - [交易](docs/trade.md) 72 | - [资产](docs/asset.md) 73 | - 当前支持交易所 74 | - [Binance 现货](example/binance) 75 | - [OKEx 现货](example/okex) 76 | - [Huobi 现货](example/huobi) 77 | 78 | - 其它 79 | - [安装RabbitMQ](docs/others/rabbitmq_deploy.md) 80 | - [日志打印](docs/others/logger.md) 81 | - [定时任务](docs/others/tasks.md) 82 | 83 | - 学习课程 84 | - [数字资产做市策略与实战](https://study.163.com/course/introduction/1209595888.htm?share=2&shareId=480000002173520) 85 | 86 | 87 | ### FAQ 88 | - [FAQ](docs/faq.md) 89 | 90 | 91 | ### 有任何问题,欢迎联系 92 | 93 | - 微信二维码 94 |

95 | 96 |

97 | -------------------------------------------------------------------------------- /docs/asset.md: -------------------------------------------------------------------------------- 1 | ## 资产 2 | 3 | 通过资产模块(asset),可以订阅任意交易平台、任意交易账户的任意资产信息。 4 | 5 | 订阅 `资产事件` 之前,需要先部署 [Asset 资产服务器](https://github.com/TheNextQuant/Asset),将需要订阅的资产信息配置到资产服务器, 6 | 资产服务器定时(默认10秒)将最新的资产信息通过 `资产事件` 的形式推送至 `事件中心` ,我们只需要订阅相关 `资产事件` 即可。 7 | 8 | 9 | ### 1. 资产模块使用 10 | 11 | > 此处以订阅 `Binance(币安)` 交易平台的资产为例,假设我们的账户为 `test@gmail.com`。 12 | ```python 13 | from quant.utils import logger 14 | from quant.const import BINANCE 15 | from quant.asset import Asset, AssetSubscribe 16 | 17 | 18 | # 资产信息回调函数,注意此处回调函数是 `async` 异步函数,回调参数为 `asset Asset` 对象,数据结构请查看下边的介绍。 19 | async def on_event_asset_update(asset: Asset): 20 | logger.info("platform:", asset.platform) # 打印资产数据的平台信息 21 | logger.info("account:", asset.account) # 打印资产数据的账户信息 22 | logger.info("asset data dict:", asset.assets) # 打印资产数据的资产详情 23 | logger.info("asset data str:", asset.data) # 打印资产数据的资产详情 24 | logger.info("timestamp:", asset.timestamp) # 打印资产数据更新时间戳 25 | logger.info("update:", asset.update) # 打印资产数据是否有更新 26 | 27 | 28 | # 订阅资产信息 29 | account = "test@gmail.com" 30 | AssetSubscribe(BINANCE, account, on_event_asset_update) 31 | ``` 32 | 33 | > 以上订阅资产数据的方式是比较通用的做法,但如果你使用了 [Trade 交易模块](./trade.md),那么通过初始化 `Trade` 模块即可订阅相应的资产数据。 34 | 35 | 36 | ### 2. 资产对象数据结构 37 | 38 | - 资产模块 39 | ```python 40 | from quant.asset import Asset 41 | 42 | Asset.platform # 交易平台名称 43 | Asset.account # 交易账户 44 | Asset.assets # 资产详细信息 45 | Asset.timestamp # 资产更新时间戳(毫秒) 46 | Asset.update # 资产是否有更新 47 | Asset.data # 资产信息 48 | ``` 49 | 50 | - 资产详细信息数据结构(assets) 51 | 52 | > 资产数据结果比较简单,一个只有2层的json格式数据结构,`key` 是资产里币种名称大写字母,`value` 是对应币种的数量。 53 | 54 | ```json 55 | { 56 | "BTC": { 57 | "free": "1.10000", 58 | "locked": "2.20000", 59 | "total": "3.30000" 60 | }, 61 | "ETH": { 62 | "free": "1.10000", 63 | "locked": "2.20000", 64 | "total": "3.30000" 65 | } 66 | } 67 | ``` 68 | 69 | - 字段说明 70 | - free `string` 可用资产 71 | - locked `string` 冻结资产 72 | - total `string` 总资产 73 | -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | # Change Logs 2 | 3 | 4 | 5 | ### v0.2.3 6 | 7 | *Date: 2019/11/19* 8 | *Summary:* 9 | - Fix bug: Add AssetEvent to EventCenter. 10 | 11 | 12 | ### v0.2.2 13 | 14 | *Date: 2019/11/09* 15 | *Summary:* 16 | - Add [Digifinex](../example/digifinex) Trade module. 17 | - Add `*args` `**kwargs` params for `create_order` function. 18 | - Add `client_order_id` for `Binance` & `OKEx`. 19 | 20 | 21 | ### v0.2.1 22 | 23 | *Date: 2019/10/09* 24 | *Summary:* 25 | - Add [Binance Future](../example/binance_future) Trade module. 26 | 27 | 28 | ### v0.2.0 29 | 30 | *Date: 2019/10/04* 31 | *Summary:* 32 | - Upgrade OKEx trading module. 33 | - Fix bug: update order remain quantity when status is CANCELED. 34 | - Add volume field in kline data when save to db. 35 | - Add routing_key for EventHeartbeat. 36 | - Change Trade module's locker name to avoid that there was the same locker in one progress. 37 | - Change AMQP login method to PLAIN. 38 | 39 | 40 | ### v0.1.9 41 | 42 | *Date: 2019/08/24* 43 | *Summary:* 44 | - Add [Huobi Future](../example/huobi_future) Trade module. 45 | 46 | 47 | ### v0.1.8 48 | 49 | *Date: 2019/08/20* 50 | *Summary:* 51 | - Upgrade config.PLATFORMS to config.ACCOUNTS. 52 | - validator for string field when field is None will return "". 53 | - logger module: printf stack information in exception. 54 | 55 | 56 | ### v0.1.7 57 | 58 | *Date: 2019/08/02* 59 | *Summary:* 60 | - Add [Kucoin](../example/kucoin) Trade module. 61 | - Add WarningEvent. 62 | 63 | 64 | ### v0.1.6 65 | 66 | *Date: 2019/07/29* 67 | *Summary:* 68 | - Add ConfigEvent to realize run-time-update. 69 | - Add web module, include http server. 70 | - Add validator module. 71 | - Add KeyboardInterrupt caught. 72 | - Enable subscribe multiple events. 73 | - Fix bug: When save kline to mongodb, convert symbol name to collection name. 74 | 75 | 76 | ### v0.1.5 77 | 78 | *Date: 2019/07/22* 79 | *Summary:* 80 | - Add [Coinsuper Premium](../example/coinsuper_pre) Trade module. 81 | - Publish OrderEvent. 82 | - Upgrade Mongodb client. 83 | 84 | 85 | ### v0.1.4 86 | 87 | *Date: 2019/07/16* 88 | *Summary:* 89 | - Add [Gate.io](../example/gate) Trade module. 90 | - Add [Kraken](../example/kraken) Trade module. 91 | - Fix bug for [Coinsuper](../example/coinsuper) : get order infos maybe more than 50 length. 92 | 93 | 94 | ### v0.1.3 95 | 96 | *Date: 2019/07/11* 97 | *Summary:* 98 | - Fix bug: order callback filter by symbol. 99 | 100 | 101 | ### v0.1.2 102 | 103 | *Date: 2019/07/04* 104 | *Summary:* 105 | - fix bug: order & position callback should be use copy.copy() to avoid modified. 106 | - Add [OKEx Swap](../example/okex_swap) Trade module. 107 | 108 | 109 | ### v0.1.1 110 | 111 | *Date: 2019/06/28* 112 | *Summary:* 113 | - Add [Coinsuper](../example/coinsuper) Trade module. 114 | 115 | 116 | ### v0.1.0 117 | 118 | *Date: 2019/06/24* 119 | *Summary:* 120 | - Add [OKEx Margin](../example/okex_margin) Trade module. 121 | - modify EventCenter's Queue name to specific different servers. 122 | 123 | 124 | ### v0.0.9 125 | 126 | *Date: 2019/06/14* 127 | *Summary:* 128 | - Add Asset data callback when create Trade module. 129 | - Add initialize status callback when create Trade module. 130 | 131 | 132 | ### v0.0.8 133 | 134 | *Date: 2019/06/03* 135 | *Summary:* 136 | - Add [Huobi](../example/huobi) module. 137 | 138 | 139 | ### v0.0.7 140 | 141 | *Date: 2019/05/31* 142 | *Summary:* 143 | - Add [Bitmex](https://www.bitmex.com) module. 144 | 145 | 146 | ### v0.0.5 147 | 148 | *Date: 2019/05/30* 149 | *Summary:* 150 | - Add [Binance](../example/binance) module. 151 | - upgrade websocket module. 152 | 153 | 154 | ### v0.0.4 155 | 156 | *Date: 2019/05/29* 157 | *Summary:* 158 | - delete Agent server 159 | - Add [Deribit](../example/deribit) module. 160 | - upgrade market module. 161 | 162 | 163 | ### v0.0.3 164 | 165 | *Date: 2019/03/12* 166 | *Summary:* 167 | - Upgrade Agent Server Protocol to newest version. 168 | - Subscribe Asset. 169 | -------------------------------------------------------------------------------- /docs/configure/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## 配置文件 3 | 4 | 框架启动的时候,需要指定一个 `json` 格式的配置文件。 5 | - [一个完整的配置文件示例](config.json) 6 | 7 | 8 | ## 配置使用 9 | 所有 `config.json` 配置文件里的 `key-value` 格式数据,都可以通过如下方式使用: 10 | ```python 11 | from quant.config import config 12 | 13 | config.name # 使用配置里的name字段 14 | config.abc # 使用配置里的abc字段 15 | ``` 16 | 17 | ## 系统配置参数 18 | > 所有系统配置参数均为 `大写字母` 为key; 19 | > 所有系统配置参数均为 `可选`; 20 | 21 | 22 | ##### 1. LOG 23 | 日志配置。包含如下配置: 24 | 25 | **示例**: 26 | ```json 27 | { 28 | "LOG": { 29 | "console": false, 30 | "level": "DEBUG", 31 | "path": "/var/log/servers/Quant", 32 | "name": "quant.log", 33 | "clear": true, 34 | "backup_count": 5 35 | } 36 | } 37 | ``` 38 | 39 | **配置说明**: 40 | - console `boolean` 是否打印到控制台,`true 打印到控制台` / `false 打印到文件`,可选,默认为 `true` 41 | - level `string` 日志打印级别 `DEBUG`/ `INFO`,可选,默认为 `DEBUG` 42 | - path `string` 日志存储路径,可选,默认为 `/var/log/servers/Quant` 43 | - name `string` 日志文件名,可选,默认为 `quant.log` 44 | - clear `boolean` 初始化的时候,是否清理之前的日志文件,`true 清理` / `false 不清理`,可选,默认为 `false` 45 | - backup_count `int` 保存按天分割的日志文件个数,默认0为永久保存所有日志文件,可选,默认为 `0` 46 | 47 | 48 | ##### 2. HEARTBEAT 49 | 服务心跳配置。 50 | 51 | **示例**: 52 | ```json 53 | { 54 | "HEARTBEAT": { 55 | "interval": 3, 56 | "broadcast": 0 57 | } 58 | } 59 | ``` 60 | 61 | **配置说明**: 62 | - interval `int` 心跳打印时间间隔(秒),0为不打印 `可选,默认为0` 63 | - broadcast `int` 心跳广播时间间隔(秒),0为不广播 `可选,默认为0` 64 | 65 | 66 | ##### 3. PROXY 67 | HTTP代理配置。 68 | 大部分交易所在国内访问都需要翻墙,所以在国内环境需要配置HTTP代理。 69 | 70 | **示例**: 71 | ```json 72 | { 73 | "PROXY": "http://127.0.0.1:1087" 74 | } 75 | ``` 76 | 77 | **配置说明**: 78 | - PROXY `string` http代理,解决翻墙问题 79 | 80 | > 注意: 此配置为全局配置,将作用到任何HTTP请求,包括Websocket; 81 | 82 | 83 | ##### 4. RABBITMQ 84 | RabbitMQ服务配置。 85 | 86 | **示例**: 87 | ```json 88 | { 89 | "RABBITMQ": { 90 | "host": "127.0.0.1", 91 | "port": 5672, 92 | "username": "test", 93 | "password": "123456" 94 | } 95 | } 96 | ``` 97 | 98 | **配置说明**: 99 | - host `string` ip地址 100 | - port `int` 端口 101 | - username `string` 用户名 102 | - password `string` 密码 103 | -------------------------------------------------------------------------------- /docs/configure/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "HEARTBEAT": { 3 | "interval": 3 4 | }, 5 | "LOG": { 6 | "console": false, 7 | "level": "DEBUG", 8 | "path": "/var/log/servers/Quant", 9 | "name": "quant.log", 10 | "clear": true, 11 | "backup_count": 5 12 | }, 13 | "RABBITMQ": { 14 | "host": "127.0.0.1", 15 | "port": 5672, 16 | "username": "test", 17 | "password": "123456" 18 | }, 19 | "PROXY": "http://127.0.0.1:1087", 20 | 21 | "name": "my test name", 22 | "abc": 123456 23 | } 24 | -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | ## FAQ 2 | 3 | 4 | ##### 1. 为什么使用Python ? 5 | 6 | - Python语言简单优雅,大部分有编程基础的人都能快速上手使用; 7 | - 在币圈行情变化日新月异的情况下,策略也会频繁更变,Python语言开发效率相对较高,比较适合快速开发部署新策略; 8 | - 运行速度跟不上?除了计算密集型任务之外(需要消耗大量CPU计算资源),Python能解决绝大部分问题;可以通过技术手段让Python运行效率非常高效; 9 | - Python社区非常活跃,遇到问题的时候寻求帮助比较容易; 10 | 11 | 12 | ##### 2. 为什么是Asyncio ? 13 | 14 | [Python Asyncio](https://docs.python.org/3/library/asyncio.html) 是Python3.x之后原生支持的异步库,底层使用操作系统内核的aio 15 | 异步函数,是真正意义上的异步事件驱动IO循环编程模型,能够最大效率完成系统IO调用、最大效率使用CPU资源。 16 | 17 | 区别于使用多线程或多进程实现的伪异步,对于系统资源的利用将大大提高,并且对于资源竞争、互斥等问题,可以更加优雅的解决。 18 | 19 | 20 | ##### 3. 为什么使用RabbitMQ ? 21 | 22 | [RabbitMQ](https://www.rabbitmq.com/) 是一个消息代理服务器,可以作为消息队列使用。 23 | 24 | 我们在框架底层封装了RabbitMQ作为 `事件中心`,负责各种业务事件的发布和订阅,这些业务事件包括订单、持仓、资产、行情、配置、监控等等。 25 | 26 | 通过 `事件中心`,我们可以很容易实现业务拆分,并实现分布式部署管理,比如: 27 | 28 | - 行情系统,负责任意交易所的任意交易对的行情收集并发布行情事件到事件中心; 29 | - 资产系统,负责任意交易所的任意账户资产收集并发布资产事件到事件中心; 30 | - 策略系统,负责所有策略实现(量化、做市),从事件中心订阅行情事件、资产事件等,并发布订单事件、持仓事件等等; 31 | - 风控系统,负责订阅任意感兴趣事件,比如订阅行情事件监控行情、订阅资产事件监控资产、订阅持仓监控当前持仓等等; 32 | - ... 33 | 34 | 35 | ##### 4. 我们与 [vnpy](https://github.com/vnpy/vnpy) 有什么区别 ? 36 | 37 | vnpy底层是通过多线程实现异步,并不是真正意义的异步,我们的主要区别有: 38 | - 基于 [Python Asyncio](https://docs.python.org/3/library/asyncio.html) 原生异步事件循环,处理更简洁,效率更高; 39 | - 任意交易所的交易方式(现货、合约)统一,相同策略只需要区别不同配置,即可无缝切换任意交易所; 40 | - 任意交易所的行情统一,并通过事件订阅的形式,回调触发策略执行不同指令; 41 | - 支持任意多个策略协同运行; 42 | - 支持任意多个策略分布式运行; 43 | - 所有延迟都是毫秒级(10毫秒内,一般瓶颈在网络延迟); 44 | - 提供任务、监控、存储、事件发布等一系列高级功能; 45 | 46 | 47 | ##### 4. 运行程序报SSL的错 48 | ```text 49 | SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1056)') 50 | ``` 51 | 52 | - 解决方法 53 | ```text 54 | aiohttp在python3.7里可能有兼容性问题,需要做一下简单的处理。 55 | 56 | MAC电脑执行以下两条命令: 57 | cd /Applications/Python\ 3.7/ 58 | ./Install\ Certificates.command 59 | ``` 60 | -------------------------------------------------------------------------------- /docs/images/framework.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpim/thenextquant/beb65b1c0590174c4cd90a2ac2a60194b73e1934/docs/images/framework.png -------------------------------------------------------------------------------- /docs/images/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpim/thenextquant/beb65b1c0590174c4cd90a2ac2a60194b73e1934/docs/images/login.png -------------------------------------------------------------------------------- /docs/images/rabbitmq_permission.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpim/thenextquant/beb65b1c0590174c4cd90a2ac2a60194b73e1934/docs/images/rabbitmq_permission.png -------------------------------------------------------------------------------- /docs/images/rabbitmq_permission2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpim/thenextquant/beb65b1c0590174c4cd90a2ac2a60194b73e1934/docs/images/rabbitmq_permission2.png -------------------------------------------------------------------------------- /docs/images/struct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpim/thenextquant/beb65b1c0590174c4cd90a2ac2a60194b73e1934/docs/images/struct.png -------------------------------------------------------------------------------- /docs/images/userpage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpim/thenextquant/beb65b1c0590174c4cd90a2ac2a60194b73e1934/docs/images/userpage.png -------------------------------------------------------------------------------- /docs/images/wx_qrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpim/thenextquant/beb65b1c0590174c4cd90a2ac2a60194b73e1934/docs/images/wx_qrcode.png -------------------------------------------------------------------------------- /docs/market.md: -------------------------------------------------------------------------------- 1 | ## 行情 2 | 3 | 通过行情模块(market),可以订阅任意交易所的任意交易对的实时行情,包括订单薄(Orderbook)、K线(KLine)、成交(Trade), 4 | 根据不同交易所提供的行情信息,实时将行情信息推送给策略; 5 | 6 | 在订阅行情之前,需要先部署 [Market 行情服务器](https://github.com/TheNextQuant/Market),行情服务器将通过 REST API 或 Websocket 的方式从交易所获取实时行情信息, 7 | 并将行情信息按照统一的数据格式打包,通过事件的形式发布至事件中心; 8 | 9 | 10 | ### 1. 行情模块使用 11 | 12 | > 此处以订单薄使用为例,订阅Binance的 `ETH/BTC` 交易对订单薄数据 13 | ```python 14 | # 导入模块 15 | from quant import const 16 | from quant.utils import logger 17 | from quant.market import Market, Orderbook 18 | 19 | 20 | # 订阅订单薄行情,注意此处注册的回调函数是 `async` 异步函数,回调参数为 `orderbook` 对象,数据结构查看下边的介绍。 21 | async def on_event_orderbook_update(orderbook: Orderbook): 22 | logger.info("orderbook:", orderbook) 23 | logger.info("platform:", orderbook.platform) # 打印行情平台 24 | logger.info("symbol:", orderbook.symbol) # 打印行情交易对 25 | logger.info("asks:", orderbook.asks) # 打印卖盘数据 26 | logger.info("bids:", orderbook.bids) # 打印买盘数据 27 | logger.info("timestamp:", orderbook.timestamp) # 打印行情更新时间戳(毫秒) 28 | 29 | 30 | Market(const.MARKET_TYPE_ORDERBOOK, const.BINANCE, "ETH/BTC", on_event_orderbook_update) 31 | ``` 32 | 33 | > 使用同样的方式,可以订阅任意的行情 34 | ```python 35 | from quant import const 36 | 37 | const.MARKET_TYPE_ORDERBOOK # 订单薄(Orderbook) 38 | const.MARKET_TYPE_KLINE # 1分钟K线(KLine) 39 | const.MARKET_TYPE_KLINE_5M # 5分钟K线(KLine) 40 | const.MARKET_TYPE_KLINE_15M # 15分钟K线(KLine) 41 | const.MARKET_TYPE_TRADE # 成交(KLine) 42 | ``` 43 | 44 | 45 | ### 2. 行情对象数据结构 46 | 47 | 所有交易平台的行情,全部使用统一的数据结构; 48 | 49 | #### 2.1 订单薄(Orderbook) 50 | 51 | - 订单薄模块 52 | ```python 53 | from quant.market import Orderbook 54 | 55 | Orderbook.platform # 订单薄平台 56 | Orderbook.symbol # 订单薄交易对 57 | Orderbook.asks # 订单薄买盘数据 58 | Orderbook.bids # 订单薄买盘数据 59 | Orderbook.timestamp # 订单薄更新时间戳(毫秒) 60 | Orderbook.data # 订单薄数据 61 | ``` 62 | 63 | - 订单薄数据结构 64 | ```json 65 | { 66 | "platform": "binance", 67 | "symbol": "ETH/USDT", 68 | "asks": [ 69 | ["8680.70000000", "0.00200000"] 70 | ], 71 | "bids": [ 72 | ["8680.60000000", "2.82696138"] 73 | ], 74 | "timestamp": 1558949307370 75 | } 76 | ``` 77 | 78 | - 字段说明 79 | - platform `string` 交易平台 80 | - symbol `string` 交易对 81 | - asks `list` 卖盘,一般默认前10档数据,一般 `price 价格` 和 `quantity 数量` 的精度为小数点后8位 `[[price, quantity], ...]` 82 | - bids `list` 买盘,一般默认前10档数据,一般 `price 价格` 和 `quantity 数量` 的精度为小数点后8位 `[[price, quantity], ...]` 83 | - timestamp `int` 时间戳(毫秒) 84 | 85 | 86 | #### 2.2 K线(KLine) 87 | 88 | - K线模块 89 | ```python 90 | from quant.market import Kline 91 | 92 | Kline.platform # 交易平台 93 | Kline.symbol # 交易对 94 | Kline.open # 开盘价 95 | Kline.high # 最高价 96 | Kline.low # 最低价 97 | Kline.close # 收盘价 98 | Kline.volume # 成交量 99 | Kline.timestamp # 时间戳(毫秒) 100 | Kline.data # K线数据 101 | ``` 102 | 103 | - K线数据结构 104 | ```json 105 | { 106 | "platform": "okex", 107 | "symbol": "BTC/USDT", 108 | "open": "8665.50000000", 109 | "high": "8668.40000000", 110 | "low": "8660.00000000", 111 | "close": "8660.00000000", 112 | "volume": "73.14728136", 113 | "timestamp": 1558946340000 114 | } 115 | ``` 116 | 117 | - 字段说明 118 | - platform `string` 交易平台 119 | - symbol `string` 交易对 120 | - open `string` 开盘价,一般精度为小数点后8位 121 | - high `string` 最高价,一般精度为小数点后8位 122 | - low `string` 最低价,一般精度为小数点后8位 123 | - close `string` 收盘价,一般精度为小数点后8位 124 | - volume `string` 成交量,一般精度为小数点后8位 125 | - timestamp `int` 时间戳(毫秒) 126 | 127 | 128 | #### 2.3 成交(Trade) 129 | 130 | - 成交模块 131 | ```python 132 | from quant.market import Trade 133 | 134 | Trade.platform # 交易平台 135 | Trade.symbol # 交易对 136 | Trade.action # 操作类型 BUY 买入 / SELL 卖出 137 | Trade.price # 价格 138 | Trade.quantity # 数量 139 | Trade.timestamp # 时间戳(毫秒) 140 | ``` 141 | 142 | - 成交数据结构 143 | ```json 144 | { 145 | "platform": "okex", 146 | "symbol": "BTC/USDT", 147 | "action": "SELL", 148 | "price": "8686.40000000", 149 | "quantity": "0.00200000", 150 | "timestamp": 1558949571111, 151 | } 152 | ``` 153 | 154 | - 字段说明 155 | - platform `string` 交易平台 156 | - symbol `string` 交易对 157 | - action `string` 操作类型 BUY 买入 / SELL 卖出 158 | - price `string` 价格,一般精度为小数点后8位 159 | - quantity `string` 数量,一般精度为小数点后8位 160 | - timestamp `int` 时间戳(毫秒) 161 | -------------------------------------------------------------------------------- /docs/others/locker.md: -------------------------------------------------------------------------------- 1 | 2 | ## 进程锁 & 线程锁 3 | 4 | 当业务复杂到使用多进程或多线程的时候,并发提高的同时,对内存共享也需要使用锁来解决资源争夺问题。 5 | 6 | 7 | ##### 1. 线程(协程)锁 8 | 9 | > 使用 10 | 11 | ```python 12 | from quant.utils.decorator import async_method_locker 13 | 14 | @async_method_locker("unique_locker_name") 15 | async def func_foo(): 16 | pass 17 | ``` 18 | 19 | - 函数定义 20 | ```python 21 | def async_method_locker(name, wait=True): 22 | """ 异步方法加锁,用于多个协程执行同一个单列的函数时候,避免共享内存相互修改 23 | @param name 锁名称 24 | @param wait 如果被锁是否等待,True等待执行完成再返回,False不等待直接返回 25 | * NOTE: 此装饰器需要加到async异步方法上 26 | """ 27 | ``` 28 | 29 | > 说明 30 | - `async_method_locker` 为装饰器,需要装饰到 `async` 异步函数上; 31 | - 装饰器需要传入一个参数 `name`,作为此函数的锁名; 32 | - 参数 `wait` 可选,如果被锁是否等待,True等待执行完成再返回,False不等待直接返回 33 | -------------------------------------------------------------------------------- /docs/others/logger.md: -------------------------------------------------------------------------------- 1 | 2 | ## 日志打印 3 | 4 | 日志可以分多个级别,打印到控制台或者文件,文件可以按天分割存储。 5 | 6 | 7 | ##### 1. 日志配置 8 | ```json 9 | { 10 | "LOG": { 11 | "console": true, 12 | "level": "DEBUG", 13 | "path": "/var/log/servers/Quant", 14 | "name": "quant.log", 15 | "clear": true, 16 | "backup_count": 5 17 | } 18 | } 19 | ``` 20 | **参数说明**: 21 | - console `boolean` 是否打印到控制台,`true 打印到控制台` / `false 打印到文件`,可选,默认为 `true` 22 | - level `string` 日志打印级别 `DEBUG`/ `INFO`,可选,默认为 `DEBUG` 23 | - path `string` 日志存储路径,可选,默认为 `/var/log/servers/Quant` 24 | - name `string` 日志文件名,可选,默认为 `quant.log` 25 | - clear `boolean` 初始化的时候,是否清理之前的日志文件,`true 清理` / `false 不清理`,可选,默认为 `false` 26 | - backup_count `int` 保存按天分割的日志文件个数,默认0为永久保存所有日志文件,可选,默认为 `0` 27 | 28 | > 配置文件可参考 [服务配置模块](../configure/README.md); 29 | 30 | 31 | ##### 2. 导入日志模块 32 | 33 | ```python 34 | from quant.utils import logger 35 | 36 | logger.debug("a:", 1, "b:", 2) 37 | logger.info("start strategy success!", caller=self) # 假设在某个类函数下调用,可以打印类名和函数名 38 | logger.warn("something may notice to me ...") 39 | logger.error("ERROR: server down!") 40 | logger.exception("something wrong!") 41 | ``` 42 | 43 | 44 | ##### 3. INFO日志 45 | ```python 46 | def info(*args, **kwargs): 47 | ``` 48 | 49 | ##### 4. WARNING日志 50 | ```python 51 | def warn(*args, **kwargs): 52 | ``` 53 | 54 | ##### 4. DEBUG日志 55 | ```python 56 | def debug(*args, **kwargs): 57 | ``` 58 | 59 | ##### 5. ERROR日志 60 | ````python 61 | def error(*args, **kwargs): 62 | ```` 63 | 64 | ##### 6. EXCEPTION日志 65 | ```python 66 | def exception(*args, **kwargs): 67 | ``` 68 | 69 | 70 | > 注意: 71 | - 所有函数的 `args` 和 `kwargs` 可以传入任意值,将会按照python的输出格式打印; 72 | - 在 `kwargs` 中指定 `caller=self` 或 `caller=cls`,可以在日志中打印出类名及函数名信息; 73 | -------------------------------------------------------------------------------- /docs/others/rabbitmq_deploy.md: -------------------------------------------------------------------------------- 1 | 2 | ## RabbitMQ服务器部署 3 | 4 | [RabbitMQ](https://www.rabbitmq.com/) 是一个消息代理服务器,可以作为消息队列使用。本文主要介绍 RabbitMQ 的安装部署以及账户分配。 5 | 6 | 7 | ### 1. 安装 8 | 9 | ##### 1.1 通过官网提供的安装文档来安装 10 | 11 | RabbitMQ的官网提供了非常详细的 [安装文档](https://www.rabbitmq.com/download.html),主流使用的操作系统都有对应安装文档说明,这里就不做过多说明了。 12 | 13 | > 注意: 14 | - 需要安装 [management](https://www.rabbitmq.com/management.html) 管理工具; 15 | 16 | 17 | ##### 1.2 通过docker安装 18 | 19 | 如果安装了 [docker server](https://www.docker.com/),那么通过docker安装是比较方便的,只需要一行代码即可启动一个RabbitMQ实例: 20 | 21 | ```bash 22 | docker run -d --restart always --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3-management 23 | ``` 24 | 25 | > 说明: 26 | - 这条命令启动了一个名叫 `rabbitmq` 的docker容器,容器内部运行了一个RabbitMQ的实例,并且和宿主机绑定了 `5672` 和 `15672` 端口; 27 | - 端口 `5672` 为RabbitMQ服务器监听的 TCP 端口; 28 | - 端口 `15672` 为RabbitMQ管理工具监听的 HTTP 端口; 29 | 30 | 31 | ### 2 设置账户密码 32 | 33 | RabbitMQ可以通过命令行来操作,但为了展示一个更加直观的结果,这里我们使用管理工具来创建账户。 34 | 35 | ##### 2.1 登陆管理工具 36 | 37 | 假设我们安装运行RabbitMQ服务器的机器ip地址为 `11.22.33.44`,那么我们可以通过 `http://11.22.33.44:15672` 打开web管理页面。 38 | 管理页面默认的登录账户和密码为 `guest` `guest`,如下图所示: 39 | ![](../images/login.png) 40 | 41 | 登录成功之后,进入 `Admin` 标签页,新增、管理登录账户和密码: 42 | ![](../images/userpage.png) 43 | 44 | 请注意,新增的账户需要设置对应的访问权限,根据需要设置权限即可,一般如果测试使用直接给根目录 `/` 的访问权限: 45 | ![](../images/rabbitmq_permission.png) 46 | 47 | ![](../images/rabbitmq_permission2.png) 48 | 49 | 恭喜你,我们的RabbitMQ服务器已经可以投入使用了! 50 | -------------------------------------------------------------------------------- /docs/others/tasks.md: -------------------------------------------------------------------------------- 1 | 2 | ## 定时任务 & 协程任务 3 | 4 | 5 | ##### 1. 注册定时任务 6 | 定时任务模块可以注册任意多个回调函数,利用服务器每秒执行一次心跳的过程,创建新的协程,在协程里执行回调函数。 7 | 8 | ```python 9 | # 导入模块 10 | from quant.tasks import LoopRunTask 11 | 12 | # 定义回调函数 13 | async def function_callback(*args, **kwargs): 14 | pass 15 | 16 | # 回调间隔时间(秒) 17 | callback_interval = 5 18 | 19 | # 注册回调函数 20 | task_id = LoopRunTask.register(function_callback, callback_interval) 21 | 22 | # 取消回调函数 23 | LoopRunTask.unregister(task_id) # 假设此定时任务已经不需要,那么取消此任务回调 24 | ``` 25 | 26 | > 注意: 27 | - 回调函数 `function_callback` 必须是 `async` 异步的,且入参必须包含 `*args` 和 `**kwargs`; 28 | - 回调时间间隔 `callback_interval` 为秒,默认为1秒; 29 | - 回调函数将会在心跳执行的时候被执行,因此可以对心跳次数 `heartbeat.count` 取余,来确定是否该执行当前任务; 30 | 31 | 32 | ##### 2. 协程任务 33 | 协程可以并发执行,提高程序运行效率。 34 | 35 | ```python 36 | # 导入模块 37 | from quant.tasks import SingleTask 38 | 39 | # 定义回调函数 40 | async def function_callback(*args, **kwargs): 41 | pass 42 | 43 | # 执行协程任务 44 | SingleTask.run(function_callback, *args, **kwargs) 45 | ``` 46 | 47 | > 注意: 48 | - 回调函数 `function_callback` 必须是 `async` 异步的; 49 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | aioamqp==0.14.0 2 | aiohttp==3.6.2 3 | motor==2.0.0 -------------------------------------------------------------------------------- /docs/trade.md: -------------------------------------------------------------------------------- 1 | ## 交易 2 | 3 | 通过交易模块(trade),可以在任意交易平台发起交易,包括下单(create_order)、撤单(revoke_order)、查询订单状态(order status)、 4 | 查询未完全成交订单(get_open_order_nos)等功能; 5 | 6 | 策略完成下单之后,底层框架将定时或实时将最新的订单状态更新通过策略注册的回调函数传递给策略,策略能够在第一时间感知到订单状态更新数据; 7 | 8 | 9 | ### 1. 交易模块使用 10 | 11 | #### 1.1 简单使用示例 12 | 13 | > 此处以在 `Binance` 交易所上的 `ETH/BTC` 交易对创建一个买单为例: 14 | 15 | ```python 16 | # 导入模块 17 | from quant import const 18 | from quant import order 19 | from quant.trade import Trade 20 | from quant.utils import logger 21 | from quant.order import Order 22 | 23 | # 初始化 24 | platform = const.BINANCE # 交易平台 假设是binance 25 | account = "abc@gmail.com" # 交易账户 26 | access_key = "ABC123" # ACCESS KEY 27 | secret_key = "abc123" # SECRET KEY 28 | symbol = "ETH/BTC" # 交易对 29 | strategy_name = "my_test_strategy" # 自定义的策略名称 30 | 31 | # 注册订单更新回调函数,注意此处注册的回调函数是 `async` 异步函数,回调参数为 `order` 对象,数据结构请查看下边的介绍。 32 | async def on_event_order_update(order: Order): 33 | logger.info("order:", order) 34 | 35 | # 创建trade对象 36 | trader = Trade(strategy_name, platform, symbol, account=account, access_key=access_key, secret_key=secret_key, 37 | order_update_callback=on_event_order_update) 38 | 39 | # 下单 40 | action = order.ORDER_ACTION_BUY # 买单 41 | price = "11.11" # 委托价格 42 | quantity = "22.22" # 委托数量 43 | order_type = order.ORDER_TYPE_LIMIT # 限价单 44 | order_no, error = await trader.create_order(action, price, quantity, order_type) # 注意,此函数需要在 `async` 异步函数里执行 45 | 46 | 47 | # 撤单 48 | order_no, error = await trader.revoke_order(order_no) # 注意,此函数需要在 `async` 异步函数里执行 49 | 50 | 51 | # 查询所有未成交订单id列表 52 | order_nos, error = await trader.get_open_order_nos() # 注意,此函数需要在 `async` 异步函数里执行 53 | 54 | 55 | # 查询当前所有未成交订单数据 56 | orders = trader.orders # orders是一个dict,key为order_no,value为order对象 57 | order = trader.orders.get(order_no) # 提取订单号为 order_no 的订单对象 58 | ``` 59 | 60 | #### 1.2 Trade交易模块初始化 61 | 62 | 初始化Trade交易模块需要传入一些必要的参数来指定需要交易的必要信息,比如:交易策略名称、交易平台、交易对、交易账户、账户的AK等等。 63 | 64 | ```python 65 | from quant.const import BINANCE 66 | from quant.trade import Trade 67 | 68 | platform = BINANCE # 交易平台 假设是binance 69 | account = "abc@gmail.com" # 交易账户 70 | access_key = "ABC123" # ACCESS KEY 71 | secret_key = "abc123" # SECRET KEY 72 | symbol = "ETH/BTC" # 交易对 73 | strategy_name = "my_test_strategy" # 自定义的策略名称 74 | 75 | ``` 76 | 77 | - 简单初始化方式 78 | ```python 79 | trader = Trade(strategy_name, platform, symbol, account=account, access_key=access_key, secret_key=secret_key) 80 | ``` 81 | 82 | - 如果需要实时获取到账户的资产变化情况,那么可以在初始化的时候指定资产更新回调函数 83 | ```python 84 | 85 | # 当资产有任何变化,将通过此函数回调变化信息,asset为资产对象(注意: 因为资产可能变化频繁,所以一般10秒钟推送一次更新回调) 86 | async def on_event_asset_update(asset): 87 | print("asset update:", asset) 88 | 89 | trader = Trade(strategy_name, platform, symbol, account=account, access_key=access_key, secret_key=secret_key, 90 | asset_update_callback=on_event_asset_update) 91 | ``` 92 | > 注意: 订阅资产更新数据,需要先部署 [Asset资产服务](https://github.com/TheNextQuant/Asset) 。 93 | 94 | - 如果需要实时获取到已提交委托单的实时变化情况,那么可以在初始化的时候指定订单更新回调函数 95 | ```python 96 | from quant.order import Order # 导入订单模块 97 | 98 | # 当委托单有任何变化,将通过此函数回调变化信息,order为订单对象,下文里将对 `Order` 有专题说明 99 | async def on_event_order_update(order: Order): 100 | print("order update:", order) 101 | 102 | trader = Trade(strategy_name, platform, symbol, account=account, access_key=access_key, secret_key=secret_key, 103 | asset_update_callback=on_event_asset_update, order_update_callback=on_event_order_update) 104 | ``` 105 | 106 | - 如果是期货,需要实时获取到当前持仓变化情况,那么可以在初始化的时候指定持仓的更新回调函数 107 | ```python 108 | from quant.position import Position # 导入持仓模块 109 | 110 | # 当持仓有任何变化,将通过此函数回调变化信息,position为持仓对象,下文里将对 `Position` 有专题说明 111 | async def on_event_position_update(position: Position): 112 | print("position update:", position) 113 | 114 | trader = Trade(strategy_name, platform, symbol, account=account, access_key=access_key, secret_key=secret_key, 115 | asset_update_callback=on_event_asset_update, order_update_callback=on_event_order_update, 116 | position_update_callback=on_event_position_update) 117 | ``` 118 | 119 | - 如果希望判断交易模块的初始化状态,比如网络连接是否正常、订阅订单/持仓数据是否正常等等,那么可以在初始化的时候指定初始化成功状态更新回调函数 120 | ```python 121 | from quant.error import Error # 引入错误模块 122 | 123 | # 初始化Trade模块成功或者失败,都将回调此函数 124 | # 如果成功,success将是True,error将是None 125 | # 如果失败,success将是False,error将携带失败信息 126 | async def on_event_init_success_callback(success: bool, error: Error, **kwargs): 127 | print("initialize trade module status:", success, "error:", error) 128 | 129 | trader = Trade(strategy_name, platform, symbol, account=account, access_key=access_key, secret_key=secret_key, 130 | asset_update_callback=on_event_asset_update, order_update_callback=on_event_order_update, 131 | position_update_callback=on_event_position_update, init_success_callback=on_event_init_success_callback) 132 | ``` 133 | 134 | #### 1.3 创建委托单 135 | `Trade.create_order` 可以创建任意的委托单,包括现货和期货,并且入参只有4个! 136 | 137 | ```python 138 | async def create_order(self, action, price, quantity, order_type=ORDER_TYPE_LIMIT): 139 | """ 创建委托单 140 | @param action 交易方向 BUY/SELL 141 | @param price 委托价格 142 | @param quantity 委托数量(当为负数时,代表合约操作空单) 143 | @param order_type 委托类型 LIMIT/MARKET 144 | @return (order_no, error) 如果成功,order_no为委托单号,error为None,否则order_no为None,error为失败信息 145 | """ 146 | ``` 147 | > 注意: 148 | - 入参 `action` 可以引入使用 `from quant.order import ORDER_ACTION_BUY, ORDER_ACTION_SELL` 149 | - 入参 `price` 最好是字符串格式,因为这样能保持原始精度,否则在数据传输过程中可能损失精度 150 | - 入参 `quantity` 最好是字符串格式,理由和 `price` 一样;另外,当为合约委托单的时候,`quantity` 有正负之分,正代表多仓,负代表空仓 151 | - 入参 `order_type` 为订单类型,当前只有 `LIMIT 限价单` 和 `MARKET 市价单` 之分,默认为 `LIMIT 限价单`,建议慎用 `MARKET 市价单` (如果真的需要,可以用代码使用限价单模拟市价单效果,而且比市价单更加安全) 152 | - 返回 `(order_no, error)` 如果成功,`order_no` 为创建的委托单号,`error` 为None;如果失败,`order_no` 为None,`error` 为 `Error` 对象携带的错误信息 153 | 154 | #### 1.4 撤销委托单 155 | `Trade.revoke_order` 可以撤销任意多个委托单。 156 | 157 | ```python 158 | async def revoke_order(self, *order_nos): 159 | """ 撤销委托单 160 | @param order_nos 订单号列表,可传入任意多个,如果不传入,那么就撤销所有订单 161 | @return (success, error) success为撤单成功列表,error为撤单失败的列表 162 | """ 163 | ``` 164 | > 注意: 165 | - 入参 `order_nos` 是一个可变参数,可以为空,或者任意多个参数 166 | - 如果 `order_nos` 为空,即 `trader.revoke_order()` 这样调用,那么代表撤销此交易对下的所有委托单; 167 | - 如果 `order_nos` 为一个参数,即 `trader.revoke_order(order_no)` 这样调用(其中order_no为委托单号),那么代表只撤销order_no的委托单; 168 | - 如果 `order_nos` 为多个参数,即 `trader.revoke_order(order_no1, order_no2, order_no3)` 这样调用(其中order_no1, order_no2, order_no3为委托单号),那么代表撤销order_no1, order_no2, order_no3的委托单; 169 | - 返回 `(success, error)`,如果成功,那么 `success` 为成功信息,`error` 为None;如果失败,那么 `success` 为None,`error` 为 `Error` 对象,携带的错误信息; 170 | 171 | #### 1.5 获取未完成委托单id列表 172 | `Trade.get_open_order_nos` 可以获取当前所有未完全成交的委托单号,包括 `已提交但未成交`、`部分成交` 的所有委托单号。 173 | 174 | ```python 175 | async def get_open_order_nos(self): 176 | """ 获取未完成委托单id列表 177 | @return (result, error) result为成功获取的未成交订单列表,error如果成功为None,如果不成功为错误信息 178 | """ 179 | ``` 180 | > 注意: 181 | - 返回 `(result, error)` 如果成功,那么 `result` 为委托单号列表,`error` 为None;如果失败,`result` 为None,`error` 为 `Error` 对象,携带的错误信息; 182 | 183 | #### 1.6 获取当前所有订单对象 184 | 185 | `Trade.orders` 可以提取当前 `Trade` 模块里所有的委托单信息,`dict` 格式,`key` 为委托单id,`value` 为 `Order` 委托单对象。 186 | 187 | #### 1.7 获取当前的持仓对象 188 | 189 | `Trade.position` 可以提取当前 `Trade` 模块里的持仓信息,即 `Position` 对象,但仅限合约使用。 190 | 191 | 192 | ### 2. 资产模块 193 | 194 | 所有资产相关的数据常量和对象在框架的 `quant.asset` 模块下,`Trade` 模块在推送资产信息回调的时候,携带的 `asset` 参数即此模块。 195 | 196 | #### 2.1 资产对象 197 | 198 | ```python 199 | from quant.asset import Asset 200 | 201 | asset = Asset(...) 202 | asset.platform # 平台 203 | asset.account # 账户 204 | asset.assets # 资产数据 205 | asset.timestamp # 资产更新时间戳(毫秒) 206 | asset.update # 相对上一次推送,是否有更新,True为有更新,False为没更新 207 | ``` 208 | 209 | 210 | ### 3. 订单模块 211 | 212 | 所有订单相关的数据常量和对象在框架的 `quant.order` 模块下,`Trade` 模块在推送订单信息回调的时候,携带的 `order` 参数即此模块。 213 | 214 | #### 3.1 订单类型 215 | ```python 216 | from quant import order 217 | 218 | order.ORDER_TYPE_LIMIT # 限价单 219 | order.ORDER_TYPE_MARKET # 市价单 220 | ``` 221 | 222 | #### 3.2 订单操作 223 | ```python 224 | from quant import order 225 | 226 | order.ORDER_ACTION_BUY # 买入 227 | order.ORDER_ACTION_SELL # 卖出 228 | ``` 229 | 230 | #### 3.3 订单状态 231 | ```python 232 | from quant import order 233 | 234 | order.ORDER_STATUS_NONE = "NONE" # 新创建的订单,无状态 235 | order.ORDER_STATUS_SUBMITTED = "SUBMITTED" # 已提交 236 | order.ORDER_STATUS_PARTIAL_FILLED = "PARTIAL-FILLED" # 部分成交 237 | order.ORDER_STATUS_FILLED = "FILLED" # 完全成交 238 | order.ORDER_STATUS_CANCELED = "CANCELED" # 取消 239 | order.ORDER_STATUS_FAILED = "FAILED" # 失败 240 | ``` 241 | 242 | #### 3.4 合约订单类型 243 | ```python 244 | from quant import order 245 | 246 | order.TRADE_TYPE_NONE = 0 # 未知订单类型,比如订单不是由 thenextquant 框架创建,且某些平台的订单不能判断订单类型 247 | order.TRADE_TYPE_BUY_OPEN = 1 # 买入开多 action=BUY, quantity>0 248 | order.TRADE_TYPE_SELL_OPEN = 2 # 卖出开空 action=SELL, quantity<0 249 | order.TRADE_TYPE_SELL_CLOSE = 3 # 卖出平多 action=SELL, quantity>0 250 | order.TRADE_TYPE_BUY_CLOSE = 4 # 买入平空 action=BUY, quantity<0 251 | ``` 252 | > 注意: 仅限合约订单使用。 253 | 254 | #### 3.5 订单对象 255 | ```python 256 | from quant import order 257 | 258 | o = order.Order(...) # 初始化订单对象 259 | 260 | o.platform # 交易平台 261 | o.account # 交易账户 262 | o.strategy # 策略名称 263 | o.order_no # 委托单号 264 | o.action # 买卖类型 SELL-卖,BUY-买 265 | o.order_type # 委托单类型 MKT-市价,LMT-限价 266 | o.symbol # 交易对 如: ETH/BTC 267 | o.price # 委托价格 268 | o.quantity # 委托数量(限价单) 269 | o.remain # 剩余未成交数量 270 | o.status # 委托单状态 271 | o.timestamp # 创建订单时间戳(毫秒) 272 | o.avg_price # 成交均价 273 | o.trade_type # 合约订单类型 开多/开空/平多/平空 274 | o.ctime # 创建订单时间戳 275 | o.utime # 交易所订单更新时间 276 | ``` 277 | 278 | 279 | ### 4. 持仓模块 280 | 281 | 所有持仓相关的对象在框架的 `quant.position` 模块下,`Trade` 模块在推送持仓信息回调的时候,携带的 `position` 参数即此模块。 282 | 283 | #### 4.1 持仓对象 284 | 285 | ```python 286 | from quant.position import Position 287 | 288 | p = Position(...) # 初始化持仓对象 289 | 290 | p.platform # 交易平台 291 | p.account # 交易账户 292 | p.strategy # 策略名称 293 | p.symbol # 交易对 294 | p.short_quantity # 空仓数量 295 | p.short_avg_price # 空仓平均价格 296 | p.long_quantity # 多仓数量 297 | p.long_avg_price # 多仓平均价格 298 | p.liquid_price # 预估爆仓价格 299 | p.utime # 更新时间戳(毫秒) 300 | ``` 301 | -------------------------------------------------------------------------------- /example/binance/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Binance(币安) 3 | 4 | 本文主要介绍如何通过框架SDK开发 [Binance(币安)](https://www.binance.com) 交易所的交易系统。 5 | 6 | ### 1. 准备条件 7 | 8 | 在开发策略之前,需要先对整套系统的运行原理有一个大致的了解,以及相应的开发环境和运行环境。 9 | 10 | - Python3.x开发环境,并安装好 `thenextquant` 开发包; 11 | - 部署 [RabbitMQ 事件中心服务](../../docs/others/rabbitmq_deploy.md) ---- 事件中心的核心组成部分; 12 | - 部署 [Market 行情服务](https://github.com/TheNextQuant/Market) ---- 订阅Binance的行情事件,如果策略不需要行情数据,那么此服务可以不用部署; 13 | - 部署 [Asset 资产服务](https://github.com/TheNextQuant/Asset) ---- 账户资产更新事件推送,如果策略不需要账户资产信息,那么此服务可以不用部署; 14 | - 注册 [Binance 币安](https://www.binance.com) 的账户,并且创建 `ACCESS KEY` 和 `SECRET KEY`,AK有操作委托单权限; 15 | 16 | 17 | ### 2. 一个简单的策略 18 | 19 | 为了在订单薄买盘提前埋伏订单,在 `BTC/USDT` 订单薄盘口距离10美金的位置挂买单,数量为1。 20 | 随着订单薄盘口价格不断变化,需要将价格已经偏离的订单取消,再重新挂单,使订单始终保持距离买盘盘口价差为 `10 ± 1` 美金。 21 | 这里设置了缓冲价差为 `1` 美金,即只要盘口价格变化在 `± 1` 美金内,都不必撤单之后重新挂单,这样设置的目的是尽量减少挂撤单的次数,因为交易所开放的交易接口有调用频率的限制, 22 | 如果调用太过频繁超过了限制可能会报错。 23 | 24 | 25 | 比如: 当前订单薄价格盘口价格为 8500.5,那么我们需要在 8490.5 的位置挂一个单买,数量为1。 26 | 如果此时订单薄价格变化为 8520.6,那么我们需要取消之前的订单,再重新在 8510.6 的位置挂一个买单,数量为1。 27 | 28 | > 注意:这里只是演示框架的基本功能,包含挂单、撤单、查看订单状态等,不涉及具体策略以及盈亏。 29 | 30 | 31 | ##### 2.1 导入必要的模块 32 | 33 | ```python 34 | import sys 35 | 36 | from quant import const # 必要的常量 37 | from quant.utils import tools # 工具集合 38 | from quant.utils import logger # 日志打印模块 39 | from quant.config import config # 系统加载的配置,根据 config.json 配置文件初始化 40 | from quant.market import Market # 行情模块 41 | from quant.trade import Trade # 交易模块 42 | from quant.order import Order # 订单模块 43 | from quant.market import Orderbook # 订单薄模块 44 | from quant.order import ORDER_ACTION_BUY, ORDER_STATUS_FAILED, ORDER_STATUS_CANCELED, ORDER_STATUS_FILLED # 订单属性常量 45 | ``` 46 | 47 | ##### 2.1 策略逻辑代码 48 | 49 | - 创建一个策略逻辑的类 ---- MyStrategy 50 | 51 | ```python 52 | class MyStrategy: 53 | 54 | def __init__(self): 55 | """ 初始化 56 | """ 57 | self.strategy = config.strategy 58 | self.platform = const.BINANCE 59 | self.account = config.accounts[0]["account"] 60 | self.access_key = config.accounts[0]["access_key"] 61 | self.secret_key = config.accounts[0]["secret_key"] 62 | self.symbol = config.symbol 63 | 64 | self.order_no = None # 创建订单的id 65 | self.create_order_price = "0.0" # 创建订单的价格 66 | 67 | # 初始化交易模块,设置订单更新回调函数 self.on_event_order_update 68 | cc = { 69 | "strategy": self.strategy, 70 | "platform": self.platform, 71 | "symbol": self.symbol, 72 | "account": self.account, 73 | "access_key": self.access_key, 74 | "secret_key": self.secret_key, 75 | "order_update_callback": self.on_event_order_update 76 | } 77 | self.trader = Trade(**cc) 78 | 79 | # 订阅行情,设置订单薄更新回调函数 self.on_event_orderbook_update 80 | Market(const.MARKET_TYPE_ORDERBOOK, const.BINANCE, self.symbol, self.on_event_orderbook_update) 81 | ``` 82 | 83 | > 在这里,我们创建了策略类 `MyStrategy`,初始化的时候设置了必要的参数、初始化交易模块、订阅相关的订单薄行情数据。 84 | 85 | - 订单薄更新回调 86 | 87 | 我们通过 `Market` 模块订阅订单薄行情,并且设置了订单薄更新回调函数 `self.on_event_orderbook_update`,订单薄数据将实时从币安服务器更新 88 | 并推送至此函数,我们需要根据订单薄数据的实时更新,来实时调整我们的挂单位置。 89 | 90 | ```python 91 | async def on_event_orderbook_update(self, orderbook: Orderbook): 92 | """ 订单薄更新 93 | """ 94 | logger.debug("orderbook:", orderbook, caller=self) 95 | ask1_price = float(orderbook.asks[0][0]) # 卖一价格 96 | bid1_price = float(orderbook.bids[0][0]) # 买一价格 97 | price = (ask1_price + bid1_price) / 2 # 为了方便,这里假设盘口价格为 `卖一` 和 `买一` 的平均值 98 | 99 | # 判断是否需要撤单 100 | if self.order_no: 101 | if (self.create_order_price + 10 > price - 1) and (self.create_order_price + 10 < price + 1): 102 | return 103 | _, error = await self.trader.revoke_order(self.order_no) 104 | if error: 105 | logger.error("revoke order error! error:", error, caller=self) 106 | return 107 | self.order_no = None 108 | logger.info("revoke order:", self.order_no, caller=self) 109 | 110 | # 创建新订单 111 | new_price = price + 10 112 | quantity = "1" # 委托数量为1 113 | action = ORDER_ACTION_BUY 114 | new_price = tools.float_to_str(new_price) # 将价格转换为字符串,保持精度 115 | quantity = tools.float_to_str(quantity) # 将数量转换为字符串,保持精度 116 | order_no, error = await self.trader.create_order(action, new_price, quantity) 117 | if error: 118 | logger.error("create order error! error:", error, caller=self) 119 | return 120 | self.order_no = order_no 121 | self.create_order_price = float(new_price) 122 | logger.info("create new order:", order_no, caller=self) 123 | ``` 124 | > 这里是关于 [行情对象](../../docs/market.md) 的详细说明。 125 | 126 | - 订单更新回调 127 | 128 | 当我们创建订单、订单状态有任何的变化,都将通过 `self.on_event_order_update` 函数返回订单对象。 129 | 130 | ```python 131 | async def on_event_order_update(self, order: Order): 132 | """ 订单状态更新 133 | """ 134 | logger.info("order update:", order, caller=self) 135 | 136 | # 如果订单失败、订单取消、订单完成交易 137 | if order.status in [ORDER_STATUS_FAILED, ORDER_STATUS_CANCELED, ORDER_STATUS_FILLED]: 138 | self.order_no = None 139 | ``` 140 | > 这里是关于 [订单对象](../../docs/trade.md) 的详细说明。 141 | 142 | > 注意: 143 | 当订单的生命周期结束之后(订单失败、订单取消、订单完成),我们需要重置订单号为空(self.order_no = None),然后进入接下来的挂单逻辑; 144 | 145 | 146 | ##### 2.2 程序入口 147 | 148 | 我们的策略逻辑已经完成,现在我们需要初始化 `thenextquant` 框架,并加载我们的策略,让底层框架驱动策略运行起来。 149 | 150 | ```python 151 | def main(): 152 | if len(sys.argv) > 1: 153 | config_file = sys.argv[1] 154 | else: 155 | config_file = None 156 | 157 | from quant.quant import quant 158 | quant.initialize(config_file) 159 | MyStrategy() 160 | quant.start() 161 | 162 | 163 | if __name__ == '__main__': 164 | main() 165 | ``` 166 | 167 | > 我们首先判断程序运行的第一个参数是否指定了配置文件,配置文件一般为 `config.json` 的json文件,如果没有指定配置文件,那么就设置配置文件为None。 168 | 其次,我们导入 `quant` 模块,调用 `quant.initialize(config_file)` 初始化配置,紧接着执行 `MyStrategy()` 初始化策略,最后执行 `quant.start()` 启动整个程序。 169 | 170 | 171 | ##### 2.3 配置文件 172 | 173 | 我们在配置文件里,加入了如下配置: 174 | - RABBITMQ 指定事件中心服务器,此配置需要和 [Market 行情服务](https://github.com/TheNextQuant/Market) 、[Asset 资产服务](https://github.com/TheNextQuant/Asset) 一致; 175 | - PROXY HTTP代理,翻墙,你懂的;(如果在不需要翻墙的环境运行,此参数可以去掉) 176 | - ACCOUNTS 指定需要使用的交易账户,注意platform是 `binance` ; 177 | - strategy 策略的名称; 178 | - symbol 策略运行的交易对; 179 | 180 | 配置文件比较简单,更多的配置可以参考 [配置文件说明](../../docs/configure/README.md)。 181 | 182 | 183 | ### 3. 启动程序 184 | 185 | 以上,我们介绍了如何使用 `thenextquant` 开发自己的策略,这里是完整的 [策略代码](./main.py) 以及 [配置文件](./config.json)。 186 | 187 | 现在让我们来启动程序: 188 | ```text 189 | python src/main.py config.json 190 | ``` 191 | 192 | 是不是非常简单,Enjoy yourself! 193 | 194 | 195 | ### 4. 参考文档 196 | 197 | - [config 服务配置](../../docs/configure/README.md) 198 | - [Market 行情](../../docs/market.md) 199 | - [Trade 交易](../../docs/trade.md) 200 | - [Asset 资产](https://github.com/TheNextQuant/Asset) 201 | - [EventCenter 安装RabbitMQ](../../docs/others/rabbitmq_deploy.md) 202 | - [Logger 日志打印](../../docs/others/logger.md) 203 | - [Tasks 协程任务](../../docs/others/tasks.md) 204 | 205 | - [框架使用系列教程](https://github.com/TheNextQuant/Documents) 206 | - [Python Asyncio](https://docs.python.org/3/library/asyncio.html) 207 | -------------------------------------------------------------------------------- /example/binance/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "RABBITMQ": { 3 | "host": "127.0.0.1", 4 | "port": 5672, 5 | "username": "test", 6 | "password": "123456" 7 | }, 8 | "PROXY": "http://127.0.0.1:1087", 9 | "ACCOUNTS": [ 10 | { 11 | "platform": "binance", 12 | "account": "abc123@gmail.com", 13 | "access_key": "ACCESS KEY", 14 | "secret_key": "SECRET KEY" 15 | } 16 | ], 17 | "strategy": "my_test_strategy", 18 | "symbol": "BTC/USDT" 19 | } 20 | -------------------------------------------------------------------------------- /example/binance/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | Binance 模块使用演示 5 | 6 | 为了在订单薄买盘提前埋伏订单,在 `BTC/USDT` 订单薄盘口距离10美金的位置挂买单,数量量为1。 7 | 随着订单薄盘口价格不断变化,需要将价格已经偏离的订单取消,再重新挂单,使订单始终保持距离盘口价差为 `10 ± 1` 美金。 8 | 这里设置了缓冲价差为 `1` 美金,即只要盘口价格变化在 `± 1` 内,都不必撤单之后重新挂单,这样设置的目的是尽量减少挂撤单的次数, 9 | 因为交易所开放的交易接口有调用频率的限制,如果调用太过频繁超过了限制可能会报错。 10 | """ 11 | 12 | import sys 13 | 14 | from quant import const 15 | from quant.utils import tools 16 | from quant.utils import logger 17 | from quant.config import config 18 | from quant.market import Market 19 | from quant.trade import Trade 20 | from quant.order import Order 21 | from quant.market import Orderbook 22 | from quant.order import ORDER_ACTION_BUY, ORDER_STATUS_FAILED, ORDER_STATUS_CANCELED, ORDER_STATUS_FILLED 23 | 24 | 25 | class MyStrategy: 26 | 27 | def __init__(self): 28 | """ 初始化 29 | """ 30 | self.strategy = config.strategy 31 | self.platform = const.BINANCE 32 | self.account = config.accounts[0]["account"] 33 | self.access_key = config.accounts[0]["access_key"] 34 | self.secret_key = config.accounts[0]["secret_key"] 35 | self.symbol = config.symbol 36 | 37 | self.order_no = None # 创建订单的id 38 | self.create_order_price = "0.0" # 创建订单的价格 39 | 40 | # 交易模块 41 | cc = { 42 | "strategy": self.strategy, 43 | "platform": self.platform, 44 | "symbol": self.symbol, 45 | "account": self.account, 46 | "access_key": self.access_key, 47 | "secret_key": self.secret_key, 48 | "order_update_callback": self.on_event_order_update 49 | } 50 | self.trader = Trade(**cc) 51 | 52 | # 订阅行情 53 | Market(const.MARKET_TYPE_ORDERBOOK, self.platform, self.symbol, self.on_event_orderbook_update) 54 | 55 | async def on_event_orderbook_update(self, orderbook: Orderbook): 56 | """ 订单薄更新 57 | """ 58 | logger.debug("orderbook:", orderbook, caller=self) 59 | ask1_price = float(orderbook.asks[0][0]) # 卖一价格 60 | bid1_price = float(orderbook.bids[0][0]) # 买一价格 61 | price = (ask1_price + bid1_price) / 2 # 为了方便,这里假设盘口价格为 卖一 和 买一 的平均值 62 | 63 | # 判断是否需要撤单 64 | if self.order_no: 65 | if (self.create_order_price + 10 > price - 1) and (self.create_order_price + 10 < price + 1): 66 | return 67 | _, error = await self.trader.revoke_order(self.order_no) 68 | if error: 69 | logger.error("revoke order error! error:", error, caller=self) 70 | return 71 | self.order_no = None 72 | logger.info("revoke order:", self.order_no, caller=self) 73 | 74 | # 创建新订单 75 | new_price = price + 10 76 | quantity = "1" # 委托数量为1 77 | action = ORDER_ACTION_BUY 78 | new_price = tools.float_to_str(new_price) # 将价格转换为字符串,保持精度 79 | quantity = tools.float_to_str(quantity) # 将数量转换为字符串,保持精度 80 | order_no, error = await self.trader.create_order(action, new_price, quantity) 81 | if error: 82 | logger.error("create order error! error:", error, caller=self) 83 | return 84 | self.order_no = order_no 85 | self.create_order_price = float(new_price) 86 | logger.info("create new order:", order_no, caller=self) 87 | 88 | async def on_event_order_update(self, order: Order): 89 | """ 订单状态更新 90 | """ 91 | logger.info("order update:", order, caller=self) 92 | 93 | # 如果订单失败、订单取消、订单完成交易 94 | if order.status in [ORDER_STATUS_FAILED, ORDER_STATUS_CANCELED, ORDER_STATUS_FILLED]: 95 | self.order_no = None 96 | 97 | 98 | def main(): 99 | if len(sys.argv) > 1: 100 | config_file = sys.argv[1] 101 | else: 102 | config_file = None 103 | 104 | from quant.quant import quant 105 | quant.initialize(config_file) 106 | MyStrategy() 107 | quant.start() 108 | 109 | 110 | if __name__ == '__main__': 111 | main() 112 | -------------------------------------------------------------------------------- /example/huobi/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Huobi(火币) 币币 3 | 4 | 本文主要介绍如何通过框架SDK开发 [Huobi(火币) 币币](https://www.hbg.com) 交易所的交易系统。 5 | 6 | ### 1. 准备条件 7 | 8 | 在开发策略之前,需要先对整套系统的运行原理有一个大致的了解,以及相应的开发环境和运行环境。 9 | 10 | - Python3.x开发环境,并安装好 `thenextquant` 开发包; 11 | - 部署 [RabbitMQ 事件中心服务](../../docs/others/rabbitmq_deploy.md) ---- 事件中心的核心组成部分; 12 | - 部署 [Market 行情服务](https://github.com/TheNextQuant/Market) ---- 订阅行情事件,如果策略不需要行情数据,那么此服务可以不用部署; 13 | - 部署 [Asset 资产服务](https://github.com/TheNextQuant/Asset) ---- 账户资产更新事件推送,如果策略不需要账户资产信息,那么此服务可以不用部署; 14 | - 注册 [Huobi(火币) 币币](https://www.hbg.com) 的账户,并且创建 `ACCESS KEY` 和 `SECRET KEY`,AK有操作委托单权限; 15 | 16 | 17 | ### 2. 一个简单的策略 18 | 19 | 为了在订单薄买盘提前埋伏订单,在 `BTC/USDT` 订单薄盘口距离10美金的位置挂买单,数量为1。 20 | 随着订单薄盘口价格不断变化,需要将价格已经偏离的订单取消,再重新挂单,使订单始终保持距离买盘盘口价差为 `10 ± 1` 美金。 21 | 这里设置了缓冲价差为 `1` 美金,即只要盘口价格变化在 `± 1` 美金内,都不必撤单之后重新挂单,这样设置的目的是尽量减少挂撤单的次数,因为交易所开放的交易接口有调用频率的限制, 22 | 如果调用太过频繁超过了限制可能会报错。 23 | 24 | 25 | 比如: 当前订单薄价格盘口价格为 8500.5,那么我们需要在 8490.5 的位置挂一个单买,数量为1。 26 | 如果此时订单薄价格变化为 8520.6,那么我们需要取消之前的订单,再重新在 8510.6 的位置挂一个买单,数量为1。 27 | 28 | > 注意:这里只是演示框架的基本功能,包含挂单、撤单、查看订单状态等,不涉及具体策略以及盈亏。 29 | 30 | 31 | ##### 2.1 导入必要的模块 32 | 33 | ```python 34 | import sys 35 | 36 | from quant import const # 必要的常量 37 | from quant.utils import tools # 工具集合 38 | from quant.utils import logger # 日志打印模块 39 | from quant.config import config # 系统加载的配置,根据 config.json 配置文件初始化 40 | from quant.market import Market # 行情模块 41 | from quant.trade import Trade # 交易模块 42 | from quant.order import Order # 订单模块 43 | from quant.market import Orderbook # 订单薄模块 44 | from quant.order import ORDER_ACTION_BUY, ORDER_STATUS_FAILED, ORDER_STATUS_CANCELED, ORDER_STATUS_FILLED # 订单属性常量 45 | ``` 46 | 47 | ##### 2.1 策略逻辑代码 48 | 49 | - 创建一个策略逻辑的类 ---- MyStrategy 50 | 51 | ```python 52 | class MyStrategy: 53 | 54 | def __init__(self): 55 | """ 初始化 56 | """ 57 | self.strategy = config.strategy 58 | self.platform = const.HUOBI 59 | self.account = config.accounts[0]["account"] 60 | self.access_key = config.accounts[0]["access_key"] 61 | self.secret_key = config.accounts[0]["secret_key"] 62 | self.symbol = config.symbol 63 | 64 | self.order_no = None # 创建订单的id 65 | self.create_order_price = "0.0" # 创建订单的价格 66 | 67 | # 初始化交易模块,设置订单更新回调函数 self.on_event_order_update 68 | cc = { 69 | "strategy": self.strategy, 70 | "platform": self.platform, 71 | "symbol": self.symbol, 72 | "account": self.account, 73 | "access_key": self.access_key, 74 | "secret_key": self.secret_key, 75 | "order_update_callback": self.on_event_order_update 76 | } 77 | self.trader = Trade(**cc) 78 | 79 | # 订阅行情,设置订单薄更新回调函数 self.on_event_orderbook_update 80 | Market(const.MARKET_TYPE_ORDERBOOK, const.platform, self.symbol, self.on_event_orderbook_update) 81 | ``` 82 | 83 | > 在这里,我们创建了策略类 `MyStrategy`,初始化的时候设置了必要的参数、初始化交易模块、订阅相关的订单薄行情数据。 84 | 85 | - 订单薄更新回调 86 | 87 | 我们通过 `Market` 模块订阅订单薄行情,并且设置了订单薄更新回调函数 `self.on_event_orderbook_update`,订单薄数据将实时从服务器更新 88 | 并推送至此函数,我们需要根据订单薄数据的实时更新,来实时调整我们的挂单位置。 89 | 90 | ```python 91 | async def on_event_orderbook_update(self, orderbook: Orderbook): 92 | """ 订单薄更新 93 | """ 94 | logger.debug("orderbook:", orderbook, caller=self) 95 | ask1_price = float(orderbook.asks[0][0]) # 卖一价格 96 | bid1_price = float(orderbook.bids[0][0]) # 买一价格 97 | price = (ask1_price + bid1_price) / 2 # 为了方便,这里假设盘口价格为 `卖一` 和 `买一` 的平均值 98 | 99 | # 判断是否需要撤单 100 | if self.order_no: 101 | if (self.create_order_price + 10 > price - 1) and (self.create_order_price + 10 < price + 1): 102 | return 103 | _, error = await self.trader.revoke_order(self.order_no) 104 | if error: 105 | logger.error("revoke order error! error:", error, caller=self) 106 | return 107 | self.order_no = None 108 | logger.info("revoke order:", self.order_no, caller=self) 109 | 110 | # 创建新订单 111 | new_price = price + 10 112 | quantity = "1" # 委托数量为1 113 | action = ORDER_ACTION_BUY 114 | new_price = tools.float_to_str(new_price) # 将价格转换为字符串,保持精度 115 | quantity = tools.float_to_str(quantity) # 将数量转换为字符串,保持精度 116 | order_no, error = await self.trader.create_order(action, new_price, quantity) 117 | if error: 118 | logger.error("create order error! error:", error, caller=self) 119 | return 120 | self.order_no = order_no 121 | self.create_order_price = float(new_price) 122 | logger.info("create new order:", order_no, caller=self) 123 | ``` 124 | > 这里是关于 [行情对象](../../docs/market.md) 的详细说明。 125 | 126 | - 订单更新回调 127 | 128 | 当我们创建订单、订单状态有任何的变化,都将通过 `self.on_event_order_update` 函数返回订单对象。 129 | 130 | ```python 131 | async def on_event_order_update(self, order: Order): 132 | """ 订单状态更新 133 | """ 134 | logger.info("order update:", order, caller=self) 135 | 136 | # 如果订单失败、订单取消、订单完成交易 137 | if order.status in [ORDER_STATUS_FAILED, ORDER_STATUS_CANCELED, ORDER_STATUS_FILLED]: 138 | self.order_no = None 139 | ``` 140 | > 这里是关于 [订单对象](../../docs/trade.md) 的详细说明。 141 | 142 | > 注意: 143 | 当订单的生命周期结束之后(订单失败、订单取消、订单完成),我们需要重置订单号为空(self.order_no = None),然后进入接下来的挂单逻辑; 144 | 145 | 146 | ##### 2.2 程序入口 147 | 148 | 我们的策略逻辑已经完成,现在我们需要初始化 `thenextquant` 框架,并加载我们的策略,让底层框架驱动策略运行起来。 149 | 150 | ```python 151 | def main(): 152 | if len(sys.argv) > 1: 153 | config_file = sys.argv[1] 154 | else: 155 | config_file = None 156 | 157 | from quant.quant import quant 158 | quant.initialize(config_file) 159 | MyStrategy() 160 | quant.start() 161 | 162 | 163 | if __name__ == '__main__': 164 | main() 165 | ``` 166 | 167 | > 我们首先判断程序运行的第一个参数是否指定了配置文件,配置文件一般为 `config.json` 的json文件,如果没有指定配置文件,那么就设置配置文件为None。 168 | 其次,我们导入 `quant` 模块,调用 `quant.initialize(config_file)` 初始化配置,紧接着执行 `MyStrategy()` 初始化策略,最后执行 `quant.start()` 启动整个程序。 169 | 170 | 171 | ##### 2.3 配置文件 172 | 173 | 我们在配置文件里,加入了如下配置: 174 | - RABBITMQ 指定事件中心服务器,此配置需要和 [Market 行情服务](https://github.com/TheNextQuant/Market) 、[Asset 资产服务](https://github.com/TheNextQuant/Asset) 一致; 175 | - PROXY HTTP代理,翻墙,你懂的;(如果在不需要翻墙的环境运行,此参数可以去掉) 176 | - ACCOUNTS 指定需要使用的交易账户,注意platform是 `huobi` ; 177 | - strategy 策略的名称; 178 | - symbol 策略运行的交易对; 179 | 180 | 配置文件比较简单,更多的配置可以参考 [配置文件说明](../../docs/configure/README.md)。 181 | 182 | 183 | ### 3. 启动程序 184 | 185 | 以上,我们介绍了如何使用 `thenextquant` 开发自己的策略,这里是完整的 [策略代码](./main.py) 以及 [配置文件](./config.json)。 186 | 187 | 现在让我们来启动程序: 188 | ```text 189 | python src/main.py config.json 190 | ``` 191 | 192 | 是不是非常简单,Enjoy yourself! 193 | 194 | 195 | ### 4. 参考文档 196 | 197 | - [config 服务配置](../../docs/configure/README.md) 198 | - [Market 行情](../../docs/market.md) 199 | - [Trade 交易](../../docs/trade.md) 200 | - [Asset 资产](https://github.com/TheNextQuant/Asset) 201 | - [EventCenter 安装RabbitMQ](../../docs/others/rabbitmq_deploy.md) 202 | - [Logger 日志打印](../../docs/others/logger.md) 203 | - [Tasks 协程任务](../../docs/others/tasks.md) 204 | 205 | - [框架使用系列教程](https://github.com/TheNextQuant/Documents) 206 | - [Python Asyncio](https://docs.python.org/3/library/asyncio.html) 207 | -------------------------------------------------------------------------------- /example/huobi/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "RABBITMQ": { 3 | "host": "127.0.0.1", 4 | "port": 5672, 5 | "username": "test", 6 | "password": "123456" 7 | }, 8 | "PROXY": "http://127.0.0.1:1087", 9 | "ACCOUNTS": [ 10 | { 11 | "platform": "huobi", 12 | "account": "abc123@gmail.com", 13 | "access_key": "ACCESS KEY", 14 | "secret_key": "SECRET KEY" 15 | } 16 | ], 17 | "strategy": "my_test_strategy", 18 | "symbol": "BTC/USDT" 19 | } 20 | -------------------------------------------------------------------------------- /example/huobi/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | Huobi 模块使用演示 5 | 6 | 为了在订单薄买盘提前埋伏订单,在 `BTC/USDT` 订单薄盘口距离10美金的位置挂买单,数量量为1。 7 | 随着订单薄盘口价格不断变化,需要将价格已经偏离的订单取消,再重新挂单,使订单始终保持距离盘口价差为 `10 ± 1` 美金。 8 | 这里设置了缓冲价差为 `1` 美金,即只要盘口价格变化在 `± 1` 内,都不必撤单之后重新挂单,这样设置的目的是尽量减少挂撤单的次数, 9 | 因为交易所开放的交易接口有调用频率的限制,如果调用太过频繁超过了限制可能会报错。 10 | """ 11 | 12 | import sys 13 | 14 | from quant import const 15 | from quant.utils import tools 16 | from quant.utils import logger 17 | from quant.config import config 18 | from quant.market import Market 19 | from quant.trade import Trade 20 | from quant.order import Order 21 | from quant.market import Orderbook 22 | from quant.order import ORDER_ACTION_BUY, ORDER_STATUS_FAILED, ORDER_STATUS_CANCELED, ORDER_STATUS_FILLED 23 | 24 | 25 | class MyStrategy: 26 | 27 | def __init__(self): 28 | """ 初始化 29 | """ 30 | self.strategy = config.strategy 31 | self.platform = const.HUOBI 32 | self.account = config.accounts[0]["account"] 33 | self.access_key = config.accounts[0]["access_key"] 34 | self.secret_key = config.accounts[0]["secret_key"] 35 | self.symbol = config.symbol 36 | 37 | self.order_no = None # 创建订单的id 38 | self.create_order_price = "0.0" # 创建订单的价格 39 | 40 | # 交易模块 41 | cc = { 42 | "strategy": self.strategy, 43 | "platform": self.platform, 44 | "symbol": self.symbol, 45 | "account": self.account, 46 | "access_key": self.access_key, 47 | "secret_key": self.secret_key, 48 | "order_update_callback": self.on_event_order_update 49 | } 50 | self.trader = Trade(**cc) 51 | 52 | # 订阅行情 53 | Market(const.MARKET_TYPE_ORDERBOOK, self.platform, self.symbol, self.on_event_orderbook_update) 54 | 55 | async def on_event_orderbook_update(self, orderbook: Orderbook): 56 | """ 订单薄更新 57 | """ 58 | logger.debug("orderbook:", orderbook, caller=self) 59 | ask1_price = float(orderbook.asks[0][0]) # 卖一价格 60 | bid1_price = float(orderbook.bids[0][0]) # 买一价格 61 | price = (ask1_price + bid1_price) / 2 # 为了方便,这里假设盘口价格为 卖一 和 买一 的平均值 62 | 63 | # 判断是否需要撤单 64 | if self.order_no: 65 | if (self.create_order_price + 10 > price - 1) and (self.create_order_price + 10 < price + 1): 66 | return 67 | _, error = await self.trader.revoke_order(self.order_no) 68 | if error: 69 | logger.error("revoke order error! error:", error, caller=self) 70 | return 71 | self.order_no = None 72 | logger.info("revoke order:", self.order_no, caller=self) 73 | 74 | # 创建新订单 75 | new_price = price + 10 76 | quantity = "1" # 委托数量为1 77 | action = ORDER_ACTION_BUY 78 | new_price = tools.float_to_str(new_price) # 将价格转换为字符串,保持精度 79 | quantity = tools.float_to_str(quantity) # 将数量转换为字符串,保持精度 80 | order_no, error = await self.trader.create_order(action, new_price, quantity) 81 | if error: 82 | logger.error("create order error! error:", error, caller=self) 83 | return 84 | self.order_no = order_no 85 | self.create_order_price = float(new_price) 86 | logger.info("create new order:", order_no, caller=self) 87 | 88 | async def on_event_order_update(self, order: Order): 89 | """ 订单状态更新 90 | """ 91 | logger.info("order update:", order, caller=self) 92 | 93 | # 如果订单失败、订单取消、订单完成交易 94 | if order.status in [ORDER_STATUS_FAILED, ORDER_STATUS_CANCELED, ORDER_STATUS_FILLED]: 95 | self.order_no = None 96 | 97 | 98 | def main(): 99 | if len(sys.argv) > 1: 100 | config_file = sys.argv[1] 101 | else: 102 | config_file = None 103 | 104 | from quant.quant import quant 105 | quant.initialize(config_file) 106 | MyStrategy() 107 | quant.start() 108 | 109 | 110 | if __name__ == '__main__': 111 | main() 112 | -------------------------------------------------------------------------------- /example/okex/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## OKEx 币币 3 | 4 | 本文主要介绍如何通过框架SDK开发 [OKEx 币币](https://www.okex.me/) 交易所的交易系统。 5 | 6 | ### 1. 准备条件 7 | 8 | 在开发策略之前,需要先对整套系统的运行原理有一个大致的了解,以及相应的开发环境和运行环境。 9 | 10 | - Python3.x开发环境,并安装好 `thenextquant` 开发包; 11 | - 部署 [RabbitMQ 事件中心服务](../../docs/others/rabbitmq_deploy.md) ---- 事件中心的核心组成部分; 12 | - 部署 [Market 行情服务](https://github.com/TheNextQuant/Market) ---- 订阅行情事件,如果策略不需要行情数据,那么此服务可以不用部署; 13 | - 部署 [Asset 资产服务](https://github.com/TheNextQuant/Asset) ---- 账户资产更新事件推送,如果策略不需要账户资产信息,那么此服务可以不用部署; 14 | - 注册 [OKEx 币币](https://www.okex.me/) 的账户,并且创建 `ACCESS KEY` 和 `SECRET KEY`,AK有操作委托单权限; 15 | 16 | 17 | ### 2. 一个简单的策略 18 | 19 | 为了在订单薄买盘提前埋伏订单,在 `BTC/USDT` 订单薄盘口距离10美金的位置挂买单,数量为1。 20 | 随着订单薄盘口价格不断变化,需要将价格已经偏离的订单取消,再重新挂单,使订单始终保持距离买盘盘口价差为 `10 ± 1` 美金。 21 | 这里设置了缓冲价差为 `1` 美金,即只要盘口价格变化在 `± 1` 美金内,都不必撤单之后重新挂单,这样设置的目的是尽量减少挂撤单的次数,因为交易所开放的交易接口有调用频率的限制, 22 | 如果调用太过频繁超过了限制可能会报错。 23 | 24 | 25 | 比如: 当前订单薄价格盘口价格为 8500.5,那么我们需要在 8490.5 的位置挂一个单买,数量为1。 26 | 如果此时订单薄价格变化为 8520.6,那么我们需要取消之前的订单,再重新在 8510.6 的位置挂一个买单,数量为1。 27 | 28 | > 注意:这里只是演示框架的基本功能,包含挂单、撤单、查看订单状态等,不涉及具体策略以及盈亏。 29 | 30 | 31 | ##### 2.1 导入必要的模块 32 | 33 | ```python 34 | import sys 35 | 36 | from quant import const # 必要的常量 37 | from quant.utils import tools # 工具集合 38 | from quant.utils import logger # 日志打印模块 39 | from quant.config import config # 系统加载的配置,根据 config.json 配置文件初始化 40 | from quant.market import Market # 行情模块 41 | from quant.trade import Trade # 交易模块 42 | from quant.order import Order # 订单模块 43 | from quant.market import Orderbook # 订单薄模块 44 | from quant.order import ORDER_ACTION_BUY, ORDER_STATUS_FAILED, ORDER_STATUS_CANCELED, ORDER_STATUS_FILLED # 订单属性常量 45 | ``` 46 | 47 | ##### 2.1 策略逻辑代码 48 | 49 | - 创建一个策略逻辑的类 ---- MyStrategy 50 | 51 | ```python 52 | class MyStrategy: 53 | 54 | def __init__(self): 55 | """ 初始化 56 | """ 57 | self.strategy = config.strategy 58 | self.platform = const.OKEX 59 | self.account = config.accounts[0]["account"] 60 | self.access_key = config.accounts[0]["access_key"] 61 | self.secret_key = config.accounts[0]["secret_key"] 62 | self.passphrase = config.accounts[0]["passphrase"] 63 | self.symbol = config.symbol 64 | 65 | self.order_no = None # 创建订单的id 66 | self.create_order_price = "0.0" # 创建订单的价格 67 | 68 | # 交易模块 69 | cc = { 70 | "strategy": self.strategy, 71 | "platform": self.platform, 72 | "symbol": self.symbol, 73 | "account": self.account, 74 | "access_key": self.access_key, 75 | "secret_key": self.secret_key, 76 | "passphrase": self.passphrase, 77 | "order_update_callback": self.on_event_order_update 78 | } 79 | self.trader = Trade(**cc) 80 | 81 | # 订阅行情 82 | Market(const.MARKET_TYPE_ORDERBOOK, self.platform, self.symbol, self.on_event_orderbook_update) 83 | ``` 84 | 85 | > 在这里,我们创建了策略类 `MyStrategy`,初始化的时候设置了必要的参数、初始化交易模块、订阅相关的订单薄行情数据。 86 | 87 | > 注意:这里我们在配置文件里需要配置 `passphrase` 参数,即 OKEx 的 API KEY 的密码,同时在初始化 `Trade` 模块的时候传入此参数。 88 | 89 | - 订单薄更新回调 90 | 91 | 我们通过 `Market` 模块订阅订单薄行情,并且设置了订单薄更新回调函数 `self.on_event_orderbook_update`,订单薄数据将实时从服务器更新 92 | 并推送至此函数,我们需要根据订单薄数据的实时更新,来实时调整我们的挂单位置。 93 | 94 | ```python 95 | async def on_event_orderbook_update(self, orderbook: Orderbook): 96 | """ 订单薄更新 97 | """ 98 | logger.debug("orderbook:", orderbook, caller=self) 99 | ask1_price = float(orderbook.asks[0][0]) # 卖一价格 100 | bid1_price = float(orderbook.bids[0][0]) # 买一价格 101 | price = (ask1_price + bid1_price) / 2 # 为了方便,这里假设盘口价格为 `卖一` 和 `买一` 的平均值 102 | 103 | # 判断是否需要撤单 104 | if self.order_no: 105 | if (self.create_order_price + 10 > price - 1) and (self.create_order_price + 10 < price + 1): 106 | return 107 | _, error = await self.trader.revoke_order(self.order_no) 108 | if error: 109 | logger.error("revoke order error! error:", error, caller=self) 110 | return 111 | self.order_no = None 112 | logger.info("revoke order:", self.order_no, caller=self) 113 | 114 | # 创建新订单 115 | new_price = price + 10 116 | quantity = "1" # 委托数量为1 117 | action = ORDER_ACTION_BUY 118 | new_price = tools.float_to_str(new_price) # 将价格转换为字符串,保持精度 119 | quantity = tools.float_to_str(quantity) # 将数量转换为字符串,保持精度 120 | order_no, error = await self.trader.create_order(action, new_price, quantity) 121 | if error: 122 | logger.error("create order error! error:", error, caller=self) 123 | return 124 | self.order_no = order_no 125 | self.create_order_price = float(new_price) 126 | logger.info("create new order:", order_no, caller=self) 127 | ``` 128 | > 这里是关于 [行情对象](../../docs/market.md) 的详细说明。 129 | 130 | - 订单更新回调 131 | 132 | 当我们创建订单、订单状态有任何的变化,都将通过 `self.on_event_order_update` 函数返回订单对象。 133 | 134 | ```python 135 | async def on_event_order_update(self, order: Order): 136 | """ 订单状态更新 137 | """ 138 | logger.info("order update:", order, caller=self) 139 | 140 | # 如果订单失败、订单取消、订单完成交易 141 | if order.status in [ORDER_STATUS_FAILED, ORDER_STATUS_CANCELED, ORDER_STATUS_FILLED]: 142 | self.order_no = None 143 | ``` 144 | > 这里是关于 [订单对象](../../docs/trade.md) 的详细说明。 145 | 146 | > 注意: 147 | 当订单的生命周期结束之后(订单失败、订单取消、订单完成),我们需要重置订单号为空(self.order_no = None),然后进入接下来的挂单逻辑; 148 | 149 | 150 | ##### 2.2 程序入口 151 | 152 | 我们的策略逻辑已经完成,现在我们需要初始化 `thenextquant` 框架,并加载我们的策略,让底层框架驱动策略运行起来。 153 | 154 | ```python 155 | def main(): 156 | if len(sys.argv) > 1: 157 | config_file = sys.argv[1] 158 | else: 159 | config_file = None 160 | 161 | from quant.quant import quant 162 | quant.initialize(config_file) 163 | MyStrategy() 164 | quant.start() 165 | 166 | 167 | if __name__ == '__main__': 168 | main() 169 | ``` 170 | 171 | > 我们首先判断程序运行的第一个参数是否指定了配置文件,配置文件一般为 `config.json` 的json文件,如果没有指定配置文件,那么就设置配置文件为None。 172 | 其次,我们导入 `quant` 模块,调用 `quant.initialize(config_file)` 初始化配置,紧接着执行 `MyStrategy()` 初始化策略,最后执行 `quant.start()` 启动整个程序。 173 | 174 | 175 | ##### 2.3 配置文件 176 | 177 | 我们在配置文件里,加入了如下配置: 178 | - RABBITMQ 指定事件中心服务器,此配置需要和 [Market 行情服务](https://github.com/TheNextQuant/Market) 、[Asset 资产服务](https://github.com/TheNextQuant/Asset) 一致; 179 | - PROXY HTTP代理,翻墙,你懂的;(如果在不需要翻墙的环境运行,此参数可以去掉) 180 | - ACCOUNTS 指定需要使用的交易账户,注意platform是 `okex`,并且需要配置 `passphrase` 参数,即 OKEx 的 API KEY 的密码; 181 | - strategy 策略的名称; 182 | - symbol 策略运行的交易对; 183 | 184 | 配置文件比较简单,更多的配置可以参考 [配置文件说明](../../docs/configure/README.md)。 185 | 186 | 187 | ### 3. 启动程序 188 | 189 | 以上,我们介绍了如何使用 `thenextquant` 开发自己的策略,这里是完整的 [策略代码](./main.py) 以及 [配置文件](./config.json)。 190 | 191 | 现在让我们来启动程序: 192 | ```text 193 | python src/main.py config.json 194 | ``` 195 | 196 | 是不是非常简单,Enjoy yourself! 197 | 198 | 199 | ### 4. 参考文档 200 | 201 | - [config 服务配置](../../docs/configure/README.md) 202 | - [Market 行情](../../docs/market.md) 203 | - [Trade 交易](../../docs/trade.md) 204 | - [Asset 资产](https://github.com/TheNextQuant/Asset) 205 | - [EventCenter 安装RabbitMQ](../../docs/others/rabbitmq_deploy.md) 206 | - [Logger 日志打印](../../docs/others/logger.md) 207 | - [Tasks 协程任务](../../docs/others/tasks.md) 208 | 209 | - [框架使用系列教程](https://github.com/TheNextQuant/Documents) 210 | - [Python Asyncio](https://docs.python.org/3/library/asyncio.html) 211 | -------------------------------------------------------------------------------- /example/okex/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "RABBITMQ": { 3 | "host": "127.0.0.1", 4 | "port": 5672, 5 | "username": "test", 6 | "password": "123456" 7 | }, 8 | "PROXY": "http://127.0.0.1:1087", 9 | "ACCOUNTS": [ 10 | { 11 | "platform": "okex", 12 | "account": "abc123@gmail.com", 13 | "access_key": "ACCESS KEY", 14 | "secret_key": "SECRET KEY", 15 | "passphrase": "abc123" 16 | } 17 | ], 18 | "strategy": "my_test_strategy", 19 | "symbol": "BTC/USDT" 20 | } 21 | -------------------------------------------------------------------------------- /example/okex/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | OKEx 模块使用演示 5 | 6 | 为了在订单薄买盘提前埋伏订单,在 `BTC/USDT` 订单薄盘口距离10美金的位置挂买单,数量量为1。 7 | 随着订单薄盘口价格不断变化,需要将价格已经偏离的订单取消,再重新挂单,使订单始终保持距离盘口价差为 `10 ± 1` 美金。 8 | 这里设置了缓冲价差为 `1` 美金,即只要盘口价格变化在 `± 1` 内,都不必撤单之后重新挂单,这样设置的目的是尽量减少挂撤单的次数, 9 | 因为交易所开放的交易接口有调用频率的限制,如果调用太过频繁超过了限制可能会报错。 10 | """ 11 | 12 | import sys 13 | 14 | from quant import const 15 | from quant.utils import tools 16 | from quant.utils import logger 17 | from quant.config import config 18 | from quant.market import Market 19 | from quant.trade import Trade 20 | from quant.order import Order 21 | from quant.market import Orderbook 22 | from quant.order import ORDER_ACTION_BUY, ORDER_STATUS_FAILED, ORDER_STATUS_CANCELED, ORDER_STATUS_FILLED 23 | 24 | 25 | class MyStrategy: 26 | 27 | def __init__(self): 28 | """ 初始化 29 | """ 30 | self.strategy = config.strategy 31 | self.platform = const.OKEX 32 | self.account = config.accounts[0]["account"] 33 | self.access_key = config.accounts[0]["access_key"] 34 | self.secret_key = config.accounts[0]["secret_key"] 35 | self.passphrase = config.accounts[0]["passphrase"] 36 | self.symbol = config.symbol 37 | 38 | self.order_no = None # 创建订单的id 39 | self.create_order_price = "0.0" # 创建订单的价格 40 | 41 | # 交易模块 42 | cc = { 43 | "strategy": self.strategy, 44 | "platform": self.platform, 45 | "symbol": self.symbol, 46 | "account": self.account, 47 | "access_key": self.access_key, 48 | "secret_key": self.secret_key, 49 | "passphrase": self.passphrase, 50 | "order_update_callback": self.on_event_order_update 51 | } 52 | self.trader = Trade(**cc) 53 | 54 | # 订阅行情 55 | Market(const.MARKET_TYPE_ORDERBOOK, self.platform, self.symbol, self.on_event_orderbook_update) 56 | 57 | async def on_event_orderbook_update(self, orderbook: Orderbook): 58 | """ 订单薄更新 59 | """ 60 | logger.debug("orderbook:", orderbook, caller=self) 61 | ask1_price = float(orderbook.asks[0][0]) # 卖一价格 62 | bid1_price = float(orderbook.bids[0][0]) # 买一价格 63 | price = (ask1_price + bid1_price) / 2 # 为了方便,这里假设盘口价格为 卖一 和 买一 的平均值 64 | 65 | # 判断是否需要撤单 66 | if self.order_no: 67 | if (self.create_order_price + 10 > price - 1) and (self.create_order_price + 10 < price + 1): 68 | return 69 | _, error = await self.trader.revoke_order(self.order_no) 70 | if error: 71 | logger.error("revoke order error! error:", error, caller=self) 72 | return 73 | self.order_no = None 74 | logger.info("revoke order:", self.order_no, caller=self) 75 | 76 | # 创建新订单 77 | new_price = price + 10 78 | quantity = "1" # 委托数量为1 79 | action = ORDER_ACTION_BUY 80 | new_price = tools.float_to_str(new_price) # 将价格转换为字符串,保持精度 81 | quantity = tools.float_to_str(quantity) # 将数量转换为字符串,保持精度 82 | order_no, error = await self.trader.create_order(action, new_price, quantity) 83 | if error: 84 | logger.error("create order error! error:", error, caller=self) 85 | return 86 | self.order_no = order_no 87 | self.create_order_price = float(new_price) 88 | logger.info("create new order:", order_no, caller=self) 89 | 90 | async def on_event_order_update(self, order: Order): 91 | """ 订单状态更新 92 | """ 93 | logger.info("order update:", order, caller=self) 94 | 95 | # 如果订单失败、订单取消、订单完成交易 96 | if order.status in [ORDER_STATUS_FAILED, ORDER_STATUS_CANCELED, ORDER_STATUS_FILLED]: 97 | self.order_no = None 98 | 99 | 100 | def main(): 101 | if len(sys.argv) > 1: 102 | config_file = sys.argv[1] 103 | else: 104 | config_file = None 105 | 106 | from quant.quant import quant 107 | quant.initialize(config_file) 108 | MyStrategy() 109 | quant.start() 110 | 111 | 112 | if __name__ == '__main__': 113 | main() 114 | -------------------------------------------------------------------------------- /quant/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | Asynchronous driven quantitative trading framework. 5 | 6 | Author: HuangTao 7 | Date: 2017/04/26 8 | Email: huangtao@ifclover.com 9 | """ 10 | 11 | __author__ = "HuangTao" 12 | __version__ = (0, 2, 3) 13 | -------------------------------------------------------------------------------- /quant/asset.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | Asset module. 5 | 6 | Author: HuangTao 7 | Date: 2019/02/16 8 | Email: huangtao@ifclover.com 9 | """ 10 | 11 | import json 12 | 13 | 14 | class Asset: 15 | """ Asset object. 16 | 17 | Args: 18 | platform: Exchange platform name, e.g. binance/bitmex. 19 | account: Trade account name, e.g. test@gmail.com. 20 | assets: Asset information, e.g. {"BTC": {"free": "1.1", "locked": "2.2", "total": "3.3"}, ... } 21 | timestamp: Published time, millisecond. 22 | update: If any update? True or False. 23 | """ 24 | 25 | def __init__(self, platform=None, account=None, assets=None, timestamp=None, update=False): 26 | """ Initialize. """ 27 | self.platform = platform 28 | self.account = account 29 | self.assets = assets 30 | self.timestamp = timestamp 31 | self.update = update 32 | 33 | @property 34 | def data(self): 35 | d = { 36 | "platform": self.platform, 37 | "account": self.account, 38 | "assets": self.assets, 39 | "timestamp": self.timestamp, 40 | "update": self.update 41 | } 42 | return d 43 | 44 | def __str__(self): 45 | info = json.dumps(self.data) 46 | return info 47 | 48 | def __repr__(self): 49 | return str(self) 50 | 51 | 52 | class AssetSubscribe: 53 | """ Subscribe Asset. 54 | 55 | Args: 56 | platform: Exchange platform name, e.g. binance/bitmex. 57 | account: Trade account name, e.g. test@gmail.com. 58 | callback: Asynchronous callback function for market data update. 59 | e.g. async def on_event_account_update(asset: Asset): 60 | pass 61 | """ 62 | 63 | def __init__(self, platform, account, callback): 64 | """ Initialize. """ 65 | if platform == "#" or account == "#": 66 | multi = True 67 | else: 68 | multi = False 69 | from quant.event import EventAsset 70 | EventAsset(platform, account).subscribe(callback, multi) 71 | -------------------------------------------------------------------------------- /quant/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | Config module. 5 | 6 | Author: HuangTao 7 | Date: 2018/05/03 8 | Email: huangtao@ifclover.com 9 | """ 10 | 11 | import json 12 | 13 | from quant.utils import tools 14 | 15 | 16 | class Config: 17 | """ Config module will load a json file like `config.json` and parse the content to json object. 18 | 1. Configure content must be key-value pair, and `key` will be set as Config module's attributes; 19 | 2. Invoking Config module's attributes cat get those values; 20 | 3. Some `key` name is upper case are the build-in, and all `key` will be set to lower case: 21 | SERVER_ID: Server id, every running process has a unique id. 22 | LOG: Logger print config. 23 | RABBITMQ: RabbitMQ config, default is None. 24 | ACCOUNTS: Trading Exchanges config list, default is []. 25 | MARKETS: Market Server config list, default is {}. 26 | HEARTBEAT: Server heartbeat config, default is {}. 27 | PROXY: HTTP proxy config, default is None. 28 | """ 29 | 30 | def __init__(self): 31 | self.server_id = None 32 | self.log = {} 33 | self.rabbitmq = {} 34 | self.accounts = [] 35 | self.markets = {} 36 | self.heartbeat = {} 37 | self.proxy = None 38 | 39 | def loads(self, config_file=None): 40 | """ Load config file. 41 | 42 | Args: 43 | config_file: config json file. 44 | """ 45 | configures = {} 46 | if config_file: 47 | try: 48 | with open(config_file) as f: 49 | data = f.read() 50 | configures = json.loads(data) 51 | except Exception as e: 52 | print(e) 53 | exit(0) 54 | if not configures: 55 | print("config json file error!") 56 | exit(0) 57 | self._update(configures) 58 | 59 | def _update(self, update_fields): 60 | """ Update config attributes. 61 | 62 | Args: 63 | update_fields: Update fields. 64 | """ 65 | self.server_id = update_fields.get("SERVER_ID", tools.get_uuid1()) 66 | self.log = update_fields.get("LOG", {}) 67 | self.rabbitmq = update_fields.get("RABBITMQ", None) 68 | self.accounts = update_fields.get("ACCOUNTS", []) 69 | self.markets = update_fields.get("MARKETS", {}) 70 | self.heartbeat = update_fields.get("HEARTBEAT", {}) 71 | self.proxy = update_fields.get("PROXY", None) 72 | 73 | for k, v in update_fields.items(): 74 | setattr(self, k, v) 75 | 76 | 77 | config = Config() 78 | -------------------------------------------------------------------------------- /quant/const.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | some constants 5 | 6 | Author: HuangTao 7 | Date: 2018/07/31 8 | Email: huangtao@ifclover.com 9 | """ 10 | 11 | 12 | # Exchange Names 13 | BINANCE = "binance" # Binance https://www.binance.com 14 | HUOBI = "huobi" # Huobi https://www.hbg.com/zh-cn/ 15 | OKEX = "okex" # OKEx SPOT https://www.okex.me/spot/trade 16 | 17 | 18 | # Market Types 19 | MARKET_TYPE_TRADE = "trade" 20 | MARKET_TYPE_ORDERBOOK = "orderbook" 21 | MARKET_TYPE_KLINE = "kline" 22 | MARKET_TYPE_KLINE_5M = "kline_5m" 23 | MARKET_TYPE_KLINE_15M = "kline_15m" 24 | -------------------------------------------------------------------------------- /quant/error.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | 错误信息 5 | 6 | Author: HuangTao 7 | Date: 2018/05/17 8 | """ 9 | 10 | 11 | class Error: 12 | 13 | def __init__(self, msg): 14 | self._msg = msg 15 | 16 | @property 17 | def msg(self): 18 | return self._msg 19 | 20 | def __str__(self): 21 | return str(self._msg) 22 | 23 | def __repr__(self): 24 | return str(self) 25 | -------------------------------------------------------------------------------- /quant/event.py: -------------------------------------------------------------------------------- 1 | # -*— coding:utf-8 -*- 2 | 3 | """ 4 | Event Center. 5 | 6 | Author: HuangTao 7 | Date: 2018/05/04 8 | Email: huangtao@ifclover.com 9 | """ 10 | 11 | import json 12 | import zlib 13 | import asyncio 14 | 15 | import aioamqp 16 | 17 | from quant import const 18 | from quant.utils import logger 19 | from quant.config import config 20 | from quant.tasks import LoopRunTask, SingleTask 21 | from quant.utils.decorator import async_method_locker 22 | from quant.market import Orderbook, Trade, Kline 23 | from quant.asset import Asset 24 | 25 | 26 | __all__ = ("EventCenter", "EventKline", "EventOrderbook", "EventTrade", "EventAsset") 27 | 28 | 29 | class Event: 30 | """ Event base. 31 | 32 | Attributes: 33 | name: Event name. 34 | exchange: Exchange name. 35 | queue: Queue name. 36 | routing_key: Routing key name. 37 | pre_fetch_count: How may message per fetched, default is 1. 38 | data: Message content. 39 | """ 40 | 41 | def __init__(self, name=None, exchange=None, queue=None, routing_key=None, pre_fetch_count=1, data=None): 42 | """Initialize.""" 43 | self._name = name 44 | self._exchange = exchange 45 | self._queue = queue 46 | self._routing_key = routing_key 47 | self._pre_fetch_count = pre_fetch_count 48 | self._data = data 49 | self._callback = None # Asynchronous callback function. 50 | 51 | @property 52 | def name(self): 53 | return self._name 54 | 55 | @property 56 | def exchange(self): 57 | return self._exchange 58 | 59 | @property 60 | def queue(self): 61 | return self._queue 62 | 63 | @property 64 | def routing_key(self): 65 | return self._routing_key 66 | 67 | @property 68 | def prefetch_count(self): 69 | return self._pre_fetch_count 70 | 71 | @property 72 | def data(self): 73 | return self._data 74 | 75 | def dumps(self): 76 | d = { 77 | "n": self.name, 78 | "d": self.data 79 | } 80 | s = json.dumps(d) 81 | b = zlib.compress(s.encode("utf8")) 82 | return b 83 | 84 | def loads(self, b): 85 | b = zlib.decompress(b) 86 | d = json.loads(b.decode("utf8")) 87 | self._name = d.get("n") 88 | self._data = d.get("d") 89 | return d 90 | 91 | def parse(self): 92 | raise NotImplemented 93 | 94 | def subscribe(self, callback, multi=False): 95 | """ Subscribe this event. 96 | 97 | Args: 98 | callback: Asynchronous callback function. 99 | multi: If subscribe multiple channels ? 100 | """ 101 | from quant.quant import quant 102 | self._callback = callback 103 | SingleTask.run(quant.event_center.subscribe, self, self.callback, multi) 104 | 105 | def publish(self): 106 | """Publish this event.""" 107 | from quant.quant import quant 108 | SingleTask.run(quant.event_center.publish, self) 109 | 110 | async def callback(self, channel, body, envelope, properties): 111 | self._exchange = envelope.exchange_name 112 | self._routing_key = envelope.routing_key 113 | self.loads(body) 114 | o = self.parse() 115 | await self._callback(o) 116 | 117 | def __str__(self): 118 | info = "EVENT: name={n}, exchange={e}, queue={q}, routing_key={r}, data={d}".format( 119 | e=self.exchange, q=self.queue, r=self.routing_key, n=self.name, d=self.data) 120 | return info 121 | 122 | def __repr__(self): 123 | return str(self) 124 | 125 | 126 | class EventKline(Event): 127 | """ Kline event. 128 | 129 | Attributes: 130 | platform: Exchange platform name, e.g. bitmex. 131 | symbol: Trading pair, e.g. BTC/USD. 132 | open: Open price. 133 | high: Highest price. 134 | low: Lowest price. 135 | close: Close price. 136 | volume: Trade volume. 137 | timestamp: Publish time, millisecond. 138 | kline_type: Kline type, kline/kline_5min/kline_15min. 139 | 140 | * NOTE: 141 | Publisher: Market server. 142 | Subscriber: Any servers. 143 | """ 144 | 145 | def __init__(self, platform=None, symbol=None, open=None, high=None, low=None, close=None, volume=None, 146 | timestamp=None, kline_type=None): 147 | """Initialize.""" 148 | if kline_type == const.MARKET_TYPE_KLINE: 149 | name = "EVENT_KLINE" 150 | exchange = "Kline" 151 | elif kline_type == const.MARKET_TYPE_KLINE_5M: 152 | name = "EVENT_KLINE_5MIN" 153 | exchange = "Kline.5min" 154 | elif kline_type == const.MARKET_TYPE_KLINE_15M: 155 | name = "EVENT_KLINE_15MIN" 156 | exchange = "Kline.15min" 157 | else: 158 | logger.error("kline_type error! kline_type:", kline_type, caller=self) 159 | return 160 | routing_key = "{platform}.{symbol}".format(platform=platform, symbol=symbol) 161 | queue = "{server_id}.{exchange}.{routing_key}".format(server_id=config.server_id, 162 | exchange=exchange, 163 | routing_key=routing_key) 164 | data = { 165 | "platform": platform, 166 | "symbol": symbol, 167 | "open": open, 168 | "high": high, 169 | "low": low, 170 | "close": close, 171 | "volume": volume, 172 | "timestamp": timestamp, 173 | "kline_type": kline_type 174 | } 175 | super(EventKline, self).__init__(name, exchange, queue, routing_key, data=data) 176 | 177 | def parse(self): 178 | kline = Kline(**self.data) 179 | return kline 180 | 181 | 182 | class EventOrderbook(Event): 183 | """ Orderbook event. 184 | 185 | Attributes: 186 | platform: Exchange platform name, e.g. bitmex. 187 | symbol: Trading pair, e.g. BTC/USD. 188 | asks: Asks, e.g. [[price, quantity], ... ] 189 | bids: Bids, e.g. [[price, quantity], ... ] 190 | timestamp: Publish time, millisecond. 191 | 192 | * NOTE: 193 | Publisher: Market server. 194 | Subscriber: Any servers. 195 | """ 196 | 197 | def __init__(self, platform=None, symbol=None, asks=None, bids=None, timestamp=None): 198 | """Initialize.""" 199 | name = "EVENT_ORDERBOOK" 200 | exchange = "Orderbook" 201 | routing_key = "{platform}.{symbol}".format(platform=platform, symbol=symbol) 202 | queue = "{server_id}.{exchange}.{routing_key}".format(server_id=config.server_id, 203 | exchange=exchange, 204 | routing_key=routing_key) 205 | data = { 206 | "platform": platform, 207 | "symbol": symbol, 208 | "asks": asks, 209 | "bids": bids, 210 | "timestamp": timestamp 211 | } 212 | super(EventOrderbook, self).__init__(name, exchange, queue, routing_key, data=data) 213 | 214 | def parse(self): 215 | orderbook = Orderbook(**self.data) 216 | return orderbook 217 | 218 | 219 | class EventTrade(Event): 220 | """ Trade event. 221 | 222 | Attributes: 223 | platform: Exchange platform name, e.g. bitmex. 224 | symbol: Trading pair, e.g. BTC/USD. 225 | action: Trading side, BUY or SELL. 226 | price: Order price. 227 | quantity: Order size. 228 | timestamp: Publish time, millisecond. 229 | 230 | * NOTE: 231 | Publisher: Market server. 232 | Subscriber: Any servers. 233 | """ 234 | 235 | def __init__(self, platform=None, symbol=None, action=None, price=None, quantity=None, timestamp=None): 236 | """ 初始化 237 | """ 238 | name = "EVENT_TRADE" 239 | exchange = "Trade" 240 | routing_key = "{platform}.{symbol}".format(platform=platform, symbol=symbol) 241 | queue = "{server_id}.{exchange}.{routing_key}".format(server_id=config.server_id, 242 | exchange=exchange, 243 | routing_key=routing_key) 244 | data = { 245 | "platform": platform, 246 | "symbol": symbol, 247 | "action": action, 248 | "price": price, 249 | "quantity": quantity, 250 | "timestamp": timestamp 251 | } 252 | super(EventTrade, self).__init__(name, exchange, queue, routing_key, data=data) 253 | 254 | def parse(self): 255 | trade = Trade(**self.data) 256 | return trade 257 | 258 | 259 | class EventAsset(Event): 260 | """ Asset event. 261 | 262 | Attributes: 263 | platform: Exchange platform name, e.g. bitmex. 264 | account: Trading account name, e.g. test@gmail.com. 265 | assets: Asset details. 266 | timestamp: Publish time, millisecond. 267 | update: If any update in this publish. 268 | 269 | * NOTE: 270 | Publisher: Asset server. 271 | Subscriber: Any servers. 272 | """ 273 | 274 | def __init__(self, platform=None, account=None, assets=None, timestamp=None, update=False): 275 | """Initialize.""" 276 | name = "EVENT_ASSET" 277 | exchange = "Asset" 278 | routing_key = "{platform}.{account}".format(platform=platform, account=account) 279 | queue = "{server_id}.{exchange}.{routing_key}".format(server_id=config.server_id, 280 | exchange=exchange, 281 | routing_key=routing_key) 282 | data = { 283 | "platform": platform, 284 | "account": account, 285 | "assets": assets, 286 | "timestamp": timestamp, 287 | "update": update 288 | } 289 | super(EventAsset, self).__init__(name, exchange, queue, routing_key, data=data) 290 | 291 | def parse(self): 292 | asset = Asset(**self.data) 293 | return asset 294 | 295 | 296 | class EventCenter: 297 | """ Event center. 298 | """ 299 | 300 | def __init__(self): 301 | self._host = config.rabbitmq.get("host", "localhost") 302 | self._port = config.rabbitmq.get("port", 5672) 303 | self._username = config.rabbitmq.get("username", "guest") 304 | self._password = config.rabbitmq.get("password", "guest") 305 | self._protocol = None 306 | self._channel = None # Connection channel. 307 | self._connected = False # If connect success. 308 | self._subscribers = [] # e.g. [(event, callback, multi), ...] 309 | self._event_handler = {} # e.g. {"exchange:routing_key": [callback_function, ...]} 310 | 311 | # Register a loop run task to check TCP connection's healthy. 312 | LoopRunTask.register(self._check_connection, 10) 313 | 314 | def initialize(self): 315 | asyncio.get_event_loop().run_until_complete(self.connect()) 316 | 317 | @async_method_locker("EventCenter.subscribe") 318 | async def subscribe(self, event: Event, callback=None, multi=False): 319 | """ Subscribe a event. 320 | 321 | Args: 322 | event: Event type. 323 | callback: Asynchronous callback. 324 | multi: If subscribe multiple channel(routing_key) ? 325 | """ 326 | logger.info("NAME:", event.name, "EXCHANGE:", event.exchange, "QUEUE:", event.queue, "ROUTING_KEY:", 327 | event.routing_key, caller=self) 328 | self._subscribers.append((event, callback, multi)) 329 | 330 | async def publish(self, event): 331 | """ Publish a event. 332 | 333 | Args: 334 | event: A event to publish. 335 | """ 336 | if not self._connected: 337 | logger.warn("RabbitMQ not ready right now!", caller=self) 338 | return 339 | data = event.dumps() 340 | await self._channel.basic_publish(payload=data, exchange_name=event.exchange, routing_key=event.routing_key) 341 | 342 | async def connect(self, reconnect=False): 343 | """ Connect to RabbitMQ server and create default exchange. 344 | 345 | Args: 346 | reconnect: If this invoke is a re-connection ? 347 | """ 348 | logger.info("host:", self._host, "port:", self._port, caller=self) 349 | if self._connected: 350 | return 351 | 352 | # Create a connection. 353 | try: 354 | transport, protocol = await aioamqp.connect(host=self._host, port=self._port, login=self._username, 355 | password=self._password, login_method="PLAIN") 356 | except Exception as e: 357 | logger.error("connection error:", e, caller=self) 358 | return 359 | finally: 360 | if self._connected: 361 | return 362 | channel = await protocol.channel() 363 | self._protocol = protocol 364 | self._channel = channel 365 | self._connected = True 366 | logger.info("Rabbitmq initialize success!", caller=self) 367 | 368 | # Create default exchanges. 369 | exchanges = ["Orderbook", "Trade", "Kline", "Kline.5min", "Kline.15min", "Asset", ] 370 | for name in exchanges: 371 | await self._channel.exchange_declare(exchange_name=name, type_name="topic") 372 | logger.debug("create default exchanges success!", caller=self) 373 | 374 | if reconnect: 375 | self._bind_and_consume() 376 | else: 377 | # Maybe we should waiting for all modules to be initialized successfully. 378 | asyncio.get_event_loop().call_later(5, self._bind_and_consume) 379 | 380 | def _bind_and_consume(self): 381 | async def do_them(): 382 | for event, callback, multi in self._subscribers: 383 | await self._initialize(event, callback, multi) 384 | SingleTask.run(do_them) 385 | 386 | async def _initialize(self, event: Event, callback=None, multi=False): 387 | if event.queue: 388 | await self._channel.queue_declare(queue_name=event.queue, auto_delete=True) 389 | queue_name = event.queue 390 | else: 391 | result = await self._channel.queue_declare(exclusive=True) 392 | queue_name = result["queue"] 393 | await self._channel.queue_bind(queue_name=queue_name, exchange_name=event.exchange, 394 | routing_key=event.routing_key) 395 | await self._channel.basic_qos(prefetch_count=event.prefetch_count) 396 | if callback: 397 | if multi: 398 | await self._channel.basic_consume(callback=callback, queue_name=queue_name, no_ack=True) 399 | logger.info("multi message queue:", queue_name, caller=self) 400 | else: 401 | await self._channel.basic_consume(self._on_consume_event_msg, queue_name=queue_name) 402 | logger.info("queue:", queue_name, caller=self) 403 | self._add_event_handler(event, callback) 404 | 405 | async def _on_consume_event_msg(self, channel, body, envelope, properties): 406 | # logger.debug("exchange:", envelope.exchange_name, "routing_key:", envelope.routing_key, 407 | # "body:", body, caller=self) 408 | try: 409 | key = "{exchange}:{routing_key}".format(exchange=envelope.exchange_name, routing_key=envelope.routing_key) 410 | funcs = self._event_handler[key] 411 | for func in funcs: 412 | SingleTask.run(func, channel, body, envelope, properties) 413 | except: 414 | logger.error("event handle error! body:", body, caller=self) 415 | return 416 | finally: 417 | await self._channel.basic_client_ack(delivery_tag=envelope.delivery_tag) # response ack 418 | 419 | def _add_event_handler(self, event: Event, callback): 420 | key = "{exchange}:{routing_key}".format(exchange=event.exchange, routing_key=event.routing_key) 421 | if key in self._event_handler: 422 | self._event_handler[key].append(callback) 423 | else: 424 | self._event_handler[key] = [callback] 425 | logger.debug("event handlers:", self._event_handler.keys(), caller=self) 426 | 427 | async def _check_connection(self, *args, **kwargs): 428 | if self._connected and self._channel and self._channel.is_open: 429 | logger.debug("RabbitMQ connection ok.", caller=self) 430 | return 431 | logger.error("CONNECTION LOSE! START RECONNECT RIGHT NOW!", caller=self) 432 | self._connected = False 433 | self._protocol = None 434 | self._channel = None 435 | self._event_handler = {} 436 | SingleTask.run(self.connect, reconnect=True) 437 | -------------------------------------------------------------------------------- /quant/heartbeat.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | 服务器心跳 5 | 6 | Author: HuangTao 7 | Date: 2018/04/26 8 | """ 9 | 10 | import asyncio 11 | 12 | from quant.utils import tools 13 | from quant.utils import logger 14 | from quant.config import config 15 | 16 | __all__ = ("heartbeat", ) 17 | 18 | 19 | class HeartBeat(object): 20 | """ 心跳 21 | """ 22 | 23 | def __init__(self): 24 | self._count = 0 # 心跳次数 25 | self._interval = 1 # 服务心跳执行时间间隔(秒) 26 | self._print_interval = config.heartbeat.get("interval", 0) # 心跳打印时间间隔(秒),0为不打印 27 | self._tasks = {} # 跟随心跳执行的回调任务列表,由 self.register 注册 {task_id: {...}} 28 | 29 | @property 30 | def count(self): 31 | return self._count 32 | 33 | def ticker(self): 34 | """ 启动心跳, 每秒执行一次 35 | """ 36 | self._count += 1 37 | 38 | # 打印心跳次数 39 | if self._print_interval > 0: 40 | if self._count % self._print_interval == 0: 41 | logger.info("do server heartbeat, count:", self._count, caller=self) 42 | 43 | # 设置下一次心跳回调 44 | asyncio.get_event_loop().call_later(self._interval, self.ticker) 45 | 46 | # 执行任务回调 47 | for task_id, task in self._tasks.items(): 48 | interval = task["interval"] 49 | if self._count % interval != 0: 50 | continue 51 | func = task["func"] 52 | args = task["args"] 53 | kwargs = task["kwargs"] 54 | kwargs["task_id"] = task_id 55 | kwargs["heart_beat_count"] = self._count 56 | asyncio.get_event_loop().create_task(func(*args, **kwargs)) 57 | 58 | def register(self, func, interval=1, *args, **kwargs): 59 | """ 注册一个任务,在每次心跳的时候执行调用 60 | @param func 心跳的时候执行的函数 61 | @param interval 执行回调的时间间隔(秒) 62 | @return task_id 任务id 63 | """ 64 | t = { 65 | "func": func, 66 | "interval": interval, 67 | "args": args, 68 | "kwargs": kwargs 69 | } 70 | task_id = tools.get_uuid1() 71 | self._tasks[task_id] = t 72 | return task_id 73 | 74 | def unregister(self, task_id): 75 | """ 注销一个任务 76 | @param task_id 任务id 77 | """ 78 | if task_id in self._tasks: 79 | self._tasks.pop(task_id) 80 | 81 | 82 | heartbeat = HeartBeat() 83 | -------------------------------------------------------------------------------- /quant/market.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | Market module. 5 | 6 | Author: HuangTao 7 | Date: 2019/02/16 8 | Email: huangtao@ifclover.com 9 | """ 10 | 11 | import json 12 | 13 | from quant import const 14 | from quant.utils import logger 15 | 16 | 17 | class Orderbook: 18 | """ Orderbook object. 19 | 20 | Args: 21 | platform: Exchange platform name, e.g. binance/bitmex. 22 | symbol: Trade pair name, e.g. ETH/BTC. 23 | asks: Asks list, e.g. [[price, quantity], [...], ...] 24 | bids: Bids list, e.g. [[price, quantity], [...], ...] 25 | timestamp: Update time, millisecond. 26 | """ 27 | 28 | def __init__(self, platform=None, symbol=None, asks=None, bids=None, timestamp=None): 29 | """ Initialize. """ 30 | self.platform = platform 31 | self.symbol = symbol 32 | self.asks = asks 33 | self.bids = bids 34 | self.timestamp = timestamp 35 | 36 | @property 37 | def data(self): 38 | d = { 39 | "platform": self.platform, 40 | "symbol": self.symbol, 41 | "asks": self.asks, 42 | "bids": self.bids, 43 | "timestamp": self.timestamp 44 | } 45 | return d 46 | 47 | def __str__(self): 48 | info = json.dumps(self.data) 49 | return info 50 | 51 | def __repr__(self): 52 | return str(self) 53 | 54 | 55 | class Trade: 56 | """ Trade object. 57 | 58 | Args: 59 | platform: Exchange platform name, e.g. binance/bitmex. 60 | symbol: Trade pair name, e.g. ETH/BTC. 61 | action: Trade action, BUY or SELL. 62 | price: Order place price. 63 | quantity: Order place quantity. 64 | timestamp: Update time, millisecond. 65 | """ 66 | 67 | def __init__(self, platform=None, symbol=None, action=None, price=None, quantity=None, timestamp=None): 68 | """ Initialize. """ 69 | self.platform = platform 70 | self.symbol = symbol 71 | self.action = action 72 | self.price = price 73 | self.quantity = quantity 74 | self.timestamp = timestamp 75 | 76 | @property 77 | def data(self): 78 | d = { 79 | "platform": self.platform, 80 | "symbol": self.symbol, 81 | "action": self.action, 82 | "price": self.price, 83 | "quantity": self.quantity, 84 | "timestamp": self.timestamp 85 | } 86 | return d 87 | 88 | def __str__(self): 89 | info = json.dumps(self.data) 90 | return info 91 | 92 | def __repr__(self): 93 | return str(self) 94 | 95 | 96 | class Kline: 97 | """ Kline object. 98 | 99 | Args: 100 | platform: Exchange platform name, e.g. binance/bitmex. 101 | symbol: Trade pair name, e.g. ETH/BTC. 102 | open: Open price. 103 | high: Highest price. 104 | low: Lowest price. 105 | close: Close price. 106 | volume: Total trade volume. 107 | timestamp: Update time, millisecond. 108 | kline_type: Kline type name, kline - 1min, kline_5min - 5min, kline_15min - 15min. 109 | """ 110 | 111 | def __init__(self, platform=None, symbol=None, open=None, high=None, low=None, close=None, volume=None, 112 | timestamp=None, kline_type=None): 113 | """ Initialize. """ 114 | self.platform = platform 115 | self.symbol = symbol 116 | self.open = open 117 | self.high = high 118 | self.low = low 119 | self.close = close 120 | self.volume = volume 121 | self.timestamp = timestamp 122 | self.kline_type = kline_type 123 | 124 | @property 125 | def data(self): 126 | d = { 127 | "platform": self.platform, 128 | "symbol": self.symbol, 129 | "open": self.open, 130 | "high": self.high, 131 | "low": self.low, 132 | "close": self.close, 133 | "volume": self.volume, 134 | "timestamp": self.timestamp, 135 | "kline_type": self.kline_type 136 | } 137 | return d 138 | 139 | def __str__(self): 140 | info = json.dumps(self.data) 141 | return info 142 | 143 | def __repr__(self): 144 | return str(self) 145 | 146 | 147 | class Market: 148 | """ Subscribe Market. 149 | 150 | Args: 151 | market_type: Market data type, 152 | MARKET_TYPE_TRADE = "trade" 153 | MARKET_TYPE_ORDERBOOK = "orderbook" 154 | MARKET_TYPE_KLINE = "kline" 155 | MARKET_TYPE_KLINE_5M = "kline_5m" 156 | MARKET_TYPE_KLINE_15M = "kline_15m" 157 | platform: Exchange platform name, e.g. binance/bitmex. 158 | symbol: Trade pair name, e.g. ETH/BTC. 159 | callback: Asynchronous callback function for market data update. 160 | e.g. async def on_event_kline_update(kline: Kline): 161 | pass 162 | """ 163 | 164 | def __init__(self, market_type, platform, symbol, callback): 165 | """ Initialize. """ 166 | if platform == "#" or symbol == "#": 167 | multi = True 168 | else: 169 | multi = False 170 | if market_type == const.MARKET_TYPE_ORDERBOOK: 171 | from quant.event import EventOrderbook 172 | EventOrderbook(platform, symbol).subscribe(callback, multi) 173 | elif market_type == const.MARKET_TYPE_TRADE: 174 | from quant.event import EventTrade 175 | EventTrade(platform, symbol).subscribe(callback, multi) 176 | elif market_type in [const.MARKET_TYPE_KLINE, const.MARKET_TYPE_KLINE_5M, const.MARKET_TYPE_KLINE_15M]: 177 | from quant.event import EventKline 178 | EventKline(platform, symbol, kline_type=market_type).subscribe(callback, multi) 179 | else: 180 | logger.error("market_type error:", market_type, caller=self) 181 | -------------------------------------------------------------------------------- /quant/order.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | Order object. 5 | 6 | Author: HuangTao 7 | Date: 2018/05/14 8 | Email: huangtao@ifclover.com 9 | """ 10 | 11 | from quant.utils import tools 12 | 13 | 14 | # Order type. 15 | ORDER_TYPE_LIMIT = "LIMIT" # Limit order. 16 | ORDER_TYPE_MARKET = "MARKET" # Market order. 17 | 18 | # Order direction. 19 | ORDER_ACTION_BUY = "BUY" # Buy 20 | ORDER_ACTION_SELL = "SELL" # Sell 21 | 22 | # Order status. 23 | ORDER_STATUS_NONE = "NONE" # New created order, no status. 24 | ORDER_STATUS_SUBMITTED = "SUBMITTED" # The order that submitted to server successfully. 25 | ORDER_STATUS_PARTIAL_FILLED = "PARTIAL-FILLED" # The order that filled partially. 26 | ORDER_STATUS_FILLED = "FILLED" # The order that filled fully. 27 | ORDER_STATUS_CANCELED = "CANCELED" # The order that canceled. 28 | ORDER_STATUS_FAILED = "FAILED" # The order that failed. 29 | 30 | # Future order trade type. 31 | TRADE_TYPE_NONE = 0 # Unknown type, some Exchange's order information couldn't known the type of trade. 32 | TRADE_TYPE_BUY_OPEN = 1 # Buy open, action = BUY & quantity > 0. 33 | TRADE_TYPE_SELL_OPEN = 2 # Sell open, action = SELL & quantity < 0. 34 | TRADE_TYPE_SELL_CLOSE = 3 # Sell close, action = SELL & quantity > 0. 35 | TRADE_TYPE_BUY_CLOSE = 4 # Buy close, action = BUY & quantity < 0. 36 | 37 | 38 | class Order: 39 | """ Order object. 40 | 41 | Attributes: 42 | account: Trading account name, e.g. test@gmail.com. 43 | platform: Exchange platform name, e.g. binance/bitmex. 44 | strategy: Strategy name, e.g. my_test_strategy. 45 | order_no: order id. 46 | symbol: Trading pair name, e.g. ETH/BTC. 47 | action: Trading side, BUY/SELL. 48 | price: Order price. 49 | quantity: Order quantity. 50 | remain: Remain quantity that not filled. 51 | status: Order status. 52 | avg_price: Average price that filled. 53 | order_type: Order type. 54 | trade_type: Trade type, only for future order. 55 | ctime: Order create time, millisecond. 56 | utime: Order update time, millisecond. 57 | """ 58 | 59 | def __init__(self, account=None, platform=None, strategy=None, order_no=None, client_order_id=None, symbol=None, 60 | action=None, price=0, quantity=0, remain=0, status=ORDER_STATUS_NONE, avg_price=0, 61 | order_type=ORDER_TYPE_LIMIT, trade_type=TRADE_TYPE_NONE, ctime=None, utime=None): 62 | self.platform = platform 63 | self.account = account 64 | self.strategy = strategy 65 | self.order_no = order_no 66 | self.client_order_id = client_order_id 67 | self.action = action 68 | self.order_type = order_type 69 | self.symbol = symbol 70 | self.price = price 71 | self.quantity = quantity 72 | self.remain = remain if remain else quantity 73 | self.status = status 74 | self.avg_price = avg_price 75 | self.trade_type = trade_type 76 | self.ctime = ctime if ctime else tools.get_cur_timestamp_ms() 77 | self.utime = utime if utime else tools.get_cur_timestamp_ms() 78 | 79 | def __str__(self): 80 | info = "[platform: {platform}, account: {account}, strategy: {strategy}, order_no: {order_no}, " \ 81 | "client_order_id: {client_order_id}, action: {action}, symbol: {symbol}, price: {price}, " \ 82 | "quantity: {quantity}, remain: {remain}, status: {status}, avg_price: {avg_price}, " \ 83 | "order_type: {order_type}, trade_type: {trade_type}, " \ 84 | "ctime: {ctime}, utime: {utime}]".format( 85 | platform=self.platform, account=self.account, strategy=self.strategy, order_no=self.order_no, 86 | client_order_id=self.client_order_id, action=self.action, symbol=self.symbol, price=self.price, 87 | quantity=self.quantity, remain=self.remain, status=self.status, avg_price=self.avg_price, 88 | order_type=self.order_type, trade_type=self.trade_type, ctime=self.ctime, utime=self.utime) 89 | return info 90 | 91 | def __repr__(self): 92 | return str(self) 93 | -------------------------------------------------------------------------------- /quant/platform/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpim/thenextquant/beb65b1c0590174c4cd90a2ac2a60194b73e1934/quant/platform/__init__.py -------------------------------------------------------------------------------- /quant/platform/binance.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | Binance Trade module. 5 | https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md 6 | 7 | Author: HuangTao 8 | Date: 2018/08/09 9 | Email: huangtao@ifclover.com 10 | """ 11 | 12 | import json 13 | import copy 14 | import hmac 15 | import hashlib 16 | from urllib.parse import urljoin 17 | 18 | from quant.error import Error 19 | from quant.utils import tools 20 | from quant.utils import logger 21 | from quant.const import BINANCE 22 | from quant.order import Order 23 | from quant.asset import Asset, AssetSubscribe 24 | from quant.tasks import SingleTask, LoopRunTask 25 | from quant.utils.decorator import async_method_locker 26 | from quant.utils.web import Websocket, AsyncHttpRequests 27 | from quant.order import ORDER_STATUS_SUBMITTED, ORDER_STATUS_PARTIAL_FILLED, ORDER_STATUS_FILLED, \ 28 | ORDER_STATUS_CANCELED, ORDER_STATUS_FAILED 29 | 30 | 31 | __all__ = ("BinanceRestAPI", "BinanceTrade", ) 32 | 33 | 34 | class BinanceRestAPI: 35 | """ Binance REST API client. 36 | 37 | Attributes: 38 | host: HTTP request host. 39 | access_key: Account's ACCESS KEY. 40 | secret_key: Account's SECRET KEY. 41 | """ 42 | 43 | def __init__(self, host, access_key, secret_key): 44 | """initialize REST API client.""" 45 | self._host = host 46 | self._access_key = access_key 47 | self._secret_key = secret_key 48 | 49 | async def get_user_account(self): 50 | """ Get user account information. 51 | 52 | Returns: 53 | success: Success results, otherwise it's None. 54 | error: Error information, otherwise it's None. 55 | """ 56 | ts = tools.get_cur_timestamp_ms() 57 | params = { 58 | "timestamp": str(ts) 59 | } 60 | success, error = await self.request("GET", "/api/v3/account", params, auth=True) 61 | return success, error 62 | 63 | async def get_server_time(self): 64 | """ Get server time. 65 | 66 | Returns: 67 | success: Success results, otherwise it's None. 68 | error: Error information, otherwise it's None. 69 | """ 70 | success, error = await self.request("GET", "/api/v1/time") 71 | return success, error 72 | 73 | async def get_exchange_info(self): 74 | """ Get exchange information. 75 | 76 | Returns: 77 | success: Success results, otherwise it's None. 78 | error: Error information, otherwise it's None. 79 | """ 80 | success, error = await self.request("GET", "/api/v1/exchangeInfo") 81 | return success, error 82 | 83 | async def get_latest_ticker(self, symbol): 84 | """ Get latest ticker. 85 | 86 | Args: 87 | symbol: Symbol name, e.g. BTCUSDT. 88 | 89 | Returns: 90 | success: Success results, otherwise it's None. 91 | error: Error information, otherwise it's None. 92 | """ 93 | params = { 94 | "symbol": symbol 95 | } 96 | success, error = await self.request("GET", "/api/v1/ticker/24hr", params=params) 97 | return success, error 98 | 99 | async def get_orderbook(self, symbol, limit=10): 100 | """ Get orderbook. 101 | 102 | Args: 103 | symbol: Symbol name, e.g. BTCUSDT. 104 | limit: Number of results per request. (default 10) 105 | 106 | Returns: 107 | success: Success results, otherwise it's None. 108 | error: Error information, otherwise it's None. 109 | """ 110 | params = { 111 | "symbol": symbol, 112 | "limit": limit 113 | } 114 | success, error = await self.request("GET", "/api/v1/depth", params=params) 115 | return success, error 116 | 117 | async def create_order(self, action, symbol, price, quantity, client_order_id=None): 118 | """ Create an order. 119 | Args: 120 | action: Trade direction, BUY or SELL. 121 | symbol: Symbol name, e.g. BTCUSDT. 122 | price: Price of each contract. 123 | quantity: The buying or selling quantity. 124 | client_order_id: Client order id. 125 | 126 | Returns: 127 | success: Success results, otherwise it's None. 128 | error: Error information, otherwise it's None. 129 | """ 130 | info = { 131 | "symbol": symbol, 132 | "side": action, 133 | "type": "LIMIT", 134 | "timeInForce": "GTC", 135 | "quantity": quantity, 136 | "price": price, 137 | "recvWindow": "5000", 138 | "newOrderRespType": "FULL", 139 | "timestamp": tools.get_cur_timestamp_ms() 140 | } 141 | if client_order_id: 142 | info["newClientOrderId"] = client_order_id 143 | success, error = await self.request("POST", "/api/v3/order", body=info, auth=True) 144 | return success, error 145 | 146 | async def revoke_order(self, symbol, order_id, client_order_id): 147 | """ Cancelling an unfilled order. 148 | Args: 149 | symbol: Symbol name, e.g. BTCUSDT. 150 | order_id: Order id. 151 | client_order_id: Client order id. 152 | 153 | Returns: 154 | success: Success results, otherwise it's None. 155 | error: Error information, otherwise it's None. 156 | """ 157 | params = { 158 | "symbol": symbol, 159 | "orderId": str(order_id), 160 | "origClientOrderId": client_order_id, 161 | "timestamp": tools.get_cur_timestamp_ms() 162 | } 163 | success, error = await self.request("DELETE", "/api/v3/order", params=params, auth=True) 164 | return success, error 165 | 166 | async def get_order_status(self, symbol, order_id, client_order_id): 167 | """ Get order details by order id. 168 | 169 | Args: 170 | symbol: Symbol name, e.g. BTCUSDT. 171 | order_id: Order id. 172 | client_order_id: Client order id. 173 | 174 | Returns: 175 | success: Success results, otherwise it's None. 176 | error: Error information, otherwise it's None. 177 | """ 178 | params = { 179 | "symbol": symbol, 180 | "orderId": str(order_id), 181 | "origClientOrderId": client_order_id, 182 | "timestamp": tools.get_cur_timestamp_ms() 183 | } 184 | success, error = await self.request("GET", "/api/v3/order", params=params, auth=True) 185 | return success, error 186 | 187 | async def get_all_orders(self, symbol): 188 | """ Get all account orders; active, canceled, or filled. 189 | Args: 190 | symbol: Symbol name, e.g. BTCUSDT. 191 | 192 | Returns: 193 | success: Success results, otherwise it's None. 194 | error: Error information, otherwise it's None. 195 | """ 196 | params = { 197 | "symbol": symbol, 198 | "timestamp": tools.get_cur_timestamp_ms() 199 | } 200 | success, error = await self.request("GET", "/api/v3/allOrders", params=params, auth=True) 201 | return success, error 202 | 203 | async def get_open_orders(self, symbol): 204 | """ Get all open order information. 205 | Args: 206 | symbol: Symbol name, e.g. BTCUSDT. 207 | 208 | Returns: 209 | success: Success results, otherwise it's None. 210 | error: Error information, otherwise it's None. 211 | """ 212 | params = { 213 | "symbol": symbol, 214 | "timestamp": tools.get_cur_timestamp_ms() 215 | } 216 | success, error = await self.request("GET", "/api/v3/openOrders", params=params, auth=True) 217 | return success, error 218 | 219 | async def get_listen_key(self): 220 | """ Get listen key, start a new user data stream 221 | 222 | Returns: 223 | success: Success results, otherwise it's None. 224 | error: Error information, otherwise it's None. 225 | """ 226 | success, error = await self.request("POST", "/api/v1/userDataStream") 227 | return success, error 228 | 229 | async def put_listen_key(self, listen_key): 230 | """ Keepalive a user data stream to prevent a time out. 231 | 232 | Args: 233 | listen_key: Listen key. 234 | 235 | Returns: 236 | success: Success results, otherwise it's None. 237 | error: Error information, otherwise it's None. 238 | """ 239 | params = { 240 | "listenKey": listen_key 241 | } 242 | success, error = await self.request("PUT", "/api/v1/userDataStream", params=params) 243 | return success, error 244 | 245 | async def delete_listen_key(self, listen_key): 246 | """ Delete a listen key. 247 | 248 | Args: 249 | listen_key: Listen key. 250 | 251 | Returns: 252 | success: Success results, otherwise it's None. 253 | error: Error information, otherwise it's None. 254 | """ 255 | params = { 256 | "listenKey": listen_key 257 | } 258 | success, error = await self.request("DELETE", "/api/v1/userDataStream", params=params) 259 | return success, error 260 | 261 | async def request(self, method, uri, params=None, body=None, headers=None, auth=False): 262 | """ Do HTTP request. 263 | 264 | Args: 265 | method: HTTP request method. GET, POST, DELETE, PUT. 266 | uri: HTTP request uri. 267 | params: HTTP query params. 268 | body: HTTP request body. 269 | headers: HTTP request headers. 270 | auth: If this request requires authentication. 271 | 272 | Returns: 273 | success: Success results, otherwise it's None. 274 | error: Error information, otherwise it's None. 275 | """ 276 | url = urljoin(self._host, uri) 277 | data = {} 278 | if params: 279 | data.update(params) 280 | if body: 281 | data.update(body) 282 | 283 | if data: 284 | query = "&".join(["=".join([str(k), str(v)]) for k, v in data.items()]) 285 | else: 286 | query = "" 287 | if auth and query: 288 | signature = hmac.new(self._secret_key.encode(), query.encode(), hashlib.sha256).hexdigest() 289 | query += "&signature={s}".format(s=signature) 290 | if query: 291 | url += ("?" + query) 292 | 293 | if not headers: 294 | headers = {} 295 | headers["X-MBX-APIKEY"] = self._access_key 296 | _, success, error = await AsyncHttpRequests.fetch(method, url, headers=headers, timeout=10, verify_ssl=False) 297 | return success, error 298 | 299 | 300 | class BinanceTrade: 301 | """ Binance Trade module. You can initialize trade object with some attributes in kwargs. 302 | 303 | Attributes: 304 | account: Account name for this trade exchange. 305 | strategy: What's name would you want to created for you strategy. 306 | symbol: Symbol name for your trade. 307 | host: HTTP request host. (default "https://api.binance.com") 308 | wss: Websocket address. (default "wss://stream.binance.com:9443") 309 | access_key: Account's ACCESS KEY. 310 | secret_key Account's SECRET KEY. 311 | asset_update_callback: You can use this param to specific a async callback function when you initializing Trade 312 | object. `asset_update_callback` is like `async def on_asset_update_callback(asset: Asset): pass` and this 313 | callback function will be executed asynchronous when received AssetEvent. 314 | order_update_callback: You can use this param to specific a async callback function when you initializing Trade 315 | object. `order_update_callback` is like `async def on_order_update_callback(order: Order): pass` and this 316 | callback function will be executed asynchronous when some order state updated. 317 | init_success_callback: You can use this param to specific a async callback function when you initializing Trade 318 | object. `init_success_callback` is like `async def on_init_success_callback(success: bool, error: Error, **kwargs): pass` 319 | and this callback function will be executed asynchronous after Trade module object initialized successfully. 320 | """ 321 | 322 | def __init__(self, **kwargs): 323 | """Initialize Trade module.""" 324 | e = None 325 | if not kwargs.get("account"): 326 | e = Error("param account miss") 327 | if not kwargs.get("strategy"): 328 | e = Error("param strategy miss") 329 | if not kwargs.get("symbol"): 330 | e = Error("param symbol miss") 331 | if not kwargs.get("host"): 332 | kwargs["host"] = "https://api.binance.com" 333 | if not kwargs.get("wss"): 334 | kwargs["wss"] = "wss://stream.binance.com:9443" 335 | if not kwargs.get("access_key"): 336 | e = Error("param access_key miss") 337 | if not kwargs.get("secret_key"): 338 | e = Error("param secret_key miss") 339 | if e: 340 | logger.error(e, caller=self) 341 | SingleTask.run(kwargs["init_success_callback"], False, e) 342 | return 343 | 344 | self._account = kwargs["account"] 345 | self._strategy = kwargs["strategy"] 346 | self._platform = BINANCE 347 | self._symbol = kwargs["symbol"] 348 | self._host = kwargs["host"] 349 | self._wss = kwargs["wss"] 350 | self._access_key = kwargs["access_key"] 351 | self._secret_key = kwargs["secret_key"] 352 | self._asset_update_callback = kwargs.get("asset_update_callback") 353 | self._order_update_callback = kwargs.get("order_update_callback") 354 | self._init_success_callback = kwargs.get("init_success_callback") 355 | 356 | super(BinanceTrade, self).__init__(self._wss) 357 | 358 | self._raw_symbol = self._symbol.replace("/", "") # Row symbol name, same as Binance Exchange. 359 | 360 | self._listen_key = None # Listen key for Websocket authentication. 361 | self._assets = {} # Asset data. e.g. {"BTC": {"free": "1.1", "locked": "2.2", "total": "3.3"}, ... } 362 | self._orders = {} # Order data. e.g. {order_no: order, ... } 363 | 364 | # Initialize our REST API client. 365 | self._rest_api = BinanceRestAPI(self._host, self._access_key, self._secret_key) 366 | 367 | # Subscribe our AssetEvent. 368 | if self._asset_update_callback: 369 | AssetSubscribe(self._platform, self._account, self.on_event_asset_update) 370 | 371 | # Create a loop run task to reset listen key every 30 minutes. 372 | LoopRunTask.register(self._reset_listen_key, 60 * 30) 373 | 374 | # Create a coroutine to initialize Websocket connection. 375 | SingleTask.run(self._init_websocket) 376 | 377 | @property 378 | def assets(self): 379 | return copy.copy(self._assets) 380 | 381 | @property 382 | def orders(self): 383 | return copy.copy(self._orders) 384 | 385 | @property 386 | def rest_api(self): 387 | return self._rest_api 388 | 389 | async def _init_websocket(self): 390 | """ Initialize Websocket connection. 391 | """ 392 | # Get listen key first. 393 | success, error = await self._rest_api.get_listen_key() 394 | if error: 395 | e = Error("get listen key failed: {}".format(error)) 396 | logger.error(e, caller=self) 397 | SingleTask.run(self._init_success_callback, False, e) 398 | return 399 | self._listen_key = success["listenKey"] 400 | uri = "/ws/" + self._listen_key 401 | url = urljoin(self._wss, uri) 402 | self._ws = Websocket(url, self.connected_callback, process_callback=self.process) 403 | self._ws.initialize() 404 | 405 | async def _reset_listen_key(self, *args, **kwargs): 406 | """ Reset listen key. 407 | """ 408 | if not self._listen_key: 409 | logger.error("listen key not initialized!", caller=self) 410 | return 411 | await self._rest_api.put_listen_key(self._listen_key) 412 | logger.info("reset listen key success!", caller=self) 413 | 414 | async def connected_callback(self): 415 | """ After websocket connection created successfully, pull back all open order information. 416 | """ 417 | logger.info("Websocket connection authorized successfully.", caller=self) 418 | order_infos, error = await self._rest_api.get_open_orders(self._raw_symbol) 419 | if error: 420 | e = Error("get open orders error: {}".format(error)) 421 | SingleTask.run(self._init_success_callback, False, e) 422 | return 423 | for order_info in order_infos: 424 | order_no = "{}_{}".format(order_info["orderId"], order_info["clientOrderId"]) 425 | if order_info["status"] == "NEW": 426 | status = ORDER_STATUS_SUBMITTED 427 | elif order_info["status"] == "PARTIALLY_FILLED": 428 | status = ORDER_STATUS_PARTIAL_FILLED 429 | elif order_info["status"] == "FILLED": 430 | status = ORDER_STATUS_FILLED 431 | elif order_info["status"] == "CANCELED": 432 | status = ORDER_STATUS_CANCELED 433 | elif order_info["status"] == "REJECTED": 434 | status = ORDER_STATUS_FAILED 435 | elif order_info["status"] == "EXPIRED": 436 | status = ORDER_STATUS_FAILED 437 | else: 438 | logger.warn("unknown status:", order_info, caller=self) 439 | continue 440 | 441 | info = { 442 | "platform": self._platform, 443 | "account": self._account, 444 | "strategy": self._strategy, 445 | "order_no": order_no, 446 | "action": order_info["side"], 447 | "order_type": order_info["type"], 448 | "symbol": self._symbol, 449 | "price": order_info["price"], 450 | "quantity": order_info["origQty"], 451 | "remain": float(order_info["origQty"]) - float(order_info["executedQty"]), 452 | "status": status, 453 | "ctime": order_info["time"], 454 | "utime": order_info["updateTime"] 455 | } 456 | order = Order(**info) 457 | self._orders[order_no] = order 458 | SingleTask.run(self._order_update_callback, copy.copy(order)) 459 | 460 | SingleTask.run(self._init_success_callback, True, None) 461 | 462 | async def create_order(self, action, price, quantity, *args, **kwargs): 463 | """ Create an order. 464 | 465 | Args: 466 | action: Trade direction, BUY or SELL. 467 | price: Price of each contract. 468 | quantity: The buying or selling quantity. 469 | 470 | Returns: 471 | order_no: Order ID if created successfully, otherwise it's None. 472 | error: Error information, otherwise it's None. 473 | """ 474 | price = tools.float_to_str(price) 475 | quantity = tools.float_to_str(quantity) 476 | client_order_id = kwargs.get("client_order_id") 477 | result, error = await self._rest_api.create_order(action, self._raw_symbol, price, quantity, 478 | client_order_id=client_order_id) 479 | if error: 480 | return None, error 481 | order_no = "{}_{}".format(result["orderId"], result["clientOrderId"]) 482 | return order_no, None 483 | 484 | async def revoke_order(self, *order_nos): 485 | """ Revoke (an) order(s). 486 | 487 | Args: 488 | order_nos: Order id list, you can set this param to 0 or multiple items. If you set 0 param, you can cancel 489 | all orders for this symbol(initialized in Trade object). If you set 1 param, you can cancel an order. 490 | If you set multiple param, you can cancel multiple orders. Do not set param length more than 100. 491 | 492 | Returns: 493 | Success or error, see bellow. 494 | """ 495 | # If len(order_nos) == 0, you will cancel all orders for this symbol(initialized in Trade object). 496 | if len(order_nos) == 0: 497 | order_infos, error = await self._rest_api.get_open_orders(self._raw_symbol) 498 | if error: 499 | return False, error 500 | for order_info in order_infos: 501 | _, error = await self._rest_api.revoke_order(self._raw_symbol, order_info["orderId"], 502 | order_info["clientOrderId"]) 503 | if error: 504 | return False, error 505 | return True, None 506 | 507 | # If len(order_nos) == 1, you will cancel an order. 508 | if len(order_nos) == 1: 509 | order_id, client_order_id = order_nos[0].split("_") 510 | success, error = await self._rest_api.revoke_order(self._raw_symbol, order_id, client_order_id) 511 | if error: 512 | return order_nos[0], error 513 | else: 514 | return order_nos[0], None 515 | 516 | # If len(order_nos) > 1, you will cancel multiple orders. 517 | if len(order_nos) > 1: 518 | success, error = [], [] 519 | for order_no in order_nos: 520 | order_id, client_order_id = order_no.split("_") 521 | _, e = await self._rest_api.revoke_order(self._raw_symbol, order_id, client_order_id) 522 | if e: 523 | error.append((order_no, e)) 524 | else: 525 | success.append(order_no) 526 | return success, error 527 | 528 | async def get_open_order_nos(self): 529 | """ Get open order no list. 530 | """ 531 | success, error = await self._rest_api.get_open_orders(self._raw_symbol) 532 | if error: 533 | return None, error 534 | else: 535 | order_nos = [] 536 | for order_info in success: 537 | order_no = "{}_{}".format(order_info["orderId"], order_info["clientOrderId"]) 538 | order_nos.append(order_no) 539 | return order_nos, None 540 | 541 | @async_method_locker("BinanceTrade.process.locker") 542 | async def process(self, msg): 543 | """ Process message that received from Websocket connection. 544 | 545 | Args: 546 | msg: message received from Websocket connection. 547 | """ 548 | logger.debug("msg:", json.dumps(msg), caller=self) 549 | e = msg.get("e") 550 | if e == "executionReport": # Order update. 551 | if msg["s"] != self._raw_symbol: 552 | return 553 | order_no = "{}_{}".format(msg["i"], msg["c"]) 554 | if msg["X"] == "NEW": 555 | status = ORDER_STATUS_SUBMITTED 556 | elif msg["X"] == "PARTIALLY_FILLED": 557 | status = ORDER_STATUS_PARTIAL_FILLED 558 | elif msg["X"] == "FILLED": 559 | status = ORDER_STATUS_FILLED 560 | elif msg["X"] == "CANCELED": 561 | status = ORDER_STATUS_CANCELED 562 | elif msg["X"] == "REJECTED": 563 | status = ORDER_STATUS_FAILED 564 | elif msg["X"] == "EXPIRED": 565 | status = ORDER_STATUS_FAILED 566 | else: 567 | logger.warn("unknown status:", msg, caller=self) 568 | return 569 | order = self._orders.get(order_no) 570 | if not order: 571 | info = { 572 | "platform": self._platform, 573 | "account": self._account, 574 | "strategy": self._strategy, 575 | "order_no": order_no, 576 | "client_order_id": msg["c"], 577 | "action": msg["S"], 578 | "order_type": msg["o"], 579 | "symbol": self._symbol, 580 | "price": msg["p"], 581 | "quantity": msg["q"], 582 | "ctime": msg["O"] 583 | } 584 | order = Order(**info) 585 | self._orders[order_no] = order 586 | order.remain = float(msg["q"]) - float(msg["z"]) 587 | order.status = status 588 | order.utime = msg["T"] 589 | SingleTask.run(self._order_update_callback, copy.copy(order)) 590 | 591 | async def on_event_asset_update(self, asset: Asset): 592 | """ Asset data update callback. 593 | 594 | Args: 595 | asset: Asset object. 596 | """ 597 | self._assets = asset 598 | SingleTask.run(self._asset_update_callback, asset) 599 | -------------------------------------------------------------------------------- /quant/platform/huobi.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | huobi 交易模块 5 | https://huobiapi.github.io/docs/spot/v1/cn 6 | 7 | Author: HuangTao 8 | Date: 2018/08/30 9 | """ 10 | 11 | import json 12 | import hmac 13 | import copy 14 | import gzip 15 | import base64 16 | import urllib 17 | import hashlib 18 | import datetime 19 | from urllib import parse 20 | from urllib.parse import urljoin 21 | 22 | from quant.error import Error 23 | from quant.utils import tools 24 | from quant.utils import logger 25 | from quant.const import HUOBI 26 | from quant.order import Order 27 | from quant.tasks import SingleTask 28 | from quant.asset import Asset, AssetSubscribe 29 | from quant.utils.decorator import async_method_locker 30 | from quant.utils.web import Websocket, AsyncHttpRequests 31 | from quant.order import ORDER_ACTION_BUY, ORDER_ACTION_SELL 32 | from quant.order import ORDER_TYPE_LIMIT, ORDER_TYPE_MARKET 33 | from quant.order import ORDER_STATUS_SUBMITTED, ORDER_STATUS_PARTIAL_FILLED, ORDER_STATUS_FILLED, \ 34 | ORDER_STATUS_CANCELED, ORDER_STATUS_FAILED 35 | 36 | 37 | __all__ = ("HuobiRestAPI", "HuobiTrade", ) 38 | 39 | 40 | class HuobiRestAPI: 41 | """ huobi REST API 封装 42 | """ 43 | 44 | def __init__(self, host, access_key, secret_key): 45 | """ 初始化 46 | @param host 请求host 47 | @param access_key API KEY 48 | @param secret_key SECRET KEY 49 | """ 50 | self._host = host 51 | self._access_key = access_key 52 | self._secret_key = secret_key 53 | self._account_id = None 54 | 55 | async def get_server_time(self): 56 | """ 获取服务器时间 57 | @return data int 服务器时间戳(毫秒) 58 | """ 59 | success, error = await self.request("GET", "/v1/common/timestamp") 60 | return success, error 61 | 62 | async def get_user_accounts(self): 63 | """ 获取账户信息 64 | """ 65 | success, error = await self.request("GET", "/v1/account/accounts") 66 | return success, error 67 | 68 | async def _get_account_id(self): 69 | """ 获取账户id 70 | """ 71 | if self._account_id: 72 | return self._account_id 73 | success, error = await self.get_user_accounts() 74 | if error: 75 | return None 76 | for item in success: 77 | if item["type"] == "spot": 78 | self._account_id = item["id"] 79 | return self._account_id 80 | return None 81 | 82 | async def get_account_balance(self): 83 | """ 获取账户信息 84 | """ 85 | account_id = await self._get_account_id() 86 | uri = "/v1/account/accounts/{account_id}/balance".format(account_id=account_id) 87 | success, error = await self.request("GET", uri) 88 | return success, error 89 | 90 | async def get_balance_all(self): 91 | """ 母账户查询其下所有子账户的各币种汇总余额 92 | """ 93 | success, error = await self.request("GET", "/v1/subuser/aggregate-balance") 94 | return success, error 95 | 96 | async def create_order(self, symbol, price, quantity, order_type): 97 | """ 创建订单 98 | @param symbol 交易对 99 | @param quantity 交易量 100 | @param price 交易价格 101 | @param order_type 订单类型 buy-market, sell-market, buy-limit, sell-limit 102 | @return order_no 订单id 103 | """ 104 | account_id = await self._get_account_id() 105 | info = { 106 | "account-id": account_id, 107 | "price": price, 108 | "amount": quantity, 109 | "source": "api", 110 | "symbol": symbol, 111 | "type": order_type 112 | } 113 | if order_type == "buy-market" or order_type == "sell-market": 114 | info.pop("price") 115 | success, error = await self.request("POST", "/v1/order/orders/place", body=info) 116 | return success, error 117 | 118 | async def revoke_order(self, order_no): 119 | """ 撤销委托单 120 | @param order_no 订单id 121 | @return True/False 122 | """ 123 | uri = "/v1/order/orders/{order_no}/submitcancel".format(order_no=order_no) 124 | success, error = await self.request("POST", uri) 125 | return success, error 126 | 127 | async def revoke_orders(self, order_nos): 128 | """ 批量撤销委托单 129 | @param order_nos 订单列表 130 | * NOTE: 单次不超过50个订单id 131 | """ 132 | body = { 133 | "order-ids": order_nos 134 | } 135 | result = await self.request("POST", "/v1/order/orders/batchcancel", body=body) 136 | return result 137 | 138 | async def get_open_orders(self, symbol): 139 | """ 获取当前还未完全成交的订单信息 140 | @param symbol 交易对 141 | * NOTE: 查询上限最多500个订单 142 | """ 143 | account_id = await self._get_account_id() 144 | params = { 145 | "account-id": account_id, 146 | "symbol": symbol, 147 | "size": 500 148 | } 149 | result = await self.request("GET", "/v1/order/openOrders", params=params) 150 | return result 151 | 152 | async def get_order_status(self, order_no): 153 | """ 获取订单的状态 154 | @param order_no 订单id 155 | """ 156 | uri = "/v1/order/orders/{order_no}".format(order_no=order_no) 157 | success, error = await self.request("GET", uri) 158 | return success, error 159 | 160 | async def request(self, method, uri, params=None, body=None): 161 | """ 发起请求 162 | @param method 请求方法 GET POST 163 | @param uri 请求uri 164 | @param params dict 请求query参数 165 | @param body dict 请求body数据 166 | """ 167 | url = urljoin(self._host, uri) 168 | timestamp = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S") 169 | params = params if params else {} 170 | params.update({"AccessKeyId": self._access_key, 171 | "SignatureMethod": "HmacSHA256", 172 | "SignatureVersion": "2", 173 | "Timestamp": timestamp}) 174 | 175 | host_name = urllib.parse.urlparse(self._host).hostname.lower() 176 | params["Signature"] = self.generate_signature(method, params, host_name, uri) 177 | 178 | if method == "GET": 179 | headers = { 180 | "Content-type": "application/x-www-form-urlencoded", 181 | "User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) " 182 | "Chrome/39.0.2171.71 Safari/537.36" 183 | } 184 | else: 185 | headers = { 186 | "Accept": "application/json", 187 | "Content-type": "application/json" 188 | } 189 | _, success, error = await AsyncHttpRequests.fetch(method, url, params=params, data=body, headers=headers, 190 | timeout=10) 191 | if error: 192 | return success, error 193 | if success.get("status") != "ok": 194 | return None, success 195 | return success.get("data"), None 196 | 197 | def generate_signature(self, method, params, host_url, request_path): 198 | """ 创建签名 199 | """ 200 | query = "&".join(["{}={}".format(k, parse.quote(str(params[k]))) for k in sorted(params.keys())]) 201 | payload = [method, host_url, request_path, query] 202 | payload = "\n".join(payload) 203 | payload = payload.encode(encoding="utf8") 204 | secret_key = self._secret_key.encode(encoding="utf8") 205 | digest = hmac.new(secret_key, payload, digestmod=hashlib.sha256).digest() 206 | signature = base64.b64encode(digest) 207 | signature = signature.decode() 208 | return signature 209 | 210 | 211 | class HuobiTrade: 212 | """ huobi Trade模块 213 | """ 214 | 215 | def __init__(self, **kwargs): 216 | """ 初始化 217 | """ 218 | e = None 219 | if not kwargs.get("account"): 220 | e = Error("param account miss") 221 | if not kwargs.get("strategy"): 222 | e = Error("param strategy miss") 223 | if not kwargs.get("symbol"): 224 | e = Error("param symbol miss") 225 | if not kwargs.get("host"): 226 | kwargs["host"] = "https://api.huobi.pro" 227 | if not kwargs.get("wss"): 228 | kwargs["wss"] = "wss://api.huobi.pro" 229 | if not kwargs.get("access_key"): 230 | e = Error("param access_key miss") 231 | if not kwargs.get("secret_key"): 232 | e = Error("param secret_key miss") 233 | if e: 234 | logger.error(e, caller=self) 235 | SingleTask.run(kwargs["init_success_callback"], False, e) 236 | return 237 | 238 | self._account = kwargs["account"] 239 | self._strategy = kwargs["strategy"] 240 | self._platform = HUOBI 241 | self._symbol = kwargs["symbol"] 242 | self._host = kwargs["host"] 243 | self._wss = kwargs["wss"] 244 | self._access_key = kwargs["access_key"] 245 | self._secret_key = kwargs["secret_key"] 246 | self._asset_update_callback = kwargs.get("asset_update_callback") 247 | self._order_update_callback = kwargs.get("order_update_callback") 248 | self._init_success_callback = kwargs.get("init_success_callback") 249 | 250 | self._raw_symbol = self._symbol.replace("/", "").lower() # 转换成交易所对应的交易对格式 251 | self._order_channel = "orders.{}".format(self._raw_symbol) # 订阅订单更新频道 252 | 253 | url = self._wss + "/ws/v1" 254 | self._ws = Websocket(url, self.connected_callback, process_binary_callback=self.process_binary) 255 | self._ws.initialize() 256 | 257 | self._assets = {} # 资产 {"BTC": {"free": "1.1", "locked": "2.2", "total": "3.3"}, ... } 258 | self._orders = {} # 订单 259 | 260 | # 初始化 REST API 对象 261 | self._rest_api = HuobiRestAPI(self._host, self._access_key, self._secret_key) 262 | 263 | # 初始化资产订阅 264 | if self._asset_update_callback: 265 | AssetSubscribe(self._platform, self._account, self.on_event_asset_update) 266 | 267 | @property 268 | def assets(self): 269 | return copy.copy(self._assets) 270 | 271 | @property 272 | def orders(self): 273 | return copy.copy(self._orders) 274 | 275 | @property 276 | def rest_api(self): 277 | return self._rest_api 278 | 279 | async def connected_callback(self): 280 | """ 建立连接之后,授权登陆,然后订阅order和position 281 | """ 282 | # 身份验证 283 | timestamp = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S") 284 | params = { 285 | "AccessKeyId": self._access_key, 286 | "SignatureMethod": "HmacSHA256", 287 | "SignatureVersion": "2", 288 | "Timestamp": timestamp 289 | } 290 | signature = self._rest_api.generate_signature("GET", params, "api.huobi.pro", "/ws/v1") 291 | params["op"] = "auth" 292 | params["Signature"] = signature 293 | await self._ws.send(params) 294 | 295 | async def _auth_success_callback(self): 296 | """ 授权成功之后回调 297 | """ 298 | # 获取当前未完成订单 299 | success, error = await self._rest_api.get_open_orders(self._raw_symbol) 300 | if error: 301 | e = Error("get open orders error: {}".format(error)) 302 | SingleTask.run(self._init_success_callback, False, e) 303 | return 304 | for order_info in success: 305 | data = { 306 | "order-id": order_info["id"], 307 | "order-type": order_info["type"], 308 | "order-state": order_info["state"], 309 | "unfilled-amount": float(order_info["amount"]) - float(order_info["filled-amount"]), 310 | "order-price": float(order_info["price"]), 311 | "price": float(order_info["price"]), 312 | "order-amount": float(order_info["amount"]), 313 | "created-at": order_info["created-at"], 314 | "utime": order_info["created-at"], 315 | } 316 | self._update_order(data) 317 | # 订阅订单更新数据 318 | params = { 319 | "op": "sub", 320 | "topic": self._order_channel 321 | } 322 | await self._ws.send(params) 323 | 324 | @async_method_locker("HuobiTrade.process_binary.locker") 325 | async def process_binary(self, raw): 326 | """ 处理websocket上接收到的消息 327 | @param raw 原始的压缩数据 328 | """ 329 | msg = json.loads(gzip.decompress(raw).decode()) 330 | logger.debug("msg:", msg, caller=self) 331 | 332 | op = msg.get("op") 333 | 334 | if op == "auth": # 授权 335 | if msg["err-code"] != 0: 336 | e = Error("Websocket connection authorized failed: {}".format(msg)) 337 | logger.error(e, caller=self) 338 | SingleTask.run(self._init_success_callback, False, e) 339 | return 340 | logger.info("Websocket connection authorized successfully.", caller=self) 341 | await self._auth_success_callback() 342 | elif op == "ping": # ping 343 | params = { 344 | "op": "pong", 345 | "ts": msg["ts"] 346 | } 347 | await self._ws.send(params) 348 | elif op == "sub": # 订阅频道返回消息 349 | if msg["topic"] != self._order_channel: 350 | return 351 | if msg["err-code"] != 0: 352 | e = Error("subscribe order event error: {}".format(msg)) 353 | SingleTask.run(self._init_success_callback, False, e) 354 | else: 355 | SingleTask.run(self._init_success_callback, True, None) 356 | elif op == "notify": # 订单更新通知 357 | if msg["topic"] != self._order_channel: 358 | return 359 | data = msg["data"] 360 | data["utime"] = msg["ts"] 361 | self._update_order(data) 362 | 363 | async def create_order(self, action, price, quantity, order_type=ORDER_TYPE_LIMIT, *args, **kwargs): 364 | """ 创建订单 365 | @param action 交易方向 BUY / SELL 366 | @param price 委托价格 367 | @param quantity 委托数量 368 | @param order_type 委托类型 LIMIT / MARKET 369 | """ 370 | if action == ORDER_ACTION_BUY: 371 | if order_type == ORDER_TYPE_LIMIT: 372 | t = "buy-limit" 373 | elif order_type == ORDER_TYPE_MARKET: 374 | t = "buy-market" 375 | else: 376 | logger.error("order_type error! order_type:", order_type, caller=self) 377 | return None, "order type error" 378 | elif action == ORDER_ACTION_SELL: 379 | if order_type == ORDER_TYPE_LIMIT: 380 | t = "sell-limit" 381 | elif order_type == ORDER_TYPE_MARKET: 382 | t = "sell-market" 383 | else: 384 | logger.error("order_type error! order_type:", order_type, caller=self) 385 | return None, "order type error" 386 | else: 387 | logger.error("action error! action:", action, caller=self) 388 | return None, "action error" 389 | price = tools.float_to_str(price) 390 | quantity = tools.float_to_str(quantity) 391 | result, error = await self._rest_api.create_order(self._raw_symbol, price, quantity, t) 392 | return result, error 393 | 394 | async def revoke_order(self, *order_nos): 395 | """ 撤销订单 396 | @param order_nos 订单号列表,可传入任意多个,如果不传入,那么就撤销所有订单 397 | """ 398 | # 如果传入order_nos为空,即撤销全部委托单 399 | if len(order_nos) == 0: 400 | order_nos, error = await self.get_open_order_nos() 401 | if error: 402 | return [], error 403 | if not order_nos: 404 | return [], None 405 | 406 | # 如果传入order_nos为一个委托单号,那么只撤销一个委托单 407 | if len(order_nos) == 1: 408 | success, error = await self._rest_api.revoke_order(order_nos[0]) 409 | if error: 410 | return order_nos[0], error 411 | else: 412 | return order_nos[0], None 413 | 414 | # 如果传入order_nos数量大于1,那么就批量撤销传入的委托单 415 | if len(order_nos) > 1: 416 | s, e = await self._rest_api.revoke_orders(order_nos) 417 | if e: 418 | return [], e 419 | success = s["success"] 420 | error = s["failed"] 421 | return success, error 422 | 423 | async def get_open_order_nos(self): 424 | """ 获取未完全成交订单号列表 425 | """ 426 | success, error = await self._rest_api.get_open_orders(self._raw_symbol) 427 | if error: 428 | return None, error 429 | else: 430 | order_nos = [] 431 | for order_info in success: 432 | order_nos.append(order_info["id"]) 433 | return order_nos, None 434 | 435 | def _update_order(self, order_info): 436 | """ 更新订单信息 437 | @param order_info 订单信息 438 | * NOTE: 439 | order-state: 订单状态, submitting , submitted 已提交, partial-filled 部分成交, partial-canceled 部分成交撤销, 440 | filled 完全成交, canceled 已撤销 441 | """ 442 | order_no = str(order_info["order-id"]) 443 | action = ORDER_ACTION_BUY if order_info["order-type"] in ["buy-market", "buy-limit"] else ORDER_ACTION_SELL 444 | state = order_info["order-state"] 445 | remain = "%.8f" % float(order_info["unfilled-amount"]) 446 | avg_price = "%.8f" % float(order_info["price"]) 447 | ctime = order_info["created-at"] 448 | utime = order_info["utime"] 449 | 450 | if state == "canceled": 451 | status = ORDER_STATUS_CANCELED 452 | elif state == "partial-canceled": 453 | status = ORDER_STATUS_CANCELED 454 | elif state == "submitting": 455 | status = ORDER_STATUS_SUBMITTED 456 | elif state == "submitted": 457 | status = ORDER_STATUS_SUBMITTED 458 | elif state == "partial-filled": 459 | status = ORDER_STATUS_PARTIAL_FILLED 460 | elif state == "filled": 461 | status = ORDER_STATUS_FILLED 462 | else: 463 | logger.error("status error! order_info:", order_info, caller=self) 464 | return None 465 | 466 | order = self._orders.get(order_no) 467 | if not order: 468 | info = { 469 | "platform": self._platform, 470 | "account": self._account, 471 | "strategy": self._strategy, 472 | "order_no": order_no, 473 | "action": action, 474 | "symbol": self._symbol, 475 | "price": "%.8f" % float(order_info["order-price"]), 476 | "quantity": "%.8f" % float(order_info["order-amount"]), 477 | "remain": remain, 478 | "status": status 479 | } 480 | order = Order(**info) 481 | self._orders[order_no] = order 482 | order.remain = remain 483 | order.status = status 484 | order.avg_price = avg_price 485 | order.ctime = ctime 486 | order.utime = utime 487 | 488 | SingleTask.run(self._order_update_callback, copy.copy(order)) 489 | if status in [ORDER_STATUS_FAILED, ORDER_STATUS_CANCELED, ORDER_STATUS_FILLED]: 490 | self._orders.pop(order_no) 491 | 492 | async def on_event_asset_update(self, asset: Asset): 493 | """ 资产数据更新回调 494 | """ 495 | self._assets = asset 496 | SingleTask.run(self._asset_update_callback, asset) 497 | -------------------------------------------------------------------------------- /quant/platform/okex.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | OKEx Trade module. 5 | https://www.okex.me/docs/zh/ 6 | 7 | Author: HuangTao 8 | Date: 2019/01/19 9 | Email: huangtao@ifclover.com 10 | """ 11 | 12 | import time 13 | import json 14 | import copy 15 | import hmac 16 | import zlib 17 | import base64 18 | from urllib.parse import urljoin 19 | 20 | from quant.error import Error 21 | from quant.utils import tools 22 | from quant.utils import logger 23 | from quant.const import OKEX 24 | from quant.order import Order 25 | from quant.tasks import SingleTask, LoopRunTask 26 | from quant.asset import Asset, AssetSubscribe 27 | from quant.utils.decorator import async_method_locker 28 | from quant.utils.web import Websocket, AsyncHttpRequests 29 | from quant.order import ORDER_ACTION_BUY, ORDER_ACTION_SELL 30 | from quant.order import ORDER_TYPE_LIMIT, ORDER_TYPE_MARKET 31 | from quant.order import ORDER_STATUS_SUBMITTED, ORDER_STATUS_PARTIAL_FILLED, ORDER_STATUS_FILLED, \ 32 | ORDER_STATUS_CANCELED, ORDER_STATUS_FAILED 33 | 34 | 35 | __all__ = ("OKExRestAPI", "OKExTrade", ) 36 | 37 | 38 | class OKExRestAPI: 39 | """ OKEx REST API client. 40 | 41 | Attributes: 42 | host: HTTP request host. 43 | access_key: Account's ACCESS KEY. 44 | secret_key: Account's SECRET KEY. 45 | passphrase: API KEY Passphrase. 46 | """ 47 | 48 | def __init__(self, host, access_key, secret_key, passphrase): 49 | """initialize.""" 50 | self._host = host 51 | self._access_key = access_key 52 | self._secret_key = secret_key 53 | self._passphrase = passphrase 54 | 55 | async def get_user_account(self): 56 | """ Get account asset information. 57 | 58 | Returns: 59 | success: Success results, otherwise it's None. 60 | error: Error information, otherwise it's None. 61 | """ 62 | result, error = await self.request("GET", "/api/spot/v3/accounts", auth=True) 63 | return result, error 64 | 65 | async def create_order(self, action, symbol, price, quantity, order_type=ORDER_TYPE_LIMIT, client_oid=None): 66 | """ Create an order. 67 | Args: 68 | action: Action type, `BUY` or `SELL`. 69 | symbol: Trading pair, e.g. `BTCUSDT`. 70 | price: Order price. 71 | quantity: Order quantity. 72 | order_type: Order type, `MARKET` or `LIMIT`. 73 | 74 | Returns: 75 | success: Success results, otherwise it's None. 76 | error: Error information, otherwise it's None. 77 | """ 78 | info = { 79 | "side": "buy" if action == ORDER_ACTION_BUY else "sell", 80 | "instrument_id": symbol, 81 | "margin_trading": 1 82 | } 83 | if order_type == ORDER_TYPE_LIMIT: 84 | info["type"] = "limit" 85 | info["price"] = price 86 | info["size"] = quantity 87 | elif order_type == ORDER_TYPE_MARKET: 88 | info["type"] = "market" 89 | if action == ORDER_ACTION_BUY: 90 | info["notional"] = quantity # buy price. 91 | else: 92 | info["size"] = quantity # sell quantity. 93 | else: 94 | logger.error("order_type error! order_type:", order_type, caller=self) 95 | return None 96 | if client_oid: 97 | info["client_oid"] = client_oid 98 | result, error = await self.request("POST", "/api/spot/v3/orders", body=info, auth=True) 99 | return result, error 100 | 101 | async def revoke_order(self, symbol, order_no): 102 | """ Cancelling an unfilled order. 103 | Args: 104 | symbol: Trading pair, e.g. BTCUSDT. 105 | order_no: order ID. 106 | 107 | Returns: 108 | success: Success results, otherwise it's None. 109 | error: Error information, otherwise it's None. 110 | """ 111 | body = { 112 | "instrument_id": symbol 113 | } 114 | uri = "/api/spot/v3/cancel_orders/{order_no}".format(order_no=order_no) 115 | result, error = await self.request("POST", uri, body=body, auth=True) 116 | if error: 117 | return order_no, error 118 | if result["result"]: 119 | return order_no, None 120 | return order_no, result 121 | 122 | async def revoke_orders(self, symbol, order_nos): 123 | """ Cancelling multiple open orders with order_id,Maximum 10 orders can be cancelled at a time for each 124 | trading pair. 125 | 126 | Args: 127 | symbol: Trading pair, e.g. BTCUSDT. 128 | order_nos: order IDs. 129 | 130 | Returns: 131 | success: Success results, otherwise it's None. 132 | error: Error information, otherwise it's None. 133 | """ 134 | if len(order_nos) > 10: 135 | logger.warn("only revoke 10 orders per request!", caller=self) 136 | body = [ 137 | { 138 | "instrument_id": symbol, 139 | "order_ids": order_nos[:10] 140 | } 141 | ] 142 | result, error = await self.request("POST", "/api/spot/v3/cancel_batch_orders", body=body, auth=True) 143 | return result, error 144 | 145 | async def get_open_orders(self, symbol, limit=100): 146 | """ Get order details by order ID. 147 | 148 | Args: 149 | symbol: Trading pair, e.g. BTCUSDT. 150 | limit: order count to return, max is 100, default is 100. 151 | 152 | Returns: 153 | success: Success results, otherwise it's None. 154 | error: Error information, otherwise it's None. 155 | """ 156 | uri = "/api/spot/v3/orders_pending" 157 | params = { 158 | "instrument_id": symbol, 159 | "limit": limit 160 | } 161 | result, error = await self.request("GET", uri, params=params, auth=True) 162 | return result, error 163 | 164 | async def get_order_status(self, symbol, order_no): 165 | """ Get order status. 166 | Args: 167 | symbol: Trading pair, e.g. BTCUSDT. 168 | order_no: order ID. 169 | 170 | Returns: 171 | success: Success results, otherwise it's None. 172 | error: Error information, otherwise it's None. 173 | """ 174 | params = { 175 | "instrument_id": symbol 176 | } 177 | uri = "/api/spot/v3/orders/{order_no}".format(order_no=order_no) 178 | result, error = await self.request("GET", uri, params=params, auth=True) 179 | return result, error 180 | 181 | async def request(self, method, uri, params=None, body=None, headers=None, auth=False): 182 | """ Do HTTP request. 183 | 184 | Args: 185 | method: HTTP request method. GET, POST, DELETE, PUT. 186 | uri: HTTP request uri. 187 | params: HTTP query params. 188 | body: HTTP request body. 189 | headers: HTTP request headers. 190 | auth: If this request requires authentication. 191 | 192 | Returns: 193 | success: Success results, otherwise it's None. 194 | error: Error information, otherwise it's None. 195 | """ 196 | if params: 197 | query = "&".join(["{}={}".format(k, params[k]) for k in sorted(params.keys())]) 198 | uri += "?" + query 199 | url = urljoin(self._host, uri) 200 | 201 | if auth: 202 | timestamp = str(time.time()).split(".")[0] + "." + str(time.time()).split(".")[1][:3] 203 | if body: 204 | body = json.dumps(body) 205 | else: 206 | body = "" 207 | message = str(timestamp) + str.upper(method) + uri + str(body) 208 | mac = hmac.new(bytes(self._secret_key, encoding="utf8"), bytes(message, encoding="utf-8"), 209 | digestmod="sha256") 210 | d = mac.digest() 211 | sign = base64.b64encode(d) 212 | 213 | if not headers: 214 | headers = {} 215 | headers["Content-Type"] = "application/json" 216 | headers["OK-ACCESS-KEY"] = self._access_key.encode().decode() 217 | headers["OK-ACCESS-SIGN"] = sign.decode() 218 | headers["OK-ACCESS-TIMESTAMP"] = str(timestamp) 219 | headers["OK-ACCESS-PASSPHRASE"] = self._passphrase 220 | _, success, error = await AsyncHttpRequests.fetch(method, url, body=body, headers=headers, timeout=10) 221 | return success, error 222 | 223 | 224 | class OKExTrade: 225 | """ OKEx Trade module. You can initialize trade object with some attributes in kwargs. 226 | 227 | Attributes: 228 | account: Account name for this trade exchange. 229 | strategy: What's name would you want to created for you strategy. 230 | symbol: Symbol name for your trade. 231 | host: HTTP request host. (default "https://www.okex.com") 232 | wss: Websocket address. (default "wss://real.okex.com:8443") 233 | access_key: Account's ACCESS KEY. 234 | secret_key Account's SECRET KEY. 235 | passphrase API KEY Passphrase. 236 | asset_update_callback: You can use this param to specific a async callback function when you initializing Trade 237 | object. `asset_update_callback` is like `async def on_asset_update_callback(asset: Asset): pass` and this 238 | callback function will be executed asynchronous when received AssetEvent. 239 | order_update_callback: You can use this param to specific a async callback function when you initializing Trade 240 | object. `order_update_callback` is like `async def on_order_update_callback(order: Order): pass` and this 241 | callback function will be executed asynchronous when some order state updated. 242 | init_success_callback: You can use this param to specific a async callback function when you initializing Trade 243 | object. `init_success_callback` is like `async def on_init_success_callback(success: bool, error: Error, **kwargs): pass` 244 | and this callback function will be executed asynchronous after Trade module object initialized successfully. 245 | """ 246 | 247 | def __init__(self, **kwargs): 248 | """Initialize.""" 249 | e = None 250 | if not kwargs.get("account"): 251 | e = Error("param account miss") 252 | if not kwargs.get("strategy"): 253 | e = Error("param strategy miss") 254 | if not kwargs.get("symbol"): 255 | e = Error("param symbol miss") 256 | if not kwargs.get("host"): 257 | kwargs["host"] = "https://www.okex.com" 258 | if not kwargs.get("wss"): 259 | kwargs["wss"] = "wss://real.okex.com:8443" 260 | if not kwargs.get("access_key"): 261 | e = Error("param access_key miss") 262 | if not kwargs.get("secret_key"): 263 | e = Error("param secret_key miss") 264 | if not kwargs.get("passphrase"): 265 | e = Error("param passphrase miss") 266 | if e: 267 | logger.error(e, caller=self) 268 | SingleTask.run(kwargs["init_success_callback"], False, e) 269 | return 270 | 271 | self._account = kwargs["account"] 272 | self._strategy = kwargs["strategy"] 273 | self._platform = OKEX 274 | self._symbol = kwargs["symbol"] 275 | self._host = kwargs["host"] 276 | self._wss = kwargs["wss"] 277 | self._access_key = kwargs["access_key"] 278 | self._secret_key = kwargs["secret_key"] 279 | self._passphrase = kwargs["passphrase"] 280 | self._asset_update_callback = kwargs.get("asset_update_callback") 281 | self._order_update_callback = kwargs.get("order_update_callback") 282 | self._init_success_callback = kwargs.get("init_success_callback") 283 | 284 | self._raw_symbol = self._symbol.replace("/", "-") 285 | self._order_channel = "spot/order:{symbol}".format(symbol=self._raw_symbol) 286 | 287 | url = self._wss + "/ws/v3" 288 | self._ws = Websocket(url, self.connected_callback, process_binary_callback=self.process_binary) 289 | self.heartbeat_msg = "ping" 290 | 291 | self._assets = {} # Asset object. e.g. {"BTC": {"free": "1.1", "locked": "2.2", "total": "3.3"}, ... } 292 | self._orders = {} # Order objects. e.g. {"order_no": Order, ... } 293 | 294 | # Initializing our REST API client. 295 | self._rest_api = OKExRestAPI(self._host, self._access_key, self._secret_key, self._passphrase) 296 | 297 | # Subscribing AssetEvent. 298 | if self._asset_update_callback: 299 | AssetSubscribe(self._platform, self._account, self.on_event_asset_update) 300 | 301 | LoopRunTask.register(self._send_heartbeat_msg, 10) 302 | 303 | @property 304 | def assets(self): 305 | return copy.copy(self._assets) 306 | 307 | @property 308 | def orders(self): 309 | return copy.copy(self._orders) 310 | 311 | @property 312 | def rest_api(self): 313 | return self._rest_api 314 | 315 | async def _send_heartbeat_msg(self, *args, **kwargs): 316 | msg = "ping" 317 | await self._ws.send(msg) 318 | 319 | async def connected_callback(self): 320 | """After websocket connection created successfully, we will send a message to server for authentication.""" 321 | timestamp = str(time.time()).split(".")[0] + "." + str(time.time()).split(".")[1][:3] 322 | message = str(timestamp) + "GET" + "/users/self/verify" 323 | mac = hmac.new(bytes(self._secret_key, encoding="utf8"), bytes(message, encoding="utf8"), digestmod="sha256") 324 | d = mac.digest() 325 | signature = base64.b64encode(d).decode() 326 | data = { 327 | "op": "login", 328 | "args": [self._access_key, self._passphrase, timestamp, signature] 329 | } 330 | await self._ws.send(data) 331 | 332 | @async_method_locker("OKExTrade.process_binary.locker") 333 | async def process_binary(self, raw): 334 | """ Process binary message that received from websocket. 335 | 336 | Args: 337 | raw: Binary message received from websocket. 338 | 339 | Returns: 340 | None. 341 | """ 342 | decompress = zlib.decompressobj(-zlib.MAX_WBITS) 343 | msg = decompress.decompress(raw) 344 | msg += decompress.flush() 345 | msg = msg.decode() 346 | if msg == "pong": 347 | return 348 | logger.debug("msg:", msg, caller=self) 349 | msg = json.loads(msg) 350 | 351 | # Authorization message received. 352 | if msg.get("event") == "login": 353 | if not msg.get("success"): 354 | e = Error("Websocket connection authorized failed: {}".format(msg)) 355 | logger.error(e, caller=self) 356 | SingleTask.run(self._init_success_callback, False, e) 357 | return 358 | logger.info("Websocket connection authorized successfully.", caller=self) 359 | 360 | # Fetch orders from server. (open + partially filled) 361 | order_infos, error = await self._rest_api.get_open_orders(self._raw_symbol) 362 | if error: 363 | e = Error("get open orders error: {}".format(msg)) 364 | SingleTask.run(self._init_success_callback, False, e) 365 | return 366 | 367 | if len(order_infos) > 100: 368 | logger.warn("order length too long! (more than 100)", caller=self) 369 | for order_info in order_infos: 370 | order_info["ctime"] = order_info["created_at"] 371 | order_info["utime"] = order_info["timestamp"] 372 | self._update_order(order_info) 373 | 374 | # Subscribe order channel. 375 | data = { 376 | "op": "subscribe", 377 | "args": [self._order_channel] 378 | } 379 | await self._ws.send(data) 380 | return 381 | 382 | # Subscribe response message received. 383 | if msg.get("event") == "subscribe": 384 | if msg.get("channel") == self._order_channel: 385 | SingleTask.run(self._init_success_callback, True, None) 386 | else: 387 | e = Error("subscribe order event error: {}".format(msg)) 388 | SingleTask.run(self._init_success_callback, False, e) 389 | return 390 | 391 | # Order update message received. 392 | if msg.get("table") == "spot/order": 393 | for data in msg["data"]: 394 | data["ctime"] = data["timestamp"] 395 | data["utime"] = data["last_fill_time"] 396 | self._update_order(data) 397 | 398 | async def create_order(self, action, price, quantity, order_type=ORDER_TYPE_LIMIT, *args, **kwargs): 399 | """ Create an order. 400 | 401 | Args: 402 | action: Trade direction, `BUY` or `SELL`. 403 | price: Price of each contract. 404 | quantity: The buying or selling quantity. 405 | order_type: Order type, `MARKET` or `LIMIT`. 406 | 407 | Returns: 408 | order_no: Order ID if created successfully, otherwise it's None. 409 | error: Error information, otherwise it's None. 410 | """ 411 | price = tools.float_to_str(price) 412 | quantity = tools.float_to_str(quantity) 413 | client_order_id = kwargs.get("client_order_id") 414 | result, error = await self._rest_api.create_order(action, self._raw_symbol, price, quantity, order_type, 415 | client_order_id) 416 | if error: 417 | return None, error 418 | if not result["result"]: 419 | return None, result 420 | return result["order_id"], None 421 | 422 | async def revoke_order(self, *order_nos): 423 | """ Revoke (an) order(s). 424 | 425 | Args: 426 | order_nos: Order id list, you can set this param to 0 or multiple items. If you set 0 param, you can cancel 427 | all orders for this symbol(initialized in Trade object). If you set 1 param, you can cancel an order. 428 | If you set multiple param, you can cancel multiple orders. Do not set param length more than 100. 429 | 430 | Returns: 431 | Success or error, see bellow. 432 | 433 | NOTEs: 434 | DO NOT INPUT MORE THAT 10 ORDER NOs, you can invoke many times. 435 | """ 436 | # If len(order_nos) == 0, you will cancel all orders for this symbol(initialized in Trade object). 437 | if len(order_nos) == 0: 438 | order_infos, error = await self._rest_api.get_open_orders(self._raw_symbol) 439 | if error: 440 | return False, error 441 | if len(order_infos) > 100: 442 | logger.warn("order length too long! (more than 100)", caller=self) 443 | for order_info in order_infos: 444 | order_no = order_info["order_id"] 445 | _, error = await self._rest_api.revoke_order(self._raw_symbol, order_no) 446 | if error: 447 | return False, error 448 | return True, None 449 | 450 | # If len(order_nos) == 1, you will cancel an order. 451 | if len(order_nos) == 1: 452 | success, error = await self._rest_api.revoke_order(self._raw_symbol, order_nos[0]) 453 | if error: 454 | return order_nos[0], error 455 | else: 456 | return order_nos[0], None 457 | 458 | # If len(order_nos) > 1, you will cancel multiple orders. 459 | if len(order_nos) > 1: 460 | success, error = [], [] 461 | for order_no in order_nos: 462 | _, e = await self._rest_api.revoke_order(self._raw_symbol, order_no) 463 | if e: 464 | error.append((order_no, e)) 465 | else: 466 | success.append(order_no) 467 | return success, error 468 | 469 | async def get_open_order_nos(self): 470 | """ Get open order id list. 471 | 472 | Args: 473 | None. 474 | 475 | Returns: 476 | order_nos: Open order id list, otherwise it's None. 477 | error: Error information, otherwise it's None. 478 | """ 479 | success, error = await self._rest_api.get_open_orders(self._raw_symbol) 480 | if error: 481 | return None, error 482 | else: 483 | if len(success) > 100: 484 | logger.warn("order length too long! (more than 100)", caller=self) 485 | order_nos = [] 486 | for order_info in success: 487 | order_nos.append(order_info["order_id"]) 488 | return order_nos, None 489 | 490 | def _update_order(self, order_info): 491 | """ Order update. 492 | 493 | Args: 494 | order_info: Order information. 495 | 496 | Returns: 497 | None. 498 | """ 499 | order_no = str(order_info["order_id"]) 500 | state = order_info["state"] 501 | remain = float(order_info["size"]) - float(order_info["filled_size"]) 502 | ctime = tools.utctime_str_to_mts(order_info["ctime"]) 503 | utime = tools.utctime_str_to_mts(order_info["utime"]) 504 | 505 | if state == "-2": 506 | status = ORDER_STATUS_FAILED 507 | elif state == "-1": 508 | status = ORDER_STATUS_CANCELED 509 | elif state == "0": 510 | status = ORDER_STATUS_SUBMITTED 511 | elif state == "1": 512 | status = ORDER_STATUS_PARTIAL_FILLED 513 | elif state == "2": 514 | status = ORDER_STATUS_FILLED 515 | else: 516 | logger.error("status error! order_info:", order_info, caller=self) 517 | return None 518 | 519 | order = self._orders.get(order_no) 520 | if order: 521 | order.remain = remain 522 | order.status = status 523 | order.price = order_info["price"] 524 | else: 525 | info = { 526 | "platform": self._platform, 527 | "account": self._account, 528 | "strategy": self._strategy, 529 | "order_no": order_no, 530 | "client_order_id": order_info["client_oid"], 531 | "action": ORDER_ACTION_BUY if order_info["side"] == "buy" else ORDER_ACTION_SELL, 532 | "symbol": self._symbol, 533 | "price": order_info["price"], 534 | "quantity": order_info["size"], 535 | "remain": remain, 536 | "status": status, 537 | "avg_price": order_info["price"] 538 | } 539 | order = Order(**info) 540 | self._orders[order_no] = order 541 | order.ctime = ctime 542 | order.utime = utime 543 | 544 | SingleTask.run(self._order_update_callback, copy.copy(order)) 545 | 546 | if status in [ORDER_STATUS_FAILED, ORDER_STATUS_CANCELED, ORDER_STATUS_FILLED]: 547 | self._orders.pop(order_no) 548 | 549 | async def on_event_asset_update(self, asset: Asset): 550 | """ Asset event data callback. 551 | 552 | Args: 553 | asset: Asset object callback from EventCenter. 554 | 555 | Returns: 556 | None. 557 | """ 558 | self._assets = asset 559 | SingleTask.run(self._asset_update_callback, asset) 560 | -------------------------------------------------------------------------------- /quant/position.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | 持仓对象 5 | 6 | Author: HuangTao 7 | Date: 2018/04/22 8 | """ 9 | 10 | from quant.utils import tools 11 | 12 | 13 | class Position: 14 | """ 持仓对象 15 | """ 16 | 17 | def __init__(self, platform=None, account=None, strategy=None, symbol=None): 18 | """ 初始化持仓对象 19 | @param platform 交易平台 20 | @param account 账户 21 | @param strategy 策略名称 22 | @param symbol 合约名称 23 | """ 24 | self.platform = platform 25 | self.account = account 26 | self.strategy = strategy 27 | self.symbol = symbol 28 | self.short_quantity = 0 # 空仓数量 29 | self.short_avg_price = 0 # 空仓持仓平均价格 30 | self.long_quantity = 0 # 多仓数量 31 | self.long_avg_price = 0 # 多仓持仓平均价格 32 | self.liquid_price = 0 # 预估爆仓价格 33 | self.utime = None # 更新时间戳 34 | 35 | def update(self, short_quantity=0, short_avg_price=0, long_quantity=0, long_avg_price=0, liquid_price=0, 36 | utime=None): 37 | self.short_quantity = short_quantity 38 | self.short_avg_price = short_avg_price 39 | self.long_quantity = long_quantity 40 | self.long_avg_price = long_avg_price 41 | self.liquid_price = liquid_price 42 | self.utime = utime if utime else tools.get_cur_timestamp_ms() 43 | 44 | def __str__(self): 45 | info = "[platform: {platform}, account: {account}, strategy: {strategy}, symbol: {symbol}, " \ 46 | "short_quantity: {short_quantity}, short_avg_price: {short_avg_price}, " \ 47 | "long_quantity: {long_quantity}, long_avg_price: {long_avg_price}, liquid_price: {liquid_price}, " \ 48 | "utime: {utime}]"\ 49 | .format(platform=self.platform, account=self.account, strategy=self.strategy, symbol=self.symbol, 50 | short_quantity=self.short_quantity, short_avg_price=self.short_avg_price, 51 | long_quantity=self.long_quantity, long_avg_price=self.long_avg_price, 52 | liquid_price=self.liquid_price, utime=self.utime) 53 | return info 54 | 55 | def __repr__(self): 56 | return str(self) 57 | -------------------------------------------------------------------------------- /quant/quant.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | Asynchronous driven quantitative trading framework. 5 | 6 | Author: HuangTao 7 | Date: 2017/04/26 8 | Email: huangtao@ifclover.com 9 | """ 10 | 11 | import signal 12 | import asyncio 13 | 14 | from quant.utils import logger 15 | from quant.config import config 16 | 17 | 18 | class Quant: 19 | """ Asynchronous driven quantitative trading framework. 20 | """ 21 | 22 | def __init__(self): 23 | self.loop = None 24 | self.event_center = None 25 | 26 | def initialize(self, config_module=None): 27 | """ Initialize. 28 | 29 | Args: 30 | config_module: config file path, normally it"s a json file. 31 | """ 32 | self._get_event_loop() 33 | self._load_settings(config_module) 34 | self._init_logger() 35 | self._init_event_center() 36 | self._do_heartbeat() 37 | 38 | def start(self): 39 | """Start the event loop.""" 40 | def keyboard_interrupt(s, f): 41 | print("KeyboardInterrupt (ID: {}) has been caught. Cleaning up...".format(s)) 42 | self.loop.stop() 43 | signal.signal(signal.SIGINT, keyboard_interrupt) 44 | 45 | logger.info("start io loop ...", caller=self) 46 | self.loop.run_forever() 47 | 48 | def stop(self): 49 | """Stop the event loop.""" 50 | logger.info("stop io loop.", caller=self) 51 | self.loop.stop() 52 | 53 | def _get_event_loop(self): 54 | """ Get a main io loop. """ 55 | if not self.loop: 56 | self.loop = asyncio.get_event_loop() 57 | return self.loop 58 | 59 | def _load_settings(self, config_module): 60 | """ Load config settings. 61 | 62 | Args: 63 | config_module: config file path, normally it"s a json file. 64 | """ 65 | config.loads(config_module) 66 | 67 | def _init_logger(self): 68 | """Initialize logger.""" 69 | console = config.log.get("console", True) 70 | level = config.log.get("level", "DEBUG") 71 | path = config.log.get("path", "/tmp/logs/Quant") 72 | name = config.log.get("name", "quant.log") 73 | clear = config.log.get("clear", False) 74 | backup_count = config.log.get("backup_count", 0) 75 | if console: 76 | logger.initLogger(level) 77 | else: 78 | logger.initLogger(level, path, name, clear, backup_count) 79 | 80 | def _init_event_center(self): 81 | """Initialize event center.""" 82 | if config.rabbitmq: 83 | from quant.event import EventCenter 84 | self.event_center = EventCenter() 85 | self.loop.run_until_complete(self.event_center.connect()) 86 | 87 | def _do_heartbeat(self): 88 | """Start server heartbeat.""" 89 | from quant.heartbeat import heartbeat 90 | self.loop.call_later(0.5, heartbeat.ticker) 91 | 92 | 93 | quant = Quant() 94 | -------------------------------------------------------------------------------- /quant/tasks.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | Tasks module. 5 | 1. Register a loop run task: 6 | a) assign a asynchronous callback function; 7 | b) assign a execute interval time(seconds), default is 1s. 8 | c) assign some input params like `*args, **kwargs`; 9 | 2. Register a single task to run: 10 | a) Create a coroutine and execute immediately. 11 | b) Create a coroutine and delay execute, delay time is seconds, default delay time is 0s. 12 | 13 | Author: HuangTao 14 | Date: 2018/04/26 15 | Email: huangtao@ifclover.com 16 | """ 17 | 18 | import asyncio 19 | import inspect 20 | 21 | from quant.heartbeat import heartbeat 22 | 23 | __all__ = ("LoopRunTask", "SingleTask") 24 | 25 | 26 | class LoopRunTask(object): 27 | """ Loop run task. 28 | """ 29 | 30 | @classmethod 31 | def register(cls, func, interval=1, *args, **kwargs): 32 | """ Register a loop run. 33 | 34 | Args: 35 | func: Asynchronous callback function. 36 | interval: execute interval time(seconds), default is 1s. 37 | 38 | Returns: 39 | task_id: Task id. 40 | """ 41 | task_id = heartbeat.register(func, interval, *args, **kwargs) 42 | return task_id 43 | 44 | @classmethod 45 | def unregister(cls, task_id): 46 | """ Unregister a loop run task. 47 | 48 | Args: 49 | task_id: Task id. 50 | """ 51 | heartbeat.unregister(task_id) 52 | 53 | 54 | class SingleTask: 55 | """ Single run task. 56 | """ 57 | 58 | @classmethod 59 | def run(cls, func, *args, **kwargs): 60 | """ Create a coroutine and execute immediately. 61 | 62 | Args: 63 | func: Asynchronous callback function. 64 | """ 65 | asyncio.get_event_loop().create_task(func(*args, **kwargs)) 66 | 67 | @classmethod 68 | def call_later(cls, func, delay=0, *args, **kwargs): 69 | """ Create a coroutine and delay execute, delay time is seconds, default delay time is 0s. 70 | 71 | Args: 72 | func: Asynchronous callback function. 73 | delay: Delay time is seconds, default delay time is 0, you can assign a float e.g. 0.5, 2.3, 5.1 ... 74 | """ 75 | if not inspect.iscoroutinefunction(func): 76 | asyncio.get_event_loop().call_later(delay, func, *args) 77 | else: 78 | def foo(f, *args, **kwargs): 79 | asyncio.get_event_loop().create_task(f(*args, **kwargs)) 80 | asyncio.get_event_loop().call_later(delay, foo, func, *args) 81 | -------------------------------------------------------------------------------- /quant/trade.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | Trade Module. 5 | 6 | Author: HuangTao 7 | Date: 2019/04/21 8 | Email: huangtao@ifclover.com 9 | """ 10 | 11 | import copy 12 | 13 | from quant import const 14 | from quant.utils import tools 15 | from quant.error import Error 16 | from quant.utils import logger 17 | from quant.tasks import SingleTask 18 | from quant.order import ORDER_TYPE_LIMIT 19 | from quant.order import Order 20 | from quant.position import Position 21 | 22 | 23 | class Trade: 24 | """ Trade Module. 25 | 26 | Attributes: 27 | strategy: What's name would you want to created for your strategy. 28 | platform: Exchange platform name. e.g. `binance` / `okex` / `bitmex`. 29 | symbol: Symbol name for your trade. e.g. `BTC/USDT`. 30 | host: HTTP request host. 31 | wss: Websocket address. 32 | account: Account name for this trade exchange. 33 | access_key: Account's ACCESS KEY. 34 | secret_key: Account's SECRET KEY. 35 | passphrase: API KEY Passphrase. (Only for `OKEx`) 36 | asset_update_callback: You can use this param to specific a async callback function when you initializing Trade 37 | object. `asset_update_callback` is like `async def on_asset_update_callback(asset: Asset): pass` and this 38 | callback function will be executed asynchronous when received AssetEvent. 39 | order_update_callback: You can use this param to specific a async callback function when you initializing Trade 40 | object. `order_update_callback` is like `async def on_order_update_callback(order: Order): pass` and this 41 | callback function will be executed asynchronous when some order state updated. 42 | position_update_callback: You can use this param to specific a async callback function when you initializing 43 | Trade object. `position_update_callback` is like `async def on_position_update_callback(position: Position): pass` 44 | and this callback function will be executed asynchronous when position updated. 45 | init_success_callback: You can use this param to specific a async callback function when you initializing Trade 46 | object. `init_success_callback` is like `async def on_init_success_callback(success: bool, error: Error, **kwargs): pass` 47 | and this callback function will be executed asynchronous after Trade module object initialized successfully. 48 | """ 49 | 50 | def __init__(self, strategy=None, platform=None, symbol=None, host=None, wss=None, account=None, access_key=None, 51 | secret_key=None, passphrase=None, asset_update_callback=None, order_update_callback=None, 52 | position_update_callback=None, init_success_callback=None, **kwargs): 53 | """initialize trade object.""" 54 | kwargs["strategy"] = strategy 55 | kwargs["platform"] = platform 56 | kwargs["symbol"] = symbol 57 | kwargs["host"] = host 58 | kwargs["wss"] = wss 59 | kwargs["account"] = account 60 | kwargs["access_key"] = access_key 61 | kwargs["secret_key"] = secret_key 62 | kwargs["passphrase"] = passphrase 63 | kwargs["asset_update_callback"] = asset_update_callback 64 | kwargs["order_update_callback"] = self._on_order_update_callback 65 | kwargs["position_update_callback"] = self._on_position_update_callback 66 | kwargs["init_success_callback"] = self._on_init_success_callback 67 | 68 | self._raw_params = copy.copy(kwargs) 69 | self._order_update_callback = order_update_callback 70 | self._position_update_callback = position_update_callback 71 | self._init_success_callback = init_success_callback 72 | 73 | if platform == const.OKEX: 74 | from quant.platform.okex import OKExTrade as T 75 | elif platform == const.BINANCE: 76 | from quant.platform.binance import BinanceTrade as T 77 | elif platform == const.HUOBI: 78 | from quant.platform.huobi import HuobiTrade as T 79 | else: 80 | logger.error("platform error:", platform, caller=self) 81 | e = Error("platform error") 82 | SingleTask.run(self._init_success_callback, False, e) 83 | return 84 | kwargs.pop("platform") 85 | self._t = T(**kwargs) 86 | 87 | @property 88 | def assets(self): 89 | return self._t.assets 90 | 91 | @property 92 | def orders(self): 93 | return self._t.orders 94 | 95 | @property 96 | def position(self): 97 | return self._t.position 98 | 99 | @property 100 | def rest_api(self): 101 | return self._t.rest_api 102 | 103 | async def create_order(self, action, price, quantity, order_type=ORDER_TYPE_LIMIT, *args, **kwargs): 104 | """ Create an order. 105 | 106 | Args: 107 | action: Trade direction, `BUY` or `SELL`. 108 | price: Price of each contract. 109 | quantity: The buying or selling quantity. 110 | order_type: Specific type of order, `LIMIT` or `MARKET`. (default is `LIMIT`) 111 | 112 | Returns: 113 | order_no: Order ID if created successfully, otherwise it's None. 114 | error: Error information, otherwise it's None. 115 | """ 116 | if not kwargs.get("client_order_id"): 117 | kwargs["client_order_id"] = tools.get_uuid1().replace("-", "") 118 | order_no, error = await self._t.create_order(action, price, quantity, order_type, **kwargs) 119 | return order_no, error 120 | 121 | async def revoke_order(self, *order_nos): 122 | """ Revoke (an) order(s). 123 | 124 | Args: 125 | order_nos: Order id list, you can set this param to 0 or multiple items. If you set 0 param, you can cancel 126 | all orders for this symbol(initialized in Trade object). If you set 1 param, you can cancel an order. 127 | If you set multiple param, you can cancel multiple orders. Do not set param length more than 100. 128 | 129 | Returns: 130 | success: If execute successfully, return success information, otherwise it's None. 131 | error: If execute failed, return error information, otherwise it's None. 132 | """ 133 | success, error = await self._t.revoke_order(*order_nos) 134 | return success, error 135 | 136 | async def get_open_order_nos(self): 137 | """ Get open order id list. 138 | 139 | Args: 140 | None. 141 | 142 | Returns: 143 | order_nos: Open order id list, otherwise it's None. 144 | error: Error information, otherwise it's None. 145 | """ 146 | result, error = await self._t.get_open_order_nos() 147 | return result, error 148 | 149 | async def _on_order_update_callback(self, order: Order): 150 | """ Order information update callback. 151 | 152 | Args: 153 | order: Order object. 154 | """ 155 | if self._order_update_callback: 156 | SingleTask.run(self._order_update_callback, order) 157 | 158 | async def _on_position_update_callback(self, position: Position): 159 | """ Position information update callback. 160 | 161 | Args: 162 | position: Position object. 163 | """ 164 | if self._position_update_callback: 165 | SingleTask.run(self._position_update_callback, position) 166 | 167 | async def _on_init_success_callback(self, success: bool, error: Error): 168 | """ Callback function when initialize Trade module finished. 169 | 170 | Args: 171 | success: `True` if initialize Trade module success, otherwise `False`. 172 | error: `Error object` if initialize Trade module failed, otherwise `None`. 173 | """ 174 | if self._init_success_callback: 175 | params = { 176 | "strategy": self._raw_params["strategy"], 177 | "platform": self._raw_params["platform"], 178 | "symbol": self._raw_params["symbol"], 179 | "account": self._raw_params["account"] 180 | } 181 | await self._init_success_callback(success, error, **params) 182 | -------------------------------------------------------------------------------- /quant/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpim/thenextquant/beb65b1c0590174c4cd90a2ac2a60194b73e1934/quant/utils/__init__.py -------------------------------------------------------------------------------- /quant/utils/decorator.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | Decorator. 5 | 6 | Author: HuangTao 7 | Date: 2018/08/03 8 | Email: Huangtao@ifclover.com 9 | """ 10 | 11 | import asyncio 12 | import functools 13 | 14 | 15 | # Coroutine lockers. e.g. {"locker_name": locker} 16 | METHOD_LOCKERS = {} 17 | 18 | 19 | def async_method_locker(name, wait=True): 20 | """ In order to share memory between any asynchronous coroutine methods, we should use locker to lock our method, 21 | so that we can avoid some un-prediction actions. 22 | 23 | Args: 24 | name: Locker name. 25 | wait: If waiting to be executed when the locker is locked? if True, waiting until to be executed, else return 26 | immediately (do not execute). 27 | 28 | NOTE: 29 | This decorator must to be used on `async method`. 30 | """ 31 | assert isinstance(name, str) 32 | 33 | def decorating_function(method): 34 | global METHOD_LOCKERS 35 | locker = METHOD_LOCKERS.get(name) 36 | if not locker: 37 | locker = asyncio.Lock() 38 | METHOD_LOCKERS[name] = locker 39 | 40 | @functools.wraps(method) 41 | async def wrapper(*args, **kwargs): 42 | if not wait and locker.locked(): 43 | return 44 | try: 45 | await locker.acquire() 46 | return await method(*args, **kwargs) 47 | finally: 48 | locker.release() 49 | return wrapper 50 | return decorating_function 51 | 52 | 53 | # class Test: 54 | # 55 | # @async_method_locker('my_fucker', False) 56 | # async def test(self, x): 57 | # print('hahaha ...', x) 58 | # await asyncio.sleep(0.1) 59 | # 60 | # 61 | # t = Test() 62 | # for i in range(10): 63 | # asyncio.get_event_loop().create_task(t.test(i)) 64 | # 65 | # asyncio.get_event_loop().run_forever() 66 | -------------------------------------------------------------------------------- /quant/utils/logger.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | 日志打印 5 | 6 | Author: HuangTao 7 | Date: 2018/04/08 8 | Update: 2018/07/16 1. 初始化日志增加参数 clear 和 backup_count; 9 | 2018/07/19 1. 修复日志初始化的时候,clear设置为Ture,但文件不存在的异常; 10 | """ 11 | 12 | import os 13 | import sys 14 | import shutil 15 | import logging 16 | import traceback 17 | from logging.handlers import TimedRotatingFileHandler 18 | 19 | initialized = False 20 | 21 | 22 | def initLogger(log_level="DEBUG", log_path=None, logfile_name=None, clear=False, backup_count=0): 23 | """ 初始化日志输出 24 | @param log_level 日志级别 DEBUG/INFO 25 | @param log_path 日志输出路径 26 | @param logfile_name 日志文件名 27 | @param clear 初始化的时候,是否清理之前的日志文件 28 | @param backup_count 保存按天分割的日志文件个数,默认0为永久保存所有日志文件 29 | """ 30 | global initialized 31 | if initialized: 32 | return 33 | logger = logging.getLogger() 34 | logger.setLevel(log_level) 35 | if logfile_name: 36 | if clear and os.path.isdir(log_path): 37 | shutil.rmtree(log_path) 38 | if not os.path.isdir(log_path): 39 | os.makedirs(log_path) 40 | logfile = os.path.join(log_path, logfile_name) 41 | handler = TimedRotatingFileHandler(logfile, "midnight", backupCount=backup_count) 42 | print("init logger ...", logfile) 43 | else: 44 | print("init logger ...") 45 | handler = logging.StreamHandler() 46 | fmt_str = "%(levelname)1.1s [%(asctime)s] %(message)s" 47 | fmt = logging.Formatter(fmt=fmt_str, datefmt=None) 48 | handler.setFormatter(fmt) 49 | logger.addHandler(handler) 50 | initialized = True 51 | 52 | 53 | def info(*args, **kwargs): 54 | func_name, kwargs = _log_msg_header(*args, **kwargs) 55 | logging.info(_log(func_name, *args, **kwargs)) 56 | 57 | 58 | def warn(*args, **kwargs): 59 | msg_header, kwargs = _log_msg_header(*args, **kwargs) 60 | logging.warning(_log(msg_header, *args, **kwargs)) 61 | 62 | 63 | def debug(*args, **kwargs): 64 | msg_header, kwargs = _log_msg_header(*args, **kwargs) 65 | logging.debug(_log(msg_header, *args, **kwargs)) 66 | 67 | 68 | def error(*args, **kwargs): 69 | logging.error("*" * 60) 70 | msg_header, kwargs = _log_msg_header(*args, **kwargs) 71 | logging.error(_log(msg_header, *args, **kwargs)) 72 | logging.error("*" * 60) 73 | 74 | 75 | def exception(*args, **kwargs): 76 | logging.error("*" * 60) 77 | msg_header, kwargs = _log_msg_header(*args, **kwargs) 78 | logging.error(_log(msg_header, *args, **kwargs)) 79 | # exc_info = sys.exc_info() 80 | # traceback.print_exception(*exc_info) 81 | logging.error(traceback.format_exc()) 82 | logging.error("*" * 60) 83 | 84 | 85 | def _log(msg_header, *args, **kwargs): 86 | _log_msg = msg_header 87 | for l in args: 88 | if type(l) == tuple: 89 | ps = str(l) 90 | else: 91 | try: 92 | ps = "%r" % l 93 | except: 94 | ps = str(l) 95 | if type(l) == str: 96 | _log_msg += ps[1:-1] + " " 97 | else: 98 | _log_msg += ps + " " 99 | if len(kwargs) > 0: 100 | _log_msg += str(kwargs) 101 | return _log_msg 102 | 103 | 104 | def _log_msg_header(*args, **kwargs): 105 | """ 打印日志的message头 106 | @param kwargs["caller"] 调用的方法所属类对象 107 | * NOTE: logger.xxx(... caller=self) for instance method 108 | logger.xxx(... caller=cls) for @classmethod 109 | """ 110 | cls_name = "" 111 | func_name = sys._getframe().f_back.f_back.f_code.co_name 112 | session_id = "-" 113 | try: 114 | _caller = kwargs.get("caller", None) 115 | if _caller: 116 | if not hasattr(_caller, "__name__"): 117 | _caller = _caller.__class__ 118 | cls_name = _caller.__name__ 119 | del kwargs["caller"] 120 | except: 121 | pass 122 | finally: 123 | msg_header = "[{session_id}] [{cls_name}.{func_name}] ".format(cls_name=cls_name, func_name=func_name, 124 | session_id=session_id) 125 | return msg_header, kwargs 126 | -------------------------------------------------------------------------------- /quant/utils/tools.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | 工具包 5 | 6 | Author: HuangTao 7 | Date: 2018/04/28 8 | Update: 2018/09/07 1. 增加函数datetime_to_timestamp; 9 | """ 10 | 11 | import uuid 12 | import time 13 | import decimal 14 | import datetime 15 | 16 | 17 | def get_cur_timestamp(): 18 | """ 获取当前时间戳 19 | """ 20 | ts = int(time.time()) 21 | return ts 22 | 23 | 24 | def get_cur_timestamp_ms(): 25 | """ 获取当前时间戳(毫秒) 26 | """ 27 | ts = int(time.time() * 1000) 28 | return ts 29 | 30 | 31 | def get_cur_datetime_m(fmt='%Y%m%d%H%M%S%f'): 32 | """ 获取当前日期时间字符串,包含 年 + 月 + 日 + 时 + 分 + 秒 + 微妙 33 | """ 34 | today = datetime.datetime.today() 35 | str_m = today.strftime(fmt) 36 | return str_m 37 | 38 | 39 | def get_datetime(fmt='%Y%m%d%H%M%S'): 40 | """ 获取日期时间字符串,包含 年 + 月 + 日 + 时 + 分 + 秒 41 | """ 42 | today = datetime.datetime.today() 43 | str_dt = today.strftime(fmt) 44 | return str_dt 45 | 46 | 47 | def get_date(fmt='%Y%m%d', delta_day=0): 48 | """ 获取日期字符串,包含 年 + 月 + 日 49 | @param fmt 返回的日期格式 50 | """ 51 | day = datetime.datetime.today() 52 | if delta_day: 53 | day += datetime.timedelta(days=delta_day) 54 | str_d = day.strftime(fmt) 55 | return str_d 56 | 57 | 58 | def date_str_to_dt(date_str=None, fmt='%Y%m%d', delta_day=0): 59 | """ 日期字符串转换到datetime对象 60 | @param date_str 日期字符串 61 | @param fmt 日期字符串格式 62 | @param delta_day 相对天数,<0减相对天数,>0加相对天数 63 | """ 64 | if not date_str: 65 | dt = datetime.datetime.today() 66 | else: 67 | dt = datetime.datetime.strptime(date_str, fmt) 68 | if delta_day: 69 | dt += datetime.timedelta(days=delta_day) 70 | return dt 71 | 72 | 73 | def dt_to_date_str(dt=None, fmt='%Y%m%d', delta_day=0): 74 | """ datetime对象转换到日期字符串 75 | @param dt datetime对象 76 | @param fmt 返回的日期字符串格式 77 | @param delta_day 相对天数,<0减相对天数,>0加相对天数 78 | """ 79 | if not dt: 80 | dt = datetime.datetime.today() 81 | if delta_day: 82 | dt += datetime.timedelta(days=delta_day) 83 | str_d = dt.strftime(fmt) 84 | return str_d 85 | 86 | 87 | def get_utc_time(): 88 | """ 获取当前utc时间 89 | """ 90 | utc_t = datetime.datetime.utcnow() 91 | return utc_t 92 | 93 | 94 | def ts_to_datetime_str(ts=None, fmt='%Y-%m-%d %H:%M:%S'): 95 | """ 将时间戳转换为日期时间格式,年-月-日 时:分:秒 96 | @param ts 时间戳,默认None即为当前时间戳 97 | @param fmt 返回的日期字符串格式 98 | """ 99 | if not ts: 100 | ts = get_cur_timestamp() 101 | dt = datetime.datetime.fromtimestamp(int(ts)) 102 | return dt.strftime(fmt) 103 | 104 | 105 | def datetime_str_to_ts(dt_str, fmt='%Y-%m-%d %H:%M:%S'): 106 | """ 将日期时间格式字符串转换成时间戳 107 | @param dt_str 日期时间字符串 108 | @param fmt 日期时间字符串格式 109 | """ 110 | ts = int(time.mktime(datetime.datetime.strptime(dt_str, fmt).timetuple())) 111 | return ts 112 | 113 | 114 | def datetime_to_timestamp(dt=None, tzinfo=None): 115 | """ 将datetime对象转换成时间戳 116 | @param dt datetime对象,如果为None,默认使用当前UTC时间 117 | @param tzinfo 时区对象,如果为None,默认使用timezone.utc 118 | @return ts 时间戳(秒) 119 | """ 120 | if not dt: 121 | dt = get_utc_time() 122 | if not tzinfo: 123 | tzinfo = datetime.timezone.utc 124 | ts = int(dt.replace(tzinfo=tzinfo).timestamp()) 125 | return ts 126 | 127 | 128 | def utctime_str_to_ts(utctime_str, fmt="%Y-%m-%dT%H:%M:%S.%fZ"): 129 | """ 将UTC日期时间格式字符串转换成时间戳 130 | @param utctime_str 日期时间字符串 eg: 2019-03-04T09:14:27.806Z 131 | @param fmt 日期时间字符串格式 132 | @return timestamp 时间戳(秒) 133 | """ 134 | dt = datetime.datetime.strptime(utctime_str, fmt) 135 | timestamp = int(dt.replace(tzinfo=datetime.timezone.utc).astimezone(tz=None).timestamp()) 136 | return timestamp 137 | 138 | 139 | def utctime_str_to_mts(utctime_str, fmt="%Y-%m-%dT%H:%M:%S.%fZ"): 140 | """ 将UTC日期时间格式字符串转换成时间戳(毫秒) 141 | @param utctime_str 日期时间字符串 eg: 2019-03-04T09:14:27.806Z 142 | @param fmt 日期时间字符串格式 143 | @return timestamp 时间戳(毫秒) 144 | """ 145 | dt = datetime.datetime.strptime(utctime_str, fmt) 146 | timestamp = int(dt.replace(tzinfo=datetime.timezone.utc).astimezone(tz=None).timestamp() * 1000) 147 | return timestamp 148 | 149 | 150 | def get_uuid1(): 151 | """ make a UUID based on the host ID and current time 152 | """ 153 | s = uuid.uuid1() 154 | return str(s) 155 | 156 | 157 | def get_uuid3(str_in): 158 | """ make a UUID using an MD5 hash of a namespace UUID and a name 159 | @param str_in 输入字符串 160 | """ 161 | s = uuid.uuid3(uuid.NAMESPACE_DNS, str_in) 162 | return str(s) 163 | 164 | 165 | def get_uuid4(): 166 | """ make a random UUID 167 | """ 168 | s = uuid.uuid4() 169 | return str(s) 170 | 171 | 172 | def get_uuid5(str_in): 173 | """ make a UUID using a SHA-1 hash of a namespace UUID and a name 174 | @param str_in 输入字符串 175 | """ 176 | s = uuid.uuid5(uuid.NAMESPACE_DNS, str_in) 177 | return str(s) 178 | 179 | 180 | def float_to_str(f, p=20): 181 | """ Convert the given float to a string, without resorting to scientific notation. 182 | @param f 浮点数参数 183 | @param p 精读 184 | """ 185 | if type(f) == str: 186 | f = float(f) 187 | ctx = decimal.Context(p) 188 | d1 = ctx.create_decimal(repr(f)) 189 | return format(d1, 'f') 190 | -------------------------------------------------------------------------------- /quant/utils/web.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | Web module. 5 | Author: HuangTao 6 | Date: 2018/08/26 7 | Email: huangtao@ifclover.com 8 | """ 9 | 10 | import json 11 | 12 | import aiohttp 13 | from urllib.parse import urlparse 14 | 15 | from quant.utils import logger 16 | from quant.config import config 17 | from quant.tasks import LoopRunTask, SingleTask 18 | 19 | 20 | __all__ = ("Websocket", "AsyncHttpRequests", ) 21 | 22 | 23 | 24 | class Websocket: 25 | """ Websocket connection. 26 | 27 | Attributes: 28 | url: Websocket connection url. 29 | connected_callback: Asynchronous callback function will be called after connected to Websocket server successfully. 30 | process_callback: Asynchronous callback function will be called if any stream data receive from Websocket 31 | connection, this function only callback `text/json` message. e.g. 32 | async def process_callback(json_message): pass 33 | process_binary_callback: Asynchronous callback function will be called if any stream data receive from Websocket 34 | connection, this function only callback `binary` message. e.g. 35 | async def process_binary_callback(binary_message): pass 36 | check_conn_interval: Check Websocket connection interval time(seconds), default is 10s. 37 | """ 38 | 39 | def __init__(self, url, connected_callback=None, process_callback=None, process_binary_callback=None, 40 | check_conn_interval=10): 41 | """Initialize.""" 42 | self._url = url 43 | self._connected_callback = connected_callback 44 | self._process_callback = process_callback 45 | self._process_binary_callback = process_binary_callback 46 | self._check_conn_interval = check_conn_interval 47 | self._ws = None # Websocket connection object. 48 | 49 | @property 50 | def ws(self): 51 | return self._ws 52 | 53 | def initialize(self): 54 | LoopRunTask.register(self._check_connection, self._check_conn_interval) 55 | SingleTask.run(self._connect) 56 | 57 | async def _connect(self): 58 | logger.info("url:", self._url, caller=self) 59 | proxy = config.proxy 60 | session = aiohttp.ClientSession() 61 | try: 62 | self._ws = await session.ws_connect(self._url, proxy=proxy) 63 | except aiohttp.client_exceptions.ClientConnectorError: 64 | logger.error("connect to Websocket server error! url:", self._url, caller=self) 65 | return 66 | if self._connected_callback: 67 | SingleTask.run(self._connected_callback) 68 | SingleTask.run(self._receive) 69 | 70 | async def _reconnect(self): 71 | """Re-connect to Websocket server.""" 72 | logger.warn("reconnecting to Websocket server right now!", caller=self) 73 | await self._connect() 74 | 75 | async def _receive(self): 76 | """Receive stream message from Websocket connection.""" 77 | async for msg in self.ws: 78 | if msg.type == aiohttp.WSMsgType.TEXT: 79 | if self._process_callback: 80 | try: 81 | data = json.loads(msg.data) 82 | except: 83 | data = msg.data 84 | SingleTask.run(self._process_callback, data) 85 | elif msg.type == aiohttp.WSMsgType.BINARY: 86 | if self._process_binary_callback: 87 | SingleTask.run(self._process_binary_callback, msg.data) 88 | elif msg.type == aiohttp.WSMsgType.CLOSED: 89 | logger.warn("receive event CLOSED:", msg, caller=self) 90 | SingleTask.run(self._reconnect) 91 | elif msg.type == aiohttp.WSMsgType.ERROR: 92 | logger.error("receive event ERROR:", msg, caller=self) 93 | else: 94 | logger.warn("unhandled msg:", msg, caller=self) 95 | 96 | async def _check_connection(self, *args, **kwargs): 97 | """Check Websocket connection, if connection closed, re-connect immediately.""" 98 | if not self.ws: 99 | logger.warn("Websocket connection not connected yet!", caller=self) 100 | return 101 | if self.ws.closed: 102 | SingleTask.run(self._reconnect) 103 | 104 | async def send(self, data): 105 | """ Send message to Websocket server. 106 | 107 | Args: 108 | data: Message content, must be dict or string. 109 | 110 | Returns: 111 | If send successfully, return True, otherwise return False. 112 | """ 113 | if not self.ws: 114 | logger.warn("Websocket connection not connected yet!", caller=self) 115 | return False 116 | if isinstance(data, dict): 117 | await self.ws.send_json(data) 118 | elif isinstance(data, str): 119 | await self.ws.send_str(data) 120 | else: 121 | logger.error("send message failed:", data, caller=self) 122 | return False 123 | logger.debug("send message:", data, caller=self) 124 | return True 125 | 126 | 127 | class AsyncHttpRequests(object): 128 | """ Asynchronous HTTP Request Client. 129 | """ 130 | 131 | # Every domain name holds a connection session, for less system resource utilization and faster request speed. 132 | _SESSIONS = {} # {"domain-name": session, ... } 133 | 134 | @classmethod 135 | async def fetch(cls, method, url, params=None, body=None, data=None, headers=None, timeout=30, **kwargs): 136 | """ Create a HTTP request. 137 | 138 | Args: 139 | method: HTTP request method. (GET/POST/PUT/DELETE) 140 | url: Request url. 141 | params: HTTP query params. 142 | body: HTTP request body, string or bytes format. 143 | data: HTTP request body, dict format. 144 | headers: HTTP request header. 145 | timeout: HTTP request timeout(seconds), default is 30s. 146 | 147 | kwargs: 148 | proxy: HTTP proxy. 149 | 150 | Return: 151 | code: HTTP response code. 152 | success: HTTP response data. If something wrong, this field is None. 153 | error: If something wrong, this field will holding a Error information, otherwise it's None. 154 | 155 | Raises: 156 | HTTP request exceptions or response data parse exceptions. All the exceptions will be captured and return 157 | Error information. 158 | """ 159 | session = cls._get_session(url) 160 | if not kwargs.get("proxy"): 161 | kwargs["proxy"] = config.proxy # If there is a HTTP PROXY assigned in config file? 162 | try: 163 | if method == "GET": 164 | response = await session.get(url, params=params, headers=headers, timeout=timeout, **kwargs) 165 | elif method == "POST": 166 | response = await session.post(url, params=params, data=body, json=data, headers=headers, 167 | timeout=timeout, **kwargs) 168 | elif method == "PUT": 169 | response = await session.put(url, params=params, data=body, json=data, headers=headers, 170 | timeout=timeout, **kwargs) 171 | elif method == "DELETE": 172 | response = await session.delete(url, params=params, data=body, json=data, headers=headers, 173 | timeout=timeout, **kwargs) 174 | else: 175 | error = "http method error!" 176 | return None, None, error 177 | except Exception as e: 178 | logger.error("method:", method, "url:", url, "headers:", headers, "params:", params, "body:", body, 179 | "data:", data, "Error:", e, caller=cls) 180 | return None, None, e 181 | code = response.status 182 | if code not in (200, 201, 202, 203, 204, 205, 206): 183 | text = await response.text() 184 | logger.error("method:", method, "url:", url, "headers:", headers, "params:", params, "body:", body, 185 | "data:", data, "code:", code, "result:", text, caller=cls) 186 | return code, None, text 187 | try: 188 | result = await response.json() 189 | except: 190 | result = await response.text() 191 | logger.warn("response data is not json format!", "method:", method, "url:", url, "headers:", headers, 192 | "params:", params, "body:", body, "data:", data, "code:", code, "result:", result, caller=cls) 193 | logger.debug("method:", method, "url:", url, "headers:", headers, "params:", params, "body:", body, 194 | "data:", data, "code:", code, "result:", json.dumps(result), caller=cls) 195 | return code, result, None 196 | 197 | @classmethod 198 | async def get(cls, url, params=None, body=None, data=None, headers=None, timeout=30, **kwargs): 199 | """ HTTP GET 200 | """ 201 | result = await cls.fetch("GET", url, params, body, data, headers, timeout, **kwargs) 202 | return result 203 | 204 | @classmethod 205 | async def post(cls, url, params=None, body=None, data=None, headers=None, timeout=30, **kwargs): 206 | """ HTTP POST 207 | """ 208 | result = await cls.fetch("POST", url, params, body, data, headers, timeout, **kwargs) 209 | return result 210 | 211 | @classmethod 212 | async def delete(cls, url, params=None, body=None, data=None, headers=None, timeout=30, **kwargs): 213 | """ HTTP DELETE 214 | """ 215 | result = await cls.fetch("DELETE", url, params, body, data, headers, timeout, **kwargs) 216 | return result 217 | 218 | @classmethod 219 | async def put(cls, url, params=None, body=None, data=None, headers=None, timeout=30, **kwargs): 220 | """ HTTP PUT 221 | """ 222 | result = await cls.fetch("PUT", url, params, body, data, headers, timeout, **kwargs) 223 | return result 224 | 225 | @classmethod 226 | def _get_session(cls, url): 227 | """ Get the connection session for url's domain, if no session, create a new. 228 | 229 | Args: 230 | url: HTTP request url. 231 | 232 | Returns: 233 | session: HTTP request session. 234 | """ 235 | parsed_url = urlparse(url) 236 | key = parsed_url.netloc or parsed_url.hostname 237 | if key not in cls._SESSIONS: 238 | session = aiohttp.ClientSession() 239 | cls._SESSIONS[key] = session 240 | return cls._SESSIONS[key] 241 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from distutils.core import setup 4 | 5 | 6 | setup( 7 | name="thenextquant", 8 | version="0.2.3", 9 | packages=[ 10 | "quant", 11 | "quant.utils", 12 | "quant.platform", 13 | ], 14 | description="Asynchronous driven quantitative trading framework.", 15 | url="https://github.com/TheNextQuant/thenextquant", 16 | author="huangtao", 17 | author_email="huangtao@ifclover.com", 18 | license="MIT", 19 | keywords=[ 20 | "thenextquant", "quant", "framework", "async", "asynchronous", "digiccy", "digital", "currency", 21 | "marketmaker", "binance", "okex", "huobi", "bitmex", "deribit", "kraken", "gemini", "kucoin", "digifinex" 22 | ], 23 | install_requires=[ 24 | "aiohttp==3.6.2", 25 | "aioamqp==0.14.0", 26 | "motor==2.0.0" 27 | ], 28 | ) 29 | --------------------------------------------------------------------------------