├── .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 | ![](./docs/images/aioq_framework2.png) 26 | 27 | - `AIOQuant` 分布式管理交易系统 28 | ![](./docs/images/aioq_framework.jpg) 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 | ![](../images/login.png) 40 | 41 | 登录成功之后,进入 `Admin` 标签页,新增、管理登录账户和密码: 42 | ![](../images/userpage.png) 43 | 44 | 请注意,新增的账户需要设置对应的访问权限,根据需要设置权限即可,一般如果测试使用直接给根目录 `/` 的访问权限: 45 | ![](../images/rabbitmq_permission.png) 46 | 47 | ![](../images/rabbitmq_permission2.png) 48 | 49 | 恭喜你,我们的RabbitMQ服务器已经可以投入使用了! 50 | -------------------------------------------------------------------------------- /docs/others/tasks.md: -------------------------------------------------------------------------------- 1 | 2 | ## 定时任务 & 协程任务 3 | 4 | 5 | ##### 1. 注册定时任务 6 | 定时任务模块可以注册任意多个回调函数,利用服务器每秒执行一次心跳的过程,创建新的协程,在协程里执行回调函数。 7 | 8 | ```python 9 | # 导入模块 10 | from 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 | --------------------------------------------------------------------------------