├── .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 | 
7 |
8 | 
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 | 
40 |
41 | 登录成功之后,进入 `Admin` 标签页,新增、管理登录账户和密码:
42 | 
43 |
44 | 请注意,新增的账户需要设置对应的访问权限,根据需要设置权限即可,一般如果测试使用直接给根目录 `/` 的访问权限:
45 | 
46 |
47 | 
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 |
--------------------------------------------------------------------------------