├── .gitignore
├── LICENSE.txt
├── README.md
├── aioquant
├── __init__.py
├── configure.py
├── const.py
├── error.py
├── heartbeat.py
├── order.py
├── platform
│ ├── __init__.py
│ ├── binance.py
│ ├── huobi.py
│ └── okex.py
├── quant.py
├── tasks.py
└── utils
│ ├── __init__.py
│ ├── decorator.py
│ ├── logger.py
│ ├── tools.py
│ └── web.py
├── docs
├── asset.md
├── configure
│ ├── README.md
│ └── config.json
├── faq.md
├── images
│ ├── aioq_framework.jpg
│ ├── aioq_framework2.png
│ ├── login.png
│ ├── rabbitmq_permission.png
│ ├── rabbitmq_permission2.png
│ └── userpage.png
├── market.md
├── others
│ ├── locker.md
│ ├── logger.md
│ ├── rabbitmq_deploy.md
│ └── tasks.md
├── requirements.txt
└── trade.md
├── example
├── Issue29
│ ├── config.json
│ └── src
│ │ ├── main.py
│ │ └── strategy
│ │ └── strategy29.py
└── demo
│ ├── README.md
│ ├── __init__.py
│ ├── config.json
│ ├── main.py
│ └── strategy
│ ├── __init__.py
│ └── strategy.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 | ## AIOQuant
3 |
4 | `AIOQuant` 是一套使用 `Python` 语言开发的 `异步事件驱动` 的 `量化交易 ` / `做市` 系统,它被设计为适应中高频策略的交易系统,
5 | 底层封装了操作系统的 `aio*库` 实现异步事件循环,业务层封装了 `RabbitMQ消息队列` 实现异步事件驱动,再加上 `Python` 语言的简单易用,
6 | 它非常适用于数字货币的高频策略和做市策略开发。
7 |
8 | `AIOQuant` 同时也被设计为一套完全解耦的量化交易系统,其主要模块包括 `行情系统模块`、`资产系统模块`、`交易系统模块`、`风控系统模块`、`存储系统模块`,
9 | 各个模块都可以任意拆卸和组合使用,甚至采用不同的开发语言设计重构,模块之间通过 `RabbitMQ消息队列` 相互驱动,所以不同模块还可以部署在不同的进程,
10 | 或不同服务器。
11 |
12 | `AIOQuant` 提供了简单而强大的功能:
13 | - 基于 [Python Asyncio](https://docs.python.org/3/library/asyncio.html) 原生异步事件循环,处理更简洁,效率更高;
14 | - 跨平台(Windows、Mac、Linux),可任意私有化部署;
15 | - 任意交易所的交易方式(现货、合约、期货)统一,相同策略只需要区别不同配置,即可无缝切换任意交易所;
16 | - 所有交易所的行情统一,并通过事件订阅的形式,回调触发策略执行不同指令;
17 | - 支持任意多个策略协同运行;
18 | - 支持任意多个策略分布式运行;
19 | - 毫秒级延迟(一般瓶颈在网络延迟);
20 | - 提供任务、监控、存储、事件发布等一系列高级功能;
21 | - 定制化Docker容器,分布式配置、部署运行;
22 | - 量化交易Web管理系统,通过管理工具,轻松实现对策略、风控、资产、服务器等进程或资源的动态管理;
23 |
24 | - `AIOQuant` 交易系统各大模块
25 | 
26 |
27 | - `AIOQuant` 分布式管理交易系统
28 | 
29 |
30 |
31 | ### 框架依赖
32 |
33 | - 运行环境
34 | - python 3.5.3 或以上版本(建议安装3.6版本)
35 |
36 | - 依赖python三方包
37 | - aiohttp>=3.2.1
38 | - aioamqp>=0.13.0(可选)
39 | - motor>=2.0.0 (可选)
40 |
41 | - RabbitMQ服务器
42 | - 事件发布、订阅
43 |
44 | - MongoDB数据库(可选)
45 | - 数据存储
46 |
47 |
48 | ### 安装
49 | 使用 `pip` 可以简单方便安装:
50 | ```text
51 | pip install aioquant
52 | ```
53 |
54 |
55 | ### Demo使用示例
56 |
57 | - 推荐创建如下结构的文件及文件夹:
58 | ```text
59 | ProjectName
60 | |----- docs
61 | | |----- README.md
62 | |----- scripts
63 | | |----- run.sh
64 | |----- config.json
65 | |----- src
66 | | |----- main.py
67 | | |----- strategy
68 | | |----- strategy1.py
69 | | |----- strategy2.py
70 | | |----- ...
71 | |----- .gitignore
72 | |----- README.md
73 | ```
74 |
75 | - 快速体验 [Demo](example/demo) 示例
76 |
77 |
78 | - 运行
79 | ```text
80 | python src/main.py config.json
81 | ```
82 |
83 |
84 | ### 推荐课程
85 | [第1期 高频交易介绍](https://www.bilibili.com/video/BV1EJ41197Fx/)
86 | [第2期 一分钟上手,开启自己的第一笔程序化交易](https://www.bilibili.com/video/BV1vJ411q799/)
87 | [第3期 利用REST API拉取行情数据](https://www.bilibili.com/video/BV15J411B7bG/)
88 | [第4期 使用实时行情动态挂单](https://www.bilibili.com/video/BV1JJ411i7hH/)
89 | [第5期 API报错 & 订单成交](https://www.bilibili.com/video/BV1nJ411y7zE/)
90 | [第6期 优雅的处理未完成订单](https://www.bilibili.com/video/BV1nJ411175f/)
91 | [第7期 配置文件](https://www.bilibili.com/video/BV1ZJ411k71z/)
92 | [第8期 日志打印](https://www.bilibili.com/video/BV1FJ411C7Ys/)
93 | [第9期 服务心跳 & 协程任务](https://www.bilibili.com/video/BV1pJ411C7dS/)
94 | [第10期 Order订单模块](https://www.bilibili.com/video/BV1UJ411C7a6/)
95 | [第11期 Trade交易模块](https://www.bilibili.com/video/BV1sJ411r73X/)
96 | [第12期 并发 & 锁](https://www.bilibili.com/video/BV1iJ411677Q/)
97 | [第13期 Market行情模块 & 行情服务器](https://www.bilibili.com/video/av79695611/)
98 | [第14期 Position合约持仓模块](https://www.bilibili.com/video/av84079197/)
99 | [第15期 现货搬砖套利原理](https://www.bilibili.com/video/av86045742/)
100 | [第16期 分析历史行情数据](https://www.bilibili.com/video/av86060852/)
101 | [第17期 现货搬砖套利策略编写](https://www.bilibili.com/video/av86493743/)
102 | [第18期 行情数据存储](https://www.bilibili.com/video/av88433058/)
103 | [第19期 推送钉钉消息](https://www.bilibili.com/video/av88463345/)
104 | [第20期 Bollinger Bands 布林带策略](https://www.bilibili.com/video/av91044647/)
105 | [第21期 一个简单的做市商策略](https://www.bilibili.com/video/av93027310/)
106 | [第22期 火币永续合约REST API模块](https://www.bilibili.com/video/BV1k5411t7bb/)
107 | [第23期 火币永续合约Trade模块](https://www.bilibili.com/video/BV1GV411Z766/)
108 | [第24期 Market行情系统升级](https://www.bilibili.com/video/BV1rk4y1R7gk/)
109 | [第25期 现货-合约无风险套利](https://www.bilibili.com/video/BV15A411b78b/)
110 | [第26期 合约的无风险套利](https://www.bilibili.com/video/BV1AK4y1k7un/)
111 | [第27期 高阶技巧 - 运行时更新](https://www.bilibili.com/video/BV1Xe411p7Pm/)
112 | [第28期 Triangular Arbitrage 三角套利原理](https://www.bilibili.com/video/BV1WZ4y1W77F/)
113 | [第29期 Triangular Arbitrage 三角套利策略编写](https://www.bilibili.com/video/BV1zz411i7xW/)
114 | [第30期 高效的交易方式](https://www.bilibili.com/video/BV1Ai4y1x7Z3/)
115 | [第31期 高阶技巧 - ClientOrderId的用法](https://www.bilibili.com/video/BV1dA411v72d/)
116 | [第32期 高阶技巧 - 订单类型的高级用法](https://www.bilibili.com/video/BV12K411n7GC/)
117 | [第33期 Trade模块的强大功能让你的策略如虎添翼](https://www.bilibili.com/video/BV1WZ4y1u7KF)
118 | [第34期 AIOQuant到底有何不同?](https://www.bilibili.com/video/BV17t4y1X74P/)
119 | [第35期 搭建本地量化策略研发环境](https://www.bilibili.com/video/BV1w54y1B7ZH/)
120 | [第36期 远程部署策略之服务器选购](https://www.bilibili.com/video/BV1m54y1S7Eg/)
121 | [第37期 远程部署策略之服务器环境搭建](https://www.bilibili.com/video/BV1Y64y1c7CV/)
122 | [第38期 远程服务器部署量化策略](https://www.bilibili.com/video/BV1zK4y1v7qX/)
123 |
124 |
125 | ### 有任何问题,欢迎联系
126 |
127 | - 微信二维码
128 |
129 |
130 |
131 |
--------------------------------------------------------------------------------
/aioquant/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding:utf-8 -*-
2 |
3 | """
4 | Asynchronous event I/O 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__ = (1, 0, 7)
13 |
14 |
15 | from aioquant.quant import AIOQuant
16 |
17 | quant = AIOQuant()
18 |
--------------------------------------------------------------------------------
/aioquant/configure.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 aioquant.utils import tools
14 |
15 |
16 | class Configure:
17 | """Configure 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) -> 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) -> None:
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 = Configure()
78 |
--------------------------------------------------------------------------------
/aioquant/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 | BINANCE_FUTURE = "binance_future" # https://www.binance-cn.com/cn/futures/BTCUSDT
15 | BITFINEX = "bitfinex"
16 | BITMEX = "bitmex" # BitMEX https://www.bitmex.com/
17 | BITZ = "bitz" # BitZeus https://www.bit-z.com/
18 | BITSTAMP = "bitstamp"
19 | COINBASE_PRO = "coinbase_pro" # Coinbase Pro https://pro.coinbase.com/
20 | DERIBIT = "deribit" # Deribit https://www.deribit.com/
21 | DERIBIT_OPTION = "deribit_option" # Deribit Option https://www.deribit.com/
22 | DIGIFINEX = "digifinex" # Digifinex https://docs.digifinex.vip/
23 | FCOIN = "fcoin" # Fcoin https://www.fcoin.com/
24 | FMEX = "fmex" # FMex https://fmex.com/
25 | FTX = "ftx" # FTX https://ftx.com/
26 | GATE = "gate" # Gate.io https://gateio.news/
27 | GEMINI = "gemini" # Gemini https://gemini.com/
28 | HUOBI = "huobi" # Huobi https://www.hbg.com/zh-cn/
29 | HUOBI_FUTURE = "huobi_future" # Huobi Future https://www.hbdm.com/en-us/contract/exchange/
30 | KUCOIN = "kucoin" # Kucoin https://www.kucoin.com/
31 | KRAKEN = "kraken" # Kraken https://www.kraken.com
32 | OKEX = "okex" # OKEx SPOT https://www.okex.me/spot/trade
33 | OKEX_MARGIN = "okex_margin" # OKEx MARGIN https://www.okex.me/spot/marginTrade
34 | OKEX_FUTURE = "okex_future" # OKEx FUTURE https://www.okex.me/future/trade
35 | OKEX_SWAP = "okex_swap" # OKEx SWAP https://www.okex.me/future/swap
36 |
37 |
38 | # Market Types
39 | MARKET_TYPE_TRADE = "trade"
40 | MARKET_TYPE_ORDERBOOK = "orderbook"
41 | MARKET_TYPE_KLINE = "kline"
42 | MARKET_TYPE_KLINE_3M = "kline_3m"
43 | MARKET_TYPE_KLINE_5M = "kline_5m"
44 | MARKET_TYPE_KLINE_15M = "kline_15m"
45 | MARKET_TYPE_KLINE_30M = "kline_30m"
46 | MARKET_TYPE_KLINE_1H = "kline_1h"
47 | MARKET_TYPE_KLINE_3H = "kline_3h"
48 | MARKET_TYPE_KLINE_6H = "kline_6h"
49 | MARKET_TYPE_KLINE_12H = "kline_12h"
50 | MARKET_TYPE_KLINE_1D = "kline_1d"
51 | MARKET_TYPE_KLINE_3D = "kline_3d"
52 | MARKET_TYPE_KLINE_1W = "kline_1w"
53 | MARKET_TYPE_KLINE_15D = "kline_15d"
54 | MARKET_TYPE_KLINE_1MON = "kline_1mon"
55 | MARKET_TYPE_KLINE_1Y = "kline_1y"
56 |
--------------------------------------------------------------------------------
/aioquant/error.py:
--------------------------------------------------------------------------------
1 | # -*- coding:utf-8 -*-
2 |
3 | """
4 | Error Message.
5 |
6 | Author: HuangTao
7 | Date: 2018/05/17
8 | Email: huangtao@ifclover.com
9 | """
10 |
11 |
12 | class Error:
13 |
14 | def __init__(self, msg):
15 | self._msg = msg
16 |
17 | @property
18 | def msg(self):
19 | return self._msg
20 |
21 | def __str__(self):
22 | return str(self._msg)
23 |
24 | def __repr__(self):
25 | return str(self)
26 |
--------------------------------------------------------------------------------
/aioquant/heartbeat.py:
--------------------------------------------------------------------------------
1 | # -*- coding:utf-8 -*-
2 |
3 | """
4 | Server heartbeat.
5 |
6 | Author: HuangTao
7 | Date: 2018/04/26
8 | Email: huangtao@ifclover.com
9 | """
10 |
11 | import asyncio
12 |
13 | from aioquant.utils import tools
14 | from aioquant.utils import logger
15 | from aioquant.configure import config
16 |
17 | __all__ = ("heartbeat", )
18 |
19 |
20 | class HeartBeat(object):
21 | """Server heartbeat.
22 | """
23 |
24 | def __init__(self):
25 | self._count = 0 # Heartbeat count.
26 | self._interval = 1 # Heartbeat interval(second).
27 | self._print_interval = config.heartbeat.get("interval", 0) # Printf heartbeat information interval(second).
28 | self._tasks = {} # Loop run tasks with heartbeat service. `{task_id: {...}}`
29 |
30 | @property
31 | def count(self):
32 | return self._count
33 |
34 | def ticker(self):
35 | """Loop run ticker per self._interval.
36 | """
37 | self._count += 1
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 | # Later call next ticker.
44 | asyncio.get_event_loop().call_later(self._interval, self.ticker)
45 |
46 | # Exec tasks.
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 | """Register an asynchronous callback function.
60 |
61 | Args:
62 | func: Asynchronous callback function.
63 | interval: Loop callback interval(second), default is `1s`.
64 |
65 | Returns:
66 | task_id: Task id.
67 | """
68 | t = {
69 | "func": func,
70 | "interval": interval,
71 | "args": args,
72 | "kwargs": kwargs
73 | }
74 | task_id = tools.get_uuid1()
75 | self._tasks[task_id] = t
76 | return task_id
77 |
78 | def unregister(self, task_id):
79 | """Unregister a task.
80 |
81 | Args:
82 | task_id: Task id.
83 | """
84 | if task_id in self._tasks:
85 | self._tasks.pop(task_id)
86 |
87 |
88 | heartbeat = HeartBeat()
89 |
--------------------------------------------------------------------------------
/aioquant/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 | import json
12 |
13 | from aioquant.utils import tools
14 |
15 |
16 | # Order type.
17 | ORDER_TYPE_LIMIT = "LIMIT" # Limit order.
18 | ORDER_TYPE_MARKET = "MARKET" # Market order.
19 |
20 | # Order direction.
21 | ORDER_ACTION_BUY = "BUY" # Buy
22 | ORDER_ACTION_SELL = "SELL" # Sell
23 |
24 | # Order status.
25 | ORDER_STATUS_NONE = "NONE" # New created order, no status.
26 | ORDER_STATUS_SUBMITTED = "SUBMITTED" # The order that submitted to server successfully.
27 | ORDER_STATUS_PARTIAL_FILLED = "PARTIAL-FILLED" # The order that filled partially.
28 | ORDER_STATUS_FILLED = "FILLED" # The order that filled fully.
29 | ORDER_STATUS_CANCELED = "CANCELED" # The order that canceled.
30 | ORDER_STATUS_FAILED = "FAILED" # The order that failed.
31 |
32 | # Future order trade type.
33 | TRADE_TYPE_NONE = 0 # Unknown type, some Exchange's order information couldn't known the type of trade.
34 | TRADE_TYPE_BUY_OPEN = 1 # Buy open, action = BUY & quantity > 0.
35 | TRADE_TYPE_SELL_OPEN = 2 # Sell open, action = SELL & quantity < 0.
36 | TRADE_TYPE_SELL_CLOSE = 3 # Sell close, action = SELL & quantity > 0.
37 | TRADE_TYPE_BUY_CLOSE = 4 # Buy close, action = BUY & quantity < 0.
38 |
39 |
40 | class Order:
41 | """Order object.
42 |
43 | Attributes:
44 | platform: Exchange platform name, e.g. `binance` / `bitmex`.
45 | account: Trading account name, e.g. `test@gmail.com`.
46 | strategy: Strategy name, e.g. `my_strategy`.
47 | order_id: Order id.
48 | client_order_id: Client order id.
49 | symbol: Trading pair name, e.g. `ETH/BTC`.
50 | action: Trading side, `BUY` / `SELL`.
51 | price: Order price.
52 | quantity: Order quantity.
53 | remain: Remain quantity that not filled.
54 | status: Order status.
55 | avg_price: Average price that filled.
56 | order_type: Order type.
57 | trade_type: Trade type, only for future order.
58 | fee: Trading fee.
59 | ctime: Order create time, millisecond.
60 | utime: Order update time, millisecond.
61 | """
62 |
63 | def __init__(self, platform=None, account=None, strategy=None, order_id=None, client_order_id=None, symbol=None,
64 | action=None, price=0, quantity=0, remain=0, status=ORDER_STATUS_NONE, avg_price=0,
65 | order_type=ORDER_TYPE_LIMIT, trade_type=TRADE_TYPE_NONE, fee=0, ctime=None, utime=None):
66 | self.platform = platform
67 | self.account = account
68 | self.strategy = strategy
69 | self.order_id = order_id
70 | self.client_order_id = client_order_id
71 | self.action = action
72 | self.order_type = order_type
73 | self.symbol = symbol
74 | self.price = price
75 | self.quantity = quantity
76 | self.remain = remain if remain else quantity
77 | self.status = status
78 | self.avg_price = avg_price
79 | self.trade_type = trade_type
80 | self.fee = fee
81 | self.ctime = ctime if ctime else tools.get_cur_timestamp_ms()
82 | self.utime = utime if utime else tools.get_cur_timestamp_ms()
83 |
84 | @property
85 | def data(self):
86 | d = {
87 | "platform": self.platform,
88 | "account": self.account,
89 | "strategy": self.strategy,
90 | "order_id": self.order_id,
91 | "client_order_id": self.client_order_id,
92 | "action": self.action,
93 | "order_type": self.order_type,
94 | "symbol": self.symbol,
95 | "price": self.price,
96 | "quantity": self.quantity,
97 | "remain": self.remain,
98 | "status": self.status,
99 | "avg_price": self.avg_price,
100 | "trade_type": self.trade_type,
101 | "fee": self.fee,
102 | "ctime": self.ctime,
103 | "utime": self.utime
104 | }
105 | return d
106 |
107 | def __str__(self):
108 | info = json.dumps(self.data)
109 | return info
110 |
111 | def __repr__(self):
112 | return str(self)
113 |
--------------------------------------------------------------------------------
/aioquant/platform/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guhhhhaa/aioquant-add-triangular-arbitrage/35f7cf5c4266ad5feac8d9d1673006d0488f7a1f/aioquant/platform/__init__.py
--------------------------------------------------------------------------------
/aioquant/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 hashlib
13 | import hmac
14 | from urllib.parse import urljoin
15 |
16 | from aioquant.utils import tools
17 | from aioquant.utils.web import AsyncHttpRequests
18 |
19 | __all__ = ("BinanceRestAPI", )
20 |
21 |
22 | class BinanceRestAPI:
23 | """Binance REST API client.
24 |
25 | Attributes:
26 | access_key: Account's ACCESS KEY.
27 | secret_key: Account's SECRET KEY.
28 | host: HTTP request host, default `https://api.binance.com`.
29 | """
30 |
31 | def __init__(self, access_key, secret_key, host=None):
32 | """Initialize REST API client."""
33 | self._host = host or "https://api.binance.com"
34 | self._access_key = access_key
35 | self._secret_key = secret_key
36 |
37 | async def ping(self):
38 | """Test connectivity.
39 |
40 | Returns:
41 | success: Success results, otherwise it's None.
42 | error: Error information, otherwise it's None.
43 | """
44 | uri = "/api/v3/ping"
45 | success, error = await self.request("GET", uri)
46 | return success, error
47 |
48 | async def get_server_time(self):
49 | """Get server time.
50 |
51 | Returns:
52 | success: Success results, otherwise it's None.
53 | error: Error information, otherwise it's None.
54 | """
55 | uri = "/api/v3/time"
56 | success, error = await self.request("GET", uri)
57 | return success, error
58 |
59 | async def get_exchange_info(self):
60 | """Get exchange information.
61 |
62 | Returns:
63 | success: Success results, otherwise it's None.
64 | error: Error information, otherwise it's None.
65 | """
66 | uri = "/api/v3/exchangeInfo"
67 | success, error = await self.request("GET", uri)
68 | return success, error
69 |
70 | async def get_orderbook(self, symbol, limit=10):
71 | """Get latest orderbook information.
72 |
73 | Args:
74 | symbol: Symbol name, e.g. `BTCUSDT`.
75 | limit: Number of results per request. (default 10, max 5000.)
76 |
77 | Returns:
78 | success: Success results, otherwise it's None.
79 | error: Error information, otherwise it's None.
80 | """
81 | uri = "/api/v3/depth"
82 | params = {
83 | "symbol": symbol,
84 | "limit": limit
85 | }
86 | success, error = await self.request("GET", uri, params=params)
87 | return success, error
88 |
89 | async def get_trade(self, symbol, limit=500):
90 | """Get latest trade information.
91 |
92 | Args:
93 | symbol: Symbol name, e.g. `BTCUSDT`.
94 | limit: Number of results per request. (Default 500, max 1000.)
95 |
96 | Returns:
97 | success: Success results, otherwise it's None.
98 | error: Error information, otherwise it's None.
99 | """
100 | uri = "/api/v3/trades"
101 | params = {
102 | "symbol": symbol,
103 | "limit": limit
104 | }
105 | success, error = await self.request("GET", uri, params=params)
106 | return success, error
107 |
108 | async def get_kline(self, symbol, interval="1m", start=None, end=None, limit=500):
109 | """Get kline information.
110 |
111 | Args:
112 | symbol: Symbol name, e.g. `BTCUSDT`.
113 | interval: Kline interval type, valid values: 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 3d, 1w, 1M
114 | start: Start timestamp(millisecond).
115 | end: End timestamp(millisecond).
116 | limit: Number of results per request. (Default 500, max 1000.)
117 |
118 | Returns:
119 | success: Success results, otherwise it's None.
120 | error: Error information, otherwise it's None.
121 |
122 | Notes:
123 | If start and end are not sent, the most recent klines are returned.
124 | """
125 | uri = "/api/v3/klines"
126 | params = {
127 | "symbol": symbol,
128 | "interval": interval,
129 | "limit": limit
130 | }
131 | if start and end:
132 | params["startTime"] = start
133 | params["endTime"] = end
134 | success, error = await self.request("GET", uri, params=params)
135 | return success, error
136 |
137 | async def get_average_price(self, symbol):
138 | """Current average price for a symbol.
139 |
140 | Args:
141 | symbol: Symbol name, e.g. `BTCUSDT`.
142 |
143 | Returns:
144 | success: Success results, otherwise it's None.
145 | error: Error information, otherwise it's None.
146 | """
147 | uri = "/api/v3/avgPrice"
148 | params = {
149 | "symbol": symbol
150 | }
151 | success, error = await self.request("GET", uri, params=params)
152 | return success, error
153 |
154 | async def get_user_account(self):
155 | """Get user account information.
156 |
157 | Returns:
158 | success: Success results, otherwise it's None.
159 | error: Error information, otherwise it's None.
160 | """
161 | uri = "/api/v3/account"
162 | ts = tools.get_cur_timestamp_ms()
163 | params = {
164 | "timestamp": str(ts)
165 | }
166 | success, error = await self.request("GET", uri, params, auth=True)
167 | return success, error
168 |
169 | async def create_order(self, action, symbol, price, quantity, client_order_id=None):
170 | """Create an order.
171 | Args:
172 | action: Trade direction, `BUY` or `SELL`.
173 | symbol: Symbol name, e.g. `BTCUSDT`.
174 | price: Price of each contract.
175 | quantity: The buying or selling quantity.
176 | client_order_id: Client order id.
177 |
178 | Returns:
179 | success: Success results, otherwise it's None.
180 | error: Error information, otherwise it's None.
181 | """
182 | uri = "/api/v3/order"
183 | data = {
184 | "symbol": symbol,
185 | "side": action,
186 | "type": "LIMIT",
187 | "timeInForce": "GTC",
188 | "quantity": quantity,
189 | "price": price,
190 | "recvWindow": "5000",
191 | "newOrderRespType": "FULL",
192 | "timestamp": tools.get_cur_timestamp_ms()
193 | }
194 | if client_order_id:
195 | data["newClientOrderId"] = client_order_id
196 | success, error = await self.request("POST", uri, body=data, auth=True)
197 | return success, error
198 |
199 | async def revoke_order(self, symbol, order_id, client_order_id=None):
200 | """Cancelling an unfilled order.
201 | Args:
202 | symbol: Symbol name, e.g. `BTCUSDT`.
203 | order_id: Order id.
204 | client_order_id: Client order id.
205 |
206 | Returns:
207 | success: Success results, otherwise it's None.
208 | error: Error information, otherwise it's None.
209 | """
210 | uri = "/api/v3/order"
211 | params = {
212 | "symbol": symbol,
213 | "orderId": order_id,
214 | "timestamp": tools.get_cur_timestamp_ms()
215 | }
216 | if client_order_id:
217 | params["origClientOrderId"] = client_order_id
218 | success, error = await self.request("DELETE", uri, params=params, auth=True)
219 | return success, error
220 |
221 | async def get_order_status(self, symbol, order_id, client_order_id):
222 | """Get order details by order id.
223 |
224 | Args:
225 | symbol: Symbol name, e.g. `BTCUSDT`.
226 | order_id: Order id.
227 | client_order_id: Client order id.
228 |
229 | Returns:
230 | success: Success results, otherwise it's None.
231 | error: Error information, otherwise it's None.
232 | """
233 | uri = "/api/v3/order"
234 | params = {
235 | "symbol": symbol,
236 | "orderId": str(order_id),
237 | "origClientOrderId": client_order_id,
238 | "timestamp": tools.get_cur_timestamp_ms()
239 | }
240 | success, error = await self.request("GET", uri, params=params, auth=True)
241 | return success, error
242 |
243 | async def get_all_orders(self, symbol):
244 | """Get all account orders; active, canceled, or filled.
245 | Args:
246 | symbol: Symbol name, e.g. `BTCUSDT`.
247 |
248 | Returns:
249 | success: Success results, otherwise it's None.
250 | error: Error information, otherwise it's None.
251 | """
252 | uri = "/api/v3/allOrders"
253 | params = {
254 | "symbol": symbol,
255 | "timestamp": tools.get_cur_timestamp_ms()
256 | }
257 | success, error = await self.request("GET", uri, params=params, auth=True)
258 | return success, error
259 |
260 | async def get_open_orders(self, symbol):
261 | """Get all open order information.
262 | Args:
263 | symbol: Symbol name, e.g. `BTCUSDT`.
264 |
265 | Returns:
266 | success: Success results, otherwise it's None.
267 | error: Error information, otherwise it's None.
268 | """
269 | uri = "/api/v3/openOrders"
270 | params = {
271 | "symbol": symbol,
272 | "timestamp": tools.get_cur_timestamp_ms()
273 | }
274 | success, error = await self.request("GET", uri, params=params, auth=True)
275 | return success, error
276 |
277 | async def get_listen_key(self):
278 | """Get listen key, start a new user data stream.
279 |
280 | Returns:
281 | success: Success results, otherwise it's None.
282 | error: Error information, otherwise it's None.
283 | """
284 | uri = "/api/v3/userDataStream"
285 | success, error = await self.request("POST", uri)
286 | return success, error
287 |
288 | async def put_listen_key(self, listen_key):
289 | """Keepalive a user data stream to prevent a time out.
290 |
291 | Args:
292 | listen_key: Listen key.
293 |
294 | Returns:
295 | success: Success results, otherwise it's None.
296 | error: Error information, otherwise it's None.
297 | """
298 | uri = "/api/v3/userDataStream"
299 | params = {
300 | "listenKey": listen_key
301 | }
302 | success, error = await self.request("PUT", uri, params=params)
303 | return success, error
304 |
305 | async def delete_listen_key(self, listen_key):
306 | """Delete a listen key.
307 |
308 | Args:
309 | listen_key: Listen key.
310 |
311 | Returns:
312 | success: Success results, otherwise it's None.
313 | error: Error information, otherwise it's None.
314 | """
315 | uri = "/api/v3/userDataStream"
316 | params = {
317 | "listenKey": listen_key
318 | }
319 | success, error = await self.request("DELETE", uri, params=params)
320 | return success, error
321 |
322 | async def request(self, method, uri, params=None, body=None, headers=None, auth=False):
323 | """Do HTTP request.
324 |
325 | Args:
326 | method: HTTP request method. `GET` / `POST` / `DELETE` / `PUT`.
327 | uri: HTTP request uri.
328 | params: HTTP query params.
329 | body: HTTP request body.
330 | headers: HTTP request headers.
331 | auth: If this request requires authentication.
332 |
333 | Returns:
334 | success: Success results, otherwise it's None.
335 | error: Error information, otherwise it's None.
336 | """
337 | url = urljoin(self._host, uri)
338 | data = {}
339 | if params:
340 | data.update(params)
341 | if body:
342 | data.update(body)
343 |
344 | if data:
345 | query = "&".join(["=".join([str(k), str(v)]) for k, v in data.items()])
346 | else:
347 | query = ""
348 | if auth and query:
349 | signature = hmac.new(self._secret_key.encode(), query.encode(), hashlib.sha256).hexdigest()
350 | query += "&signature={s}".format(s=signature)
351 | if query:
352 | url += ("?" + query)
353 |
354 | if not headers:
355 | headers = {}
356 | headers["X-MBX-APIKEY"] = self._access_key
357 | _, success, error = await AsyncHttpRequests.fetch(method, url, headers=headers, timeout=10, verify_ssl=False)
358 | return success, error
359 |
--------------------------------------------------------------------------------
/aioquant/platform/huobi.py:
--------------------------------------------------------------------------------
1 | # -*- coding:utf-8 -*-
2 |
3 | """
4 | Huobi Trade module.
5 | https://huobiapi.github.io/docs/spot/v1/cn
6 |
7 | Author: HuangTao
8 | Date: 2018/08/30
9 | Email: huangtao@ifclover.com
10 | """
11 |
12 | import base64
13 | import datetime
14 | import hashlib
15 | import hmac
16 | import json
17 | import urllib
18 | from urllib import parse
19 | from urllib.parse import urljoin
20 |
21 | from aioquant.utils.web import AsyncHttpRequests
22 |
23 | __all__ = ("HuobiRestAPI", )
24 |
25 |
26 | class HuobiRestAPI:
27 | """Huobi REST API client.
28 |
29 | Attributes:
30 | access_key: Account's ACCESS KEY.
31 | secret_key: Account's SECRET KEY.
32 | host: HTTP request host, default `https://api.huobi.pro`.
33 | """
34 |
35 | def __init__(self, access_key, secret_key, host=None):
36 | """Initialize REST API client."""
37 | self._host = host or "https://api.huobi.pro"
38 | self._access_key = access_key
39 | self._secret_key = secret_key
40 | self._account_id = None
41 |
42 | async def get_server_time(self):
43 | """This endpoint returns the current system time in milliseconds adjusted to Singapore time zone.
44 |
45 | Returns:
46 | success: Success results, otherwise it's None.
47 | error: Error information, otherwise it's None.
48 | """
49 | uri = "/v1/common/timestamp"
50 | success, error = await self.request("GET", uri)
51 | return success, error
52 |
53 | async def get_exchange_info(self):
54 | """Get exchange information.
55 |
56 | Returns:
57 | success: Success results, otherwise it's None.
58 | error: Error information, otherwise it's None.
59 | """
60 | uri = "/v1/common/symbols"
61 | success, error = await self.request("GET", uri)
62 | return success, error
63 |
64 | async def get_orderbook(self, symbol, depth=20, step="step0"):
65 | """Get latest orderbook information.
66 |
67 | Args:
68 | symbol: Symbol name, e.g. `ethusdt`.
69 | depth: The number of market depth to return on each side, `5` / `10` / `20`, default is 10.
70 | step: Market depth aggregation level, `step0` / `step1` / `step2` / `step3` / `step4` / `step5`.
71 |
72 | Returns:
73 | success: Success results, otherwise it's None.
74 | error: Error information, otherwise it's None.
75 |
76 | Note:
77 | When type is set to `step0`, the default value of `depth` is 150 instead of 20.
78 | """
79 | uri = "/market/depth"
80 | params = {
81 | "symbol": symbol,
82 | "depth": depth,
83 | "type": step
84 | }
85 | success, error = await self.request("GET", uri, params=params)
86 | return success, error
87 |
88 | async def get_trade(self, symbol):
89 | """Get latest trade information.
90 |
91 | Args:
92 | symbol: Symbol name, e.g. `ethusdt`.
93 |
94 | Returns:
95 | success: Success results, otherwise it's None.
96 | error: Error information, otherwise it's None.
97 | """
98 | uri = "/market/trade"
99 | params = {
100 | "symbol": symbol
101 | }
102 | success, error = await self.request("GET", uri, params=params)
103 | return success, error
104 |
105 | async def get_kline(self, symbol, interval="1min", limit=150):
106 | """Get kline information.
107 |
108 | Args:
109 | symbol: Symbol name, e.g. `ethusdt`.
110 | interval: Kline interval type, `1min` / `5min` / `15min` / `30min` / `60min` / `4hour` / `1day` / `1mon` / `1week` / `1year`.
111 | limit: Number of results per request. (default 150, max 2000.)
112 |
113 | Returns:
114 | success: Success results, otherwise it's None.
115 | error: Error information, otherwise it's None.
116 |
117 | Notes:
118 | If start and end are not sent, the most recent klines are returned.
119 | """
120 | uri = "/market/history/kline"
121 | params = {
122 | "symbol": symbol,
123 | "period": interval,
124 | "size": limit
125 | }
126 | success, error = await self.request("GET", uri, params=params)
127 | return success, error
128 |
129 | async def get_user_accounts(self):
130 | """This endpoint returns a list of accounts owned by this API user.
131 |
132 | Returns:
133 | success: Success results, otherwise it's None.
134 | error: Error information, otherwise it's None.
135 | """
136 | uri = "/v1/account/accounts"
137 | success, error = await self.request("GET", uri, auth=True)
138 | return success, error
139 |
140 | async def _get_account_id(self):
141 | if self._account_id:
142 | return self._account_id
143 | success, error = await self.get_user_accounts()
144 | if error:
145 | return None
146 | for item in success["data"]:
147 | if item["type"] == "spot":
148 | self._account_id = item["id"]
149 | return self._account_id
150 | return None
151 |
152 | async def get_account_balance(self):
153 | """This endpoint returns the balance of an account specified by account id.
154 |
155 | Returns:
156 | success: Success results, otherwise it's None.
157 | error: Error information, otherwise it's None.
158 | """
159 | account_id = await self._get_account_id()
160 | uri = "/v1/account/accounts/{account_id}/balance".format(account_id=account_id)
161 | success, error = await self.request("GET", uri, auth=True)
162 | return success, error
163 |
164 | async def get_balance_all(self):
165 | """This endpoint returns the balances of all the sub-account aggregated.
166 |
167 | Returns:
168 | success: Success results, otherwise it's None.
169 | error: Error information, otherwise it's None.
170 | """
171 | uri = "/v1/subuser/aggregate-balance"
172 | success, error = await self.request("GET", uri, auth=True)
173 | return success, error
174 |
175 | async def create_order(self, symbol, price, quantity, order_type, client_order_id=None):
176 | """Create an order.
177 | Args:
178 | symbol: Symbol name, e.g. `ethusdt`.
179 | price: Price of each contract.
180 | quantity: The buying or selling quantity.
181 | order_type: Order type, `buy-market` / `sell-market` / `buy-limit` / `sell-limit`.
182 | client_order_id: Client order id.
183 |
184 | Returns:
185 | success: Success results, otherwise it's None.
186 | error: Error information, otherwise it's None.
187 | """
188 | uri = "/v1/order/orders/place"
189 | account_id = await self._get_account_id()
190 | info = {
191 | "account-id": account_id,
192 | "price": price,
193 | "amount": quantity,
194 | "source": "api",
195 | "symbol": symbol,
196 | "type": order_type
197 | }
198 | if order_type == "buy-limit" or order_type == "sell-limit":
199 | info["price"] = price
200 | if client_order_id:
201 | info["client-order-id"] = client_order_id
202 | success, error = await self.request("POST", uri, body=info, auth=True)
203 | return success, error
204 |
205 | async def revoke_order(self, order_id):
206 | """Cancelling an unfilled order.
207 | Args:
208 | order_id: Order id.
209 |
210 | Returns:
211 | success: Success results, otherwise it's None.
212 | error: Error information, otherwise it's None.
213 | """
214 | uri = "/v1/order/orders/{order_id}/submitcancel".format(order_id=order_id)
215 | success, error = await self.request("POST", uri, auth=True)
216 | return success, error
217 |
218 | async def revoke_orders(self, order_ids):
219 | """Cancelling unfilled orders.
220 | Args:
221 | order_ids: Order id list.
222 |
223 | Returns:
224 | success: Success results, otherwise it's None.
225 | error: Error information, otherwise it's None.
226 | """
227 | uri = "/v1/order/orders/batchcancel"
228 | body = {
229 | "order-ids": order_ids
230 | }
231 | success, error = await self.request("POST", uri, body=body, auth=True)
232 | return success, error
233 |
234 | async def get_open_orders(self, symbol, limit=500):
235 | """Get all open order information.
236 |
237 | Args:
238 | symbol: Symbol name, e.g. `ethusdt`.
239 | limit: The number of orders to return, [1, 500].
240 |
241 | Returns:
242 | success: Success results, otherwise it's None.
243 | error: Error information, otherwise it's None.
244 | """
245 | uri = "/v1/order/openOrders"
246 | account_id = await self._get_account_id()
247 | params = {
248 | "account-id": account_id,
249 | "symbol": symbol,
250 | "size": limit
251 | }
252 | success, error = await self.request("GET", uri, params=params, auth=True)
253 | return success, error
254 |
255 | async def get_order_status(self, order_id):
256 | """Get order details by order id.
257 |
258 | Args:
259 | order_id: Order id.
260 |
261 | Returns:
262 | success: Success results, otherwise it's None.
263 | error: Error information, otherwise it's None.
264 | """
265 | uri = "/v1/order/orders/{order_id}".format(order_id=order_id)
266 | success, error = await self.request("GET", uri, auth=True)
267 | return success, error
268 |
269 | async def request(self, method, uri, params=None, body=None, auth=False):
270 | """Do HTTP request.
271 |
272 | Args:
273 | method: HTTP request method. `GET` / `POST` / `DELETE` / `PUT`.
274 | uri: HTTP request uri.
275 | params: HTTP query params.
276 | body: HTTP request body.
277 | auth: If this request requires authentication.
278 |
279 | Returns:
280 | success: Success results, otherwise it's None.
281 | error: Error information, otherwise it's None.
282 | """
283 | url = urljoin(self._host, uri)
284 | if auth:
285 | timestamp = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S")
286 | params = params if params else {}
287 | params.update({"AccessKeyId": self._access_key,
288 | "SignatureMethod": "HmacSHA256",
289 | "SignatureVersion": "2",
290 | "Timestamp": timestamp})
291 |
292 | host_name = urllib.parse.urlparse(self._host).hostname.lower()
293 | params["Signature"] = self.generate_signature(method, params, host_name, uri)
294 |
295 | if method == "GET":
296 | headers = {
297 | "Content-type": "application/x-www-form-urlencoded",
298 | "User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) "
299 | "Chrome/39.0.2171.71 Safari/537.36"
300 | }
301 | else:
302 | headers = {
303 | "Accept": "application/json",
304 | "Content-type": "application/json"
305 | }
306 | _, success, error = await AsyncHttpRequests.fetch(method, url, params=params, data=body, headers=headers,
307 | timeout=10)
308 | if error:
309 | return success, error
310 | if not isinstance(success, dict):
311 | success = json.loads(success)
312 | if success.get("status") != "ok":
313 | return None, success
314 | return success, None
315 |
316 | def generate_signature(self, method, params, host_url, request_path):
317 | query = "&".join(["{}={}".format(k, parse.quote(str(params[k]))) for k in sorted(params.keys())])
318 | payload = [method, host_url, request_path, query]
319 | payload = "\n".join(payload)
320 | payload = payload.encode(encoding="utf8")
321 | secret_key = self._secret_key.encode(encoding="utf8")
322 | digest = hmac.new(secret_key, payload, digestmod=hashlib.sha256).digest()
323 | signature = base64.b64encode(digest)
324 | signature = signature.decode()
325 | return signature
326 |
--------------------------------------------------------------------------------
/aioquant/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 base64
13 | import hmac
14 | import json
15 | import time
16 | from urllib.parse import urljoin
17 |
18 | from aioquant.order import ORDER_ACTION_BUY
19 | from aioquant.order import ORDER_TYPE_LIMIT, ORDER_TYPE_MARKET
20 | from aioquant.utils import logger
21 | from aioquant.utils.web import AsyncHttpRequests
22 |
23 | __all__ = ("OKExRestAPI", )
24 |
25 |
26 | class OKExRestAPI:
27 | """ OKEx REST API client.
28 |
29 | Attributes:
30 | access_key: Account's ACCESS KEY.
31 | secret_key: Account's SECRET KEY.
32 | passphrase: API KEY Passphrase.
33 | host: HTTP request host, default `https://www.okex.com`
34 | """
35 |
36 | def __init__(self, access_key, secret_key, passphrase, host=None):
37 | """Initialize."""
38 | self._host = host or "https://www.okex.com"
39 | self._access_key = access_key
40 | self._secret_key = secret_key
41 | self._passphrase = passphrase
42 |
43 | async def get_orderbook(self, symbol, depth=None, limit=10):
44 | """Get latest orderbook information.
45 |
46 | Args:
47 | symbol: Symbol name, e.g. `BTC-USDT`.
48 | depth: Aggregation of the order book. e.g . 0.1, 0.001.
49 | limit: Number of results per request. (default 10, max 200.)
50 |
51 | Returns:
52 | success: Success results, otherwise it's None.
53 | error: Error information, otherwise it's None.
54 | """
55 | uri = "/api/spot/v3/instruments/{symbol}/book".format(symbol=symbol)
56 | params = {
57 | "size": limit
58 | }
59 | if depth:
60 | params["depth"] = depth
61 | success, error = await self.request("GET", uri, params=params)
62 | return success, error
63 |
64 | async def get_trade(self, symbol, limit=10):
65 | """Get latest trade information.
66 |
67 | Args:
68 | symbol: Symbol name, e.g. `BTC-USDT`.
69 | limit: Number of results per request. (Default 10, max 60.)
70 |
71 | Returns:
72 | success: Success results, otherwise it's None.
73 | error: Error information, otherwise it's None.
74 | """
75 | uri = "/api/spot/v3/instruments/{symbol}/trades".format(symbol=symbol)
76 | params = {
77 | "limit": limit
78 | }
79 | success, error = await self.request("GET", uri, params=params)
80 | return success, error
81 |
82 | async def get_kline(self, symbol, interval="60", start=None, end=None):
83 | """Get kline information.
84 |
85 | Args:
86 | symbol: Symbol name, e.g. `BTCUSDT`.
87 | interval: Kline interval type, valid values: 60/180/300/900/1800/3600/7200/14400/21600/43200/86400/604800.
88 | start: Start time in ISO 8601. e.g. 2019-03-19T16:00:00.000Z
89 | end: End time in ISO 8601. e.g. 2019-03-19T16:00:00.000Z
90 |
91 | Returns:
92 | success: Success results, otherwise it's None.
93 | error: Error information, otherwise it's None.
94 |
95 | Notes:
96 | Both parameters will be ignored if either one of start or end are not provided. The last 200 records of
97 | data will be returned if the time range is not specified in the request.
98 | """
99 | uri = "/api/spot/v3/instruments/{symbol}/candles".format(symbol=symbol)
100 | params = {
101 | "granularity": interval
102 | }
103 | if start and end:
104 | params["start"] = start
105 | params["end"] = end
106 | success, error = await self.request("GET", uri, params=params)
107 | return success, error
108 |
109 | async def get_user_account(self):
110 | """Get account asset information.
111 |
112 | Returns:
113 | success: Success results, otherwise it's None.
114 | error: Error information, otherwise it's None.
115 | """
116 | uri = "/api/spot/v3/accounts"
117 | result, error = await self.request("GET", uri, auth=True)
118 | return result, error
119 |
120 | async def create_order(self, action, symbol, price, quantity, order_type=ORDER_TYPE_LIMIT, client_oid=None):
121 | """Create an order.
122 | Args:
123 | action: Action type, `BUY` or `SELL`.
124 | symbol: Trading pair, e.g. `BTC-USDT`.
125 | price: Order price.
126 | quantity: Order quantity.
127 | order_type: Order type, `MARKET` or `LIMIT`.
128 | client_oid: Client order id, default is `None`.
129 |
130 | Returns:
131 | success: Success results, otherwise it's None.
132 | error: Error information, otherwise it's None.
133 | """
134 | uri = "/api/spot/v3/orders"
135 | data = {
136 | "side": "buy" if action == ORDER_ACTION_BUY else "sell",
137 | "instrument_id": symbol,
138 | "margin_trading": 1
139 | }
140 | if order_type == ORDER_TYPE_LIMIT:
141 | data["type"] = "limit"
142 | data["price"] = price
143 | data["size"] = quantity
144 | elif order_type == ORDER_TYPE_MARKET:
145 | data["type"] = "market"
146 | if action == ORDER_ACTION_BUY:
147 | data["notional"] = quantity # buy price.
148 | else:
149 | data["size"] = quantity # sell quantity.
150 | else:
151 | logger.error("order_type error! order_type:", order_type, caller=self)
152 | return None, "order type error!"
153 | if client_oid:
154 | data["client_oid"] = client_oid
155 | result, error = await self.request("POST", uri, body=data, auth=True)
156 | return result, error
157 |
158 | async def revoke_order(self, symbol, order_id=None, client_oid=None):
159 | """Cancelling an unfilled order.
160 | Args:
161 | symbol: Trading pair, e.g. `BTC-USDT`.
162 | order_id: Order id, default is `None`.
163 | client_oid: Client order id, default is `None`.
164 |
165 | Returns:
166 | success: Success results, otherwise it's None.
167 | error: Error information, otherwise it's None.
168 |
169 | Notes:
170 | `order_id` and `order_oid` must exist one, using order_id first.
171 | """
172 | if order_id:
173 | uri = "/api/spot/v3/cancel_orders/{order_id}".format(order_id=order_id)
174 | elif client_oid:
175 | uri = "/api/spot/v3/cancel_orders/{client_oid}".format(client_oid=client_oid)
176 | else:
177 | return None, "order id error!"
178 | data = {
179 | "instrument_id": symbol
180 | }
181 | result, error = await self.request("POST", uri, body=data, auth=True)
182 | if error:
183 | return order_id, error
184 | if result["result"]:
185 | return order_id, None
186 | return order_id, result
187 |
188 | async def revoke_orders(self, symbol, order_ids=None, client_oids=None):
189 | """Cancelling multiple open orders with order_id,Maximum 10 orders can be cancelled at a time for each
190 | trading pair.
191 |
192 | Args:
193 | symbol: Trading pair, e.g. `BTC-USDT`.
194 | order_ids: Order id list, default is `None`.
195 | client_oids: Client order id list, default is `None`.
196 |
197 | Returns:
198 | success: Success results, otherwise it's None.
199 | error: Error information, otherwise it's None.
200 |
201 | Notes:
202 | `order_ids` and `order_oids` must exist one, using order_ids first.
203 | """
204 | uri = "/api/spot/v3/cancel_batch_orders"
205 | if order_ids:
206 | if len(order_ids) > 10:
207 | logger.warn("only revoke 10 orders per request!", caller=self)
208 | body = [
209 | {
210 | "instrument_id": symbol,
211 | "order_ids": order_ids[:10]
212 | }
213 | ]
214 | elif client_oids:
215 | if len(client_oids) > 10:
216 | logger.warn("only revoke 10 orders per request!", caller=self)
217 | body = [
218 | {
219 | "instrument_id": symbol,
220 | "client_oids": client_oids[:10]
221 | }
222 | ]
223 | else:
224 | return None, "order id list error!"
225 | result, error = await self.request("POST", uri, body=body, auth=True)
226 | return result, error
227 |
228 | async def get_open_orders(self, symbol, limit=100):
229 | """Get order details by order id.
230 |
231 | Args:
232 | symbol: Trading pair, e.g. `BTC-USDT`.
233 | limit: order count to return, max is 100, default is 100.
234 |
235 | Returns:
236 | success: Success results, otherwise it's None.
237 | error: Error information, otherwise it's None.
238 | """
239 | uri = "/api/spot/v3/orders_pending"
240 | params = {
241 | "instrument_id": symbol,
242 | "limit": limit
243 | }
244 | result, error = await self.request("GET", uri, params=params, auth=True)
245 | return result, error
246 |
247 | async def get_order_status(self, symbol, order_id=None, client_oid=None):
248 | """Get order status.
249 | Args:
250 | symbol: Trading pair, e.g. `BTC-USDT`.
251 | order_id: Order id.
252 | client_oid: Client order id, default is `None`.
253 |
254 | Returns:
255 | success: Success results, otherwise it's None.
256 | error: Error information, otherwise it's None.
257 |
258 | Notes:
259 | `order_id` and `order_oid` must exist one, using order_id first.
260 | """
261 | if order_id:
262 | uri = "/api/spot/v3/orders/{order_id}".format(order_id=order_id)
263 | elif client_oid:
264 | uri = "/api/spot/v3/orders/{client_oid}".format(client_oid=client_oid)
265 | else:
266 | return None, "order id error!"
267 | params = {
268 | "instrument_id": symbol
269 | }
270 | result, error = await self.request("GET", uri, params=params, auth=True)
271 | return result, error
272 |
273 | async def request(self, method, uri, params=None, body=None, headers=None, auth=False):
274 | """Do HTTP request.
275 |
276 | Args:
277 | method: HTTP request method. `GET` / `POST` / `DELETE` / `PUT`.
278 | uri: HTTP request uri.
279 | params: HTTP query params.
280 | body: HTTP request body.
281 | headers: HTTP request headers.
282 | auth: If this request requires authentication.
283 |
284 | Returns:
285 | success: Success results, otherwise it's None.
286 | error: Error information, otherwise it's None.
287 | """
288 | if params:
289 | query = "&".join(["{}={}".format(k, params[k]) for k in sorted(params.keys())])
290 | uri += "?" + query
291 | url = urljoin(self._host, uri)
292 |
293 | if auth:
294 | timestamp = str(time.time()).split(".")[0] + "." + str(time.time()).split(".")[1][:3]
295 | if body:
296 | body = json.dumps(body)
297 | else:
298 | body = ""
299 | message = str(timestamp) + str.upper(method) + uri + str(body)
300 | mac = hmac.new(bytes(self._secret_key, encoding="utf8"), bytes(message, encoding="utf-8"),
301 | digestmod="sha256")
302 | d = mac.digest()
303 | sign = base64.b64encode(d)
304 |
305 | if not headers:
306 | headers = {}
307 | headers["Content-Type"] = "application/json"
308 | headers["OK-ACCESS-KEY"] = self._access_key.encode().decode()
309 | headers["OK-ACCESS-SIGN"] = sign.decode()
310 | headers["OK-ACCESS-TIMESTAMP"] = str(timestamp)
311 | headers["OK-ACCESS-PASSPHRASE"] = self._passphrase
312 | _, success, error = await AsyncHttpRequests.fetch(method, url, body=body, headers=headers, timeout=10)
313 | return success, error
314 |
--------------------------------------------------------------------------------
/aioquant/quant.py:
--------------------------------------------------------------------------------
1 | # -*- coding:utf-8 -*-
2 |
3 | """
4 | Asynchronous event I/O 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 | import inspect
14 |
15 | from aioquant.utils import logger
16 | from aioquant.configure import config
17 |
18 |
19 | class AIOQuant:
20 | """Asynchronous event I/O driven quantitative trading framework.
21 | """
22 |
23 | def __init__(self) -> None:
24 | self.loop = None
25 | self.event_center = None
26 |
27 | def _initialize(self, config_file):
28 | """Initialize."""
29 | self._get_event_loop()
30 | self._load_settings(config_file)
31 | self._init_logger()
32 | self._do_heartbeat()
33 | return self
34 |
35 | def start(self, config_file=None, entrance_func=None) -> None:
36 | """Start the event loop."""
37 | def keyboard_interrupt(s, f):
38 | print("KeyboardInterrupt (ID: {}) has been caught. Cleaning up...".format(s))
39 | self.stop()
40 | signal.signal(signal.SIGINT, keyboard_interrupt)
41 |
42 | self._initialize(config_file)
43 | if entrance_func:
44 | if inspect.iscoroutinefunction(entrance_func):
45 | self.loop.create_task(entrance_func())
46 | else:
47 | entrance_func()
48 |
49 | logger.info("start io loop ...", caller=self)
50 | self.loop.run_forever()
51 |
52 | def stop(self) -> None:
53 | """Stop the event loop."""
54 | logger.info("stop io loop.", caller=self)
55 | # TODO: clean up running coroutine
56 | self.loop.stop()
57 |
58 | def _get_event_loop(self) -> asyncio.events.get_event_loop():
59 | """Get a main io loop."""
60 | if not self.loop:
61 | self.loop = asyncio.get_event_loop()
62 | return self.loop
63 |
64 | def _load_settings(self, config_module) -> None:
65 | """Load config settings.
66 |
67 | Args:
68 | config_module: config file path, normally it's a json file.
69 | """
70 | config.loads(config_module)
71 |
72 | def _init_logger(self) -> None:
73 | """Initialize logger."""
74 | logger.initLogger(**config.log)
75 |
76 | def _do_heartbeat(self) -> None:
77 | """Start server heartbeat."""
78 | from aioquant.heartbeat import heartbeat
79 | self.loop.call_later(0.5, heartbeat.ticker)
80 |
--------------------------------------------------------------------------------
/aioquant/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 aioquant.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 |
--------------------------------------------------------------------------------
/aioquant/utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guhhhhaa/aioquant-add-triangular-arbitrage/35f7cf5c4266ad5feac8d9d1673006d0488f7a1f/aioquant/utils/__init__.py
--------------------------------------------------------------------------------
/aioquant/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, timeout=1):
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 | timeout: Timeout time to be locked, default is 1s.
28 |
29 | NOTE:
30 | This decorator must to be used on `async method`.
31 | """
32 | assert isinstance(name, str)
33 |
34 | def decorating_function(method):
35 | global METHOD_LOCKERS
36 | locker = METHOD_LOCKERS.get(name)
37 | if not locker:
38 | locker = asyncio.Lock()
39 | METHOD_LOCKERS[name] = locker
40 |
41 | @functools.wraps(method)
42 | async def wrapper(*args, **kwargs):
43 | if not wait and locker.locked():
44 | return
45 | try:
46 | await locker.acquire()
47 | return await asyncio.wait_for(method(*args, **kwargs), timeout)
48 | # return await method(*args, **kwargs)
49 | finally:
50 | locker.release()
51 | return wrapper
52 | return decorating_function
53 |
--------------------------------------------------------------------------------
/aioquant/utils/logger.py:
--------------------------------------------------------------------------------
1 | # -*- coding:utf-8 -*-
2 |
3 | """
4 | Log printer.
5 |
6 | Author: HuangTao
7 | Date: 2018/04/08
8 | Email: huangtao@ifclover.com
9 | """
10 |
11 | import os
12 | import sys
13 | import shutil
14 | import logging
15 | import traceback
16 | from logging.handlers import TimedRotatingFileHandler
17 |
18 | initialized = False
19 |
20 |
21 | def initLogger(level="DEBUG", path=None, name=None, clear=False, backup_count=0, console=True):
22 | """Initialize logger.
23 |
24 | Args:
25 | level: Log level, `DEBUG` or `INFO`, default is `DEBUG`.
26 | path: Log path, default is `/var/log/aioquant`.
27 | name: Log file name, default is `quant.log`.
28 | clear: If clear all history log file when initialize, default is `False`.
29 | backup_count: How many log file to be saved. We will save log file per day at middle nigh,
30 | default is `0` to save file permanently.
31 | console: If print log to console, otherwise print to log file.
32 | """
33 | global initialized
34 | if initialized:
35 | return
36 | path = path or "/var/log/aioquant"
37 | name = name or "quant.log"
38 | logger = logging.getLogger()
39 | logger.setLevel(level)
40 | if console:
41 | print("init logger ...")
42 | handler = logging.StreamHandler()
43 | else:
44 | if clear and os.path.isdir(path):
45 | shutil.rmtree(path)
46 | if not os.path.isdir(path):
47 | os.makedirs(path)
48 | logfile = os.path.join(path, name)
49 | handler = TimedRotatingFileHandler(logfile, "midnight", backupCount=backup_count)
50 | print("init logger ...", logfile)
51 | fmt_str = "%(levelname)1.1s [%(asctime)s] %(message)s"
52 | fmt = logging.Formatter(fmt=fmt_str, datefmt=None)
53 | handler.setFormatter(fmt)
54 | logger.addHandler(handler)
55 | initialized = True
56 |
57 |
58 | def info(*args, **kwargs):
59 | func_name, kwargs = _log_msg_header(*args, **kwargs)
60 | logging.info(_log(func_name, *args, **kwargs))
61 |
62 |
63 | def warn(*args, **kwargs):
64 | msg_header, kwargs = _log_msg_header(*args, **kwargs)
65 | logging.warning(_log(msg_header, *args, **kwargs))
66 |
67 |
68 | def debug(*args, **kwargs):
69 | msg_header, kwargs = _log_msg_header(*args, **kwargs)
70 | logging.debug(_log(msg_header, *args, **kwargs))
71 |
72 |
73 | def error(*args, **kwargs):
74 | logging.error("*" * 60)
75 | msg_header, kwargs = _log_msg_header(*args, **kwargs)
76 | logging.error(_log(msg_header, *args, **kwargs))
77 | logging.error("*" * 60)
78 |
79 |
80 | def exception(*args, **kwargs):
81 | logging.error("*" * 60)
82 | msg_header, kwargs = _log_msg_header(*args, **kwargs)
83 | logging.error(_log(msg_header, *args, **kwargs))
84 | logging.error(traceback.format_exc())
85 | logging.error("*" * 60)
86 |
87 |
88 | def _log(msg_header, *args, **kwargs):
89 | _log_msg = msg_header
90 | for l in args:
91 | if type(l) == tuple:
92 | ps = str(l)
93 | else:
94 | try:
95 | ps = "%r" % l
96 | except:
97 | ps = str(l)
98 | if type(l) == str:
99 | _log_msg += ps[1:-1] + " "
100 | else:
101 | _log_msg += ps + " "
102 | if len(kwargs) > 0:
103 | _log_msg += str(kwargs)
104 | return _log_msg
105 |
106 |
107 | def _log_msg_header(*args, **kwargs):
108 | """Fetch log message header.
109 |
110 | NOTE:
111 | logger.xxx(... , caller=self) for instance method.
112 | logger.xxx(... , caller=cls) for class method.
113 | """
114 | cls_name = ""
115 | func_name = sys._getframe().f_back.f_back.f_code.co_name
116 | session_id = "-"
117 | try:
118 | _caller = kwargs.get("caller", None)
119 | if _caller:
120 | if not hasattr(_caller, "__name__"):
121 | _caller = _caller.__class__
122 | cls_name = _caller.__name__
123 | del kwargs["caller"]
124 | except:
125 | pass
126 | finally:
127 | msg_header = "[{session_id}] [{cls_name}.{func_name}] ".format(cls_name=cls_name, func_name=func_name,
128 | session_id=session_id)
129 | return msg_header, kwargs
130 |
--------------------------------------------------------------------------------
/aioquant/utils/tools.py:
--------------------------------------------------------------------------------
1 | # -*- coding:utf-8 -*-
2 |
3 | """
4 | Tools Bag.
5 |
6 | Author: HuangTao
7 | Date: 2018/04/28
8 | Email: huangtao@ifclover.com
9 | """
10 |
11 | import uuid
12 | import time
13 | import decimal
14 | import datetime
15 |
16 |
17 | def get_cur_timestamp():
18 | """Get current timestamp(second)."""
19 | ts = int(time.time())
20 | return ts
21 |
22 |
23 | def get_cur_timestamp_ms():
24 | """Get current timestamp(millisecond)."""
25 | ts = int(time.time() * 1000)
26 | return ts
27 |
28 |
29 | def get_datetime_str(fmt="%Y-%m-%d %H:%M:%S"):
30 | """Get date time string, year + month + day + hour + minute + second.
31 |
32 | Args:
33 | fmt: Date format, default is `%Y-%m-%d %H:%M:%S`.
34 |
35 | Returns:
36 | str_dt: Date time string.
37 | """
38 | today = datetime.datetime.today()
39 | str_dt = today.strftime(fmt)
40 | return str_dt
41 |
42 |
43 | def get_date_str(fmt="%Y%m%d", delta_days=0):
44 | """Get date string, year + month + day.
45 |
46 | Args:
47 | fmt: Date format, default is `%Y%m%d`.
48 | delta_days: Delta days for currently, default is 0.
49 |
50 | Returns:
51 | str_d: Date string.
52 | """
53 | day = datetime.datetime.today()
54 | if delta_days:
55 | day += datetime.timedelta(days=delta_days)
56 | str_d = day.strftime(fmt)
57 | return str_d
58 |
59 |
60 | def ts_to_datetime_str(ts=None, fmt="%Y-%m-%d %H:%M:%S"):
61 | """Convert timestamp to date time string.
62 |
63 | Args:
64 | ts: Timestamp, millisecond.
65 | fmt: Date time format, default is `%Y-%m-%d %H:%M:%S`.
66 |
67 | Returns:
68 | Date time string.
69 | """
70 | if not ts:
71 | ts = get_cur_timestamp()
72 | dt = datetime.datetime.fromtimestamp(int(ts))
73 | return dt.strftime(fmt)
74 |
75 |
76 | def datetime_str_to_ts(dt_str, fmt="%Y-%m-%d %H:%M:%S"):
77 | """Convert date time string to timestamp.
78 |
79 | Args:
80 | dt_str: Date time string.
81 | fmt: Date time format, default is `%Y-%m-%d %H:%M:%S`.
82 |
83 | Returns:
84 | ts: Timestamp, millisecond.
85 | """
86 | ts = int(time.mktime(datetime.datetime.strptime(dt_str, fmt).timetuple()))
87 | return ts
88 |
89 |
90 | def get_utc_time():
91 | """Get current UTC time."""
92 | utc_t = datetime.datetime.utcnow()
93 | return utc_t
94 |
95 |
96 | def utctime_str_to_ts(utctime_str, fmt="%Y-%m-%dT%H:%M:%S.%fZ"):
97 | """Convert UTC time string to timestamp(second).
98 |
99 | Args:
100 | utctime_str: UTC time string, e.g. `2019-03-04T09:14:27.806Z`.
101 | fmt: UTC time format, e.g. `%Y-%m-%dT%H:%M:%S.%fZ`.
102 |
103 | Returns:
104 | timestamp: Timestamp(second).
105 | """
106 | dt = datetime.datetime.strptime(utctime_str, fmt)
107 | timestamp = int(dt.replace(tzinfo=datetime.timezone.utc).astimezone(tz=None).timestamp())
108 | return timestamp
109 |
110 |
111 | def utctime_str_to_ms(utctime_str, fmt="%Y-%m-%dT%H:%M:%S.%fZ"):
112 | """Convert UTC time string to timestamp(millisecond).
113 |
114 | Args:
115 | utctime_str: UTC time string, e.g. `2019-03-04T09:14:27.806Z`.
116 | fmt: UTC time format, e.g. `%Y-%m-%dT%H:%M:%S.%fZ`.
117 |
118 | Returns:
119 | timestamp: Timestamp(millisecond).
120 | """
121 | dt = datetime.datetime.strptime(utctime_str, fmt)
122 | timestamp = int(dt.replace(tzinfo=datetime.timezone.utc).astimezone(tz=None).timestamp() * 1000)
123 | return timestamp
124 |
125 |
126 | def get_utctime_str(fmt="%Y-%m-%dT%H:%M:%S.%fZ"):
127 | """Get current UTC time string.
128 |
129 | Args:
130 | fmt: UTC time format, e.g. `%Y-%m-%dT%H:%M:%S.%fZ`.
131 |
132 | Returns:
133 | utctime_str: UTC time string, e.g. `2019-03-04T09:14:27.806Z`.
134 | """
135 | utctime = get_utc_time()
136 | utctime_str = utctime.strftime(fmt)
137 | return utctime_str
138 |
139 |
140 | def get_uuid1():
141 | """Generate a UUID based on the host ID and current time
142 |
143 | Returns:
144 | s: UUID1 string.
145 | """
146 | uid1 = uuid.uuid1()
147 | s = str(uid1)
148 | return s
149 |
150 |
151 | def get_uuid3(str_in):
152 | """Generate a UUID using an MD5 hash of a namespace UUID and a name
153 |
154 | Args:
155 | str_in: Input string.
156 |
157 | Returns:
158 | s: UUID3 string.
159 | """
160 | uid3 = uuid.uuid3(uuid.NAMESPACE_DNS, str_in)
161 | s = str(uid3)
162 | return s
163 |
164 |
165 | def get_uuid4():
166 | """Generate a random UUID.
167 |
168 | Returns:
169 | s: UUID5 string.
170 | """
171 | uid4 = uuid.uuid4()
172 | s = str(uid4)
173 | return s
174 |
175 |
176 | def get_uuid5(str_in):
177 | """Generate a UUID using a SHA-1 hash of a namespace UUID and a name
178 |
179 | Args:
180 | str_in: Input string.
181 |
182 | Returns:
183 | s: UUID5 string.
184 | """
185 | uid5 = uuid.uuid5(uuid.NAMESPACE_DNS, str_in)
186 | s = str(uid5)
187 | return s
188 |
189 |
190 | def float_to_str(f, p=20):
191 | """Convert the given float to a string, without resorting to scientific notation.
192 |
193 | Args:
194 | f: Float params.
195 | p: Precision length.
196 |
197 | Returns:
198 | s: String format data.
199 | """
200 | if type(f) == str:
201 | f = float(f)
202 | ctx = decimal.Context(p)
203 | d1 = ctx.create_decimal(repr(f))
204 | s = format(d1, 'f')
205 | return s
206 |
--------------------------------------------------------------------------------
/aioquant/utils/web.py:
--------------------------------------------------------------------------------
1 | # -*- coding:utf-8 -*-
2 |
3 | """
4 | Web module.
5 |
6 | Author: HuangTao
7 | Date: 2018/08/26
8 | Email: huangtao@ifclover.com
9 | """
10 |
11 | import json
12 |
13 | import aiohttp
14 | from urllib.parse import urlparse
15 |
16 | from aioquant.utils import logger
17 | from aioquant.configure import config
18 | from aioquant.tasks import LoopRunTask, SingleTask
19 | from aioquant.utils.decorator import async_method_locker
20 |
21 |
22 | __all__ = ("AsyncHttpRequests", )
23 |
24 |
25 | class AsyncHttpRequests(object):
26 | """ Asynchronous HTTP Request Client.
27 | """
28 |
29 | # Every domain name holds a connection session, for less system resource utilization and faster request speed.
30 | _SESSIONS = {} # {"domain-name": session, ... }
31 |
32 | @classmethod
33 | async def fetch(cls, method, url, params=None, body=None, data=None, headers=None, timeout=30, **kwargs):
34 | """ Create a HTTP request.
35 |
36 | Args:
37 | method: HTTP request method. `GET` / `POST` / `PUT` / `DELETE`
38 | url: Request url.
39 | params: HTTP query params.
40 | body: HTTP request body, string or bytes format.
41 | data: HTTP request body, dict format.
42 | headers: HTTP request header.
43 | timeout: HTTP request timeout(seconds), default is 30s.
44 |
45 | kwargs:
46 | proxy: HTTP proxy.
47 |
48 | Return:
49 | code: HTTP response code.
50 | success: HTTP response data. If something wrong, this field is None.
51 | error: If something wrong, this field will holding a Error information, otherwise it's None.
52 |
53 | Raises:
54 | HTTP request exceptions or response data parse exceptions. All the exceptions will be captured and return
55 | Error information.
56 | """
57 | session = cls._get_session(url)
58 | if not kwargs.get("proxy"):
59 | kwargs["proxy"] = config.proxy # If there is a `HTTP PROXY` Configuration in config file?
60 | try:
61 | if method == "GET":
62 | response = await session.get(url, params=params, headers=headers, timeout=timeout, **kwargs)
63 | elif method == "POST":
64 | response = await session.post(url, params=params, data=body, json=data, headers=headers,
65 | timeout=timeout, **kwargs)
66 | elif method == "PUT":
67 | response = await session.put(url, params=params, data=body, json=data, headers=headers,
68 | timeout=timeout, **kwargs)
69 | elif method == "DELETE":
70 | response = await session.delete(url, params=params, data=body, json=data, headers=headers,
71 | timeout=timeout, **kwargs)
72 | else:
73 | error = "http method error!"
74 | return None, None, error
75 | except Exception as e:
76 | logger.error("method:", method, "url:", url, "headers:", headers, "params:", params, "body:", body,
77 | "data:", data, "Error:", e, caller=cls)
78 | return None, None, e
79 | code = response.status
80 | if code not in (200, 201, 202, 203, 204, 205, 206):
81 | text = await response.text()
82 | logger.error("method:", method, "url:", url, "headers:", headers, "params:", params, "body:", body,
83 | "data:", data, "code:", code, "result:", text, caller=cls)
84 | return code, None, text
85 | try:
86 | result = await response.json()
87 | except:
88 | result = await response.text()
89 | logger.debug("response data is not json format!", "method:", method, "url:", url, "headers:", headers,
90 | "params:", params, "body:", body, "data:", data, "code:", code, "result:", result, caller=cls)
91 | logger.debug("method:", method, "url:", url, "headers:", headers, "params:", params, "body:", body,
92 | "data:", data, "code:", code, "result:", json.dumps(result), caller=cls)
93 | return code, result, None
94 |
95 | @classmethod
96 | async def get(cls, url, params=None, body=None, data=None, headers=None, timeout=30, **kwargs):
97 | """ HTTP GET
98 | """
99 | result = await cls.fetch("GET", url, params, body, data, headers, timeout, **kwargs)
100 | return result
101 |
102 | @classmethod
103 | async def post(cls, url, params=None, body=None, data=None, headers=None, timeout=30, **kwargs):
104 | """ HTTP POST
105 | """
106 | result = await cls.fetch("POST", url, params, body, data, headers, timeout, **kwargs)
107 | return result
108 |
109 | @classmethod
110 | async def delete(cls, url, params=None, body=None, data=None, headers=None, timeout=30, **kwargs):
111 | """ HTTP DELETE
112 | """
113 | result = await cls.fetch("DELETE", url, params, body, data, headers, timeout, **kwargs)
114 | return result
115 |
116 | @classmethod
117 | async def put(cls, url, params=None, body=None, data=None, headers=None, timeout=30, **kwargs):
118 | """ HTTP PUT
119 | """
120 | result = await cls.fetch("PUT", url, params, body, data, headers, timeout, **kwargs)
121 | return result
122 |
123 | @classmethod
124 | def _get_session(cls, url):
125 | """ Get the connection session for url's domain, if no session, create a new.
126 |
127 | Args:
128 | url: HTTP request url.
129 |
130 | Returns:
131 | session: HTTP request session.
132 | """
133 | parsed_url = urlparse(url)
134 | key = parsed_url.netloc or parsed_url.hostname
135 | if key not in cls._SESSIONS:
136 | session = aiohttp.ClientSession()
137 | cls._SESSIONS[key] = session
138 | return cls._SESSIONS[key]
139 |
--------------------------------------------------------------------------------
/docs/asset.md:
--------------------------------------------------------------------------------
1 | ## 资产
2 |
3 | 通过资产模块(asset),可以订阅任意交易平台、任意交易账户的任意资产信息。
4 |
5 | 订阅 `资产事件` 之前,需要先部署 `Asset` 资产服务器,将需要订阅的资产信息配置到资产服务器,资产服务器定时(默认10秒)将最新的资产信息通过 `资产事件` 的形式推送至 `事件中心` ,我们只需要订阅相关 `资产事件` 即可。
6 |
7 |
8 | ### 1. 资产模块使用
9 |
10 | > 此处以订阅 `Binance(币安)` 交易平台的资产为例,假设我们的账户为 `test@gmail.com`。
11 | ```python
12 | from aioquant.utils import logger
13 | from aioquant.const import BINANCE
14 | from aioquant.asset import Asset, AssetSubscribe
15 |
16 |
17 | # 资产信息回调函数,注意此处回调函数是 `async` 异步函数,回调参数为 `asset Asset` 对象,数据结构请查看下边的介绍。
18 | async def on_event_asset_update(asset: Asset):
19 | logger.info("platform:", asset.platform) # 打印资产数据的平台信息
20 | logger.info("account:", asset.account) # 打印资产数据的账户信息
21 | logger.info("asset data dict:", asset.assets) # 打印资产数据的资产详情
22 | logger.info("asset data str:", asset.data) # 打印资产数据的资产详情
23 | logger.info("timestamp:", asset.timestamp) # 打印资产数据更新时间戳
24 | logger.info("update:", asset.update) # 打印资产数据是否有更新
25 |
26 |
27 | # 订阅资产信息
28 | account = "test@gmail.com"
29 | AssetSubscribe(BINANCE, account, on_event_asset_update)
30 | ```
31 |
32 | > 以上订阅资产数据的方式是比较通用的做法,但如果你使用了 [Trade 交易模块](./trade.md),那么通过初始化 `Trade` 模块即可订阅相应的资产数据。
33 |
34 |
35 | ### 2. 资产对象数据结构
36 |
37 | - 资产模块
38 | ```python
39 | from aioquant.asset import Asset
40 |
41 | Asset.platform # 交易平台名称
42 | Asset.account # 交易账户
43 | Asset.assets # 资产详细信息
44 | Asset.timestamp # 资产更新时间戳(毫秒)
45 | Asset.update # 资产是否有更新
46 |
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/configure/README.md:
--------------------------------------------------------------------------------
1 |
2 | ## 配置文件
3 |
4 | 框架启动的时候,需要指定一个 `json` 格式的配置文件。
5 | - [一个完整的配置文件示例](config.json)
6 |
7 |
8 | ## 配置使用
9 | 所有 `config.json` 配置文件里的 `key-value` 格式数据,都可以通过如下方式使用:
10 | ```python
11 | from aioquant.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/aioquant",
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/aioquant`
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/aioquant",
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. 我们与其它开源量化框架有什么区别 ?
36 |
37 | 目前市面上开源的量化交易框架不多,各有优劣,主要痛点有如下几点:
38 | - 多线程或多进程实现的伪异步,效率低;
39 | - 无法实现本地部署,策略和数据全部放在供应商服务器上,安全无法保障;
40 | - 仅提供交易所公开接口的简单封装,或一些简单的工具类函数,只能作为脚本运行,无法规模化;
41 | - 缺少稳定的行情支撑;
42 | - 缺少风控层的支撑;
43 | - 缺少数据存储的支撑;
44 | - ...
45 |
46 | `AIOQuant` 和它们的主要区别有:
47 | - 基于 [Python Asyncio](https://docs.python.org/3/library/asyncio.html) 原生异步事件循环,处理更简洁,效率更高;
48 | - 跨平台(Windows、Mac、Linux),可任意私有化部署;
49 | - 任意交易所的交易方式(现货、合约)统一,相同策略只需要区别不同配置,即可无缝切换任意交易所;
50 | - 所有交易所的行情统一,并通过事件订阅的形式,回调触发策略执行不同指令;
51 | - 支持任意多个策略协同运行;
52 | - 支持任意多个策略分布式运行;
53 | - 毫秒级延迟(一般瓶颈在网络延迟);
54 | - 提供任务、监控、存储、事件发布等一系列高级功能;
55 | - 定制化Docker容器分布式部署、配置运行;
56 | - 量化交易Web管理系统,通过管理工具,轻松实现对策略、风控、资产、服务器等进程或资源的动态管理;
57 |
--------------------------------------------------------------------------------
/docs/images/aioq_framework.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guhhhhaa/aioquant-add-triangular-arbitrage/35f7cf5c4266ad5feac8d9d1673006d0488f7a1f/docs/images/aioq_framework.jpg
--------------------------------------------------------------------------------
/docs/images/aioq_framework2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guhhhhaa/aioquant-add-triangular-arbitrage/35f7cf5c4266ad5feac8d9d1673006d0488f7a1f/docs/images/aioq_framework2.png
--------------------------------------------------------------------------------
/docs/images/login.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guhhhhaa/aioquant-add-triangular-arbitrage/35f7cf5c4266ad5feac8d9d1673006d0488f7a1f/docs/images/login.png
--------------------------------------------------------------------------------
/docs/images/rabbitmq_permission.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guhhhhaa/aioquant-add-triangular-arbitrage/35f7cf5c4266ad5feac8d9d1673006d0488f7a1f/docs/images/rabbitmq_permission.png
--------------------------------------------------------------------------------
/docs/images/rabbitmq_permission2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guhhhhaa/aioquant-add-triangular-arbitrage/35f7cf5c4266ad5feac8d9d1673006d0488f7a1f/docs/images/rabbitmq_permission2.png
--------------------------------------------------------------------------------
/docs/images/userpage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guhhhhaa/aioquant-add-triangular-arbitrage/35f7cf5c4266ad5feac8d9d1673006d0488f7a1f/docs/images/userpage.png
--------------------------------------------------------------------------------
/docs/market.md:
--------------------------------------------------------------------------------
1 | ## 行情
2 |
3 | 通过行情模块(market),可以订阅任意交易所的任意交易对的实时行情,包括订单薄(Orderbook)、K线(KLine)、成交(Trade),
4 | 根据不同交易所提供的行情信息,实时将行情信息推送给策略;
5 |
6 | 在订阅行情之前,需要先部署 `Market` 行情服务器,行情服务器将通过 REST API 或 Websocket 的方式从交易所获取实时行情信息,并将行情信息按照统一的数据格式打包,通过事件的形式发布至事件中心;
7 |
8 |
9 | ### 1. 行情模块使用
10 |
11 | > 此处以订单薄使用为例,订阅Binance的 `ETH/BTC` 交易对订单薄数据
12 | ```python
13 | # 导入模块
14 | from aioquant import const
15 | from aioquant.utils import logger
16 | from aioquant.market import MarketSubscribe, Orderbook
17 |
18 |
19 | # 订阅订单薄行情,注意此处注册的回调函数是 `async` 异步函数,回调参数为 `orderbook` 对象,数据结构查看下边的介绍。
20 | async def on_event_orderbook_update(orderbook: Orderbook):
21 | logger.info("orderbook:", orderbook)
22 | logger.info("platform:", orderbook.platform) # 打印行情平台
23 | logger.info("symbol:", orderbook.symbol) # 打印行情交易对
24 | logger.info("asks:", orderbook.asks) # 打印卖盘数据
25 | logger.info("bids:", orderbook.bids) # 打印买盘数据
26 | logger.info("timestamp:", orderbook.timestamp) # 打印行情更新时间戳(毫秒)
27 |
28 |
29 | MarketSubscribe(const.MARKET_TYPE_ORDERBOOK, const.BINANCE, "ETH/BTC", on_event_orderbook_update)
30 | ```
31 |
32 | > 使用同样的方式,可以订阅任意的行情
33 | ```python
34 | from aioquant import const
35 |
36 | const.MARKET_TYPE_ORDERBOOK # 订单薄(Orderbook)
37 | const.MARKET_TYPE_KLINE # 1分钟K线(KLine)
38 | const.MARKET_TYPE_KLINE_5M # 5分钟K线(KLine)
39 | const.MARKET_TYPE_KLINE_15M # 15分钟K线(KLine)
40 | const.MARKET_TYPE_TRADE # 成交(Trade)
41 | ```
42 |
43 |
44 | ### 2. 行情对象数据结构
45 |
46 | 所有交易平台的行情,全部使用统一的数据结构;
47 |
48 | #### 2.1 订单薄(Orderbook)
49 |
50 | - 订单薄模块
51 | ```python
52 | from aioquant.market import Orderbook
53 |
54 | Orderbook.platform # 订单薄平台
55 | Orderbook.symbol # 订单薄交易对
56 | Orderbook.asks # 订单薄买盘数据
57 | Orderbook.bids # 订单薄买盘数据
58 | Orderbook.timestamp # 订单薄更新时间戳(毫秒)
59 | Orderbook.data # 订单薄数据
60 | ```
61 |
62 | - 订单薄数据结构
63 | ```json
64 | {
65 | "platform": "binance",
66 | "symbol": "ETH/USDT",
67 | "asks": [
68 | ["8680.70000000", "0.00200000"]
69 | ],
70 | "bids": [
71 | ["8680.60000000", "2.82696138"]
72 | ],
73 | "timestamp": 1558949307370
74 | }
75 | ```
76 |
77 | - 字段说明
78 | - platform `string` 交易平台
79 | - symbol `string` 交易对
80 | - asks `list` 卖盘,一般默认前10档数据,一般 `price 价格` 和 `quantity 数量` 的精度为小数点后8位 `[[price, quantity], ...]`
81 | - bids `list` 买盘,一般默认前10档数据,一般 `price 价格` 和 `quantity 数量` 的精度为小数点后8位 `[[price, quantity], ...]`
82 | - timestamp `int` 时间戳(毫秒)
83 |
84 |
85 | #### 2.2 K线(KLine)
86 |
87 | - K线模块
88 | ```python
89 | from aioquant.market import Kline
90 |
91 | Kline.platform # 交易平台
92 | Kline.symbol # 交易对
93 | Kline.open # 开盘价
94 | Kline.high # 最高价
95 | Kline.low # 最低价
96 | Kline.close # 收盘价
97 | Kline.volume # 成交量
98 | Kline.timestamp # 时间戳(毫秒)
99 | Kline.data # K线数据
100 | ```
101 |
102 | - K线数据结构
103 | ```json
104 | {
105 | "platform": "okex",
106 | "symbol": "BTC/USDT",
107 | "open": "8665.50000000",
108 | "high": "8668.40000000",
109 | "low": "8660.00000000",
110 | "close": "8660.00000000",
111 | "volume": "73.14728136",
112 | "timestamp": 1558946340000
113 | }
114 | ```
115 |
116 | - 字段说明
117 | - platform `string` 交易平台
118 | - symbol `string` 交易对
119 | - open `string` 开盘价,一般精度为小数点后8位
120 | - high `string` 最高价,一般精度为小数点后8位
121 | - low `string` 最低价,一般精度为小数点后8位
122 | - close `string` 收盘价,一般精度为小数点后8位
123 | - volume `string` 成交量,一般精度为小数点后8位
124 | - timestamp `int` 时间戳(毫秒)
125 |
126 |
127 | #### 2.3 成交(Trade)
128 |
129 | - 成交模块
130 | ```python
131 | from aioquant.market import Trade
132 |
133 | Trade.platform # 交易平台
134 | Trade.symbol # 交易对
135 | Trade.action # 操作类型 BUY 买入 / SELL 卖出
136 | Trade.price # 价格
137 | Trade.quantity # 数量
138 | Trade.timestamp # 时间戳(毫秒)
139 | ```
140 |
141 | - 成交数据结构
142 | ```json
143 | {
144 | "platform": "okex",
145 | "symbol": "BTC/USDT",
146 | "action": "SELL",
147 | "price": "8686.40000000",
148 | "quantity": "0.00200000",
149 | "timestamp": 1558949571111
150 | }
151 | ```
152 |
153 | - 字段说明
154 | - platform `string` 交易平台
155 | - symbol `string` 交易对
156 | - action `string` 操作类型 BUY 买入 / SELL 卖出
157 | - price `string` 价格,一般精度为小数点后8位
158 | - quantity `string` 数量,一般精度为小数点后8位
159 | - timestamp `int` 时间戳(毫秒)
160 |
--------------------------------------------------------------------------------
/docs/others/locker.md:
--------------------------------------------------------------------------------
1 |
2 | ## 进程锁 & 线程锁
3 |
4 | 当业务复杂到使用多进程或多线程的时候,并发提高的同时,对内存共享也需要使用锁来解决资源争夺问题。
5 |
6 |
7 | ##### 1. 线程(协程)锁
8 |
9 | > 使用
10 |
11 | ```python
12 | from aioquant.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/aioquant",
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/aioquant`
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 aioquant.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 aioquant.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 | - 回调函数将会在心跳执行的时候被执行,因此可以对心跳次数 `kwargs["heart_beat_count"]` 取余,来确定是否该执行当前任务;
30 |
31 |
32 | ##### 2. 协程任务
33 | 协程可以并发执行,提高程序运行效率。
34 |
35 | ```python
36 | # 导入模块
37 | from aioquant.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)、查询未完全成交订单(get_open_order_ids)等功能;
4 |
5 | 策略完成下单之后,底层框架将定时或实时将最新的订单状态更新通过策略注册的回调函数传递给策略,策略能够在第一时间感知到订单状态更新数据;
6 |
7 |
8 | ### 1. 交易模块使用
9 |
10 | #### 1.1 简单使用示例
11 |
12 | > 此处以在 `Binance` 交易所上的 `ETH/BTC` 交易对创建一个买单为例:
13 |
14 | ```python
15 | # 导入模块
16 | from aioquant import const
17 | from aioquant import order
18 | from aioquant.trade import Trade
19 | from aioquant.utils import logger
20 | from aioquant.order import Order
21 |
22 | # 初始化
23 | platform = const.BINANCE # 交易平台 假设是binance
24 | account = "abc@gmail.com" # 交易账户
25 | access_key = "ABC123" # ACCESS KEY
26 | secret_key = "abc123" # SECRET KEY
27 | symbol = "ETH/BTC" # 交易对
28 | strategy_name = "my_test_strategy" # 自定义的策略名称
29 |
30 | # 注册订单更新回调函数,注意此处注册的回调函数是 `async` 异步函数,回调参数为 `order` 对象,数据结构请查看下边的介绍。
31 | async def on_event_order_update(order: Order):
32 | logger.info("order:", order)
33 |
34 | # 创建trade对象
35 | trader = Trade(strategy_name, platform, symbol, account=account, access_key=access_key, secret_key=secret_key,
36 | order_update_callback=on_event_order_update)
37 |
38 | # 下单
39 | action = order.ORDER_ACTION_BUY # 买单
40 | price = "11.11" # 委托价格
41 | quantity = "22.22" # 委托数量
42 | order_id, error = await trader.create_order(action, price, quantity) # 注意,此函数需要在 `async` 异步函数里执行
43 |
44 |
45 | # 撤单
46 | order_id, error = await trader.revoke_order(order_id) # 注意,此函数需要在 `async` 异步函数里执行
47 |
48 |
49 | # 查询所有未成交订单id列表
50 | order_ids, error = await trader.get_open_order_ids() # 注意,此函数需要在 `async` 异步函数里执行
51 |
52 |
53 | # 查询当前所有未成交订单数据
54 | orders = trader.orders # orders是一个dict,key为order_id,value为order对象
55 | order = trader.orders.get(order_id) # 提取订单号为 order_id 的订单对象
56 | ```
57 |
58 | #### 1.2 Trade交易模块初始化
59 |
60 | 初始化Trade交易模块需要传入一些必要的参数来指定需要交易的必要信息,比如:交易策略名称、交易平台、交易对、交易账户、账户的AK等等。
61 |
62 | ```python
63 | from aioquant.const import BINANCE
64 | from aioquant.trade import Trade
65 |
66 | platform = BINANCE # 交易平台 假设是binance
67 | account = "abc@gmail.com" # 交易账户
68 | access_key = "ABC123" # ACCESS KEY
69 | secret_key = "abc123" # SECRET KEY
70 | symbol = "ETH/BTC" # 交易对
71 | strategy_name = "my_test_strategy" # 自定义的策略名称
72 |
73 | ```
74 |
75 | - 简单初始化方式
76 | ```python
77 | trader = Trade(strategy_name, platform, symbol, account=account, access_key=access_key, secret_key=secret_key)
78 | ```
79 |
80 | - 如果需要实时获取到已提交委托单的实时变化情况,那么可以在初始化的时候指定订单更新回调函数
81 | ```python
82 | from aioquant.order import Order # 导入订单模块
83 |
84 | # 当委托单有任何变化,将通过此函数回调变化信息,order为订单对象,下文里将对 `Order` 有专题说明
85 | async def on_event_order_update(order: Order):
86 | print("order update:", order)
87 |
88 | trader = Trade(strategy_name, platform, symbol, account=account, access_key=access_key, secret_key=secret_key,
89 | order_update_callback=on_event_order_update)
90 | ```
91 |
92 | - 如果是期货,需要实时获取到当前持仓变化情况,那么可以在初始化的时候指定持仓的更新回调函数
93 | ```python
94 | from aioquant.position import Position # 导入持仓模块
95 |
96 | # 当持仓有任何变化,将通过此函数回调变化信息,position为持仓对象,下文里将对 `Position` 有专题说明
97 | async def on_event_position_update(position: Position):
98 | print("position update:", position)
99 |
100 | trader = Trade(strategy_name, platform, symbol, account=account, access_key=access_key, secret_key=secret_key,
101 | order_update_callback=on_event_order_update, position_update_callback=on_event_position_update)
102 | ```
103 |
104 | - 如果希望判断交易模块的初始化状态,比如网络连接是否正常、订阅订单/持仓数据是否正常等等,那么可以在初始化的时候指定初始化成功状态更新回调函数
105 | ```python
106 | from aioquant.error import Error # 引入错误模块
107 |
108 | # 初始化Trade模块成功或者失败,都将回调此函数
109 | # 如果成功,success将是True,error将是None
110 | # 如果失败,success将是False,error将携带失败信息
111 | async def on_event_init_success_callback(success: bool, error: Error, **kwargs):
112 | print("initialize trade module status:", success, "error:", error)
113 |
114 | trader = Trade(strategy_name, platform, symbol, account=account, access_key=access_key, secret_key=secret_key,
115 | order_update_callback=on_event_order_update, position_update_callback=on_event_position_update,
116 | init_success_callback=on_event_init_success_callback)
117 | ```
118 |
119 | #### 1.3 创建委托单
120 | `Trade.create_order` 可以创建任意的委托单,包括现货和期货,并且入参只有4个!
121 |
122 | ```python
123 | async def create_order(self, action, price, quantity, *args, **kwargs):
124 | """ 创建委托单
125 | @param action 交易方向 BUY/SELL
126 | @param price 委托价格
127 | @param quantity 委托数量(当为负数时,代表合约操作空单)
128 | @return (order_id, error) 如果成功,order_id为委托单号,error为None,否则order_id为None,error为失败信息
129 | """
130 | ```
131 | > 注意:
132 | - 入参 `action` 可以引入使用 `from aioquant.order import ORDER_ACTION_BUY, ORDER_ACTION_SELL`
133 | - 入参 `price` 最好是字符串格式,因为这样能保持原始精度,否则在数据传输过程中可能损失精度
134 | - 入参 `quantity` 最好是字符串格式,理由和 `price` 一样;另外,当为合约委托单的时候,`quantity` 有正负之分,正代表多仓,负代表空仓
135 | - 返回 `(order_id, error)` 如果成功,`order_id` 为创建的委托单号,`error` 为None;如果失败,`order_id` 为None,`error` 为 `Error` 对象携带的错误信息
136 |
137 | #### 1.4 撤销委托单
138 | `Trade.revoke_order` 可以撤销任意多个委托单。
139 |
140 | ```python
141 | async def revoke_order(self, *order_ids):
142 | """ 撤销委托单
143 | @param order_ids 订单号列表,可传入任意多个,如果不传入,那么就撤销所有订单
144 | @return (success, error) success为撤单成功列表,error为撤单失败的列表
145 | """
146 | ```
147 | > 注意:
148 | - 入参 `order_ids` 是一个可变参数,可以为空,或者任意多个参数
149 | - 如果 `order_ids` 为空,即 `trader.revoke_order()` 这样调用,那么代表撤销此交易对下的所有委托单;
150 | - 如果 `order_ids` 为一个参数,即 `trader.revoke_order(order_id)` 这样调用(其中order_id为委托单号),那么代表只撤销order_id的委托单;
151 | - 如果 `order_ids` 为多个参数,即 `trader.revoke_order(order_id1, order_id2, order_id3)` 这样调用(其中order_id1, order_id2, order_id3为委托单号),那么代表撤销order_id1, order_id2, order_id3的委托单;
152 | - 返回 `(success, error)`,如果成功,那么 `success` 为成功信息,`error` 为None;如果失败,那么 `success` 为None,`error` 为 `Error` 对象,携带的错误信息;
153 |
154 | #### 1.5 获取未完成委托单id列表
155 | `Trade.get_open_order_ids` 可以获取当前所有未完全成交的委托单号,包括 `已提交但未成交`、`部分成交` 的所有委托单号。
156 |
157 | ```python
158 | async def get_open_order_ids(self):
159 | """ 获取未完成委托单id列表
160 | @return (result, error) result为成功获取的未成交订单列表,error如果成功为None,如果不成功为错误信息
161 | """
162 | ```
163 | > 注意:
164 | - 返回 `(result, error)` 如果成功,那么 `result` 为委托单号列表,`error` 为None;如果失败,`result` 为None,`error` 为 `Error` 对象,携带的错误信息;
165 |
166 | #### 1.6 获取当前所有订单对象
167 |
168 | `Trade.orders` 可以提取当前 `Trade` 模块里所有的委托单信息,`dict` 格式,`key` 为委托单id,`value` 为 `Order` 委托单对象。
169 |
170 | #### 1.7 获取当前的持仓对象
171 |
172 | `Trade.position` 可以提取当前 `Trade` 模块里的持仓信息,即 `Position` 对象,仅限合约使用。
173 |
174 |
175 | ### 2. 订单模块
176 |
177 | 所有订单相关的数据常量和对象在框架的 `quant.order` 模块下,`Trade` 模块在推送订单信息回调的时候,携带的 `order` 参数即此模块。
178 |
179 | #### 2.1 订单类型
180 | ```python
181 | from aioquant import order
182 |
183 | order.ORDER_TYPE_LIMIT # 限价单
184 | order.ORDER_TYPE_MARKET # 市价单
185 | ```
186 |
187 | #### 2.2 订单操作
188 | ```python
189 | from aioquant import order
190 |
191 | order.ORDER_ACTION_BUY # 买入
192 | order.ORDER_ACTION_SELL # 卖出
193 | ```
194 |
195 | #### 2.3 订单状态
196 | ```python
197 | from aioquant import order
198 |
199 | order.ORDER_STATUS_NONE # 新创建的订单,无状态
200 | order.ORDER_STATUS_SUBMITTED # 已提交
201 | order.ORDER_STATUS_PARTIAL_FILLED # 部分成交
202 | order.ORDER_STATUS_FILLED # 完全成交
203 | order.ORDER_STATUS_CANCELED # 取消
204 | order.ORDER_STATUS_FAILED # 失败
205 | ```
206 |
207 | #### 2.4 合约订单类型
208 | ```python
209 | from aioquant import order
210 |
211 | order.TRADE_TYPE_NONE # 未知订单类型,比如订单不是由 thenextquant 框架创建,且某些平台的订单不能判断订单类型
212 | order.TRADE_TYPE_BUY_OPEN # 买入开多 action=BUY, quantity>0
213 | order.TRADE_TYPE_SELL_OPEN # 卖出开空 action=SELL, quantity<0
214 | order.TRADE_TYPE_SELL_CLOSE # 卖出平多 action=SELL, quantity>0
215 | order.TRADE_TYPE_BUY_CLOSE # 买入平空 action=BUY, quantity<0
216 | ```
217 | > 注意: 仅限合约订单使用。
218 |
219 | #### 2.5 订单对象
220 | ```python
221 | from aioquant import order
222 |
223 | o = order.Order(...) # 初始化订单对象
224 |
225 | o.platform # 交易平台
226 | o.account # 交易账户
227 | o.strategy # 策略名称
228 | o.order_id # 委托单号
229 | o.client_order_id # 自定义客户端订单id
230 | o.action # 买卖类型 SELL-卖,BUY-买
231 | o.order_type # 委托单类型 MKT-市价,LMT-限价
232 | o.symbol # 交易对 如: ETH/BTC
233 | o.price # 委托价格
234 | o.quantity # 委托数量(限价单)
235 | o.remain # 剩余未成交数量
236 | o.status # 委托单状态
237 | o.timestamp # 创建订单时间戳(毫秒)
238 | o.avg_price # 成交均价
239 | o.trade_type # 合约订单类型 开多/开空/平多/平空
240 | o.ctime # 创建订单时间戳
241 | o.utime # 交易所订单更新时间
242 | ```
243 |
244 |
245 | ### 3. 持仓模块
246 |
247 | 所有持仓相关的对象在框架的 `aioquant.position` 模块下,`Trade` 模块在推送持仓信息回调的时候,携带的 `position` 参数即此模块。
248 |
249 | #### 3.1 持仓对象
250 |
251 | ```python
252 | from aioquant.position import Position
253 |
254 | p = Position(...) # 初始化持仓对象
255 |
256 | p.platform # 交易平台
257 | p.account # 交易账户
258 | p.strategy # 策略名称
259 | p.symbol # 交易对
260 | p.short_quantity # 空仓数量
261 | p.short_avg_price # 空仓平均价格
262 | p.long_quantity # 多仓数量
263 | p.long_avg_price # 多仓平均价格
264 | p.liquid_price # 预估爆仓价格
265 | p.utime # 更新时间戳(毫秒)
266 | ```
267 |
--------------------------------------------------------------------------------
/example/Issue29/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "LOG": {
3 | "level": "INFO"
4 | },
5 | "RABBITMQ": {
6 | "host": "127.0.0.1",
7 | "port": 5672,
8 | "username": "test",
9 | "password": 123456
10 | },
11 | "PROXY": "http://127.0.0.1:1087",
12 |
13 | "strategy": "test_strategy",
14 |
15 | "A": {
16 | "platform": "huobi",
17 | "account": "account_test",
18 | "access_key": "ACCESS KEY",
19 | "secret_key": "SECRET KEY",
20 | "symbol": "ETH/USDT"
21 | },
22 | "B": {
23 | "platform": "huobi",
24 | "account": "account_test",
25 | "access_key": "ACCESS KEY",
26 | "secret_key": "SECRET KEY",
27 | "symbol": "BTC/USDT"
28 | },
29 | "C": {
30 | "platform": "huobi",
31 | "account": "account_test",
32 | "access_key": "ACCESS KEY",
33 | "secret_key": "SECRET KEY",
34 | "symbol": "ETH/BTC"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/example/Issue29/src/main.py:
--------------------------------------------------------------------------------
1 | # -*- coding:utf-8 -*-
2 |
3 | """
4 | 三角套利策略
5 | """
6 |
7 | import sys
8 | from aioquant import quant
9 |
10 |
11 | def start_strategy():
12 | from strategy.strategy29 import Strategy29
13 | Strategy29()
14 |
15 |
16 | if __name__ == "__main__":
17 | config_file = sys.argv[1]
18 | quant.start(config_file, start_strategy)
19 |
--------------------------------------------------------------------------------
/example/Issue29/src/strategy/strategy29.py:
--------------------------------------------------------------------------------
1 | # -*- coding:utf-8 -*-
2 |
3 | """
4 | 三角套利策略
5 | """
6 |
7 | from aioquant.configure import config
8 | from aioquant import const
9 | from aioquant.tasks import LoopRunTask, SingleTask
10 | from aioquant.trade import Trade
11 | from aioquant.utils import logger
12 | from aioquant.order import Order, ORDER_TYPE_MARKET
13 | from aioquant.market import MarketSubscribe, Orderbook
14 | from aioquant.utils.decorator import async_method_locker
15 |
16 |
17 | class Strategy29:
18 |
19 | def __init__(self):
20 | self._a_orderbook_ok = False
21 | self._a_orderbook = None
22 | self._b_orderbook_ok = False
23 | self._b_orderbook = None
24 | self._c_orderbook_ok = False
25 | self._c_orderbook = None
26 |
27 | # 交易对象A (ETH/USDT)
28 | params = {
29 | "strategy": config.strategy,
30 | "platform": config.A["platform"],
31 | "symbol": config.A["symbol"],
32 | "account": config.A["account"],
33 | "access_key": config.A["access_key"],
34 | "secret_key": config.A["secret_key"],
35 | "order_update_callback": self.on_event_order_update_callback,
36 | "init_callback": self.on_event_init_callback,
37 | "error_callback": self.on_event_error_callback
38 | }
39 | self._a_trader = Trade(**params)
40 |
41 | # 交易对象B (BTC/USDT)
42 | params = {
43 | "strategy": config.strategy,
44 | "platform": config.B["platform"],
45 | "symbol": config.B["symbol"],
46 | "account": config.B["account"],
47 | "access_key": config.B["access_key"],
48 | "secret_key": config.B["secret_key"],
49 | "order_update_callback": self.on_event_order_update_callback,
50 | "init_callback": self.on_event_init_callback,
51 | "error_callback": self.on_event_error_callback
52 | }
53 | self._b_trader = Trade(**params)
54 |
55 | # 交易对象C (ETH/BTC)
56 | params = {
57 | "strategy": config.strategy,
58 | "platform": config.C["platform"],
59 | "symbol": config.C["symbol"],
60 | "account": config.C["account"],
61 | "access_key": config.C["access_key"],
62 | "secret_key": config.C["secret_key"],
63 | "order_update_callback": self.on_event_order_update_callback,
64 | "init_callback": self.on_event_init_callback,
65 | "error_callback": self.on_event_error_callback
66 | }
67 | self._c_trader = Trade(**params)
68 |
69 | # 订阅行情
70 | MarketSubscribe(const.MARKET_TYPE_ORDERBOOK, config.A["platform"], config.A["platform"], self.on_event_orderbook_update)
71 | MarketSubscribe(const.MARKET_TYPE_ORDERBOOK, config.B["platform"], config.B["platform"], self.on_event_orderbook_update)
72 | MarketSubscribe(const.MARKET_TYPE_ORDERBOOK, config.C["platform"], config.C["platform"], self.on_event_orderbook_update)
73 |
74 | # 定时任务
75 | LoopRunTask.register(self.check_orderbook, 60) # 定时每隔60秒检查一次订单薄
76 |
77 | @async_method_locker("on_event_init_callback", wait=False, timeout=5)
78 | async def on_event_init_callback(self, success: bool, **kwargs):
79 | """初始化回调"""
80 | logger.info("success:", success, "kwargs:", kwargs, caller=self)
81 | # 1. 判断A,B,C是否都初始化成功
82 | # 2. 如果失败了,推送消息通知,同时标记程序不能再继续往下执行了
83 |
84 | @async_method_locker("on_event_error_callback", wait=False, timeout=5)
85 | async def on_event_error_callback(self, error, **kwargs):
86 | """错误回调"""
87 | logger.info("error:", error, "kwargs:", kwargs, caller=self)
88 | # 1. 推送报错信息 。。。
89 | # 2. 标记错误,是否对程序有影响,是否需要对程序做出必要调整。。。
90 |
91 | async def on_event_orderbook_update(self, orderbook: Orderbook):
92 | """订单薄更新回调"""
93 |
94 | if orderbook.platform == config.A["platform"] and orderbook.symbol == config.A["symbol"]:
95 | self._a_orderbook_ok = True
96 | self._a_orderbook = orderbook
97 | elif orderbook.platform == config.B["platform"] and orderbook.symbol == config.B["symbol"]:
98 | self._b_orderbook_ok = True
99 | self._b_orderbook = orderbook
100 | elif orderbook.platform == config.C["platform"] and orderbook.symbol == config.C["symbol"]:
101 | self._c_orderbook_ok = True
102 | self._c_orderbook = orderbook
103 |
104 | # 判断maker和taker订单薄是否准备就绪
105 | if not self._a_orderbook_ok or not self._b_orderbook_ok or not self._c_orderbook_ok:
106 | logger.warn("orderbook not ok.", caller=self)
107 | return
108 |
109 | # A同时进行买入和卖出ETH的检查
110 | SingleTask.run(self.a_do_action_buy)
111 | SingleTask.run(self.a_do_action_sell)
112 |
113 | @async_method_locker("a_do_action_buy", wait=False, timeout=5)
114 | async def a_do_action_buy(self):
115 | """A市场用USDT买入ETH"""
116 | # 1. 当前是否存在未完成的一次三角套利循环;
117 | # 2. 是否满足买入条件
118 | # 3. 挂单,需要计算挂单价格、数量;
119 | # 4. 记录挂单的订单id,订单价格,订单量
120 | # 5. 挂单失败之后处理。。。
121 |
122 | @async_method_locker("a_do_action_sell", wait=False, timeout=5)
123 | async def a_do_action_sell(self):
124 | """A市场把ETH卖出成USDT"""
125 | # 1. 当前是否存在未完成的一次三角套利循环;
126 | # 2. 是否满足买入条件
127 | # 3. 挂单,需要计算挂单价格、数量;
128 | # 4. 记录挂单的订单id,订单价格,订单量
129 | # 5. 挂单失败之后处理。。。
130 |
131 | @async_method_locker("on_event_order_update_callback", wait=True, timeout=5)
132 | async def on_event_order_update_callback(self, order: Order):
133 | """订单成交"""
134 | # 1. 订单是否成交?完全成交或部分成交?
135 | # 2. 如果是A订单成交,根据成交量,判断是否立即进行对冲追单;使用协程并发执行对冲追单;
136 | # SingleTask.run(self._b_trader.create_order, b_price, b_quantity, order_type=ORDER_TYPE_MARKET)
137 | # SingleTask.run(self._c_trader.create_order, c_price, c_quantity, order_type=ORDER_TYPE_MARKET)
138 | # 3. 如果是B,C订单成交,进入检查程序,检查此次三角套利是否完成;
139 | # 4. 如果B,C对冲追单失败,如何处理?发出警报?
140 |
141 | @async_method_locker("check_orderbook", wait=False, timeout=2)
142 | async def check_orderbook(self, *args, **kwargs):
143 | """检查订单薄"""
144 | pass
145 | # 1. 检查A,B,C订单薄是否正常
146 | # 2. 如果存在行情异常,停止三角套利;
147 | # 3. 推送报警消息。。停止程序。。
148 |
--------------------------------------------------------------------------------
/example/demo/README.md:
--------------------------------------------------------------------------------
1 |
2 | ## Demo使用示例
3 |
4 | 本示例策略简单实现了在币安(Binance)交易所的`ETH/BTC` 订单薄买盘挂单吃毛刺的功能。
5 | 策略首先订阅了 `ETH/BTC` 的订单薄实时行情,拿到买3(bid3)和买4(bid4)的价格,然后计算买3和买4的平均价格(price = (bid3+bid4)/2),
6 | 同时判断是否已经有挂单。如果已经挂单,那么判断挂单价格是否超过当前bid3和bid4的价格区间,如果超过那么就撤单之后重新挂单;如果没有挂单,
7 | 那么直接使用price挂单。
8 |
9 | > NOTE: 示例策略只是简单演示本框架的使用方法,策略本身还需要进一步优化,比如成交之后的追单,或对冲等。
10 |
11 |
12 | #### 推荐创建如下结构的文件及文件夹:
13 | ```text
14 | ProjectName
15 | |----- docs
16 | | |----- README.md
17 | |----- scripts
18 | | |----- run.sh
19 | |----- config.json
20 | |----- src
21 | | |----- main.py
22 | | |----- strategy
23 | | |----- strategy1.py
24 | | |----- strategy2.py
25 | | |----- ...
26 | |----- .gitignore
27 | |----- README.md
28 | ```
29 |
30 | #### 策略服务配置
31 |
32 | 策略服务配置文件为 [config.json](config.json),其中:
33 |
34 | - ACCOUNTS `list` 策略将使用的交易平台账户配置;
35 | - strategy `string` 策略名称
36 | - symbol `string` 策略运行交易对
37 |
38 | > 服务配置文件使用方式: [配置文件](../../docs/configure/README.md)
39 |
40 |
41 | ##### 运行
42 |
43 | ```text
44 | python src/main.py config.json
45 | ```
46 |
--------------------------------------------------------------------------------
/example/demo/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guhhhhaa/aioquant-add-triangular-arbitrage/35f7cf5c4266ad5feac8d9d1673006d0488f7a1f/example/demo/__init__.py
--------------------------------------------------------------------------------
/example/demo/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": "ETH/BTC"
19 | }
20 |
--------------------------------------------------------------------------------
/example/demo/main.py:
--------------------------------------------------------------------------------
1 | # -*- coding:utf-8 -*-
2 |
3 | import sys
4 |
5 | from aioquant import quant
6 |
7 |
8 | def initialize():
9 | from strategy.strategy import MyStrategy
10 | MyStrategy()
11 |
12 |
13 | if __name__ == "__main__":
14 | config_file = sys.argv[1]
15 | quant.start(config_file, initialize)
16 |
--------------------------------------------------------------------------------
/example/demo/strategy/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guhhhhaa/aioquant-add-triangular-arbitrage/35f7cf5c4266ad5feac8d9d1673006d0488f7a1f/example/demo/strategy/__init__.py
--------------------------------------------------------------------------------
/example/demo/strategy/strategy.py:
--------------------------------------------------------------------------------
1 | # -*- coding:utf-8 -*-
2 |
3 | # 策略实现
4 |
5 | from aioquant import const
6 | from aioquant.utils import logger
7 | from aioquant.configure import config
8 | from aioquant.market import MarketSubscribe
9 | from aioquant.trade import Trade
10 | from aioquant.const import BINANCE
11 | from aioquant.order import Order
12 | from aioquant.market import Orderbook
13 | from aioquant.order import ORDER_ACTION_BUY, ORDER_STATUS_FAILED, ORDER_STATUS_CANCELED, ORDER_STATUS_FILLED
14 |
15 |
16 | class MyStrategy:
17 |
18 | def __init__(self):
19 | """ 初始化
20 | """
21 | self.strategy = "my_strategy"
22 | self.platform = BINANCE
23 | self.account = config.accounts[0]["account"]
24 | self.access_key = config.accounts[0]["access_key"]
25 | self.secret_key = config.accounts[0]["secret_key"]
26 | self.symbol = config.symbol
27 |
28 | self.order_id = None # 创建订单的id
29 | self.create_order_price = "0.0" # 创建订单的价格
30 |
31 | # 交易模块
32 | cc = {
33 | "strategy": self.strategy,
34 | "platform": self.platform,
35 | "symbol": self.symbol,
36 | "account": self.account,
37 | "access_key": self.access_key,
38 | "secret_key": self.secret_key,
39 | "order_update_callback": self.on_event_order_update,
40 | }
41 | self.trader = Trade(**cc)
42 |
43 | # 订阅行情
44 | MarketSubscribe(const.MARKET_TYPE_ORDERBOOK, const.BINANCE, self.symbol, self.on_event_orderbook_update)
45 |
46 | async def on_event_orderbook_update(self, orderbook: Orderbook):
47 | """ 订单薄更新
48 | """
49 | logger.debug("orderbook:", orderbook, caller=self)
50 | bid3_price = orderbook.bids[2][0] # 买三价格
51 | bid4_price = orderbook.bids[3][0] # 买四价格
52 |
53 | # 判断是否需要撤单
54 | if self.order_id:
55 | if float(self.create_order_price) < float(bid3_price) or float(self.create_order_price) > float(bid4_price):
56 | return
57 | _, error = await self.trader.revoke_order(self.order_id)
58 | if error:
59 | logger.error("revoke order error! error:", error, caller=self)
60 | return
61 | self.order_id = None
62 | logger.info("revoke order:", self.order_id, caller=self)
63 |
64 | # 创建新订单
65 | price = (float(bid3_price) + float(bid4_price)) / 2
66 | quantity = "0.1" # 假设委托数量为0.1
67 | action = ORDER_ACTION_BUY
68 | order_id, error = await self.trader.create_order(action, price, quantity)
69 | if error:
70 | logger.error("create order error! error:", error, caller=self)
71 | return
72 | self.order_id = order_id
73 | self.create_order_price = price
74 | logger.info("create new order:", order_id, caller=self)
75 |
76 | async def on_event_order_update(self, order: Order):
77 | """ 订单状态更新
78 | """
79 | logger.info("order update:", order, caller=self)
80 |
81 | # 如果订单失败、订单取消、订单完成交易
82 | if order.status in [ORDER_STATUS_FAILED, ORDER_STATUS_CANCELED, ORDER_STATUS_FILLED]:
83 | self.order_id = None
84 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # -*- coding:utf-8 -*-
2 |
3 | from distutils.core import setup
4 |
5 |
6 | setup(
7 | name="aioquant",
8 | version="1.0.7",
9 | packages=[
10 | "aioquant",
11 | "aioquant.utils",
12 | "aioquant.platform",
13 | ],
14 | description="Asynchronous event I/O driven quantitative trading framework.",
15 | url="https://github.com/JiaoziMatrix/aioquant",
16 | author="HuangTao",
17 | author_email="huangtao@ifclover.com",
18 | license="MIT",
19 | keywords=[
20 | "aioquant", "quant", "framework", "async", "asynchronous", "digiccy", "digital", "currency", "marketmaker",
21 | "binance", "okex", "huobi", "bitmex", "deribit", "kraken", "gemini", "kucoin"
22 | ],
23 | install_requires=[
24 | "aiohttp==3.6.2",
25 | "aioamqp==0.14.0",
26 | "motor==2.0.0"
27 | ],
28 | )
29 |
--------------------------------------------------------------------------------