├── example ├── __init__.py ├── strategy │ ├── __init__.py │ └── strategy.py ├── main.py ├── config.json └── README.md ├── quant ├── utils │ ├── __init__.py │ ├── telegram.py │ ├── twilio.py │ ├── decorator.py │ ├── agent.py │ ├── dingding.py │ ├── sendmail.py │ ├── logger.py │ ├── websocket.py │ ├── http_client.py │ ├── tools.py │ └── mongo.py ├── platform │ ├── __init__.py │ ├── deribit.py │ ├── okex.py │ ├── binance.py │ └── okex_future.py ├── __init__.py ├── const.py ├── tasks.py ├── position.py ├── quant.py ├── heartbeat.py ├── order.py ├── config.py ├── trade.py ├── market.py ├── data.py └── event.py ├── docs ├── requirements.txt ├── faq.md ├── changelog.md ├── configure │ ├── config.json │ └── README.md ├── others │ ├── message.md │ ├── locker.md │ ├── tasks.md │ └── logger.md ├── market.md └── trade.md ├── .gitignore ├── setup.py ├── LICENSE.txt └── README.md /example/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /quant/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/strategy/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /quant/platform/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.2.1 2 | aioamqp==0.10.0 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.pyc 3 | build 4 | dist 5 | tbag.egg-info 6 | MANIFEST 7 | test -------------------------------------------------------------------------------- /quant/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | 量化交易框架 5 | 6 | Author: HuangTao 7 | Date: 2017/04/26 8 | """ 9 | 10 | __author__ = "HuangTao" 11 | __version__ = (0, 0, 6) 12 | -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | ## FAQ 2 | 3 | ##### 1. 运行程序报SSL的错 4 | ```text 5 | SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1056)') 6 | ``` 7 | 8 | - 解决方法 9 | ```text 10 | aiohttp在python3.7里可能有兼容性问题,需要做一下简单的处理。 11 | 12 | MAC电脑执行以下两条命令: 13 | cd /Applications/Python\ 3.7/ 14 | ./Install\ Certificates.command 15 | ``` 16 | -------------------------------------------------------------------------------- /example/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | import sys 4 | 5 | 6 | def initialize(): 7 | from strategy.strategy import MyStrategy 8 | MyStrategy() 9 | 10 | 11 | def main(): 12 | if len(sys.argv) > 1: 13 | config_file = sys.argv[1] 14 | else: 15 | config_file = None 16 | 17 | from quant.quant import quant 18 | quant.initialize(config_file) 19 | initialize() 20 | quant.start() 21 | 22 | 23 | if __name__ == '__main__': 24 | main() 25 | -------------------------------------------------------------------------------- /example/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 | "PLATFORMS": { 10 | "binance": { 11 | "account": "abc123@gmail.com", 12 | "access_key": "ACCESS KEY", 13 | "secret_key": "SECRET KEY" 14 | } 15 | }, 16 | "strategy": "my_test_strategy", 17 | "symbol": "ETH/BTC" 18 | } 19 | -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | # Change Logs 2 | 3 | 4 | ### v0.0.5 5 | 6 | *Date: 2019/05/30* 7 | *Summary:* 8 | - Add [Binance exchange](https://www.binance.com) module 9 | - upgrade websocket module 10 | 11 | 12 | ### v0.0.4 13 | 14 | *Date: 2019/05/29* 15 | *Summary:* 16 | - delete Agent server 17 | - Add [Deribit exchange](https://www.deribit.com) module 18 | - upgrade market module 19 | 20 | 21 | ### v0.0.3 22 | 23 | *Date: 2019/03/12* 24 | *Summary:* 25 | - Upgrade Agent Server Protocol to newest version 26 | - Subscribe Asset 27 | -------------------------------------------------------------------------------- /docs/configure/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "HEARTBEAT": { 3 | "interval": 3 4 | }, 5 | "LOG": { 6 | "console": false, 7 | "level": "DEBUG", 8 | "path": "/var/log/servers/Quant", 9 | "name": "quant.log", 10 | "clear": true, 11 | "backup_count": 5 12 | }, 13 | "RABBITMQ": { 14 | "host": "127.0.0.1", 15 | "port": 5672, 16 | "username": "test", 17 | "password": "123456" 18 | }, 19 | "PROXY": "http://127.0.0.1:1087", 20 | 21 | "name": "my test name", 22 | "abc": 123456 23 | } 24 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from distutils.core import setup 4 | 5 | 6 | setup( 7 | name="thenextquant", 8 | version="0.0.6", 9 | packages=["quant", 10 | "quant.utils", 11 | "quant.platform", 12 | ], 13 | description="Quant Trader Framework", 14 | url="https://github.com/TheNextQuant/thenextquant", 15 | author="huangtao", 16 | author_email="huangtao@ifclover.com", 17 | license="MIT", 18 | keywords=["thenextquant", "quant"], 19 | install_requires=[ 20 | "aiohttp==3.2.1", 21 | "aioamqp==0.10.0", 22 | ], 23 | ) 24 | -------------------------------------------------------------------------------- /docs/others/message.md: -------------------------------------------------------------------------------- 1 | ## 消息推送 & 拨打电话 2 | 3 | 4 | ##### 1. 推送钉钉消息 5 | 6 | > 使用 7 | 8 | ```python 9 | from quant.utils.dingding import DingTalk 10 | 11 | await DingTalk.send_text_msg(access_token, content, phones, is_at_all) 12 | ``` 13 | 14 | > 说明 15 | - 钉钉群消息每个 `access_token` 每分钟推送消息不能超过20条; 16 | 17 | 18 | ##### 2. 推送Telegram消息 19 | 20 | > 使用 21 | 22 | ```python 23 | from quant.utils.telegram import TelegramBot 24 | 25 | await TelegramBot.send_text_msg(token, chat_id, content) 26 | ``` 27 | 28 | 29 | ##### 3. 拨打电话 30 | 31 | > 使用 32 | 33 | ```python 34 | from quant.utils.twilio import Twilio 35 | 36 | await Twilio.call_phone(account_sid, token, _from, to) 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/others/locker.md: -------------------------------------------------------------------------------- 1 | 2 | ## 进程锁 & 线程锁 3 | 4 | 当业务复杂到使用多进程或多线程的时候,并发提高的同时,对内存共享也需要使用锁来解决资源争夺问题。 5 | 6 | 7 | ##### 1. 线程(协程)锁 8 | 9 | > 使用 10 | 11 | ```python 12 | from quant.utils.decorator import async_method_locker 13 | 14 | @async_method_locker("unique_locker_name") 15 | async def func_foo(): 16 | pass 17 | ``` 18 | 19 | - 函数定义 20 | ```python 21 | def async_method_locker(name, wait=True): 22 | """ 异步方法加锁,用于多个协程执行同一个单列的函数时候,避免共享内存相互修改 23 | @param name 锁名称 24 | @param wait 如果被锁是否等待,True等待执行完成再返回,False不等待直接返回 25 | * NOTE: 此装饰器需要加到async异步方法上 26 | """ 27 | ``` 28 | 29 | > 说明 30 | - `async_method_locker` 为装饰器,需要装饰到 `async` 异步函数上; 31 | - 装饰器需要传入一个参数 `name`,作为此函数的锁名; 32 | - 参数 `wait` 可选,如果被锁是否等待,True等待执行完成再返回,False不等待直接返回 33 | -------------------------------------------------------------------------------- /quant/const.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | 常量 5 | 6 | Author: HuangTao 7 | Date: 2018/07/31 8 | """ 9 | 10 | 11 | # 交易所名称 12 | BCOIN = "bcoin" 13 | GBX = "gbx" 14 | BITFINEX = "bitfinex" 15 | BINANCE = "binance" 16 | OKEX = "okex" # OKEx现货 17 | OKEX_FUTURE = "okex_future" # OKEx交割合约 18 | OKEX_SWAP = "okex_swap" # OKEx永续合约 19 | BITMEX = "bitmex" 20 | HUOBI = "huobi" 21 | HUOBI_FUTURE = "huobi_future" 22 | OKCOIN = "okcoin" 23 | COINBASE = "coinbase" 24 | MXC = "mxc" 25 | DERIBIT = "deribit" 26 | KRAKEN = "kraken" 27 | BITSTAMP = "bitstamp" 28 | GEMINI = "gemini" 29 | FOTA = "fota" 30 | BIBOX = "bibox" 31 | 32 | 33 | # 行情类型 34 | MARKET_TYPE_TICKER = "ticker" 35 | MARKET_TYPE_TRADE = "trade" 36 | MARKET_TYPE_ORDERBOOK = "orderbook" 37 | MARKET_TYPE_KLINE = "kline" 38 | MARKET_TYPE_KLINE_5M = "kline_5m" 39 | MARKET_TYPE_KLINE_15M = "kline_15m" 40 | -------------------------------------------------------------------------------- /quant/utils/telegram.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | Telegram机器人接口 5 | 6 | Author: HuangTao 7 | Date: 2018/12/04 8 | """ 9 | 10 | from quant.utils import logger 11 | from quant.utils.http_client import AsyncHttpRequests 12 | 13 | 14 | class TelegramBot: 15 | """ Telegram机器人接口 16 | """ 17 | BASE_URL = "https://api.telegram.org" 18 | 19 | @classmethod 20 | async def send_text_msg(cls, token, chat_id, content, proxy=None): 21 | """ 发送文本消息 22 | @param token Telegram机器人token 23 | @param chat_id Telegram的chat_id 24 | @param content 消息内容 25 | @param proxy HTTP代理 26 | """ 27 | url = "{base_url}/bot{token}/sendMessage?chat_id={chat_id}&text={content}".format( 28 | base_url=cls.BASE_URL, 29 | token=token, 30 | chat_id=chat_id, 31 | content=content 32 | ) 33 | result = await AsyncHttpRequests.get(url, proxy=proxy) 34 | logger.info("url:", url, "result:", result, caller=cls) 35 | -------------------------------------------------------------------------------- /docs/others/tasks.md: -------------------------------------------------------------------------------- 1 | 2 | ## 定时任务 & 协程任务 3 | 4 | 5 | ##### 1. 注册回调任务 6 | 定时任务模块可以注册任意多个回调函数,利用服务器每秒执行一次心跳的过程,创建新的协程,在协程里执行回调函数。 7 | 8 | ```python 9 | # 导入模块 10 | from quant.tasks import LoopRunTask 11 | 12 | # 定义回调函数 13 | async def function_callback(*args, **kwargs): 14 | pass 15 | 16 | # 回调间隔时间(秒) 17 | callback_interval = 5 18 | 19 | # 注册回调函数 20 | task_id = LoopRunTask.register(function_callback, callback_interval) 21 | 22 | # 取消回调函数 23 | LoopRunTask.unregister(task_id) # 假设此定时任务已经不需要,那么取消此任务回调 24 | ``` 25 | 26 | > 注意: 27 | - 回调函数 `function_callback` 必须是 `async` 异步的,且入参必须包含 `*args` 和 `**kwargs`; 28 | - 回调时间间隔 `callback_interval` 为秒,默认为1秒; 29 | - 回调函数将会在心跳执行的时候被执行,因此可以对心跳次数 `heartbeat.count` 取余,来确定是否该执行当前任务; 30 | 31 | 32 | ##### 2. 协程任务 33 | 协程可以并发执行,提高程序运行效率。 34 | 35 | ```python 36 | # 导入模块 37 | from quant.tasks import SingleTask 38 | 39 | # 定义回调函数 40 | async def function_callback(*args, **kwargs): 41 | pass 42 | 43 | # 执行协程任务 44 | SingleTask.run(function_callback, *args, **kwargs) 45 | ``` 46 | 47 | > 注意: 48 | - 回调函数 `function_callback` 必须是 `async` 异步的; 49 | -------------------------------------------------------------------------------- /example/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 | - platforms `dict` 策略将使用的交易平台配置; 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 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /quant/utils/twilio.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | twilio打电话接口 5 | 6 | Author: HuangTao 7 | Date: 2018/12/04 8 | """ 9 | 10 | from quant.utils import logger 11 | from quant.utils.http_client import AsyncHttpRequests 12 | 13 | 14 | class Twilio: 15 | """ twilio打电话接口 16 | """ 17 | BASE_URL = "https://api.twilio.com" 18 | 19 | @classmethod 20 | async def call_phone(cls, account_sid, token, _from, to, proxy=None): 21 | """ 发送文本消息 22 | @param account_sid Twilio的Account Sid 23 | @param token Twilio的Auth Token 24 | @param _from 拨打出去的电话号码 eg: +17173666644 25 | @param to 被拨的电话号码 eg: +8513123456789 26 | @param proxy HTTP代理 27 | """ 28 | url = "https://{account_sid}:{token}@api.twilio.com/2010-04-01/Accounts/{account_sid}/Calls.json".format( 29 | account_sid=account_sid, 30 | token=token 31 | ) 32 | data = { 33 | "Url": "http://demo.twilio.com/docs/voice.xml", 34 | "To": to, 35 | "From": _from 36 | } 37 | result = await AsyncHttpRequests.fetch("POST", url, body=data, proxy=proxy) 38 | logger.info("url:", url, "result:", result, caller=cls) 39 | -------------------------------------------------------------------------------- /quant/utils/decorator.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | 装饰器 5 | 6 | Author: HuangTao 7 | Date: 2018/08/03 8 | """ 9 | 10 | import asyncio 11 | import functools 12 | 13 | 14 | # 协程间加锁,锁名对应的锁对象 {"locker_name": locker} 15 | METHOD_LOCKERS = {} 16 | 17 | 18 | def async_method_locker(name, wait=True): 19 | """ 异步方法加锁,用于多个协程执行同一个单列的函数时候,避免共享内存相互修改 20 | @param name 锁名称 21 | @param wait 如果被锁是否等待,True等待执行完成再返回,False不等待直接返回 22 | * NOTE: 此装饰器需要加到async异步方法上 23 | """ 24 | assert isinstance(name, str) 25 | 26 | def decorating_function(method): 27 | global METHOD_LOCKERS 28 | locker = METHOD_LOCKERS.get(name) 29 | if not locker: 30 | locker = asyncio.Lock() 31 | METHOD_LOCKERS[name] = locker 32 | 33 | @functools.wraps(method) 34 | async def wrapper(*args, **kwargs): 35 | if not wait and locker.locked(): 36 | return 37 | try: 38 | await locker.acquire() 39 | return await method(*args, **kwargs) 40 | finally: 41 | locker.release() 42 | return wrapper 43 | return decorating_function 44 | 45 | 46 | # class Test: 47 | # 48 | # @async_method_locker('my_fucker', False) 49 | # async def test(self, x): 50 | # print('hahaha ...', x) 51 | # await asyncio.sleep(0.1) 52 | # 53 | # 54 | # t = Test() 55 | # for i in range(10): 56 | # asyncio.get_event_loop().create_task(t.test(i)) 57 | # 58 | # asyncio.get_event_loop().run_forever() 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## TheNextQuant 3 | 异步事件驱动的量化交易/做市系统。 4 | 5 | 6 | ### 框架依赖 7 | 8 | - 运行环境 9 | - python 3.5.3 或以上版本 10 | 11 | - 依赖python三方包 12 | - aiohttp>=3.2.1 13 | - aioamqp>=0.10.0 14 | 15 | - RabbitMQ服务器 16 | - 事件发布、订阅 17 | 18 | 19 | ### 安装 20 | 使用 `pip` 可以简单方便安装: 21 | ```text 22 | pip install -e git+https://github.com/TheNextQuant/thenextquant.git#egg=thenextquant 23 | ``` 24 | 25 | or 26 | 27 | ```text 28 | pip install thenextquant 29 | ``` 30 | 31 | ### Demo使用示例 32 | 33 | - 推荐创建如下结构的文件及文件夹: 34 | ```text 35 | ProjectName 36 | |----- docs 37 | | |----- README.md 38 | |----- scripts 39 | | |----- run.sh 40 | |----- config.json 41 | |----- src 42 | | |----- main.py 43 | | |----- strategy 44 | | |----- strategy1.py 45 | | |----- strategy2.py 46 | | |----- ... 47 | |----- .gitignore 48 | |----- README.md 49 | ``` 50 | 51 | - 快速体验示例 52 | [Demo](example) 53 | 54 | 55 | - 运行 56 | ```text 57 | python src/main.py config.json 58 | ``` 59 | 60 | 61 | ### 使用文档 62 | 63 | 本框架使用的是Python原生异步库(asyncio)实现异步事件驱动,所以在使用之前,需要先了解 [Python Asyncio](https://docs.python.org/3/library/asyncio.html)。 64 | 65 | - [服务配置](docs/configure/README.md) 66 | - [日志打印](docs/others/logger.md) 67 | - [行情](docs/market.md) 68 | - [交易](docs/trade.md) 69 | - [定时任务 & 服务器心跳](docs/others/tasks.md) 70 | 71 | 72 | ### 当前支持交易所 73 | - [Binance](https://www.binance.com) 74 | - [OKEx](https://www.okex.me/) 75 | - [OKEx Future](https://www.okex.me/future/trade) 76 | - [Deribit](https://www.deribit.com/) 77 | - To be continued ... 78 | 79 | 80 | ### Change Logs 81 | - [Change Logs](/docs/changelog.md) 82 | 83 | 84 | ### FAQ 85 | - [FAQ](docs/faq.md) 86 | -------------------------------------------------------------------------------- /docs/others/logger.md: -------------------------------------------------------------------------------- 1 | 2 | ## 日志打印 3 | 4 | 日志可以分多个级别,打印到控制台或者文件,文件可以按天分割存储。 5 | 6 | 7 | ##### 1. 日志配置 8 | ```json 9 | { 10 | "LOG": { 11 | "console": true, 12 | "level": "DEBUG", 13 | "path": "/var/log/servers/Quant", 14 | "name": "quant.log", 15 | "clear": true, 16 | "backup_count": 5 17 | } 18 | } 19 | ``` 20 | **参数说明**: 21 | - console `boolean` 是否打印到控制台 22 | - level `string` 日志打印级别 `DEBUG`/ `INFO` 23 | - path `string` 日志存储路径 24 | - name `string` 日志文件名 25 | - clear `boolean` 初始化的时候,是否清理之前的日志文件 26 | - backup_count `int` 保存按天分割的日志文件个数,默认0为永久保存所有日志文件 27 | 28 | > 配置文件可参考 [服务配置模块](../configure/README.md); 29 | 30 | 31 | ##### 2. 导入日志模块 32 | 33 | ```python 34 | from quant.utils import logger 35 | 36 | logger.debug("a:", 1, "b:", 2) 37 | logger.info("start strategy success!", caller=self) # 假设在某个类函数下调用,可以打印类名和函数名 38 | logger.warn("something may notice to me ...") 39 | logger.error("ERROR: server down!") 40 | logger.exception("something wrong!") 41 | ``` 42 | 43 | **参数说明**: 44 | - log_level `string` 日志级别 DEBUG/INFO 45 | - log_path `string` 日志输出路径 46 | - logfile_name string 日志文件名 47 | - clear `boolean` 初始化的时候,是否清理之前的日志文件 48 | - backup_count `int` 保存按天分割的日志文件个数,默认0为永久保存所有日志文件 49 | 50 | 51 | ##### 3. INFO日志 52 | ```python 53 | def info(*args, **kwargs): 54 | ``` 55 | 56 | ##### 4. WARNING日志 57 | ```python 58 | def warn(*args, **kwargs): 59 | ``` 60 | 61 | ##### 4. DEBUG日志 62 | ```python 63 | def debug(*args, **kwargs): 64 | ``` 65 | 66 | ##### 5. ERROR日志 67 | ````python 68 | def error(*args, **kwargs): 69 | ```` 70 | 71 | ##### 6. EXCEPTION日志 72 | ```python 73 | def exception(*args, **kwargs): 74 | ``` 75 | 76 | 77 | > 注意: 78 | - 所有函数的 `args` 和 `kwargs` 可以传入任意值,将会按照python的输出格式打印; 79 | - 在 `kwargs` 中指定 `caller=self` 或 `caller=cls`,可以在日志中打印出类名及函数名信息; 80 | -------------------------------------------------------------------------------- /quant/tasks.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | 任务模块 5 | 1. 注册一个循环执行任务:指定执行函数、指定执行间隔时间、指定函数入参; 6 | 2. 启动一个协程来执行函数任务; 7 | 8 | Author: HuangTao 9 | Date: 2018/04/26 10 | """ 11 | 12 | import asyncio 13 | import inspect 14 | 15 | from quant.heartbeat import heartbeat 16 | 17 | __all__ = ("LoopRunTask", "SingleTask") 18 | 19 | 20 | class LoopRunTask(object): 21 | """ 独立协程 循环执行任务 22 | """ 23 | 24 | @classmethod 25 | def register(cls, func, interval=1, *args, **kwargs): 26 | """ 注册一个任务,在每次心跳的时候执行调用 27 | @param func 心跳的时候执行的函数,必须的async异步函数 28 | @param interval 执行回调的时间间隔(秒),必须是整秒 29 | @return task_id 任务id 30 | """ 31 | task_id = heartbeat.register(func, interval, *args, **kwargs) 32 | return task_id 33 | 34 | @classmethod 35 | def unregister(cls, task_id): 36 | """ 注销一个任务 37 | @param task_id 任务id 38 | """ 39 | heartbeat.unregister(task_id) 40 | 41 | 42 | class SingleTask: 43 | """ 独立协程 执行任务 44 | """ 45 | 46 | @classmethod 47 | def run(cls, func, *args, **kwargs): 48 | """ 运行独立函数func 49 | @param func 需要独立在协程里运行的函数,必须的async异步函数 50 | """ 51 | asyncio.get_event_loop().create_task(func(*args, **kwargs)) 52 | 53 | @classmethod 54 | def call_later(cls, func, delay=0, *args, **kwargs): 55 | """ 延迟执行func函数 56 | @param func 需要被执行的函数 57 | @param delay 函数被延迟执行的时间(秒),可以为小数,如0.5秒 58 | """ 59 | if not inspect.iscoroutinefunction(func): 60 | asyncio.get_event_loop().call_later(delay, func, *args) 61 | else: 62 | def foo(f, *args, **kwargs): 63 | asyncio.get_event_loop().create_task(f(*args, **kwargs)) 64 | asyncio.get_event_loop().call_later(delay, foo, func, *args) 65 | -------------------------------------------------------------------------------- /docs/configure/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## 配置文件 3 | 4 | 框架启动的时候,需要指定一个 `json` 格式的配置文件。 5 | - [一个完整的配置文件示例](config.json) 6 | 7 | 8 | ## 配置使用 9 | 所有 `config.json` 配置文件里的 `key-value` 格式数据,都可以通过如下方式使用: 10 | ```python 11 | from quant.config import config 12 | 13 | config.name # 使用配置里的name字段 14 | config.abc # 使用配置里的abd字段 15 | ``` 16 | 17 | ## 系统配置参数 18 | > 所有系统配置参数均为 `大写字母` 为key; 19 | > 所有系统配置参数均为 `可选`; 20 | 21 | 22 | ##### 1. LOG 23 | 日志配置。包含如下配置: 24 | 25 | **示例**: 26 | ```json 27 | { 28 | "LOG": { 29 | "console": false, 30 | "level": "DEBUG", 31 | "path": "/var/log/servers/Quant", 32 | "name": "quant.log", 33 | "clear": true, 34 | "backup_count": 5 35 | } 36 | } 37 | ``` 38 | 39 | **配置说明**: 40 | - console `boolean` 是否在命令行打印日志 `true 打印日志到命令行` / `false 打印日志到文件` 41 | - level `string` 日志级别 `DEBUG` / `INFO`, 默认 `DEBUG` 42 | - path `string` 日志路径 `默认 /var/log/servers/Quant` 43 | - name `string` 日志文件名 `默认 quant.log` 44 | - clear `boolean` 重启的时候,是否清理历史日志(true将删除整个日志保存文件夹) 45 | - backup_count `int` 日志保存个数(日志按天分割,默认保留5天日志, `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请求; 81 | 82 | 83 | ##### 3. 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 | -------------------------------------------------------------------------------- /quant/position.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | 持仓对象 5 | 6 | Author: HuangTao 7 | Date: 2018/04/22 8 | """ 9 | 10 | from quant.utils import tools 11 | 12 | 13 | class Position: 14 | """ 持仓对象 15 | """ 16 | 17 | def __init__(self, platform=None, account=None, strategy=None, symbol=None): 18 | """ 初始化持仓对象 19 | @param platform 交易平台 20 | @param account 账户 21 | @param strategy 策略名称 22 | @param symbol 合约名称 23 | """ 24 | self.platform = platform 25 | self.account = account 26 | self.strategy = strategy 27 | self.symbol = symbol 28 | self.short_quantity = 0 # 空仓数量 29 | self.short_avg_price = 0 # 空仓平均价格 30 | self.long_quantity = 0 # 多仓数量 31 | self.long_avg_price = 0 # 多仓平均价格 32 | self.liquid_price = 0 # 预估爆仓价格 33 | self.utime = None # 更新时间戳 34 | 35 | def update(self, short_quantity=0, short_avg_price=0, long_quantity=0, long_avg_price=0, liquid_price=0, 36 | utime=None): 37 | self.short_quantity = short_quantity 38 | self.short_avg_price = short_avg_price 39 | self.long_quantity = long_quantity 40 | self.long_avg_price = long_avg_price 41 | self.liquid_price = liquid_price 42 | self.utime = utime if utime else tools.get_cur_timestamp_ms() 43 | 44 | def __str__(self): 45 | info = "[platform: {platform}, account: {account}, strategy: {strategy}, symbol: {symbol}, " \ 46 | "short_quantity: {short_quantity}, short_avg_price: {short_avg_price}, " \ 47 | "long_quantity: {long_quantity}, long_avg_price: {long_avg_price}, liquid_price: {liquid_price}, " \ 48 | "utime: {utime}]"\ 49 | .format(platform=self.platform, account=self.account, strategy=self.strategy, symbol=self.symbol, 50 | short_quantity=self.short_quantity, short_avg_price=self.short_avg_price, 51 | long_quantity=self.long_quantity, long_avg_price=self.long_avg_price, 52 | liquid_price=self.liquid_price, utime=self.utime) 53 | return info 54 | 55 | def __repr__(self): 56 | return str(self) 57 | -------------------------------------------------------------------------------- /quant/utils/agent.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | 服务代理 5 | 6 | Author: HuangTao 7 | Date: 2019/02/16 8 | """ 9 | 10 | import asyncio 11 | 12 | from quant.utils import tools 13 | from quant.utils.websocket import Websocket 14 | 15 | 16 | class Agent(Websocket): 17 | """ websocket长连接代理 18 | """ 19 | 20 | def __init__(self, wss, proxy=None, connected_callback=None, update_callback=None): 21 | """ 初始化 22 | @param wss websocket地址 23 | @param proxy HTTP代理 24 | @param connected_callback websocket连接建立成功回调 25 | @param update_callback websocket数据更新回调 26 | """ 27 | self._connected_callback = connected_callback 28 | self._update_callback = update_callback 29 | self._queries = {} # 未完成的请求对象 {"request_id": future} 30 | 31 | super(Agent, self).__init__(wss, proxy) 32 | self.initialize() 33 | 34 | async def connected_callback(self): 35 | """ websocket连接建立成功回调 36 | """ 37 | if self._connected_callback: 38 | await asyncio.sleep(0.1) # 延迟0.1秒执行回调,等待初始化函数完成准备工作 39 | await self._connected_callback() 40 | 41 | async def do_request(self, option, params): 42 | """ 发送请求 43 | @param option 操作类型 44 | @param params 请求参数 45 | """ 46 | request_id = tools.get_uuid1() 47 | data = { 48 | "id": request_id, 49 | "option": option, 50 | "params": params 51 | } 52 | await self.ws.send_json(data) 53 | f = asyncio.futures.Future() 54 | self._queries[request_id] = f 55 | result = await f 56 | if result["code"] == 0: 57 | return True, result["msg"], result["data"] 58 | else: 59 | return False, result["msg"], result["data"] 60 | 61 | async def process(self, msg): 62 | """ 处理消息 63 | """ 64 | request_id = msg.get("id") 65 | if request_id in self._queries: 66 | f = self._queries.pop(request_id) 67 | if f.done(): 68 | return 69 | f.set_result(msg) 70 | else: 71 | if self._update_callback: 72 | asyncio.get_event_loop().create_task(self._update_callback(msg["option"], msg["data"])) 73 | -------------------------------------------------------------------------------- /quant/utils/dingding.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | 钉钉机器人接口 5 | 6 | Author: HuangTao 7 | Date: 2018/08/04 8 | """ 9 | 10 | from quant.utils import logger 11 | from quant.utils.http_client import AsyncHttpRequests 12 | 13 | 14 | class DingTalk: 15 | """ 钉钉机器人接口 16 | """ 17 | BASE_URL = "https://oapi.dingtalk.com/robot/send?access_token=" 18 | 19 | @classmethod 20 | async def send_text_msg(cls, access_token, content, phones=None, is_at_all=False): 21 | """ 发送文本消息 22 | @param access_token 钉钉消息access_token 23 | @param content 消息内容 24 | @param phones 需要@提醒的群成员手机号列表 25 | @param is_at_all 是否需要@所有人,默认为False 26 | """ 27 | body = { 28 | "msgtype": "text", 29 | "text": { 30 | "content": content 31 | } 32 | } 33 | if is_at_all: 34 | body["at"] = {"isAtAll": True} 35 | if phones: 36 | assert isinstance(phones, list) 37 | body["at"] = {"atMobiles": phones} 38 | url = cls.BASE_URL + access_token 39 | headers = {"Content-Type": "application/json"} 40 | result = await AsyncHttpRequests.post(url, data=body, headers=headers) 41 | logger.info("url:", url, "body:", body, "result:", result, caller=cls) 42 | 43 | @classmethod 44 | async def send_markdown_msg(cls, access_token, title, text, phones=None, is_at_all=False): 45 | """ 发送文本消息 46 | @param access_token 钉钉消息access_token 47 | @param title 首屏会话透出的展示内容 48 | @param text markdown格式的消息 49 | @param phones 需要@提醒的群成员手机号列表 50 | @param is_at_all 是否需要@所有人,默认为False 51 | """ 52 | body = { 53 | "msgtype": "markdown", 54 | "markdown": { 55 | "title": title, 56 | "text": text 57 | } 58 | } 59 | if is_at_all: 60 | body["at"] = {"isAtAll": True} 61 | if phones: 62 | assert isinstance(phones, list) 63 | body["at"] = {"atMobiles": phones} 64 | url = cls.BASE_URL + access_token 65 | headers = {"Content-Type": "application/json"} 66 | result = await AsyncHttpRequests.post(url, data=body, headers=headers) 67 | logger.info("url:", url, "body:", body, "result:", result, caller=cls) 68 | -------------------------------------------------------------------------------- /quant/utils/sendmail.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | 4 | import email 5 | import asyncio 6 | from email.mime.text import MIMEText 7 | from email.mime.multipart import MIMEMultipart 8 | 9 | import aiosmtplib 10 | 11 | from quant.utils import logger 12 | 13 | 14 | class SendEmail: 15 | """ 发送邮件 16 | """ 17 | 18 | def __init__(self, host, port, username, password, to_emails, subject, content, timeout=30, tls=True): 19 | """ 初始化 20 | @param host 邮件服务端主机 21 | @param port 邮件服务器端口 22 | @param username 用户名 23 | @param password 密码 24 | @param to_emails 发送到邮箱列表 25 | @param title 标题 26 | @param content 内容 27 | @param timeout 超时时间,默认30秒 28 | @param tls 是否使用TLS,默认使用 29 | """ 30 | self._host = host 31 | self._port = port 32 | self._username = username 33 | self._password = password 34 | self._to_emails = to_emails 35 | self._subject = subject 36 | self._content = content 37 | self._timeout = timeout 38 | self._tls = tls 39 | 40 | async def send(self): 41 | """ 发送邮件 42 | """ 43 | message = MIMEMultipart('related') 44 | message['Subject'] = self._subject 45 | message['From'] = self._username 46 | message['To'] = ",".join(self._to_emails) 47 | message['Date'] = email.utils.formatdate() 48 | message.preamble = 'This is a multi-part message in MIME format.' 49 | ma = MIMEMultipart('alternative') 50 | mt = MIMEText(self._content, 'plain', 'GB2312') 51 | ma.attach(mt) 52 | message.attach(ma) 53 | 54 | smtp = aiosmtplib.SMTP(hostname=self._host, port=self._port, timeout=self._timeout, use_tls=self._tls) 55 | await smtp.connect() 56 | await smtp.login(self._username, self._password) 57 | await smtp.send_message(message) 58 | logger.info('send email success! FROM:', self._username, 'TO:', self._to_emails, 'CONTENT:', self._content, 59 | caller=self) 60 | 61 | 62 | if __name__ == "__main__": 63 | h = 'hwhzsmtp.qiye.163.com' 64 | p = 994 65 | u = 'huangtao@ifclover.com' 66 | pw = '123456' 67 | t = ['huangtao@ifclover.com'] 68 | s = 'Test Send Email 测试' 69 | c = "Just a test. \n 测试。" 70 | 71 | sender = SendEmail(h, p, u, pw, t, s, c) 72 | asyncio.get_event_loop().create_task(sender.send()) 73 | asyncio.get_event_loop().run_forever() 74 | -------------------------------------------------------------------------------- /docs/market.md: -------------------------------------------------------------------------------- 1 | ## 行情 2 | 3 | 通过行情模块(market),可以订阅任意交易所的任意交易对的实时行情,包括订单薄(Orderbook)、K线(KLine)、成交(Trade)、交易(Ticker), 4 | 根据不同交易所提供的行情信息,实时将行情信息推送给策略; 5 | 6 | 在订阅行情之前,需要先部署 `行情服务器`,行情服务器将通过 REST API 或 Websocket 的方式从交易所获取实时行情信息,并将行情信息按照 7 | 统一的数据格式打包,通过事件的形式发布至事件中心; 8 | 9 | 10 | ### 行情模块使用 11 | 12 | ```python 13 | # 导入模块 14 | from quant import const 15 | from quant.market import Market, Orderbook 16 | 17 | 18 | # 订阅订单薄行情,注意此处注册的回调函数是`async` 异步函数,回调参数为 `orderbook` 对象,数据结构查看下边的介绍。 19 | async def on_event_orderbook_update(orderbook: Orderbook): pass 20 | Market(const.MARKET_TYPE_ORDERBOOK, const.BINANCE, "ETH/BTC", on_event_orderbook_update) 21 | ``` 22 | 23 | > 使用同样的方式,可以订阅任意的行情 24 | ```python 25 | from quant import const 26 | 27 | const.MARKET_TYPE_ORDERBOOK # 订单薄(Orderbook) 28 | const.MARKET_TYPE_KLINE # K线(KLine) 29 | const.MARKET_TYPE_TRADE # K线(KLine) 30 | ``` 31 | 32 | 33 | ### 行情数据结构 34 | 35 | 所有交易平台的行情,全部使用统一的数据结构; 36 | 37 | #### 订单薄(Orderbook) 38 | ```json 39 | { 40 | "platform": "binance", 41 | "symbol": "ETH/USDT", 42 | "asks": [ 43 | ["8680.70000000", "0.00200000"] 44 | ], 45 | "bids": [ 46 | ["8680.60000000", "2.82696138"] 47 | ], 48 | "timestamp": 1558949307370 49 | } 50 | ``` 51 | 52 | **字段说明**: 53 | - platform `string` 交易平台 54 | - symbol `string` 交易对 55 | - asks `list` 卖盘 `[price, quantity]` 56 | - bids `list` 买盘 `[price, quantity]` 57 | - timestamp `int` 时间戳(毫秒) 58 | 59 | 60 | #### K线(KLine) 61 | ```json 62 | { 63 | "platform": "okex", 64 | "symbol": "BTC/USDT", 65 | "open": "8665.50000000", 66 | "high": "8668.40000000", 67 | "low": "8660.00000000", 68 | "close": "8660.00000000", 69 | "volume": "73.14728136", 70 | "timestamp": 1558946340000 71 | } 72 | ``` 73 | 74 | **字段说明**: 75 | - platform `string` 交易平台 76 | - symbol `string` 交易对 77 | - open `string` 开盘价 78 | - high `string` 最高价 79 | - low `string` 最低价 80 | - close `string` 收盘价 81 | - volume `string` 成交量 82 | - timestamp `int` 时间戳(毫秒) 83 | 84 | 85 | #### 交易数据(Trade) 86 | ```json 87 | { 88 | "platform": "okex", 89 | "symbol": "BTC/USDT", 90 | "action": "SELL", 91 | "price": "8686.40000000", 92 | "quantity": "0.00200000", 93 | "timestamp": 1558949571111, 94 | } 95 | ``` 96 | 97 | **字段说明**: 98 | - platform `string` 交易平台 99 | - symbol `string` 交易对 100 | - action `string` 操作类型 BUY 买入 / SELL 卖出 101 | - price `string` 价格 102 | - quantity `string` 数量 103 | - timestamp `int` 时间戳(毫秒) 104 | -------------------------------------------------------------------------------- /quant/quant.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | 初始化日志、数据库、消息队列、启动服务器心跳 5 | 6 | Author: HuangTao 7 | Date: 2017/04/26 8 | """ 9 | 10 | import asyncio 11 | 12 | from quant.utils import logger 13 | from quant.config import config 14 | 15 | 16 | class Quant: 17 | """ 初始化日志、数据库连接,启动服务器心跳 18 | """ 19 | 20 | def __init__(self): 21 | """ 初始化 22 | """ 23 | self.loop = None 24 | self.event_center = None 25 | 26 | def initialize(self, config_module=None): 27 | """ 初始化 28 | @param config_module 配置模块 29 | """ 30 | self._get_event_loop() 31 | self._load_settings(config_module) 32 | self._init_logger() 33 | self._init_db_instance() 34 | self._init_event_center() 35 | self._do_heartbeat() 36 | 37 | def start(self): 38 | """ 启动 39 | """ 40 | logger.info("start io loop ...", caller=self) 41 | self.loop.run_forever() 42 | 43 | def _get_event_loop(self): 44 | """ 获取主事件io loop 45 | """ 46 | if not self.loop: 47 | self.loop = asyncio.get_event_loop() 48 | return self.loop 49 | 50 | def _load_settings(self, config_module): 51 | """ 加载配置 52 | """ 53 | config.loads(config_module) 54 | 55 | def _init_logger(self): 56 | """ 初始化日志 57 | """ 58 | console = config.log.get("console", True) # 是否打印日志到命令行 59 | level = config.log.get("level", "DEBUG") # 打印日志的级别 60 | path = config.log.get("path", "/tmp/logs/Quant") # 日志文件存放的路径 61 | name = config.log.get("name", "quant.log") # 日志文件名 62 | clear = config.log.get("clear", False) # 是否清理历史日志 63 | backup_count = config.log.get("backup_count", 0) # 保存按天分割的日志文件个数 64 | if console: 65 | logger.initLogger(level) 66 | else: 67 | logger.initLogger(level, path, name, clear, backup_count) 68 | 69 | def _init_db_instance(self): 70 | """ 初始化数据库对象 71 | """ 72 | if config.mongodb: 73 | from quant.utils.mongo import initMongodb 74 | initMongodb(**config.mongodb) 75 | 76 | def _init_event_center(self): 77 | """ 初始化事件中心 78 | """ 79 | if config.rabbitmq: 80 | from quant.event import EventCenter 81 | self.event_center = EventCenter() 82 | self.loop.run_until_complete(self.event_center.connect()) 83 | config.initialize() # 订阅配置更新事件 84 | 85 | def _do_heartbeat(self): 86 | """ 服务器心跳 87 | """ 88 | from quant.heartbeat import heartbeat 89 | self.loop.call_later(0.5, heartbeat.ticker) 90 | 91 | 92 | quant = Quant() 93 | -------------------------------------------------------------------------------- /quant/heartbeat.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | 服务器心跳 5 | 6 | Author: HuangTao 7 | Date: 2018/04/26 8 | """ 9 | 10 | import asyncio 11 | 12 | from quant.utils import tools 13 | from quant.utils import logger 14 | from quant.config import config 15 | 16 | __all__ = ("heartbeat", ) 17 | 18 | 19 | class HeartBeat(object): 20 | """ 心跳 21 | """ 22 | 23 | def __init__(self): 24 | self._count = 0 # 心跳次数 25 | self._interval = 1 # 服务心跳执行时间间隔(秒) 26 | self._print_interval = config.heartbeat.get("interval", 0) # 心跳打印时间间隔(秒),0为不打印 27 | self._broadcast_interval = config.heartbeat.get("broadcast", 0) # 心跳广播间隔(秒),0为不广播 28 | self._tasks = {} # 跟随心跳执行的回调任务列表,由 self.register 注册 {task_id: {...}} 29 | 30 | @property 31 | def count(self): 32 | return self._count 33 | 34 | def ticker(self): 35 | """ 启动心跳, 每秒执行一次 36 | """ 37 | self._count += 1 38 | 39 | # 打印心跳次数 40 | if self._print_interval > 0: 41 | if self._count % self._print_interval == 0: 42 | logger.info("do server heartbeat, count:", self._count, caller=self) 43 | 44 | # 设置下一次心跳回调 45 | asyncio.get_event_loop().call_later(self._interval, self.ticker) 46 | 47 | # 执行任务回调 48 | for task in self._tasks.values(): 49 | interval = task["interval"] 50 | if self._count % interval != 0: 51 | continue 52 | func = task["func"] 53 | args = task["args"] 54 | kwargs = task["kwargs"] 55 | kwargs["heart_beat_count"] = self._count 56 | asyncio.get_event_loop().create_task(func(*args, **kwargs)) 57 | 58 | # 广播服务进程心跳 59 | if self._broadcast_interval > 0: 60 | if self._count % self._broadcast_interval == 0: 61 | self.alive() 62 | 63 | def register(self, func, interval=1, *args, **kwargs): 64 | """ 注册一个任务,在每次心跳的时候执行调用 65 | @param func 心跳的时候执行的函数 66 | @param interval 执行回调的时间间隔(秒) 67 | @return task_id 任务id 68 | """ 69 | t = { 70 | "func": func, 71 | "interval": interval, 72 | "args": args, 73 | "kwargs": kwargs 74 | } 75 | task_id = tools.get_uuid1() 76 | self._tasks[task_id] = t 77 | return task_id 78 | 79 | def unregister(self, task_id): 80 | """ 注销一个任务 81 | @param task_id 任务id 82 | """ 83 | if task_id in self._tasks: 84 | self._tasks.pop(task_id) 85 | 86 | def alive(self): 87 | """ 服务进程广播心跳 88 | """ 89 | from quant.event import EventHeartbeat 90 | EventHeartbeat(config.server_id, self.count).publish() 91 | 92 | 93 | heartbeat = HeartBeat() 94 | -------------------------------------------------------------------------------- /docs/trade.md: -------------------------------------------------------------------------------- 1 | ## 交易 2 | 3 | 通过交易模块(trade),可以在任意交易平台发起交易,包括下单(create_order)、撤单(revoke_order)、查询订单状态(order_status)、 4 | 查询未完全成交订单(orders)等功能; 5 | 6 | 策略完成下单之后,底层框架将定时或实时将最新的订单状态更新通过策略注册的回调函数传递给策略,策略能够在第一时间感知到拿到订单状态 7 | 更新数据; 8 | 9 | ``` 10 | 11 | 12 | ### 交易模块使用 13 | 14 | ```python 15 | # 导入模块 16 | from quant import const 17 | from quant import order 18 | from quant.trade import Trade 19 | 20 | # 初始化 21 | platform = const.BINANCE # 交易平台 假设是binance 22 | account = "abc@gmail.com" # 交易账户 23 | access_key = "ABC123" # API KEY 24 | secret_key = "abc123" # SECRET KEY 25 | symbol = "ETH/BTC" # 交易对 26 | name = "my_test_strategy" # 自定义的策略名称 27 | 28 | # 注册订单更新回调函数,注意此处注册的回调函数是 `async` 异步函数,回调参数为 `order` 对象,数据结构请查看下边的介绍。 29 | async def on_event_order_update(order): pass 30 | 31 | # 注册订单更新回调函数,注意此处注册的回调函数是 `async` 异步函数,回调参数为 `asset` 对象 32 | async def on_event_asset_update(asset): pass 33 | 34 | # 创建trade对象 35 | trader = Trade(platform, account, access_key, secret_key, symbol, name, 36 | asset_update_callback=on_event_asset_update, 37 | order_update_callback=on_event_order_update) 38 | 39 | # 下单 40 | action = order.ORDER_ACTION_BUY # 买单 41 | price = "11.11" # 委托价格 42 | quantity = "22.22" # 委托数量 43 | order_type = order.ORDER_TYPE_LIMIT # 限价单 44 | order_no = await trader.create_order(action, price, quantity, order_type) # 注意,此函数需要在 `async` 异步函数里执行 45 | 46 | 47 | # 撤单 48 | await trader.revoke_order(order_no) # 注意,此函数需要在 `async` 异步函数里执行 49 | 50 | 51 | # 查询所有未成交订单id列表 52 | order_nos = await trader.get_open_orders() # 注意,此函数需要在 `async` 异步函数里执行 53 | 54 | 55 | # 查询当前所有未成交订单数据 56 | orders = trader.orders # orders是一个dict,key为order_no,value为order对象 57 | ``` 58 | 59 | ### 订单对象模块 60 | 61 | 所有订单相关的数据常量和对象在框架的 `quant.order` 模块下。 62 | 63 | - 订单类型 64 | ```python 65 | from quant import order 66 | 67 | order.ORDER_TYPE_LIMIT # 限价单 68 | order.ORDER_TYPE_MARKET # 市价单 69 | ``` 70 | 71 | - 订单操作 72 | ```python 73 | from quant import order 74 | 75 | order.ORDER_ACTION_BUY # 买入 76 | order.ORDER_ACTION_SELL # 卖出 77 | ``` 78 | 79 | - 订单状态 80 | ```python 81 | from quant import order 82 | 83 | order.ORDER_STATUS_NONE = "NONE" # 新创建的订单,无状态 84 | order.ORDER_STATUS_SUBMITTED = "SUBMITTED" # 已提交 85 | order.ORDER_STATUS_PARTIAL_FILLED = "PARTIAL-FILLED" # 部分处理 86 | order.ORDER_STATUS_FILLED = "FILLED" # 处理 87 | order.ORDER_STATUS_CANCELED = "CANCELED" # 取消 88 | order.ORDER_STATUS_FAILED = "FAILED" # 失败订单 89 | ``` 90 | 91 | - 订单对象 92 | ```python 93 | from quant import order 94 | 95 | o = order.Order() 96 | o.platform # 交易平台 97 | o.account # 交易账户 98 | o.strategy # 策略名称 99 | o.order_no # 委托单号 100 | o.action # 买卖类型 SELL-卖,BUY-买 101 | o.order_type # 委托单类型 MKT-市价,LMT-限价 102 | o.symbol # 交易对 如: ETH/BTC 103 | o.price # 委托价格 104 | o.quantity # 委托数量(限价单) 105 | o.remain # 剩余未成交数量 106 | o.status # 委托单状态 107 | o.timestamp # 创建订单时间戳(毫秒) 108 | ``` 109 | -------------------------------------------------------------------------------- /quant/order.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | 订单 5 | 6 | Author: HuangTao 7 | Date: 2018/05/14 8 | """ 9 | 10 | from quant.utils import tools 11 | 12 | 13 | # 订单类型 14 | ORDER_TYPE_LIMIT = "LIMIT" # 限价单 15 | ORDER_TYPE_MARKET = "MARKET" # 市价单 16 | 17 | # 订单操作 买/卖 18 | ORDER_ACTION_BUY = "BUY" # 买 19 | ORDER_ACTION_SELL = "SELL" # 卖 20 | 21 | # 订单状态 22 | ORDER_STATUS_NONE = "NONE" # 新创建的订单,无状态 23 | ORDER_STATUS_SUBMITTED = "SUBMITTED" # 已提交 24 | ORDER_STATUS_PARTIAL_FILLED = "PARTIAL-FILLED" # 部分处理 25 | ORDER_STATUS_FILLED = "FILLED" # 处理 26 | ORDER_STATUS_CANCELED = "CANCELED" # 取消 27 | ORDER_STATUS_FAILED = "FAILED" # 失败订单 28 | 29 | # 合约订单类型 30 | TRADE_TYPE_NONE = 0 # 未知订单类型,订单不是由框架创建,且某些平外的订单不能判断订单类型 31 | TRADE_TYPE_BUY_OPEN = 1 # 买入开多 action=BUY, quantity>0 32 | TRADE_TYPE_SELL_OPEN = 2 # 卖出开空 action=SELL, quantity<0 33 | TRADE_TYPE_SELL_CLOSE = 3 # 卖出平多 action=SELL, quantity>0 34 | TRADE_TYPE_BUY_CLOSE = 4 # 买入平空 action=BUY, quantity<0 35 | 36 | 37 | class Order: 38 | """ 订单对象 39 | """ 40 | 41 | def __init__(self, account=None, platform=None, strategy=None, order_no=None, symbol=None, action=None, price=0, 42 | quantity=0, remain=0, status=ORDER_STATUS_NONE, avg_price=0, order_type=ORDER_TYPE_LIMIT, 43 | trade_type=TRADE_TYPE_NONE, ctime=None, utime=None): 44 | self.platform = platform # 交易平台 45 | self.account = account # 交易账户 46 | self.strategy = strategy # 策略名称 47 | self.order_no = order_no # 委托单号 48 | self.action = action # 买卖类型 SELL-卖,BUY-买 49 | self.order_type = order_type # 委托单类型 MARKET-市价,LIMIT-限价 50 | self.symbol = symbol # 交易对 如: ETH/BTC 51 | self.price = price # 委托价格 52 | self.quantity = quantity # 委托数量(限价单) 53 | self.remain = remain # 剩余未成交数量 54 | self.status = status # 委托单状态 55 | self.avg_price = avg_price # 成交均价 56 | self.trade_type = trade_type # 合约订单类型 开多/开空/平多/平空 57 | self.ctime = ctime if ctime else tools.get_cur_timestamp() # 创建订单时间戳 58 | self.utime = utime if utime else tools.get_cur_timestamp() # 交易所订单更新时间 59 | 60 | def __str__(self): 61 | info = "[platform: {platform}, account: {account}, strategy: {strategy}, order_no: {order_no}, " \ 62 | "action: {action}, symbol: {symbol}, price: {price}, quantity: {quantity}, remain: {remain}, " \ 63 | "status: {status}, avg_price: {avg_price}, order_type: {order_type}, trade_type: {trade_type}, " \ 64 | "ctime: {ctime}, utime: {utime}]".format( 65 | platform=self.platform, account=self.account, strategy=self.strategy, order_no=self.order_no, 66 | action=self.action, symbol=self.symbol, price=self.price, quantity=self.quantity, 67 | remain=self.remain, status=self.status, avg_price=self.avg_price, order_type=self.order_type, 68 | trade_type=self.trade_type, ctime=self.ctime, utime=self.utime) 69 | return info 70 | 71 | def __repr__(self): 72 | return str(self) 73 | -------------------------------------------------------------------------------- /example/strategy/strategy.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | # 策略实现 4 | 5 | from quant import const 6 | from quant.utils import tools 7 | from quant.utils import logger 8 | from quant.config import config 9 | from quant.market import Market 10 | from quant.trade import Trade 11 | from quant.const import BINANCE 12 | from quant.order import ORDER_ACTION_BUY, ORDER_STATUS_FAILED, ORDER_STATUS_CANCELED, ORDER_STATUS_FILLED 13 | 14 | 15 | class MyStrategy: 16 | 17 | def __init__(self): 18 | """ 初始化 19 | """ 20 | self.strategy = "my_strategy" 21 | self.platform = BINANCE 22 | self.account = config.platforms.get(self.platform, {}).get("account") 23 | self.access_key = config.platforms.get(self.platform, {}).get("access_key") 24 | self.secret_key = config.platforms.get(self.platform, {}).get("secret_key") 25 | self.symbol = config.symbol 26 | self.name = config.strategy 27 | 28 | self.order_no = None # 创建订单的id 29 | self.create_order_price = "0.0" # 创建订单的价格 30 | 31 | # 交易模块 32 | self.trader = Trade(self.strategy, self.platform, self.symbol, self.account, 33 | asset_update_callback=self.on_event_asset_update, 34 | order_update_callback=self.on_event_order_update) 35 | 36 | # 订阅行情 37 | Market(const.MARKET_TYPE_ORDERBOOK, const.BINANCE, self.symbol, self.on_event_orderbook_update) 38 | 39 | async def on_event_orderbook_update(self, orderbook): 40 | """ 订单薄更新 41 | """ 42 | logger.debug("orderbook:", orderbook, caller=self) 43 | bid3_price = orderbook["bids"][2][0] # 买三价格 44 | bid4_price = orderbook["bids"][3][0] # 买四价格 45 | 46 | # 判断是否需要撤单 47 | if self.order_no: 48 | if float(self.create_order_price) < float(bid3_price) or float(self.create_order_price) > float(bid4_price): 49 | return 50 | await self.trader.revoke_order(self.order_no) 51 | self.order_no = None 52 | logger.info("revoke order:", self.order_no, caller=self) 53 | 54 | # 创建新订单 55 | new_price = (float(bid3_price) + float(bid4_price)) / 2 56 | quantity = "0.1" # 假设委托数量为0.1 57 | action = ORDER_ACTION_BUY 58 | price = tools.float_to_str(new_price) 59 | quantity = tools.float_to_str(quantity) 60 | order_no = await self.trader.create_order(action, price, quantity) 61 | self.order_no = order_no 62 | self.create_order_price = price 63 | logger.info("create new order:", order_no, caller=self) 64 | 65 | async def on_event_asset_update(self, asset): 66 | """ 资产更新 67 | """ 68 | logger.info("asset:", asset, caller=self) 69 | 70 | async def on_event_order_update(self, order): 71 | """ 订单状态更新 72 | """ 73 | logger.info("order update:", order, caller=self) 74 | 75 | # 如果订单失败、订单取消、订单完成交易 76 | if order.status in [ORDER_STATUS_FAILED, ORDER_STATUS_CANCELED, ORDER_STATUS_FILLED]: 77 | self.order_no = None 78 | -------------------------------------------------------------------------------- /quant/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | 服务配置 5 | 6 | Author: HuangTao 7 | Date: 2018/05/03 8 | """ 9 | 10 | import json 11 | 12 | from quant.utils import logger 13 | 14 | 15 | class Config: 16 | """ 服务配置 17 | """ 18 | 19 | def __init__(self): 20 | """ 配置项 21 | `SERVER_ID` 服务ID 22 | `RUN_TIME_UPDATE` 是否支持配置动态更新 23 | `LOG` 日志配置 24 | `RABBITMQ` RabbitMQ配置 25 | `MONGODB` mongodb配置 26 | `REDIS` redis配置 27 | `PLATFORMS` 交易所配置 28 | `HEARTBEAT` 服务心跳配置 {"interval": 0, "broadcast": 0} 29 | `PROXY` HTTP代理配置 30 | """ 31 | self.server_id = None # 服务id(manager服务创建) 32 | self.run_time_update = False # 是否支持配置动态更新 33 | self.log = {} # 日志配置 34 | self.rabbitmq = {} # RabbitMQ配置 35 | self.mongodb = {} # Mongodb配置 36 | self.redis = {} # Redis配置 37 | self.platforms = {} # 交易所配置 38 | self.heartbeat = {} # 服务心跳配置 39 | self.service = {} # 代理服务配置 40 | self.proxy = None # HTTP代理配置 41 | 42 | def initialize(self): 43 | """ 初始化 44 | """ 45 | # 订阅事件 做市参数更新 46 | if self.run_time_update: 47 | from quant.event import EventConfig 48 | EventConfig(self.server_id).subscribe(self.on_event_config, False) 49 | 50 | async def on_event_config(self, event): 51 | """ 更新参数 52 | @param event 事件对象 53 | """ 54 | from quant.event import EventConfig 55 | event = EventConfig().duplicate(event) 56 | if event.server_id != self.server_id: 57 | return 58 | if not isinstance(event.params, dict): 59 | logger.error("params format error! params:", event.params, caller=self) 60 | return 61 | 62 | # 将配置文件中的数据按照dict格式解析并设置成config的属性 63 | self.update(event.params) 64 | logger.info("config update success!", caller=self) 65 | 66 | def loads(self, config_file=None): 67 | """ 加载配置 68 | @param config_file json配置文件 69 | """ 70 | configures = {} 71 | if config_file: 72 | try: 73 | with open(config_file) as f: 74 | data = f.read() 75 | configures = json.loads(data) 76 | except Exception as e: 77 | print(e) 78 | exit(0) 79 | if not configures: 80 | print("config json file error!") 81 | exit(0) 82 | self.update(configures) 83 | 84 | def update(self, update_fields): 85 | """ 更新配置 86 | @param update_fields 更新字段 87 | """ 88 | self.server_id = update_fields.get("SERVER_ID") # 服务id 89 | self.run_time_update = update_fields.get("RUN_TIME_UPDATE", False) # 是否支持配置动态更新 90 | self.log = update_fields.get("LOG", {}) # 日志配置 91 | self.rabbitmq = update_fields.get("RABBITMQ", None) # RabbitMQ配置 92 | self.mongodb = update_fields.get("MONGODB", None) # mongodb配置 93 | self.redis = update_fields.get("REDIS", None) # redis配置 94 | self.platforms = update_fields.get("PLATFORMS", {}) # 交易所配置 95 | self.heartbeat = update_fields.get("HEARTBEAT", {}) # 服务心跳配置 96 | self.service = update_fields.get("SERVICE", {}) # 代理服务配置 97 | self.proxy = update_fields.get("PROXY", None) # HTTP代理配置 98 | 99 | # 将配置文件中的数据按照dict格式解析并设置成config的属性 100 | for k, v in update_fields.items(): 101 | setattr(self, k, v) 102 | 103 | 104 | config = Config() 105 | -------------------------------------------------------------------------------- /quant/trade.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | Trade 交易模块,整合所有交易所为一体 5 | 6 | Author: HuangTao 7 | Date: 2019/04/21 8 | """ 9 | 10 | from quant.utils import logger 11 | from quant.order import ORDER_TYPE_LIMIT 12 | from quant.const import OKEX, OKEX_FUTURE, DERIBIT, BITMEX, BINANCE 13 | from quant.platform.okex import OKExTrade 14 | # from quant.platform.bitmex.trade import BitmexTrade 15 | from quant.platform.binance import BinanceTrade 16 | from quant.platform.deribit import DeribitTrade 17 | from quant.platform.okex_future import OKExFutureTrade 18 | 19 | 20 | class Trade: 21 | """ 交易模块 22 | """ 23 | 24 | def __init__(self, strategy, platform, symbol, host=None, wss=None, account=None, access_key=None, secret_key=None, 25 | passphrase=None, order_update_callback=None, position_update_callback=None, **kwargs): 26 | """ 初始化 27 | @param strategy 策略名称 28 | @param platform 交易平台 29 | @param symbol 交易对 30 | @param order_update_callback 订单更新回调 31 | @param position_update_callback 持仓更新回调 32 | """ 33 | self._platform = platform 34 | self._strategy = strategy 35 | self._symbol = symbol 36 | 37 | if platform == OKEX: 38 | self._t = OKExTrade(account, strategy, symbol, host, wss, access_key, secret_key, passphrase, 39 | order_update_callback=order_update_callback) 40 | elif platform == OKEX_FUTURE: 41 | self._t = OKExFutureTrade(account, strategy, symbol, host, wss, access_key, secret_key, passphrase, 42 | order_update_callback=order_update_callback, 43 | position_update_callback=position_update_callback) 44 | elif platform == DERIBIT: 45 | self._t = DeribitTrade(account, strategy, symbol, host, wss, access_key, secret_key, 46 | order_update_callback=order_update_callback, 47 | position_update_callback=position_update_callback) 48 | elif platform == BITMEX: 49 | self._t = BitmexTrade(kwargs["account"], strategy, symbol, kwargs["host"], kwargs["wss"], 50 | kwargs["access_key"], kwargs["secret_key"], 51 | order_update_callback=order_update_callback, 52 | position_update_callback=position_update_callback) 53 | elif platform == BINANCE: 54 | self._t = BinanceTrade(account, strategy, symbol, host, wss, access_key, secret_key, 55 | order_update_callback=order_update_callback) 56 | else: 57 | logger.error("platform error:", platform, caller=self) 58 | exit(-1) 59 | 60 | @property 61 | def position(self): 62 | return self._t.position 63 | 64 | @property 65 | def orders(self): 66 | return self._t.orders 67 | 68 | async def create_order(self, action, price, quantity, order_type=ORDER_TYPE_LIMIT): 69 | """ 创建委托单 70 | @param action 交易方向 BUY/SELL 71 | @param price 委托价格 72 | @param quantity 委托数量(当为负数时,代表合约操作空单) 73 | @param order_type 委托类型 LIMIT/MARKET 74 | @return (order_no, error) 如果成功,order_no为委托单号,error为None,否则order_no为None,error为失败信息 75 | """ 76 | order_no, error = await self._t.create_order(action, price, quantity, order_type) 77 | return order_no, error 78 | 79 | async def revoke_order(self, *order_nos): 80 | """ 撤销委托单 81 | @param order_nos 订单号列表,可传入任意多个,如果不传入,那么就撤销所有订单 82 | @return (success, error) success为撤单成功列表,error为撤单失败的列表 83 | """ 84 | success, error = await self._t.revoke_order(*order_nos) 85 | return success, error 86 | 87 | async def get_open_order_nos(self): 88 | """ 获取未完成委托单id列表 89 | @return (result, error) result为成功获取的未成交订单列表,error如果成功为None,如果不成功为错误信息 90 | """ 91 | result, error = await self._t.get_open_order_nos() 92 | return result, error 93 | -------------------------------------------------------------------------------- /quant/utils/logger.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | 日志打印 5 | 6 | Author: HuangTao 7 | Date: 2018/04/08 8 | Update: 2018/07/16 1. 初始化日志增加参数 clear 和 backup_count; 9 | 2018/07/19 1. 修复日志初始化的时候,clear设置为Ture,但文件不存在的异常; 10 | """ 11 | 12 | import os 13 | import sys 14 | import shutil 15 | import logging 16 | import traceback 17 | from logging.handlers import TimedRotatingFileHandler 18 | 19 | initialized = False 20 | 21 | 22 | def initLogger(log_level="DEBUG", log_path=None, logfile_name=None, clear=False, backup_count=0): 23 | """ 初始化日志输出 24 | @param log_level 日志级别 DEBUG/INFO 25 | @param log_path 日志输出路径 26 | @param logfile_name 日志文件名 27 | @param clear 初始化的时候,是否清理之前的日志文件 28 | @param backup_count 保存按天分割的日志文件个数,默认0为永久保存所有日志文件 29 | """ 30 | global initialized 31 | if initialized: 32 | return 33 | logger = logging.getLogger() 34 | logger.setLevel(log_level) 35 | if logfile_name: 36 | if clear and os.path.isdir(log_path): 37 | shutil.rmtree(log_path) 38 | if not os.path.isdir(log_path): 39 | os.makedirs(log_path) 40 | logfile = os.path.join(log_path, logfile_name) 41 | handler = TimedRotatingFileHandler(logfile, "midnight", backupCount=backup_count) 42 | print("init logger ...", logfile) 43 | else: 44 | print("init logger ...") 45 | handler = logging.StreamHandler() 46 | fmt_str = "%(levelname)1.1s [%(asctime)s] %(message)s" 47 | fmt = logging.Formatter(fmt=fmt_str, datefmt=None) 48 | handler.setFormatter(fmt) 49 | logger.addHandler(handler) 50 | initialized = True 51 | 52 | 53 | def info(*args, **kwargs): 54 | func_name, kwargs = _log_msg_header(*args, **kwargs) 55 | logging.info(_log(func_name, *args, **kwargs)) 56 | 57 | 58 | def warn(*args, **kwargs): 59 | msg_header, kwargs = _log_msg_header(*args, **kwargs) 60 | logging.warning(_log(msg_header, *args, **kwargs)) 61 | 62 | 63 | def debug(*args, **kwargs): 64 | msg_header, kwargs = _log_msg_header(*args, **kwargs) 65 | logging.debug(_log(msg_header, *args, **kwargs)) 66 | 67 | 68 | def error(*args, **kwargs): 69 | logging.error("*" * 60) 70 | msg_header, kwargs = _log_msg_header(*args, **kwargs) 71 | logging.error(_log(msg_header, *args, **kwargs)) 72 | logging.error("*" * 60) 73 | 74 | 75 | def exception(*args, **kwargs): 76 | logging.error("*" * 60) 77 | msg_header, kwargs = _log_msg_header(*args, **kwargs) 78 | logging.error(_log(msg_header, *args, **kwargs)) 79 | traceback.print_stack() 80 | logging.error("*" * 60) 81 | 82 | 83 | def _log(msg_header, *args, **kwargs): 84 | _log_msg = msg_header 85 | for l in args: 86 | if type(l) == tuple: 87 | ps = str(l) 88 | else: 89 | try: 90 | ps = "%r" % l 91 | except: 92 | ps = str(l) 93 | if type(l) == str: 94 | _log_msg += ps[1:-1] + " " 95 | else: 96 | _log_msg += ps + " " 97 | if len(kwargs) > 0: 98 | _log_msg += str(kwargs) 99 | return _log_msg 100 | 101 | 102 | def _log_msg_header(*args, **kwargs): 103 | """ 打印日志的message头 104 | @param kwargs["caller"] 调用的方法所属类对象 105 | * NOTE: logger.xxx(... caller=self) for instance method 106 | logger.xxx(... caller=cls) for @classmethod 107 | """ 108 | cls_name = "" 109 | func_name = sys._getframe().f_back.f_back.f_code.co_name 110 | session_id = "-" 111 | try: 112 | _caller = kwargs.get("caller", None) 113 | if _caller: 114 | if not hasattr(_caller, "__name__"): 115 | _caller = _caller.__class__ 116 | cls_name = _caller.__name__ 117 | del kwargs["caller"] 118 | except: 119 | pass 120 | finally: 121 | msg_header = "[{session_id}] [{cls_name}.{func_name}] ".format(cls_name=cls_name, func_name=func_name, 122 | session_id=session_id) 123 | return msg_header, kwargs 124 | -------------------------------------------------------------------------------- /quant/market.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | 行情数据订阅模块 5 | 6 | Author: HuangTao 7 | Date: 2019/02/16 8 | """ 9 | 10 | import json 11 | 12 | from quant import const 13 | 14 | 15 | class Orderbook: 16 | """ 订单薄 17 | """ 18 | 19 | def __init__(self, platform=None, symbol=None, asks=None, bids=None, timestamp=None): 20 | """ 初始化 21 | @param platform 交易平台 22 | @param symbol 交易对 23 | @param asks 买盘数据 [[price, quantity], [...], ...] 24 | @param bids 卖盘数据 [[price, quantity], [...], ...] 25 | @param timestamp 时间戳(毫秒) 26 | """ 27 | self.platform = platform 28 | self.symbol = symbol 29 | self.asks = asks 30 | self.bids = bids 31 | self.timestamp = timestamp 32 | 33 | @property 34 | def data(self): 35 | d = { 36 | "platform": self.platform, 37 | "symbol": self.symbol, 38 | "asks": self.asks, 39 | "bids": self.bids, 40 | "timestamp": self.timestamp 41 | } 42 | return d 43 | 44 | def __str__(self): 45 | info = json.dumps(self.data) 46 | return info 47 | 48 | def __repr__(self): 49 | return str(self) 50 | 51 | 52 | class Trade: 53 | """ 交易数据 54 | """ 55 | 56 | def __init__(self, platform=None, symbol=None, action=None, price=None, quantity=None, timestamp=None): 57 | """ 初始化 58 | @param platform 交易平台 59 | @param symbol 交易对 60 | @param action 操作 BUY / SELL 61 | @param price 价格 62 | @param quantity 数量 63 | @param timestamp 时间戳(毫秒) 64 | """ 65 | self.platform = platform 66 | self.symbol = symbol 67 | self.action = action 68 | self.price = price 69 | self.quantity = quantity 70 | self.timestamp = timestamp 71 | 72 | @property 73 | def data(self): 74 | d = { 75 | "platform": self.platform, 76 | "symbol": self.symbol, 77 | "action": self.action, 78 | "price": self.price, 79 | "quantity": self.quantity, 80 | "timestamp": self.timestamp 81 | } 82 | return d 83 | 84 | def __str__(self): 85 | info = json.dumps(self.data) 86 | return info 87 | 88 | def __repr__(self): 89 | return str(self) 90 | 91 | 92 | class Kline: 93 | """ K线 1分钟 94 | """ 95 | 96 | def __init__(self, platform=None, symbol=None, open=None, high=None, low=None, close=None, volume=None, 97 | timestamp=None): 98 | """ 初始化 99 | @param platform 平台 100 | @param symbol 交易对 101 | @param open 开盘价 102 | @param high 最高价 103 | @param low 最低价 104 | @param close 收盘价 105 | @param volume 成交量 106 | @param timestamp 时间戳(毫秒) 107 | """ 108 | self.platform = platform 109 | self.symbol = symbol 110 | self.open = open 111 | self.high = high 112 | self.low = low 113 | self.close = close 114 | self.volume = volume 115 | self.timestamp = timestamp 116 | 117 | @property 118 | def data(self): 119 | d = { 120 | "platform": self.platform, 121 | "symbol": self.symbol, 122 | "open": self.open, 123 | "high": self.high, 124 | "low": self.low, 125 | "close": self.close, 126 | "volume": self.volume, 127 | "timestamp": self.timestamp 128 | } 129 | return d 130 | 131 | def __str__(self): 132 | info = json.dumps(self.data) 133 | return info 134 | 135 | def __repr__(self): 136 | return str(self) 137 | 138 | 139 | class Market: 140 | """ 行情订阅模块 141 | """ 142 | 143 | def __init__(self, market_type, platform, symbol, callback): 144 | """ 初始化 145 | @param market_type 行情类型 146 | @param platform 交易平台 147 | @param symbol 交易对 148 | @param callback 更新回调函数 149 | """ 150 | if market_type == const.MARKET_TYPE_ORDERBOOK: 151 | from quant.event import EventOrderbook 152 | EventOrderbook(platform, symbol).subscribe(callback) 153 | elif market_type == const.MARKET_TYPE_TRADE: 154 | from quant.event import EventTrade 155 | EventTrade(platform, symbol).subscribe(callback) 156 | elif market_type == const.MARKET_TYPE_KLINE: 157 | from quant.event import EventKline 158 | EventKline(platform, symbol).subscribe(callback) 159 | -------------------------------------------------------------------------------- /quant/utils/websocket.py: -------------------------------------------------------------------------------- 1 | # -*— coding:utf-8 -*- 2 | 3 | """ 4 | websocket接口封装 5 | 6 | Author: HuangTao 7 | Date: 2018/06/29 8 | """ 9 | 10 | import json 11 | import aiohttp 12 | import asyncio 13 | 14 | from quant.utils import logger 15 | from quant.config import config 16 | from quant.heartbeat import heartbeat 17 | 18 | 19 | class Websocket: 20 | """ websocket接口封装 21 | """ 22 | 23 | def __init__(self, url, check_conn_interval=10, send_hb_interval=10): 24 | """ 初始化 25 | @param url 建立websocket的地址 26 | @param check_conn_interval 检查websocket连接时间间隔 27 | @param send_hb_interval 发送心跳时间间隔 28 | """ 29 | self._url = url 30 | self._ws = None # websocket连接对象 31 | self._check_conn_interval = check_conn_interval 32 | self._send_hb_interval = send_hb_interval 33 | self.heartbeat_msg = None # 心跳消息 34 | 35 | def initialize(self): 36 | """ 初始化 37 | """ 38 | # 注册服务 检查连接是否正常 39 | heartbeat.register(self._check_connection, self._check_conn_interval) 40 | # 注册服务 发送心跳 41 | heartbeat.register(self._send_heartbeat_msg, self._send_hb_interval) 42 | # 建立websocket连接 43 | asyncio.get_event_loop().create_task(self._connect()) 44 | 45 | async def _connect(self): 46 | logger.info("url:", self._url, caller=self) 47 | proxy = config.proxy 48 | session = aiohttp.ClientSession() 49 | try: 50 | self.ws = await session.ws_connect(self._url, proxy=proxy) 51 | except aiohttp.client_exceptions.ClientConnectorError: 52 | logger.error("connect to server error! url:", self._url, caller=self) 53 | return 54 | asyncio.get_event_loop().create_task(self.connected_callback()) 55 | asyncio.get_event_loop().create_task(self.receive()) 56 | 57 | async def _reconnect(self): 58 | """ 重新建立websocket连接 59 | """ 60 | logger.warn("reconnecting websocket right now!", caller=self) 61 | await self._connect() 62 | 63 | async def connected_callback(self): 64 | """ 连接建立成功的回调函数 65 | * NOTE: 子类继承实现 66 | """ 67 | pass 68 | 69 | async def receive(self): 70 | """ 接收消息 71 | """ 72 | async for msg in self.ws: 73 | if msg.type == aiohttp.WSMsgType.TEXT: 74 | try: 75 | data = json.loads(msg.data) 76 | except: 77 | data = msg.data 78 | await asyncio.get_event_loop().create_task(self.process(data)) 79 | elif msg.type == aiohttp.WSMsgType.BINARY: 80 | await asyncio.get_event_loop().create_task(self.process_binary(msg.data)) 81 | elif msg.type == aiohttp.WSMsgType.CLOSED: 82 | logger.warn("receive event CLOSED:", msg, caller=self) 83 | await asyncio.get_event_loop().create_task(self._reconnect()) 84 | return 85 | elif msg.type == aiohttp.WSMsgType.ERROR: 86 | logger.error("receive event ERROR:", msg, caller=self) 87 | else: 88 | logger.warn("unhandled msg:", msg, caller=self) 89 | 90 | async def process(self, msg): 91 | """ 处理websocket上接收到的消息 text 类型 92 | * NOTE: 子类继承实现 93 | """ 94 | raise NotImplementedError 95 | 96 | async def process_binary(self, msg): 97 | """ 处理websocket上接收到的消息 binary类型 98 | * NOTE: 子类继承实现 99 | """ 100 | raise NotImplementedError 101 | 102 | async def _check_connection(self, *args, **kwargs): 103 | """ 检查连接是否正常 104 | """ 105 | # 检查websocket连接是否关闭,如果关闭,那么立即重连 106 | if not self.ws: 107 | logger.warn("websocket connection not connected yet!", caller=self) 108 | return 109 | if self.ws.closed: 110 | await asyncio.get_event_loop().create_task(self._reconnect()) 111 | return 112 | 113 | async def _send_heartbeat_msg(self, *args, **kwargs): 114 | """ 发送心跳给服务器 115 | """ 116 | if self.heartbeat_msg: 117 | if isinstance(self.heartbeat_msg, dict): 118 | await self.ws.send_json(self.heartbeat_msg) 119 | elif isinstance(self.heartbeat_msg, str): 120 | await self.ws.send_str(self.heartbeat_msg) 121 | else: 122 | logger.error("send heartbeat msg failed! heartbeat msg:", self.heartbeat_msg, caller=self) 123 | return 124 | logger.debug("send ping message:", self.heartbeat_msg, caller=self) 125 | -------------------------------------------------------------------------------- /quant/utils/http_client.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | aiohttp client接口封装 5 | 6 | Author: HuangTao 7 | Date: 2018/05/03 8 | """ 9 | 10 | import aiohttp 11 | from urllib.parse import urlparse 12 | 13 | from quant.utils import logger 14 | from quant.config import config 15 | 16 | 17 | class AsyncHttpRequests(object): 18 | """ HTTP异步请求封装 19 | """ 20 | 21 | _SESSIONS = {} # 每个域名保持一个公用的session连接(每个session持有自己的连接池),这样可以节省资源、加快请求速度 22 | 23 | @classmethod 24 | async def fetch(cls, method, url, params=None, body=None, data=None, headers=None, timeout=30, **kwargs): 25 | """ 发起HTTP请求 26 | @param method 请求方法 GET/POST/PUT/DELETE 27 | @param url 请求的url 28 | @param params 请求的uri参数 29 | @param body 请求的body参数 30 | @param data json格式的数据 31 | @param headers 请求的headers 32 | @param timeout 超时时间(秒) 33 | @return (code, success, error) 如果成功,error为None,失败success为None,error为失败信息 34 | """ 35 | session = cls._get_session(url) 36 | if not kwargs.get("proxy"): 37 | kwargs["proxy"] = config.proxy # HTTP代理配置 38 | try: 39 | if method == "GET": 40 | response = await session.get(url, params=params, headers=headers, timeout=timeout, **kwargs) 41 | elif method == "POST": 42 | response = await session.post(url, params=params, data=body, json=data, headers=headers, 43 | timeout=timeout, **kwargs) 44 | elif method == "PUT": 45 | response = await session.put(url, params=params, data=body, json=data, headers=headers, 46 | timeout=timeout, **kwargs) 47 | elif method == "DELETE": 48 | response = await session.delete(url, params=params, data=body, json=data, headers=headers, 49 | timeout=timeout, **kwargs) 50 | else: 51 | error = "http method error!" 52 | return None, None, error 53 | except Exception as e: 54 | logger.error("method:", method, "url:", url, "params:", params, "body:", body, "data:", data, "Error:", e, 55 | caller=cls) 56 | return None, None, e 57 | code = response.status 58 | if code not in (200, 201, 202, 203, 204, 205, 206): 59 | text = await response.text() 60 | logger.error("method:", method, "url:", url, "params:", params, "body:", body, "headers:", headers, 61 | "code:", code, "result:", text, caller=cls) 62 | return code, None, text 63 | try: 64 | result = await response.json() 65 | except: 66 | logger.warn("response data is not json format!", "method:", method, "url:", url, "params:", params, 67 | caller=cls) 68 | result = await response.text() 69 | logger.debug("method:", method, "url:", url, "params:", params, "body:", body, "data:", data, 70 | "code:", code, "result:", result, caller=cls) 71 | return code, result, None 72 | 73 | @classmethod 74 | async def get(cls, url, params=None, body=None, data=None, headers=None, timeout=30, **kwargs): 75 | """ HTTP GET 请求 76 | """ 77 | result = await cls.fetch("GET", url, params, body, data, headers, timeout, **kwargs) 78 | return result 79 | 80 | @classmethod 81 | async def post(cls, url, params=None, body=None, data=None, headers=None, timeout=30, **kwargs): 82 | """ HTTP POST 请求 83 | """ 84 | result = await cls.fetch("POST", url, params, body, data, headers, timeout, **kwargs) 85 | return result 86 | 87 | @classmethod 88 | async def delete(cls, url, params=None, body=None, data=None, headers=None, timeout=30, **kwargs): 89 | """ HTTP DELETE 请求 90 | """ 91 | result = await cls.fetch("DELETE", url, params, body, data, headers, timeout, **kwargs) 92 | return result 93 | 94 | @classmethod 95 | async def put(cls, url, params=None, body=None, data=None, headers=None, timeout=30, **kwargs): 96 | """ HTTP PUT 请求 97 | """ 98 | result = await cls.fetch("PUT", url, params, body, data, headers, timeout, **kwargs) 99 | return result 100 | 101 | @classmethod 102 | def _get_session(cls, url): 103 | """ 获取url对应的session连接 104 | """ 105 | parsed_url = urlparse(url) 106 | key = parsed_url.netloc or parsed_url.hostname 107 | if key not in cls._SESSIONS: 108 | session = aiohttp.ClientSession() 109 | cls._SESSIONS[key] = session 110 | return cls._SESSIONS[key] 111 | -------------------------------------------------------------------------------- /quant/utils/tools.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | 工具包 5 | 6 | Author: HuangTao 7 | Date: 2018/04/28 8 | Update: 2018/09/07 1. 增加函数datetime_to_timestamp; 9 | """ 10 | 11 | import uuid 12 | import time 13 | import decimal 14 | import datetime 15 | 16 | 17 | def get_cur_timestamp(): 18 | """ 获取当前时间戳 19 | """ 20 | ts = int(time.time()) 21 | return ts 22 | 23 | 24 | def get_cur_timestamp_ms(): 25 | """ 获取当前时间戳(毫秒) 26 | """ 27 | ts = int(time.time() * 1000) 28 | return ts 29 | 30 | 31 | def get_cur_datetime_m(fmt='%Y%m%d%H%M%S%f'): 32 | """ 获取当前日期时间字符串,包含 年 + 月 + 日 + 时 + 分 + 秒 + 微妙 33 | """ 34 | today = datetime.datetime.today() 35 | str_m = today.strftime(fmt) 36 | return str_m 37 | 38 | 39 | def get_datetime(fmt='%Y%m%d%H%M%S'): 40 | """ 获取日期时间字符串,包含 年 + 月 + 日 + 时 + 分 + 秒 41 | """ 42 | today = datetime.datetime.today() 43 | str_dt = today.strftime(fmt) 44 | return str_dt 45 | 46 | 47 | def get_date(fmt='%Y%m%d', delta_day=0): 48 | """ 获取日期字符串,包含 年 + 月 + 日 49 | @param fmt 返回的日期格式 50 | """ 51 | day = datetime.datetime.today() 52 | if delta_day: 53 | day += datetime.timedelta(days=delta_day) 54 | str_d = day.strftime(fmt) 55 | return str_d 56 | 57 | 58 | def date_str_to_dt(date_str=None, fmt='%Y%m%d', delta_day=0): 59 | """ 日期字符串转换到datetime对象 60 | @param date_str 日期字符串 61 | @param fmt 日期字符串格式 62 | @param delta_day 相对天数,<0减相对天数,>0加相对天数 63 | """ 64 | if not date_str: 65 | dt = datetime.datetime.today() 66 | else: 67 | dt = datetime.datetime.strptime(date_str, fmt) 68 | if delta_day: 69 | dt += datetime.timedelta(days=delta_day) 70 | return dt 71 | 72 | 73 | def dt_to_date_str(dt=None, fmt='%Y%m%d', delta_day=0): 74 | """ datetime对象转换到日期字符串 75 | @param dt datetime对象 76 | @param fmt 返回的日期字符串格式 77 | @param delta_day 相对天数,<0减相对天数,>0加相对天数 78 | """ 79 | if not dt: 80 | dt = datetime.datetime.today() 81 | if delta_day: 82 | dt += datetime.timedelta(days=delta_day) 83 | str_d = dt.strftime(fmt) 84 | return str_d 85 | 86 | 87 | def get_utc_time(): 88 | """ 获取当前utc时间 89 | """ 90 | utc_t = datetime.datetime.utcnow() 91 | return utc_t 92 | 93 | 94 | def ts_to_datetime_str(ts=None, fmt='%Y-%m-%d %H:%M:%S'): 95 | """ 将时间戳转换为日期时间格式,年-月-日 时:分:秒 96 | @param ts 时间戳,默认None即为当前时间戳 97 | @param fmt 返回的日期字符串格式 98 | """ 99 | if not ts: 100 | ts = get_cur_timestamp() 101 | dt = datetime.datetime.fromtimestamp(int(ts)) 102 | return dt.strftime(fmt) 103 | 104 | 105 | def datetime_str_to_ts(dt_str, fmt='%Y-%m-%d %H:%M:%S'): 106 | """ 将日期时间格式字符串转换成时间戳 107 | @param dt_str 日期时间字符串 108 | @param fmt 日期时间字符串格式 109 | """ 110 | ts = int(time.mktime(datetime.datetime.strptime(dt_str, fmt).timetuple())) 111 | return ts 112 | 113 | 114 | def datetime_to_timestamp(dt=None, tzinfo=None): 115 | """ 将datetime对象转换成时间戳 116 | @param dt datetime对象,如果为None,默认使用当前UTC时间 117 | @param tzinfo 时区对象,如果为None,默认使用timezone.utc 118 | @return ts 时间戳(秒) 119 | """ 120 | if not dt: 121 | dt = get_utc_time() 122 | if not tzinfo: 123 | tzinfo = datetime.timezone.utc 124 | ts = int(dt.replace(tzinfo=tzinfo).timestamp()) 125 | return ts 126 | 127 | 128 | def utctime_str_to_ts(utctime_str, fmt="%Y-%m-%dT%H:%M:%S.%fZ"): 129 | """ 将UTC日期时间格式字符串转换成时间戳 130 | @param utctime_str 日期时间字符串 eg: 2019-03-04T09:14:27.806Z 131 | @param fmt 日期时间字符串格式 132 | @return timestamp 时间戳(秒) 133 | """ 134 | dt = datetime.datetime.strptime(utctime_str, fmt) 135 | timestamp = int(dt.replace(tzinfo=datetime.timezone.utc).astimezone(tz=None).timestamp()) 136 | return timestamp 137 | 138 | 139 | def utctime_str_to_mts(utctime_str, fmt="%Y-%m-%dT%H:%M:%S.%fZ"): 140 | """ 将UTC日期时间格式字符串转换成时间戳(毫秒) 141 | @param utctime_str 日期时间字符串 eg: 2019-03-04T09:14:27.806Z 142 | @param fmt 日期时间字符串格式 143 | @return timestamp 时间戳(毫秒) 144 | """ 145 | dt = datetime.datetime.strptime(utctime_str, fmt) 146 | timestamp = int(dt.replace(tzinfo=datetime.timezone.utc).astimezone(tz=None).timestamp() * 1000) 147 | return timestamp 148 | 149 | 150 | def get_uuid1(): 151 | """ make a UUID based on the host ID and current time 152 | """ 153 | s = uuid.uuid1() 154 | return str(s) 155 | 156 | 157 | def get_uuid3(str_in): 158 | """ make a UUID using an MD5 hash of a namespace UUID and a name 159 | @param str_in 输入字符串 160 | """ 161 | s = uuid.uuid3(uuid.NAMESPACE_DNS, str_in) 162 | return str(s) 163 | 164 | 165 | def get_uuid4(): 166 | """ make a random UUID 167 | """ 168 | s = uuid.uuid4() 169 | return str(s) 170 | 171 | 172 | def get_uuid5(str_in): 173 | """ make a UUID using a SHA-1 hash of a namespace UUID and a name 174 | @param str_in 输入字符串 175 | """ 176 | s = uuid.uuid5(uuid.NAMESPACE_DNS, str_in) 177 | return str(s) 178 | 179 | 180 | def float_to_str(f, p=20): 181 | """ Convert the given float to a string, without resorting to scientific notation. 182 | @param f 浮点数参数 183 | @param p 精读 184 | """ 185 | if type(f) == str: 186 | f = float(f) 187 | ctx = decimal.Context(p) 188 | d1 = ctx.create_decimal(repr(f)) 189 | return format(d1, 'f') 190 | -------------------------------------------------------------------------------- /quant/utils/mongo.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | mongodb async操作接口 5 | 6 | Author: HuangTao 7 | Date: 2018/04/28 8 | Update: 2018/12/11 1. 取消初始化使用类变量 DB 和 COLLECTION,直接在 self.__init__ 函数传入 db 和 collection; 9 | 2. 修改名称 self.conn 到 self._conn; 10 | 3. 修改名称 self.cursor 到 self._cursor; 11 | """ 12 | 13 | import copy 14 | 15 | import motor.motor_asyncio 16 | from bson.objectid import ObjectId 17 | from urllib.parse import quote_plus 18 | 19 | from quant.utils import tools 20 | from quant.utils import logger 21 | 22 | 23 | __all__ = ("initMongodb", "MongoDBBase", ) 24 | 25 | 26 | MONGO_CONN = None 27 | DELETE_FLAG = "delete" # True 已经删除,False 或者没有该字段表示没有删除 28 | 29 | 30 | def initMongodb(host="127.0.0.1", port=27017, username="", password="", dbname="admin"): 31 | """ 初始化mongodb连接 32 | """ 33 | if username and password: 34 | uri = "mongodb://{username}:{password}@{host}:{port}/{dbname}".format(username=quote_plus(username), 35 | password=quote_plus(password), 36 | host=quote_plus(host), 37 | port=port, 38 | dbname=dbname) 39 | else: 40 | uri = "mongodb://{host}:{port}/{dbname}".format(host=host, port=port, dbname=dbname) 41 | mongo_client = motor.motor_asyncio.AsyncIOMotorClient(uri) 42 | global MONGO_CONN 43 | MONGO_CONN = mongo_client 44 | logger.info("create mongodb connection pool.") 45 | 46 | 47 | class MongoDBBase(object): 48 | """ mongodb 数据库操作接口 49 | """ 50 | 51 | def __init__(self, db, collection): 52 | """ 初始化 53 | @param db 数据库 54 | @param collection 表 55 | """ 56 | self._db = db 57 | self._collection = collection 58 | self._conn = MONGO_CONN 59 | self._cursor = self._conn[db][collection] 60 | 61 | async def get_list(self, spec={}, fields=None, sort=[], skip=0, limit=9999, cursor=None): 62 | """ 批量获取数据 63 | @param spec 查询条件 64 | @param fields 返回数据的字段 65 | @param sort 排序规则 66 | @param skip 查询起点 67 | @param limit 返回数据条数 68 | @param cursor 查询游标,如不指定默认使用self._cursor 69 | * NOTE: 必须传入limit,否则默认返回数据条数可能因为pymongo的默认值而改变 70 | """ 71 | if not cursor: 72 | cursor = self._cursor 73 | if "_id" in spec: 74 | spec["_id"] = self._convert_id_object(spec["_id"]) 75 | spec[DELETE_FLAG] = {"$ne": True} 76 | datas = [] 77 | result = cursor.find(spec, fields, sort=sort, skip=skip, limit=limit) 78 | async for item in result: 79 | item["_id"] = str(item["_id"]) 80 | datas.append(item) 81 | return datas 82 | 83 | async def find_one(self, spec={}, fields=None, sort=[], cursor=None): 84 | """ 查找单条数据 85 | @param spec 查询条件 86 | @param fields 返回数据的字段 87 | @param sort 排序规则 88 | @param cursor 查询游标,如不指定默认使用self._cursor 89 | """ 90 | if not cursor: 91 | cursor = self._cursor 92 | data = await self.get_list(spec, fields, sort, limit=1, cursor=cursor) 93 | if data: 94 | return data[0] 95 | else: 96 | return None 97 | 98 | async def count(self, spec={}, cursor=None): 99 | """ 计算数据条数 100 | @param spec 查询条件 101 | @param n 返回查询的条数 102 | @param cursor 查询游标,如不指定默认使用self._cursor 103 | """ 104 | if not cursor: 105 | cursor = self._cursor 106 | spec[DELETE_FLAG] = {"$ne": True} 107 | n = await cursor.count(spec) 108 | return n 109 | 110 | async def insert(self, docs_data, cursor=None): 111 | """ 插入数据 112 | @param docs_data 插入数据 dict或list 113 | @param ret_ids 插入数据的id列表 114 | @param cursor 查询游标,如不指定默认使用self._cursor 115 | """ 116 | if not cursor: 117 | cursor = self._cursor 118 | docs = copy.deepcopy(docs_data) 119 | ret_ids = [] 120 | is_one = False 121 | create_time = tools.get_cur_timestamp() 122 | if not isinstance(docs, list): 123 | docs = [docs] 124 | is_one = True 125 | for doc in docs: 126 | doc["_id"] = ObjectId() 127 | doc["create_time"] = create_time 128 | doc["update_time"] = create_time 129 | ret_ids.append(str(doc["_id"])) 130 | cursor.insert_many(docs) 131 | if is_one: 132 | return ret_ids[0] 133 | else: 134 | return ret_ids 135 | 136 | async def update(self, spec, update_fields, upsert=False, multi=False, cursor=None): 137 | """ 更新 138 | @param spec 更新条件 139 | @param update_fields 更新字段 140 | @param upsert 如果不满足条件,是否插入新数据 141 | @param multi 是否批量更新 142 | @return modified_count 更新数据条数 143 | @param cursor 查询游标,如不指定默认使用self._cursor 144 | """ 145 | if not cursor: 146 | cursor = self._cursor 147 | update_fields = copy.deepcopy(update_fields) 148 | spec[DELETE_FLAG] = {"$ne": True} 149 | if "_id" in spec: 150 | spec["_id"] = self._convert_id_object(spec["_id"]) 151 | set_fields = update_fields.get("$set", {}) 152 | set_fields["update_time"] = tools.get_cur_timestamp() 153 | update_fields["$set"] = set_fields 154 | if not multi: 155 | result = await cursor.update_one(spec, update_fields, upsert=upsert) 156 | return result.modified_count 157 | else: 158 | result = await cursor.update_many(spec, update_fields, upsert=upsert) 159 | return result.modified_count 160 | 161 | async def delete(self, spec, cursor=None): 162 | """ 软删除 163 | @param spec 删除条件 164 | @return delete_count 删除数据的条数 165 | @param cursor 查询游标,如不指定默认使用self._cursor 166 | """ 167 | if not cursor: 168 | cursor = self._cursor 169 | spec[DELETE_FLAG] = {"$ne": True} 170 | if "_id" in spec: 171 | spec["_id"] = self._convert_id_object(spec["_id"]) 172 | update_fields = {"$set": {DELETE_FLAG: True}} 173 | delete_count = await self.update(spec, update_fields, multi=True, cursor=cursor) 174 | return delete_count 175 | 176 | async def remove(self, spec, multi=False, cursor=None): 177 | """ 彻底删除数据 178 | @param spec 删除条件 179 | @param multi 是否全部删除 180 | @return deleted_count 删除数据的条数 181 | @param cursor 查询游标,如不指定默认使用self._cursor 182 | """ 183 | if not cursor: 184 | cursor = self._cursor 185 | if not multi: 186 | result = await cursor.delete_one(spec) 187 | return result.deleted_count 188 | else: 189 | result = await cursor.delete_many(spec) 190 | return result.deleted_count 191 | 192 | async def distinct(self, key, spec={}, cursor=None): 193 | """ distinct查询 194 | @param key 查询的key 195 | @param spec 查询条件 196 | @return result 过滤结果list 197 | @param cursor 查询游标,如不指定默认使用self._cursor 198 | """ 199 | if not cursor: 200 | cursor = self._cursor 201 | spec[DELETE_FLAG] = {"$ne": True} 202 | if "_id" in spec: 203 | spec["_id"] = self._convert_id_object(spec["_id"]) 204 | result = await cursor.distinct(key, spec) 205 | return result 206 | 207 | async def find_one_and_update(self, spec, update_fields, upsert=False, return_document=False, fields=None, cursor=None): 208 | """ 查询一条指定数据,并修改这条数据 209 | @param spec 查询条件 210 | @param update_fields 更新字段 211 | @param upsert 如果不满足条件,是否插入新数据,默认False 212 | @param return_document 返回修改之前数据或修改之后数据,默认False为修改之前数据 213 | @param fields 需要返回的字段,默认None为返回全部数据 214 | @return result 修改之前或之后的数据 215 | @param cursor 查询游标,如不指定默认使用self._cursor 216 | """ 217 | if not cursor: 218 | cursor = self._cursor 219 | spec[DELETE_FLAG] = {"$ne": True} 220 | if "_id" in spec: 221 | spec["_id"] = self._convert_id_object(spec["_id"]) 222 | set_fields = update_fields.get("$set", {}) 223 | set_fields["update_time"] = tools.get_cur_timestamp() 224 | update_fields["$set"] = set_fields 225 | result = await cursor.find_one_and_update(spec, update_fields, projection=fields, upsert=upsert, 226 | return_document=return_document) 227 | if result and "_id" in result: 228 | result["_id"] = str(result["_id"]) 229 | return result 230 | 231 | async def find_one_and_delete(self, spec={}, fields=None, cursor=None): 232 | """ 查询一条指定数据,并删除这条数据 233 | @param spec 删除条件 234 | @param fields 需要返回的字段,默认None为返回全部数据 235 | @param result 删除之前的数据 236 | @param cursor 查询游标,如不指定默认使用self._cursor 237 | """ 238 | if not cursor: 239 | cursor = self._cursor 240 | spec[DELETE_FLAG] = {"$ne": True} 241 | if "_id" in spec: 242 | spec["_id"] = self._convert_id_object(spec["_id"]) 243 | result = await cursor.find_one_and_delete(spec, projection=fields) 244 | if result and "_id" in result: 245 | result["_id"] = str(result["_id"]) 246 | return result 247 | 248 | def _convert_id_object(self, origin): 249 | """ 将字符串的_id转换成ObjectId类型 250 | """ 251 | if isinstance(origin, str): 252 | return ObjectId(origin) 253 | elif isinstance(origin, (list, set)): 254 | return [ObjectId(item) for item in origin] 255 | elif isinstance(origin, dict): 256 | for key, value in origin.items(): 257 | origin[key] = self._convert_id_object(value) 258 | return origin 259 | -------------------------------------------------------------------------------- /quant/data.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | 外盘行情数据存储 5 | 6 | Author: HuangTao 7 | Date: 2018/05/17 8 | Update: None 9 | """ 10 | 11 | from quant.utils import tools 12 | from quant.utils.mongo import MongoDBBase 13 | 14 | 15 | class TickerData(MongoDBBase): 16 | """ Ticker行情数据存储 17 | ticker行情数据格式: 18 | { 19 | "a": ask, # 卖一价 20 | "A": ask_quantity, # 卖一量 21 | "b": bid, # 买一价 22 | "B": bid_quantity, # 买一量 23 | "t": timestamp # 时间戳(秒) 24 | } 25 | """ 26 | 27 | def __init__(self, platform): 28 | """ 初始化 29 | @param platform 交易平台 30 | """ 31 | self._db = platform # 将交易平台名称作为数据库名称 32 | self._collection = 'ticker' # 表名 33 | self._platform = platform 34 | self._t_to_c = {} # ticker行情 交易对对应的数据库cursor {"BTC/USDT": "ticker_btc_usdt"} 35 | super(TickerData, self).__init__(self._db, self._collection) 36 | 37 | async def create_new_ticker(self, symbol, ask, ask_quantity, bid, bid_quantity, timestamp): 38 | """ 创建新ticker行情数据 39 | @param symbol 交易对 40 | @param ask 卖一价 41 | @param ask_quantity 卖一量 42 | @param bid 买一价 43 | @param bid_quantity 卖一量 44 | @param timestamp 时间戳 45 | """ 46 | cursor = self._get_ticker_cursor_by_symbol(symbol) 47 | data = { 48 | 'a': ask, 49 | 'A': ask_quantity, 50 | 'b': bid, 51 | 'B': bid_quantity, 52 | 't': timestamp 53 | } 54 | price_id = await self.insert(data, cursor=cursor) 55 | return price_id 56 | 57 | async def get_latest_ticker_by_symbol(self, symbol): 58 | """ 根据交易对,获取ticker行情数据 59 | @param symbol 交易对 60 | """ 61 | cursor = self._get_ticker_cursor_by_symbol(symbol) 62 | sort = [('create_time', -1)] 63 | result = await self.find_one(sort=sort, cursor=cursor) 64 | return result 65 | 66 | def _get_ticker_cursor_by_symbol(self, symbol): 67 | """ collection对应的交易对 68 | @param symbol 交易对 69 | * NOTE: BTC/USDT => bitfinex.price_btc_usdt 70 | """ 71 | cursor = self._t_to_c.get(symbol) 72 | if not cursor: 73 | x, y = symbol.split('/') 74 | collection = 'ticker_{x}_{y}'.format(x=x.lower(), y=y.lower()) 75 | cursor = self._conn[self._db][collection] 76 | self._t_to_c[symbol] = cursor 77 | return cursor 78 | 79 | 80 | class KLineData(MongoDBBase): 81 | """ K线数据存储 82 | K线数据格式: 83 | { 84 | "o": open, # 开盘价 85 | "h": high, # 最高价 86 | "l": low, # 最低价 87 | "c": close, # 收盘价 88 | "a": ask, # 卖一实时价 89 | "b": bid, # 买一实时价 90 | "t": timestamp # 时间戳 91 | } 92 | """ 93 | 94 | def __init__(self, platform): 95 | """ 初始化 96 | @param platform 交易平台 97 | """ 98 | self._db = platform # 将交易平台名称作为数据库名称 99 | self._collection = 'kline' # 表名 100 | self._platform = platform 101 | self._k_to_c = {} # K线 交易对对应的数据库cursor {"BTC/USD": "kline_btc_usd"} 102 | super(KLineData, self).__init__(self._db, self._collection) 103 | 104 | async def create_new_kline(self, symbol, open, high, low, close, ask, bid, timestamp): 105 | """ 创建新K线数据 106 | @param symbol 交易对 107 | @param open 开盘价 108 | @param high 最高价 109 | @param low 最低价 110 | @param close 收盘价 111 | @param ask 卖一价 112 | @param bid 买一价 113 | @param timestamp 时间戳(秒) 114 | """ 115 | cursor = self._get_kline_cursor_by_symbol(symbol) 116 | data = { 117 | 'o': open, 118 | 'h': high, 119 | 'l': low, 120 | 'c': close, 121 | 'a': ask, 122 | 'b': bid, 123 | 't': timestamp 124 | } 125 | kline_id = await self.insert(data, cursor=cursor) 126 | return kline_id 127 | 128 | async def get_kline_at_ts(self, symbol, ts=None): 129 | """ 获取一条指定时间戳的K线数据 130 | @param symbol 交易对 131 | @param ts 时间戳(秒) 如果为空,那么就是当前时间戳 132 | """ 133 | cursor = self._get_kline_cursor_by_symbol(symbol) 134 | if ts: 135 | spec = {'t': {'$lte': ts}} 136 | else: 137 | spec = {} 138 | _sort = [('t', -1)] 139 | result = await self.find_one(spec, sort=_sort, cursor=cursor) 140 | return result 141 | 142 | async def get_latest_kline_by_symbol(self, symbol): 143 | """ 根据交易对,获取K线数据 144 | @param symbol 交易对 145 | """ 146 | cursor = self._get_kline_cursor_by_symbol(symbol) 147 | sort = [('create_time', -1)] 148 | result = await self.find_one(sort=sort, cursor=cursor) 149 | return result 150 | 151 | async def get_kline_between_ts(self, symbol, start_ts, end_ts): 152 | """ 获取一段时间范围内的K线数据 153 | @param symbol 交易对 154 | @param start_ts 开始时间戳(秒) 155 | @param end_ts 结束时间戳(秒) 156 | """ 157 | cursor = self._get_kline_cursor_by_symbol(symbol) 158 | spec = { 159 | 't': { 160 | '$gte': start_ts, 161 | '$lte': end_ts 162 | } 163 | } 164 | fields = { 165 | 'create_time': 0, 166 | 'update_time': 0 167 | } 168 | _sort = [('t', 1)] 169 | datas = await self.get_list(spec, fields=fields, sort=_sort, cursor=cursor) 170 | return datas 171 | 172 | def _get_kline_cursor_by_symbol(self, symbol): 173 | """ collection对应的交易对 174 | @param symbol 交易对 175 | * NOTE: BTC/USDT => bitfinex.kline_btc_usdt 176 | """ 177 | cursor = self._k_to_c.get(symbol) 178 | if not cursor: 179 | x, y = symbol.split('/') 180 | collection = 'kline_{x}_{y}'.format(x=x.lower(), y=y.lower()) 181 | cursor = self._conn[self._db][collection] 182 | self._k_to_c[symbol] = cursor 183 | return cursor 184 | 185 | 186 | class AssetData(MongoDBBase): 187 | """ 资产数据存储 188 | 资产数据结构: 189 | {} 190 | """ 191 | 192 | def __init__(self): 193 | """ 初始化 194 | """ 195 | self._db = 'strategy' # 数据库名 196 | self._collection = 'asset' # 表名 197 | super(AssetData, self).__init__(self._db, self._collection) 198 | 199 | async def create_new_asset(self, platform, account, asset): 200 | """ 创建新的资产信息 201 | @param platform 交易平台 202 | @param account 账户 203 | @param asset 资产详情 204 | """ 205 | d = { 206 | 'platform': platform, 207 | 'account': account 208 | } 209 | for key, value in asset.items(): 210 | d[key] = value 211 | asset_id = await self.insert(d) 212 | return asset_id 213 | 214 | async def update_asset(self, platform, account, asset, delete=None): 215 | """ 更新资产 216 | @param platform 交易平台 217 | @param account string 账户 218 | @param asset dict 资产详情 219 | @param delete list 需要清除的币列表(已经置零的资产) 220 | """ 221 | spec = { 222 | 'platform': platform, 223 | 'account': account 224 | } 225 | update_fields = {'$set': asset} 226 | if delete: 227 | d = {} 228 | for key in delete: 229 | d[key] = 1 230 | update_fields['$unset'] = d 231 | await self.update(spec, update_fields=update_fields, upsert=True) 232 | 233 | async def get_latest_asset(self, platform, account): 234 | """ 查询最新的资产信息 235 | @param platform 交易平台 236 | @param account 账户 237 | """ 238 | spec = { 239 | 'platform': platform, 240 | 'account': account 241 | } 242 | _sort = [('update_time', -1)] 243 | fields = { 244 | 'platform': 0, 245 | 'account': 0, 246 | 'index': 0, 247 | 'create_time': 0, 248 | 'update_time': 0 249 | } 250 | asset = await self.find_one(spec, sort=_sort, fields=fields) 251 | if asset: 252 | del asset['_id'] 253 | return asset 254 | 255 | 256 | class AssetSnapshotData(MongoDBBase): 257 | """ 资产数据快照存储 每隔一个小时,从 strategy.asset 表中,创建一次快照数据 258 | 资产数据结构: 259 | {} 260 | """ 261 | 262 | def __init__(self): 263 | """ 初始化 264 | """ 265 | self._db = 'strategy' # 数据库名 266 | self._collection = 'asset_snapshot' # 表名 267 | super(AssetSnapshotData, self).__init__(self._db, self._collection) 268 | 269 | async def create_new_asset(self, platform, account, asset): 270 | """ 创建新的资产信息 271 | @param platform 交易平台 272 | @param account 账户 273 | @param asset 资产详情 274 | """ 275 | d = { 276 | 'platform': platform, 277 | 'account': account 278 | } 279 | for key, value in asset.items(): 280 | d[key] = value 281 | asset_id = await self.insert(d) 282 | return asset_id 283 | 284 | async def get_asset_snapshot(self, platform, account, start=None, end=None): 285 | """ 获取资产快照 286 | @param platform 交易平台 287 | @param account 账户 288 | @param start 开始时间戳(秒) 289 | @param end 结束时间戳(秒) 290 | """ 291 | if not end: 292 | end = tools.get_cur_timestamp() # 截止时间默认当前时间 293 | if not start: 294 | start = end - 60 * 60 * 24 # 开始时间默认一天前 295 | spec = { 296 | 'platform': platform, 297 | 'account': account, 298 | 'create_time': { 299 | '$gte': start, 300 | '$lte': end 301 | } 302 | } 303 | fields = { 304 | 'platform': 0, 305 | 'account': 0, 306 | 'update_time': 0 307 | } 308 | datas = await self.get_list(spec, fields=fields) 309 | return datas 310 | 311 | async def get_latest_asset_snapshot(self, platform, account): 312 | """ 查询最新的资产快照 313 | @param platform 交易平台 314 | @param account 账户 315 | """ 316 | spec = { 317 | 'platform': platform, 318 | 'account': account 319 | } 320 | _sort = [('update_time', -1)] 321 | asset = await self.find_one(spec, sort=_sort) 322 | if asset: 323 | del asset['_id'] 324 | return asset 325 | 326 | 327 | class OrderData(MongoDBBase): 328 | """ 订单数据存储 329 | """ 330 | 331 | def __init__(self): 332 | """ 初始化 333 | @param db 数据库 334 | @param collection 表 335 | """ 336 | self._db = 'strategy' # 数据库名 337 | self._collection = 'order' # 表名 338 | super(OrderData, self).__init__(self._db, self._collection) 339 | 340 | async def create_new_order(self, order): 341 | """ 创建新订单 342 | @param order 订单对象 343 | """ 344 | data = { 345 | 'platform': order.platform, 346 | "account": order.account, 347 | 'strategy': order.strategy, 348 | 'symbol': order.symbol, 349 | 'order_no': order.order_no, 350 | 'action': order.action, 351 | 'order_type': order.order_type, 352 | 'status': order.status, 353 | 'price': order.price, 354 | 'quantity': order.quantity, 355 | 'remain': order.remain, 356 | 'timestamp': order.timestamp, 357 | } 358 | order_id = await self.insert(data) 359 | return order_id 360 | 361 | async def get_order_by_no(self, platform, order_no): 362 | """ 获取订单最新信息 363 | @param platform 交易平台 364 | @param order_no 订单号 365 | """ 366 | spec = { 367 | 'platform': platform, 368 | 'order_no': order_no 369 | } 370 | data = await self.find_one(spec) 371 | return data 372 | 373 | async def get_order_by_nos(self, platform, order_nos): 374 | """ 批量获取订单状态 375 | @param platform 交易平台 376 | @param order_nos 订单号列表 377 | """ 378 | spec = { 379 | 'platform': platform, 380 | 'order_no': {'$in': order_nos} 381 | } 382 | fields = { 383 | 'status': 1 384 | } 385 | datas = await self.get_list(spec, fields=fields) 386 | return datas 387 | 388 | async def update_order_infos(self, order): 389 | """ 更新订单信息 390 | @param order 订单对象 391 | """ 392 | spec = { 393 | 'platform': order.platform, 394 | 'order_no': order.order_no 395 | } 396 | update_fields = { 397 | 'status': order.status, 398 | 'remain': order.remain 399 | } 400 | await self.update(spec, update_fields={'$set': update_fields}) 401 | 402 | async def get_latest_order(self, platform, symbol): 403 | """ 获取一条最新的订单 404 | @param platform 交易平台 405 | @param symbol 交易对 406 | """ 407 | spec = { 408 | 'platform': platform, 409 | 'symbol': symbol 410 | } 411 | _sort = [('update_time', -1)] 412 | data = await self.find_one(spec, sort=_sort) 413 | return data 414 | -------------------------------------------------------------------------------- /quant/platform/deribit.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | Deribit Trade module 交易模块 5 | https://docs.deribit.com/v2/ 6 | 7 | Author: HuangTao 8 | Date: 2019/04/20 9 | """ 10 | 11 | import json 12 | import copy 13 | import asyncio 14 | 15 | from quant.utils import logger 16 | from quant.const import DERIBIT 17 | from quant.position import Position 18 | from quant.utils.websocket import Websocket 19 | from quant.tasks import LoopRunTask, SingleTask 20 | from quant.utils.decorator import async_method_locker 21 | from quant.order import Order 22 | from quant.order import ORDER_ACTION_BUY, ORDER_ACTION_SELL 23 | from quant.order import ORDER_TYPE_LIMIT, ORDER_TYPE_MARKET 24 | from quant.order import ORDER_STATUS_SUBMITTED, ORDER_STATUS_PARTIAL_FILLED, ORDER_STATUS_FILLED, \ 25 | ORDER_STATUS_CANCELED, ORDER_STATUS_FAILED 26 | from quant.order import TRADE_TYPE_BUY_OPEN, TRADE_TYPE_SELL_OPEN, TRADE_TYPE_SELL_CLOSE, TRADE_TYPE_BUY_CLOSE 27 | 28 | 29 | class DeribitTrade(Websocket): 30 | """ Deribit Trade module 交易模块 31 | """ 32 | 33 | def __init__(self, account, strategy, symbol, host=None, wss=None, access_key=None, secret_key=None, 34 | order_update_callback=None, position_update_callback=None): 35 | """ 初始化 36 | @param account 账户 37 | @param strategy 策略名称 38 | @param symbol 交易对(合约名称) 39 | @param host HTTP请求主机地址 40 | @param wss websocket连接地址 41 | @param access_key ACCESS KEY 42 | @param secret_key SECRET KEY 43 | @param order_update_callback 订单更新回调 44 | @param position_update_callback 持仓更新回调 45 | """ 46 | self._account = account 47 | self._strategy = strategy 48 | self._platform = DERIBIT 49 | self._symbol = symbol 50 | self._host = host if host else "https://www.deribit.com" 51 | self._wss = wss if wss else "wss://deribit.com/ws/api/v2" 52 | self._access_key = access_key 53 | self._secret_key = secret_key 54 | 55 | self._order_update_callback = order_update_callback 56 | self._position_update_callback = position_update_callback 57 | 58 | self._order_channel = "user.orders.{symbol}.raw".format(symbol=symbol) # 订单订阅频道 59 | 60 | super(DeribitTrade, self).__init__(self._wss, send_hb_interval=5) 61 | 62 | self._orders = {} # 订单 63 | self._position = Position(self._platform, self._account, strategy, symbol) # 仓位 64 | 65 | self._query_id = 0 # 消息序号id,用来唯一标识请求消息 66 | self._queries = {} # 未完成的post请求 {"request_id": future} 67 | 68 | self.initialize() 69 | 70 | # 注册定时任务 71 | LoopRunTask.register(self._do_auth, 60 * 60) # 每隔1小时重新授权 72 | LoopRunTask.register(self._check_position_update, 1) # 获取持仓 73 | 74 | self._ok = False # 是否建立授权成功的websocket连接 75 | 76 | @property 77 | def position(self): 78 | return copy.copy(self._position) 79 | 80 | @property 81 | def orders(self): 82 | return copy.copy(self._orders) 83 | 84 | async def connected_callback(self): 85 | """ 建立连接之后,授权登陆,然后订阅order和position 86 | """ 87 | # 授权 88 | success, error = await self._do_auth() 89 | if error: 90 | return 91 | if success.get("access_token"): 92 | self._ok = True 93 | else: 94 | return 95 | 96 | # 获取未完全成交的订单 97 | success, error = await self.get_open_orders() 98 | if error: 99 | return 100 | for order_info in success: 101 | order = self._update_order(order_info) 102 | if self._order_update_callback: 103 | SingleTask.run(self._order_update_callback, order) 104 | 105 | # 获取持仓 106 | await self._check_position_update() 107 | 108 | # 授权成功之后,订阅数据 109 | method = "private/subscribe" 110 | params = { 111 | "channels": [ 112 | self._order_channel 113 | ] 114 | } 115 | await self._send_message(method, params) 116 | 117 | async def _do_auth(self, *args, **kwargs): 118 | """ 鉴权 119 | """ 120 | method = "public/auth" 121 | params = { 122 | "grant_type": "client_credentials", 123 | "client_id": self._access_key, 124 | "client_secret": self._secret_key 125 | } 126 | success, error = await self._send_message(method, params) 127 | return success, error 128 | 129 | async def get_server_time(self): 130 | """ 获取服务器时间 131 | """ 132 | method = "public/get_time" 133 | params = {} 134 | success, error = await self._send_message(method, params) 135 | return success, error 136 | 137 | async def get_position(self): 138 | """ 获取当前持仓 139 | """ 140 | method = "private/get_position" 141 | params = {"instrument_name": self._symbol} 142 | success, error = await self._send_message(method, params) 143 | return success, error 144 | 145 | async def create_order(self, action, price, quantity, order_type=ORDER_TYPE_LIMIT): 146 | """ 创建订单 147 | @param action 委托方向 BUY SELL 148 | @param price 委托价格 149 | @param quantity 委托数量 150 | @param order_type 委托类型 limit/market 151 | """ 152 | if int(quantity) > 0: 153 | if action == ORDER_ACTION_BUY: 154 | trade_type = TRADE_TYPE_BUY_OPEN 155 | else: 156 | trade_type = TRADE_TYPE_SELL_CLOSE 157 | else: 158 | if action == ORDER_ACTION_BUY: 159 | trade_type = TRADE_TYPE_BUY_CLOSE 160 | else: 161 | trade_type = TRADE_TYPE_SELL_OPEN 162 | quantity = abs(int(quantity)) 163 | if action == ORDER_ACTION_BUY: 164 | method = "private/buy" 165 | elif action == ORDER_ACTION_SELL: 166 | method = "private/sell" 167 | else: 168 | logger.error("action error! action:", action, caller=self) 169 | return None 170 | if order_type == ORDER_TYPE_LIMIT: 171 | type_ = "limit" 172 | else: 173 | type_ = "market" 174 | params = { 175 | "instrument_name": self._symbol, 176 | "price": price, 177 | "amount": quantity, 178 | "type": type_, 179 | "label": str(trade_type) 180 | } 181 | success, error = await self._send_message(method, params) 182 | if error: 183 | return None, error 184 | order_no = success["order"]["order_id"] 185 | return order_no, None 186 | 187 | async def revoke_order(self, *order_nos): 188 | """ 撤销订单 189 | @param order_nos 订单号,如果没有指定订单号,那么撤销所有订单 190 | * NOTE: 单次调换最多只能撤销100个订单,如果订单超过100个,请多次调用 191 | """ 192 | # 如果传入order_nos为空,即撤销全部委托单 193 | if len(order_nos) == 0: 194 | method = "private/cancel_all_by_instrument" 195 | params = {"instrument_name": self._symbol} 196 | success, error = await self._send_message(method, params) 197 | if error: 198 | return False, error 199 | else: 200 | return True, None 201 | 202 | # 如果传入order_nos为一个委托单号,那么只撤销一个委托单 203 | if len(order_nos) == 1: 204 | method = "private/cancel" 205 | params = {"order_id": order_nos[0]} 206 | success, error = await self._send_message(method, params) 207 | if error: 208 | return order_nos[0], error 209 | else: 210 | return order_nos[0], None 211 | 212 | # 如果传入order_nos数量大于1,那么就批量撤销传入的委托单 213 | if len(order_nos) > 1: 214 | success, error = [], [] 215 | method = "private/cancel" 216 | for order_no in order_nos: 217 | params = {"order_id": order_no} 218 | r, e = await self._send_message(method, params) 219 | if e: 220 | error.append((order_no, e)) 221 | else: 222 | success.append(order_no) 223 | return success, error 224 | 225 | async def get_order_status(self, order_no): 226 | """ 获取订单状态 227 | @param order_no 订单号 228 | """ 229 | method = "private/get_order_state" 230 | params = {"order_id": order_no} 231 | success, error = await self._send_message(method, params) 232 | return success, error 233 | 234 | async def get_open_orders(self): 235 | """ 获取未完全成交订单 236 | """ 237 | method = "private/get_open_orders_by_instrument" 238 | params = {"instrument_name": self._symbol} 239 | success, error = await self._send_message(method, params) 240 | return success, error 241 | 242 | async def get_open_order_nos(self): 243 | """ 获取未完全成交订单号列表 244 | """ 245 | method = "private/get_open_orders_by_instrument" 246 | params = {"instrument_name": self._symbol} 247 | success, error = await self._send_message(method, params) 248 | if error: 249 | return None, error 250 | else: 251 | order_nos = [] 252 | for item in success: 253 | order_nos.append(item["order_id"]) 254 | return order_nos, None 255 | 256 | async def _send_message(self, method, params): 257 | """ 发送消息 258 | """ 259 | f = asyncio.futures.Future() 260 | request_id = await self._generate_query_id() 261 | self._queries[request_id] = f 262 | data = { 263 | "jsonrpc": "2.0", 264 | "id": request_id, 265 | "method": method, 266 | "params": params 267 | } 268 | await self.ws.send_json(data) 269 | logger.debug("send message:", data, caller=self) 270 | success, error = await f 271 | if error: 272 | logger.error("data:", data, "error:", error, caller=self) 273 | return success, error 274 | 275 | @async_method_locker("generate_query_id.locker") 276 | async def _generate_query_id(self): 277 | """ 生成query id,加锁,确保每个请求id唯一 278 | """ 279 | self._query_id += 1 280 | return self._query_id 281 | 282 | @async_method_locker("process.locker") 283 | async def process(self, msg): 284 | """ 处理websocket消息 285 | """ 286 | logger.debug("msg:", json.dumps(msg), caller=self) 287 | 288 | # 请求消息 289 | request_id = msg.get("id") 290 | if request_id: 291 | f = self._queries.pop(request_id) 292 | if f.done(): 293 | return 294 | success = msg.get("result") 295 | error = msg.get("error") 296 | f.set_result((success, error)) 297 | 298 | # 推送订阅消息 299 | if msg.get("method") == "subscription": 300 | if msg["params"]["channel"] == self._order_channel: 301 | order_info = msg["params"]["data"] 302 | order = self._update_order(order_info) 303 | if self._order_update_callback: 304 | SingleTask.run(self._order_update_callback, copy.copy(order)) 305 | 306 | async def _check_position_update(self, *args, **kwargs): 307 | """ 定时获取持仓 308 | """ 309 | if not self._ok: 310 | return 311 | update = False 312 | success, error = await self.get_position() 313 | if error: 314 | return 315 | if not self._position.utime: # 如果持仓还没有被初始化,那么初始化之后推送一次 316 | update = True 317 | self._position.update() 318 | size = int(success["size"]) 319 | average_price = float(success["average_price"]) 320 | liquid_price = float(success["estimated_liquidation_price"]) 321 | if size > 0: 322 | if self._position.long_quantity != size: 323 | update = True 324 | self._position.update(0, 0, size, average_price, liquid_price) 325 | elif size < 0: 326 | if self._position.short_quantity != abs(size): 327 | update = True 328 | self._position.update(abs(size), average_price, 0, 0, liquid_price) 329 | elif size == 0: 330 | if self._position.long_quantity != 0 or self._position.short_quantity != 0: 331 | update = True 332 | self._position.update() 333 | if update: 334 | await self._position_update_callback(self._position) 335 | 336 | def _update_order(self, order_info): 337 | """ 更新订单信息 338 | @param order_info 订单信息 339 | """ 340 | order_no = order_info["order_id"] 341 | quantity = int(order_info["amount"]) 342 | filled_amount = int(order_info["filled_amount"]) 343 | remain = quantity - filled_amount 344 | average_price = order_info.get("average_price") 345 | state = order_info["order_state"] 346 | if state == "open": 347 | status = ORDER_STATUS_SUBMITTED 348 | if filled_amount > 0: 349 | status = ORDER_STATUS_PARTIAL_FILLED 350 | elif state == "filled": 351 | status = ORDER_STATUS_FILLED 352 | elif state == "cancelled": 353 | status = ORDER_STATUS_CANCELED 354 | else: 355 | status = ORDER_STATUS_FAILED 356 | 357 | order = self._orders.get(order_no) 358 | if not order: 359 | action = ORDER_ACTION_BUY if order_info["direction"] == "buy" else ORDER_ACTION_SELL 360 | trade_type = int(order_info.get("label")) 361 | info = { 362 | "platform": self._platform, 363 | "account": self._account, 364 | "strategy": self._strategy, 365 | "symbol": self._symbol, 366 | "order_no": order_no, 367 | "action": action, 368 | "price": order_info["price"], 369 | "quantity": quantity, 370 | "remain": remain, 371 | "trade_type": trade_type 372 | } 373 | order = Order(**info) 374 | self._orders[order_no] = order 375 | order.status = status 376 | order.remain = remain 377 | order.avg_price = average_price 378 | order.ctime = order_info["creation_timestamp"] 379 | order.utime = order_info["last_update_timestamp"] 380 | if order.status in [ORDER_STATUS_FILLED, ORDER_STATUS_CANCELED, ORDER_STATUS_FAILED]: 381 | self._orders.pop(order.order_no) 382 | return order 383 | -------------------------------------------------------------------------------- /quant/platform/okex.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | OKEx现货交易 Trade 模块 5 | https://www.okex.com/docs/zh/ 6 | 7 | Author: HuangTao 8 | Date: 2019/01/19 9 | """ 10 | 11 | import time 12 | import json 13 | import copy 14 | import hmac 15 | import zlib 16 | import base64 17 | from urllib.parse import urljoin 18 | 19 | from quant.utils import tools 20 | from quant.utils import logger 21 | from quant.const import OKEX 22 | from quant.order import Order 23 | from quant.tasks import SingleTask 24 | from quant.utils.websocket import Websocket 25 | from quant.utils.decorator import async_method_locker 26 | from quant.utils.http_client import AsyncHttpRequests 27 | from quant.order import ORDER_ACTION_BUY, ORDER_ACTION_SELL 28 | from quant.order import ORDER_TYPE_LIMIT, ORDER_TYPE_MARKET 29 | from quant.order import ORDER_STATUS_SUBMITTED, ORDER_STATUS_PARTIAL_FILLED, ORDER_STATUS_FILLED, \ 30 | ORDER_STATUS_CANCELED, ORDER_STATUS_FAILED 31 | 32 | 33 | __all__ = ("OKExRestAPI", "OKExTrade", ) 34 | 35 | 36 | class OKExRestAPI: 37 | """ OKEx现货交易 REST API 封装 38 | """ 39 | 40 | def __init__(self, host, access_key, secret_key, passphrase): 41 | """ 初始化 42 | @param host 请求的host 43 | @param access_key 请求的access_key 44 | @param secret_key 请求的secret_key 45 | @param passphrase API KEY的密码 46 | """ 47 | self._host = host 48 | self._access_key = access_key 49 | self._secret_key = secret_key 50 | self._passphrase = passphrase 51 | 52 | async def get_user_account(self): 53 | """ 获取账户信息 54 | """ 55 | result, error = await self.request("GET", "/api/spot/v3/accounts", auth=True) 56 | return result, error 57 | 58 | async def create_order(self, action, symbol, price, quantity, order_type=ORDER_TYPE_LIMIT): 59 | """ 创建订单 60 | @param action 操作类型 BUY SELL 61 | @param symbol 交易对 62 | @param quantity 交易量 63 | @param price 交易价格 64 | @param order_type 订单类型 市价 / 限价 65 | """ 66 | info = { 67 | "side": "buy" if action == ORDER_ACTION_BUY else "sell", 68 | "instrument_id": symbol, 69 | "margin_trading": 1 70 | } 71 | if order_type == ORDER_TYPE_LIMIT: 72 | info["type"] = "limit" 73 | info["price"] = price 74 | info["size"] = quantity 75 | elif order_type == ORDER_TYPE_MARKET: 76 | info["type"] = "market" 77 | if action == ORDER_ACTION_BUY: 78 | info["notional"] = quantity # 买入金额,市价买入是必填notional 79 | else: 80 | info["size"] = quantity # 卖出数量,市价卖出时必填size 81 | else: 82 | logger.error("order_type error! order_type:", order_type, caller=self) 83 | return None 84 | result, error = await self.request("POST", "/api/spot/v3/orders", body=info, auth=True) 85 | return result, error 86 | 87 | async def revoke_order(self, symbol, order_no): 88 | """ 撤销委托单 89 | @param symbol 交易对 90 | @param order_no 订单id 91 | """ 92 | body = { 93 | "instrument_id": symbol 94 | } 95 | uri = "/api/spot/v3/cancel_orders/{order_no}".format(order_no=order_no) 96 | result, error = await self.request("POST", uri, body=body, auth=True) 97 | if error: 98 | return order_no, error 99 | if result["result"]: 100 | return order_no, None 101 | return order_no, result 102 | 103 | async def revoke_orders(self, symbol, order_nos): 104 | """ 批量撤销委托单 105 | @param symbol 交易对 106 | @param order_nos 订单列表 107 | * NOTE: 单次不超过4个订单id 108 | """ 109 | if len(order_nos) > 4: 110 | logger.warn("only revoke 4 orders per request!", caller=self) 111 | body = [ 112 | { 113 | "instrument_id": symbol, 114 | "order_ids": order_nos[:4] 115 | } 116 | ] 117 | result, error = await self.request("POST", "/api/spot/v3/cancel_batch_orders", body=body, auth=True) 118 | return result, error 119 | 120 | async def get_open_orders(self, symbol): 121 | """ 获取当前还未完全成交的订单信息 122 | @param symbol 交易对 123 | * NOTE: 查询上限最多100个订单 124 | """ 125 | params = { 126 | "instrument_id": symbol 127 | } 128 | result, error = await self.request("GET", "/api/spot/v3/orders_pending", params=params, auth=True) 129 | return result, error 130 | 131 | async def get_order_status(self, symbol, order_no): 132 | """ 获取订单的状态 133 | @param symbol 交易对 134 | @param order_no 订单id 135 | """ 136 | params = { 137 | "instrument_id": symbol 138 | } 139 | uri = "/api/spot/v3/orders/{order_no}".format(order_no=order_no) 140 | result, error = await self.request("GET", uri, params=params, auth=True) 141 | return result, error 142 | 143 | async def request(self, method, uri, params=None, body=None, headers=None, auth=False): 144 | """ 发起请求 145 | @param method 请求方法 GET / POST / DELETE / PUT 146 | @param uri 请求uri 147 | @param params dict 请求query参数 148 | @param body dict 请求body数据 149 | @param headers 请求http头 150 | @param auth boolean 是否需要加入权限校验 151 | """ 152 | if params: 153 | query = "&".join(["{}={}".format(k, params[k]) for k in sorted(params.keys())]) 154 | uri += "?" + query 155 | url = urljoin(self._host, uri) 156 | 157 | # 增加签名 158 | if auth: 159 | timestamp = str(time.time()).split(".")[0] + "." + str(time.time()).split(".")[1][:3] 160 | if body: 161 | body = json.dumps(body) 162 | else: 163 | body = "" 164 | message = str(timestamp) + str.upper(method) + uri + str(body) 165 | mac = hmac.new(bytes(self._secret_key, encoding="utf8"), bytes(message, encoding="utf-8"), 166 | digestmod="sha256") 167 | d = mac.digest() 168 | sign = base64.b64encode(d) 169 | 170 | if not headers: 171 | headers = {} 172 | headers["Content-Type"] = "application/json" 173 | headers["OK-ACCESS-KEY"] = self._access_key.encode().decode() 174 | headers["OK-ACCESS-SIGN"] = sign.decode() 175 | headers["OK-ACCESS-TIMESTAMP"] = str(timestamp) 176 | headers["OK-ACCESS-PASSPHRASE"] = self._passphrase 177 | _, success, error = await AsyncHttpRequests.fetch(method, url, body=body, headers=headers, timeout=10) 178 | return success, error 179 | 180 | 181 | class OKExTrade(Websocket): 182 | """ OKEX Trade模块 183 | """ 184 | 185 | def __init__(self, account, strategy, symbol, host=None, wss=None, access_key=None, secret_key=None, 186 | passphrase=None, order_update_callback=None): 187 | """ 初始化 188 | @param account 账户 189 | @param strategy 策略名称 190 | @param symbol 交易对(合约名称) 191 | @param host HTTP请求主机地址 192 | @param wss websocket连接地址 193 | @param access_key ACCESS KEY 194 | @param secret_key SECRET KEY 195 | @param passphrase 密码 196 | @param order_update_callback 订单更新回调 197 | """ 198 | self._account = account 199 | self._strategy = strategy 200 | self._platform = OKEX 201 | self._symbol = symbol 202 | self._host = host if host else "https://www.okex.com" 203 | self._wss = wss if wss else "wss://real.okex.com:10442/ws/v3" 204 | self._access_key = access_key 205 | self._secret_key = secret_key 206 | self._passphrase = passphrase 207 | self._order_update_callback = order_update_callback 208 | 209 | self._raw_symbol = symbol.replace("/", "-") # 转换成交易所对应的交易对格式 210 | 211 | super(OKExTrade, self).__init__(self._wss, send_hb_interval=5) 212 | self.heartbeat_msg = "ping" 213 | 214 | self._orders = {} # 订单 215 | 216 | # 初始化 REST API 对象 217 | self._rest_api = OKExRestAPI(self._host, self._access_key, self._secret_key, self._passphrase) 218 | 219 | self.initialize() 220 | 221 | @property 222 | def orders(self): 223 | return copy.copy(self._orders) 224 | 225 | async def connected_callback(self): 226 | """ 建立连接之后,授权登陆,然后订阅order和position 227 | """ 228 | # 身份验证 229 | timestamp = str(time.time()).split('.')[0] + '.' + str(time.time()).split('.')[1][:3] 230 | message = str(timestamp) + "GET" + "/users/self/verify" 231 | mac = hmac.new(bytes(self._secret_key, encoding="utf8"), bytes(message, encoding="utf8"), digestmod="sha256") 232 | d = mac.digest() 233 | signature = base64.b64encode(d).decode() 234 | data = { 235 | "op": "login", 236 | "args": [self._access_key, self._passphrase, timestamp, signature] 237 | } 238 | await self.ws.send_json(data) 239 | 240 | # 获取当前等待成交和部分成交的订单信息 241 | order_infos, error = await self._rest_api.get_open_orders(self._raw_symbol) 242 | if error: 243 | return 244 | for order_info in order_infos: 245 | order_info["ctime"] = order_info["created_at"] 246 | order_info["utime"] = order_info["timestamp"] 247 | order = self._update_order(order_info) 248 | if self._order_update_callback: 249 | SingleTask.run(self._order_update_callback, order) 250 | 251 | @async_method_locker("process_binary.locker") 252 | async def process_binary(self, raw): 253 | """ 处理websocket上接收到的消息 254 | @param raw 原始的压缩数据 255 | """ 256 | decompress = zlib.decompressobj(-zlib.MAX_WBITS) 257 | msg = decompress.decompress(raw) 258 | msg += decompress.flush() 259 | msg = msg.decode() 260 | if msg == "pong": # 心跳返回 261 | return 262 | msg = json.loads(msg) 263 | logger.debug('msg:', msg, caller=self) 264 | 265 | # 登陆成功之后再订阅数据 266 | if msg.get("event") == "login": 267 | if not msg.get("success"): 268 | logger.error("websocket login error!", caller=self) 269 | return 270 | logger.info("Websocket connection authorized successfully.", caller=self) 271 | 272 | # 订阅 order 273 | ch_order = "spot/order:{symbol}".format(symbol=self._raw_symbol) 274 | data = { 275 | "op": "subscribe", 276 | "args": [ch_order] 277 | } 278 | await self.ws.send_json(data) 279 | logger.info("subscribe order successfully.", caller=self) 280 | return 281 | 282 | table = msg.get("table") 283 | if table not in ["spot/order", ]: 284 | return 285 | 286 | for data in msg["data"]: 287 | if table == "spot/order": 288 | data["ctime"] = data["timestamp"] 289 | data["utime"] = data["last_fill_time"] 290 | order = self._update_order(data) 291 | if order and self._order_update_callback: 292 | SingleTask.run(self._order_update_callback, order) 293 | 294 | async def create_order(self, action, price, quantity, order_type=ORDER_TYPE_LIMIT): 295 | """ 创建订单 296 | @param action 交易方向 BUY/SELL 297 | @param price 委托价格 298 | @param quantity 委托数量 299 | @param order_type 委托类型 LIMIT / MARKET 300 | """ 301 | price = tools.float_to_str(price) 302 | quantity = tools.float_to_str(quantity) 303 | result, error = await self._rest_api.create_order(action, self._raw_symbol, price, quantity, order_type) 304 | if error: 305 | return None, error 306 | if not result["result"]: 307 | return None, result 308 | return result["order_id"], None 309 | 310 | async def revoke_order(self, *order_nos): 311 | """ 撤销订单 312 | @param order_nos 订单号列表,可传入任意多个,如果不传入,那么就撤销所有订单 313 | * NOTE: 单次调用最多只能撤销4个订单,如果订单超过4个,请多次调用 314 | """ 315 | # 如果传入order_nos为空,即撤销全部委托单 316 | if len(order_nos) == 0: 317 | order_infos, error = await self._rest_api.get_open_orders(self._raw_symbol) 318 | if error: 319 | return False, error 320 | for order_info in order_infos: 321 | order_no = order_info["order_id"] 322 | _, error = await self._rest_api.revoke_order(self._raw_symbol, order_no) 323 | if error: 324 | return False, error 325 | return True, None 326 | 327 | # 如果传入order_nos为一个委托单号,那么只撤销一个委托单 328 | if len(order_nos) == 1: 329 | success, error = await self._rest_api.revoke_order(self._raw_symbol, order_nos[0]) 330 | if error: 331 | return order_nos[0], error 332 | else: 333 | return order_nos[0], None 334 | 335 | # 如果传入order_nos数量大于1,那么就批量撤销传入的委托单 336 | if len(order_nos) > 1: 337 | success, error = [], [] 338 | for order_no in order_nos: 339 | _, e = await self._rest_api.revoke_order(self._raw_symbol, order_no) 340 | if e: 341 | error.append((order_no, e)) 342 | else: 343 | success.append(order_no) 344 | return success, error 345 | 346 | async def get_open_order_nos(self): 347 | """ 获取未完全成交订单号列表 348 | """ 349 | success, error = await self._rest_api.get_open_orders(self._raw_symbol) 350 | if error: 351 | return None, error 352 | else: 353 | order_nos = [] 354 | for order_info in success: 355 | order_nos.append(order_info["order_id"]) 356 | return order_nos, None 357 | 358 | def _update_order(self, order_info): 359 | """ 更新订单信息 360 | @param order_info 订单信息 361 | """ 362 | order_no = str(order_info["order_id"]) 363 | state = order_info["state"] 364 | remain = float(order_info["size"]) - float(order_info["filled_size"]) 365 | ctime = tools.utctime_str_to_mts(order_info["ctime"]) 366 | utime = tools.utctime_str_to_mts(order_info["utime"]) 367 | 368 | if state == "-2": 369 | status = ORDER_STATUS_FAILED 370 | elif state == "-1": 371 | status = ORDER_STATUS_CANCELED 372 | elif state == "0": 373 | status = ORDER_STATUS_SUBMITTED 374 | elif state == "1": 375 | status = ORDER_STATUS_PARTIAL_FILLED 376 | elif state == "2": 377 | status = ORDER_STATUS_FILLED 378 | else: 379 | logger.error("status error! order_info:", order_info, caller=self) 380 | return None 381 | 382 | order = self._orders.get(order_no) 383 | if order: 384 | order.remain = remain 385 | order.status = status 386 | order.price = order_info["price"] 387 | else: 388 | info = { 389 | "platform": self._platform, 390 | "account": self._account, 391 | "strategy": self._strategy, 392 | "order_no": order_no, 393 | "action": ORDER_ACTION_BUY if order_info["side"] == "buy" else ORDER_ACTION_SELL, 394 | "symbol": self._symbol, 395 | "price": order_info["price"], 396 | "quantity": order_info["size"], 397 | "remain": remain, 398 | "status": status, 399 | "avg_price": order_info["price"] 400 | } 401 | order = Order(**info) 402 | self._orders[order_no] = order 403 | order.ctime = ctime 404 | order.utime = utime 405 | if status in [ORDER_STATUS_FAILED, ORDER_STATUS_CANCELED, ORDER_STATUS_FILLED]: 406 | self._orders.pop(order_no) 407 | return order 408 | -------------------------------------------------------------------------------- /quant/platform/binance.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | Binance Trade 模块 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 | """ 10 | 11 | import json 12 | import copy 13 | import hmac 14 | import hashlib 15 | from urllib.parse import urljoin 16 | 17 | from quant.utils import tools 18 | from quant.utils import logger 19 | from quant.const import BINANCE 20 | from quant.order import Order 21 | from quant.utils.websocket import Websocket 22 | from quant.tasks import SingleTask, LoopRunTask 23 | from quant.utils.http_client import AsyncHttpRequests 24 | from quant.utils.decorator import async_method_locker 25 | from quant.order import ORDER_TYPE_LIMIT, ORDER_TYPE_MARKET 26 | from quant.order import ORDER_STATUS_SUBMITTED, ORDER_STATUS_PARTIAL_FILLED, ORDER_STATUS_FILLED, \ 27 | ORDER_STATUS_CANCELED, ORDER_STATUS_FAILED 28 | 29 | 30 | __all__ = ("BinanceRestAPI", ) 31 | 32 | 33 | class BinanceRestAPI: 34 | """ Binance REST API 封装 35 | """ 36 | 37 | def __init__(self, host, access_key, secret_key): 38 | """ 初始化 39 | @param host 请求的host 40 | @param access_key 请求的access_key 41 | @param secret_key 请求的secret_key 42 | """ 43 | self._host = host 44 | self._access_key = access_key 45 | self._secret_key = secret_key 46 | 47 | async def get_user_account(self): 48 | """ 获取账户信息 49 | """ 50 | ts = tools.get_cur_timestamp_ms() 51 | params = { 52 | "timestamp": str(ts) 53 | } 54 | success, error = await self.request("GET", "/api/v3/account", params, auth=True) 55 | return success, error 56 | 57 | async def get_server_time(self): 58 | """ 获取服务器时间 59 | """ 60 | success, error = await self.request("GET", "/api/v1/time") 61 | return success, error 62 | 63 | async def get_exchange_info(self): 64 | """ 获取交易所信息 65 | """ 66 | success, error = await self.request("GET", "/api/v1/exchangeInfo") 67 | return success, error 68 | 69 | async def get_latest_ticker(self, symbol): 70 | """ 获取交易对实时ticker行情 71 | """ 72 | params = { 73 | "symbol": symbol 74 | } 75 | success, error = await self.request("GET", "/api/v1/ticker/24hr", params=params) 76 | return success, error 77 | 78 | async def get_orderbook(self, symbol, limit=10): 79 | """ 获取订单薄数据 80 | @param symbol 交易对 81 | @param limit 订单薄的档位数,默认为10,可选 5, 10, 20, 50, 100, 500, 1000 82 | """ 83 | params = { 84 | "symbol": symbol, 85 | "limit": limit 86 | } 87 | success, error = await self.request("GET", "/api/v1/depth", params=params) 88 | return success, error 89 | 90 | async def create_order(self, action, symbol, price, quantity): 91 | """ 创建订单 92 | @param action 操作类型 BUY SELL 93 | @param symbol 交易对 94 | @param quantity 交易量 95 | @param price 交易价格 96 | * NOTE: 仅实现了限价单 97 | """ 98 | info = { 99 | "symbol": symbol, 100 | "side": action, 101 | "type": "LIMIT", 102 | "timeInForce": "GTC", 103 | "quantity": quantity, 104 | "price": price, 105 | "recvWindow": "5000", 106 | "newOrderRespType": "FULL", 107 | "timestamp": tools.get_cur_timestamp_ms() 108 | } 109 | success, error = await self.request("POST", "/api/v3/order", body=info, auth=True) 110 | return success, error 111 | 112 | async def revoke_order(self, symbol, order_id, client_order_id): 113 | """ 撤销订单 114 | @param symbol 交易对 115 | @param order_id 订单id 116 | @param client_order_id 创建订单返回的客户端信息 117 | """ 118 | params = { 119 | "symbol": symbol, 120 | "orderId": str(order_id), 121 | "origClientOrderId": client_order_id, 122 | "timestamp": tools.get_cur_timestamp_ms() 123 | } 124 | success, error = await self.request("DELETE", "/api/v3/order", params=params, auth=True) 125 | return success, error 126 | 127 | async def get_order_status(self, symbol, order_id, client_order_id): 128 | """ 获取订单的状态 129 | @param symbol 交易对 130 | @param order_id 订单id 131 | @param client_order_id 创建订单返回的客户端信息 132 | """ 133 | params = { 134 | "symbol": symbol, 135 | "orderId": str(order_id), 136 | "origClientOrderId": client_order_id, 137 | "timestamp": tools.get_cur_timestamp_ms() 138 | } 139 | success, error = await self.request("GET", "/api/v3/order", params=params, auth=True) 140 | return success, error 141 | 142 | async def get_all_orders(self, symbol): 143 | """ 获取所有订单信息 144 | @param symbol 交易对 145 | """ 146 | params = { 147 | "symbol": symbol, 148 | "timestamp": tools.get_cur_timestamp_ms() 149 | } 150 | success, error = await self.request("GET", "/api/v3/allOrders", params=params, auth=True) 151 | return success, error 152 | 153 | async def get_open_orders(self, symbol): 154 | """ 获取当前还未完全成交的订单信息 155 | @param symbol 交易对 156 | """ 157 | params = { 158 | "symbol": symbol, 159 | "timestamp": tools.get_cur_timestamp_ms() 160 | } 161 | success, error = await self.request("GET", "/api/v3/openOrders", params=params, auth=True) 162 | return success, error 163 | 164 | async def get_listen_key(self): 165 | """ 获取一个新的用户数据流key 166 | @return listen_key string wss监听用户数据的key 167 | """ 168 | success, error = await self.request("POST", "/api/v1/userDataStream") 169 | return success, error 170 | 171 | async def put_listen_key(self, listen_key): 172 | """ 保持listen key连接 173 | @param listen_key string wss监听用户数据的key 174 | """ 175 | params = { 176 | "listenKey": listen_key 177 | } 178 | success, error = await self.request("PUT", "/api/v1/userDataStream", params=params) 179 | return success, error 180 | 181 | async def delete_listen_key(self, listen_key): 182 | """ 删除一个listen key 183 | @param listen_key string wss监听用户数据的key 184 | """ 185 | params = { 186 | "listenKey": listen_key 187 | } 188 | success, error = await self.request("DELETE", "/api/v1/userDataStream", params=params) 189 | return success, error 190 | 191 | async def request(self, method, uri, params=None, body=None, headers=None, auth=False): 192 | """ 发起请求 193 | @param method 请求方法 GET POST DELETE PUT 194 | @param uri 请求uri 195 | @param params dict 请求query参数 196 | @param body dict 请求body数据 197 | @param headers 请求http头 198 | @param auth boolean 是否需要加入权限校验 199 | """ 200 | url = urljoin(self._host, uri) 201 | data = {} 202 | if params: 203 | data.update(params) 204 | if body: 205 | data.update(body) 206 | 207 | if data: 208 | query = "&".join(["=".join([str(k), str(v)]) for k, v in data.items()]) 209 | else: 210 | query = "" 211 | if auth and query: 212 | signature = hmac.new(self._secret_key.encode(), query.encode(), hashlib.sha256).hexdigest() 213 | query += "&signature={s}".format(s=signature) 214 | if query: 215 | url += ("?" + query) 216 | 217 | if not headers: 218 | headers = {} 219 | headers["X-MBX-APIKEY"] = self._access_key 220 | _, success, error = await AsyncHttpRequests.fetch(method, url, headers=headers, timeout=10, verify_ssl=False) 221 | return success, error 222 | 223 | 224 | class BinanceTrade(Websocket): 225 | """ binance用户数据流 226 | """ 227 | 228 | def __init__(self, account, strategy, symbol, host=None, wss=None, access_key=None, secret_key=None, 229 | order_update_callback=None): 230 | """ 初始化 231 | @param account 账户 232 | @param strategy 策略名称 233 | @param symbol 交易对(合约名称) 234 | @param host HTTP请求主机地址 235 | @param wss websocket连接地址 236 | @param access_key ACCESS KEY 237 | @param secret_key SECRET KEY 238 | @param order_update_callback 订单更新回调 239 | """ 240 | self._account = account 241 | self._strategy = strategy 242 | self._platform = BINANCE 243 | self._symbol = symbol 244 | self._raw_symbol = symbol.replace("/", "") 245 | self._host = host if host else "https://api.binance.com" 246 | self._wss = wss if wss else "wss://stream.binance.com:9443" 247 | self._access_key = access_key 248 | self._secret_key = secret_key 249 | self._order_update_callback = order_update_callback 250 | 251 | super(BinanceTrade, self).__init__(self._wss) 252 | 253 | self._listen_key = None # websocket连接鉴权使用 254 | self._orders = {} # 订单 255 | 256 | # 初始化 REST API 对象 257 | self._rest_api = BinanceRestAPI(self._host, self._access_key, self._secret_key) 258 | 259 | # 30分钟重置一下listen key 260 | LoopRunTask.register(self._reset_listen_key, 60 * 30) 261 | # 获取listen key 262 | SingleTask.run(self._init_websocket) 263 | 264 | @property 265 | def orders(self): 266 | return copy.copy(self._orders) 267 | 268 | async def _init_websocket(self): 269 | """ 初始化websocket 270 | """ 271 | # 获取listen key 272 | success, error = await self._rest_api.get_listen_key() 273 | if error: 274 | logger.error("get listen key error:", error, caller=self) 275 | return 276 | self._listen_key = success["listenKey"] 277 | uri = "/ws/" + self._listen_key 278 | self._url = urljoin(self._wss, uri) 279 | self.initialize() 280 | 281 | async def _reset_listen_key(self, *args, **kwargs): 282 | """ 重置listen key 283 | """ 284 | if not self._listen_key: 285 | logger.error("listen key not initialized!", caller=self) 286 | return 287 | await self._rest_api.put_listen_key(self._listen_key) 288 | logger.info("reset listen key success!", caller=self) 289 | 290 | async def connected_callback(self): 291 | """ 建立连接之后,获取当前所有未完全成交的订单 292 | """ 293 | order_infos, error = await self._rest_api.get_open_orders(self._raw_symbol) 294 | if error: 295 | return 296 | for order_info in order_infos: 297 | order_no = "{}_{}".format(order_info["orderId"], order_info["clientOrderId"]) 298 | if order_info["status"] == "NEW": # 部分成交 299 | status = ORDER_STATUS_SUBMITTED 300 | elif order_info["status"] == "PARTIALLY_FILLED": # 部分成交 301 | status = ORDER_STATUS_PARTIAL_FILLED 302 | elif order_info["status"] == "FILLED": # 完全成交 303 | status = ORDER_STATUS_FILLED 304 | elif order_info["status"] == "CANCELED": # 取消 305 | status = ORDER_STATUS_CANCELED 306 | elif order_info["status"] == "REJECTED": # 拒绝 307 | status = ORDER_STATUS_FAILED 308 | elif order_info["status"] == "EXPIRED": # 过期 309 | status = ORDER_STATUS_FAILED 310 | else: 311 | logger.warn("unknown status:", order_info, caller=self) 312 | return 313 | 314 | info = { 315 | "platform": self._platform, 316 | "account": self._account, 317 | "strategy": self._strategy, 318 | "order_no": order_no, 319 | "action": order_info["side"], 320 | "order_type": order_info["type"], 321 | "symbol": self._symbol, 322 | "price": order_info["price"], 323 | "quantity": order_info["origQty"], 324 | "remain": float(order_info["origQty"]) - float(order_info["executedQty"]), 325 | "status": status, 326 | "ctime": order_info["time"], 327 | "utime": order_info["updateTime"] 328 | } 329 | order = Order(**info) 330 | self._orders[order_no] = order 331 | if self._order_update_callback: 332 | SingleTask.run(self._order_update_callback, order) 333 | 334 | async def create_order(self, action, price, quantity, order_type=ORDER_TYPE_LIMIT): 335 | """ 创建订单 336 | @param action 交易方向 BUY/SELL 337 | @param price 委托价格 338 | @param quantity 委托数量 339 | @param order_type 委托类型 LIMIT / MARKET 340 | """ 341 | price = tools.float_to_str(price) 342 | quantity = tools.float_to_str(quantity) 343 | result, error = await self._rest_api.create_order(action, self._raw_symbol, price, quantity) 344 | if error: 345 | return None, error 346 | order_no = "{}_{}".format(result["orderId"], result["clientOrderId"]) 347 | return order_no, None 348 | 349 | async def revoke_order(self, *order_nos): 350 | """ 撤销订单 351 | @param order_nos 订单号列表,可传入任意多个,如果不传入,那么就撤销所有订单 352 | """ 353 | # 如果传入order_nos为空,即撤销全部委托单 354 | if len(order_nos) == 0: 355 | order_infos, error = await self._rest_api.get_open_orders(self._raw_symbol) 356 | if error: 357 | return False, error 358 | for order_info in order_infos: 359 | _, error = await self._rest_api.revoke_order(self._raw_symbol, order_info["orderId"], 360 | order_info["clientOrderId"]) 361 | if error: 362 | return False, error 363 | return True, None 364 | 365 | # 如果传入order_nos为一个委托单号,那么只撤销一个委托单 366 | if len(order_nos) == 1: 367 | order_id, client_order_id = order_nos[0].split("_") 368 | success, error = await self._rest_api.revoke_order(self._raw_symbol, order_id, client_order_id) 369 | if error: 370 | return order_nos[0], error 371 | else: 372 | return order_nos[0], None 373 | 374 | # 如果传入order_nos数量大于1,那么就批量撤销传入的委托单 375 | if len(order_nos) > 1: 376 | success, error = [], [] 377 | for order_no in order_nos: 378 | order_id, client_order_id = order_no.split("_") 379 | _, e = await self._rest_api.revoke_order(self._raw_symbol, order_id, client_order_id) 380 | if e: 381 | error.append((order_no, e)) 382 | else: 383 | success.append(order_no) 384 | return success, error 385 | 386 | async def get_open_order_nos(self): 387 | """ 获取未完全成交订单号列表 388 | """ 389 | success, error = await self._rest_api.get_open_orders(self._raw_symbol) 390 | if error: 391 | return None, error 392 | else: 393 | order_nos = [] 394 | for order_info in success: 395 | order_no = "{}_{}".format(order_info["orderId"], order_info["clientOrderId"]) 396 | order_nos.append(order_no) 397 | return order_nos, None 398 | 399 | @async_method_locker("process.locker") 400 | async def process(self, msg): 401 | """ 处理websocket上接收到的消息 402 | """ 403 | logger.debug("msg:", json.dumps(msg), caller=self) 404 | e = msg.get("e") 405 | if e == "executionReport": # 订单更新 406 | order_no = "{}_{}".format(msg["i"], msg["c"]) 407 | if msg["X"] == "NEW": # 部分成交 408 | status = ORDER_STATUS_SUBMITTED 409 | elif msg["X"] == "PARTIALLY_FILLED": # 部分成交 410 | status = ORDER_STATUS_PARTIAL_FILLED 411 | elif msg["X"] == "FILLED": # 完全成交 412 | status = ORDER_STATUS_FILLED 413 | elif msg["X"] == "CANCELED": # 取消 414 | status = ORDER_STATUS_CANCELED 415 | elif msg["X"] == "REJECTED": # 拒绝 416 | status = ORDER_STATUS_FAILED 417 | elif msg["X"] == "EXPIRED": # 过期 418 | status = ORDER_STATUS_FAILED 419 | else: 420 | logger.warn("unknown status:", msg, caller=self) 421 | return 422 | order = self._orders.get(order_no) 423 | if not order: 424 | info = { 425 | "platform": self._platform, 426 | "account": self._account, 427 | "strategy": self._strategy, 428 | "order_no": order_no, 429 | "action": msg["S"], 430 | "order_type": msg["o"], 431 | "symbol": self._symbol, 432 | "price": msg["p"], 433 | "quantity": msg["q"], 434 | "ctime": msg["O"] 435 | } 436 | order = Order(**info) 437 | self._orders[order_no] = order 438 | order.remain = float(msg["q"]) - float(msg["z"]) 439 | order.status = status 440 | order.utime = msg["T"] 441 | if self._order_update_callback: 442 | SingleTask.run(self._order_update_callback, order) 443 | # elif e == "outboundAccountInfo": # 账户资产更新 444 | # for func in self._account_update_cb_funcs: 445 | # asyncio.get_event_loop().create_task(func(msg)) 446 | -------------------------------------------------------------------------------- /quant/platform/okex_future.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | OKEx Future Trade module 交易模块 5 | https://www.okex.me/docs/zh/ 6 | 7 | Author: HuangTao 8 | Date: 2019/01/19 9 | """ 10 | 11 | import time 12 | import zlib 13 | import json 14 | import copy 15 | import hmac 16 | import base64 17 | import asyncio 18 | from urllib.parse import urljoin 19 | 20 | from quant.order import Order 21 | from quant.utils import tools 22 | from quant.utils import logger 23 | from quant.tasks import SingleTask 24 | from quant.position import Position 25 | from quant.const import OKEX_FUTURE 26 | from quant.utils.websocket import Websocket 27 | from quant.utils.http_client import AsyncHttpRequests 28 | from quant.utils.decorator import async_method_locker 29 | from quant.order import ORDER_ACTION_BUY, ORDER_ACTION_SELL 30 | from quant.order import ORDER_TYPE_LIMIT, ORDER_TYPE_MARKET 31 | from quant.order import ORDER_STATUS_SUBMITTED, ORDER_STATUS_PARTIAL_FILLED, ORDER_STATUS_FILLED, \ 32 | ORDER_STATUS_CANCELED, ORDER_STATUS_FAILED 33 | 34 | 35 | class OKExFutureRestAPI: 36 | """ OKEx期货交易 交割合约 REST API 封装 37 | """ 38 | 39 | def __init__(self, host, access_key, secret_key, passphrase): 40 | """ 初始化 41 | @param host 请求的host 42 | @param access_key 请求的access_key 43 | @param secret_key 请求的secret_key 44 | @param passphrase API KEY的密码 45 | """ 46 | self._host = host 47 | self._access_key = access_key 48 | self._secret_key = secret_key 49 | self._passphrase = passphrase 50 | 51 | async def get_user_account(self): 52 | """ 获取账户信息 53 | """ 54 | success, error = await self.request("GET", "/api/futures/v3/accounts", auth=True) 55 | return success, error 56 | 57 | async def get_position(self, instrument_id): 58 | """ 获取单个合约持仓信息 59 | @param instrument_id 合约ID,如BTC-USD-180213 60 | """ 61 | uri = "/api/futures/v3/{instrument_id}/position".format(instrument_id=instrument_id) 62 | success, error = await self.request("GET", uri, auth=True) 63 | return success, error 64 | 65 | async def create_order(self, instrument_id, trade_type, price, size, match_price=0, leverage=20): 66 | """ 下单 67 | @param instrument_id 合约ID,如BTC-USD-180213 68 | @param trade_type 交易类型,1 开多 / 2 开空 / 3 平多 / 4 平空 69 | @param price 每张合约的价格 70 | @param size 买入或卖出合约的数量(以张计数) 71 | @param match_price 是否以对手价下单(0 不是 / 1 是),默认为0,当取值为1时。price字段无效 72 | @param leverage 要设定的杠杆倍数,10或20 73 | """ 74 | body = { 75 | "instrument_id": instrument_id, 76 | "type": str(trade_type), 77 | "price": price, 78 | "size": size, 79 | "match_price": match_price, 80 | "leverage": leverage 81 | } 82 | success, error = await self.request("POST", "/api/futures/v3/order", body=body, auth=True) 83 | return success, error 84 | 85 | async def revoke_order(self, instrument_id, order_no): 86 | """ 撤单 87 | @param instrument_id 合约ID,如BTC-USD-180213 88 | @param order_id 订单ID 89 | """ 90 | uri = "/api/futures/v3/cancel_order/{instrument_id}/{order_id}".format( 91 | instrument_id=instrument_id, order_id=order_no) 92 | success, error = await self.request("POST", uri, auth=True) 93 | if error: 94 | return None, error 95 | if not success["result"]: 96 | return None, success 97 | return success, None 98 | 99 | async def revoke_orders(self, instrument_id, order_ids): 100 | """ 批量撤单 101 | @param instrument_id 合约ID,如BTC-USD-180213 102 | @param order_ids 订单id列表 103 | """ 104 | assert isinstance(order_ids, list) 105 | uri = "/api/futures/v3/cancel_batch_orders/{instrument_id}".format(instrument_id=instrument_id) 106 | body = { 107 | "order_ids": order_ids 108 | } 109 | success, error = await self.request("POST", uri, body=body, auth=True) 110 | if error: 111 | return None, error 112 | if not success["result"]: 113 | return None, success 114 | return success, None 115 | 116 | async def get_order_info(self, instrument_id, order_id): 117 | """ 获取订单信息 118 | @param instrument_id 合约ID,如BTC-USD-180213 119 | @param order_id 订单ID 120 | """ 121 | uri = "/api/futures/v3/orders/{instrument_id}/{order_id}".format( 122 | instrument_id=instrument_id, order_id=order_id) 123 | success, error = await self.request("GET", uri, auth=True) 124 | return success, error 125 | 126 | async def get_order_list(self, instrument_id, status, _from=1, to=100, limit=100): 127 | """ 获取订单列表 128 | @param instrument_id 合约ID,如BTC-USD-180213 129 | @param status Order Status 订单状态("-2":失败,"-1":撤单成功,"0":等待成交 ,"1":部分成交, "2":完全成交,"3":下单中,"4":撤单中,"6": 未完成(等待成交+部分成交),"7":已完成(撤单成功+完全成交)) 130 | @param _from Request paging content for this page number.(Example: 1,2,3,4,5. From 4 we only have 4, to 4 we only have 3) 131 | @param to Request page after (older) this pagination id. (Example: 1,2,3,4,5. From 4 we only have 4, to 4 we only have 3) 132 | @param limit Number of results per request. Maximum 100. (default 100) 133 | """ 134 | uri = "/api/futures/v3/orders/{instrument_id}".format(instrument_id=instrument_id) 135 | params = { 136 | "status": status, 137 | "from": _from, 138 | "to": to, 139 | "limit": limit 140 | } 141 | success, error = await self.request("GET", uri, params=params, auth=True) 142 | return success, error 143 | 144 | async def request(self, method, uri, params=None, body=None, headers=None, auth=False): 145 | """ 发起请求 146 | @param method 请求方法 GET / POST / DELETE / PUT 147 | @param uri 请求uri 148 | @param params dict 请求query参数 149 | @param body dict 请求body数据 150 | @param headers 请求http头 151 | @param auth boolean 是否需要加入权限校验 152 | """ 153 | if params: 154 | query = "&".join(["{}={}".format(k, params[k]) for k in sorted(params.keys())]) 155 | uri += "?" + query 156 | url = urljoin(self._host, uri) 157 | 158 | # 增加签名 159 | if auth: 160 | timestamp = str(time.time()).split(".")[0] + "." + str(time.time()).split(".")[1][:3] 161 | if body: 162 | body = json.dumps(body) 163 | else: 164 | body = "" 165 | message = str(timestamp) + str.upper(method) + uri + str(body) 166 | mac = hmac.new(bytes(self._secret_key, encoding="utf8"), bytes(message, encoding="utf-8"), 167 | digestmod="sha256") 168 | d = mac.digest() 169 | sign = base64.b64encode(d) 170 | 171 | if not headers: 172 | headers = {} 173 | headers["Content-Type"] = "application/json" 174 | headers["OK-ACCESS-KEY"] = self._access_key.encode().decode() 175 | headers["OK-ACCESS-SIGN"] = sign.decode() 176 | headers["OK-ACCESS-TIMESTAMP"] = str(timestamp) 177 | headers["OK-ACCESS-PASSPHRASE"] = self._passphrase 178 | 179 | _, success, error = await AsyncHttpRequests.fetch(method, url, body=body, headers=headers, timeout=10) 180 | return success, error 181 | 182 | 183 | class OKExFutureTrade(Websocket): 184 | """ OKEX Future Trade module 185 | """ 186 | 187 | def __init__(self, account, strategy, symbol, host=None, wss=None, access_key=None, secret_key=None, 188 | passphrase=None, order_update_callback=None, position_update_callback=None): 189 | """ 初始化 190 | @param account 账户 191 | @param strategy 策略名称 192 | @param symbol 交易对(合约名称) 193 | @param host HTTP请求主机地址 194 | @param wss websocket连接地址 195 | @param access_key ACCESS KEY 196 | @param secret_key SECRET KEY 197 | @param passphrase 密码 198 | @param order_update_callback 订单更新回调 199 | @param position_update_callback 持仓更新回调 200 | """ 201 | self._account = account 202 | self._strategy = strategy 203 | self._platform = OKEX_FUTURE 204 | self._symbol = symbol 205 | self._host = host if host else "https://www.okex.com" 206 | self._wss = wss if wss else "wss://real.okex.com:10442/ws/v3" 207 | self._access_key = access_key 208 | self._secret_key = secret_key 209 | self._passphrase = passphrase 210 | 211 | self._order_update_callback = order_update_callback 212 | self._position_update_callback = position_update_callback 213 | 214 | super(OKExFutureTrade, self).__init__(self._wss, send_hb_interval=5) 215 | self.heartbeat_msg = "ping" 216 | 217 | self._orders = {} # 订单 218 | self._position = Position(self._platform, self._account, strategy, symbol) # 仓位 219 | 220 | # 初始化 REST API 对象 221 | self._rest_api = OKExFutureRestAPI(self._host, self._access_key, self._secret_key, self._passphrase) 222 | 223 | self.initialize() 224 | 225 | @property 226 | def position(self): 227 | return copy.copy(self._position) 228 | 229 | @property 230 | def orders(self): 231 | return copy.copy(self._orders) 232 | 233 | async def connected_callback(self): 234 | """ 建立连接之后,授权登陆,然后订阅order和position 235 | """ 236 | # 身份验证 237 | timestamp = str(time.time()).split(".")[0] + "." + str(time.time()).split(".")[1][:3] 238 | message = str(timestamp) + "GET" + "/users/self/verify" 239 | mac = hmac.new(bytes(self._secret_key, encoding="utf8"), bytes(message, encoding="utf8"), digestmod="sha256") 240 | d = mac.digest() 241 | signature = base64.b64encode(d).decode() 242 | data = { 243 | "op": "login", 244 | "args": [self._access_key, self._passphrase, timestamp, signature] 245 | } 246 | await self.ws.send_json(data) 247 | 248 | # 获取当前等待成交和部分成交的订单信息 249 | result, error = await self._rest_api.get_order_list(self._symbol, 6) 250 | if error: 251 | return 252 | for order_info in result["order_info"]: 253 | order = self._update_order(order_info) 254 | if self._order_update_callback: 255 | SingleTask.run(self._order_update_callback, order) 256 | 257 | # 获取当前持仓 258 | position, error = await self._rest_api.get_position(self._symbol) 259 | if error: 260 | return 261 | if len(position["holding"]) > 0: 262 | self._update_position(position["holding"][0]) 263 | if self._position_update_callback: 264 | SingleTask.run(self._position_update_callback, self.position) 265 | 266 | @async_method_locker("process_binary.locker") 267 | async def process_binary(self, raw): 268 | """ 处理websocket上接收到的消息 269 | @param raw 原始的压缩数据 270 | """ 271 | decompress = zlib.decompressobj(-zlib.MAX_WBITS) 272 | msg = decompress.decompress(raw) 273 | msg += decompress.flush() 274 | msg = msg.decode() 275 | if msg == "pong": # 心跳返回 276 | return 277 | msg = json.loads(msg) 278 | # logger.debug("msg:", msg, caller=self) 279 | 280 | # 登陆成功之后再订阅数据 281 | if msg.get("event") == "login": 282 | if not msg.get("success"): 283 | logger.error("websocket login error!", caller=self) 284 | return 285 | logger.info("Websocket connection authorized successfully.", caller=self) 286 | 287 | # 订阅account, order, position 288 | ch_account = "futures/account:BTC" 289 | ch_order = "futures/order:{symbol}".format(symbol=self._symbol) 290 | ch_position = "futures/position:{symbol}".format(symbol=self._symbol) 291 | data = { 292 | "op": "subscribe", 293 | "args": [ch_account, ch_order, ch_position] 294 | } 295 | await self.ws.send_json(data) 296 | logger.info("subscribe account/order/position successfully.", caller=self) 297 | return 298 | 299 | table = msg.get("table") 300 | if table not in ["futures/order", "futures/position"]: 301 | return 302 | 303 | for data in msg["data"]: 304 | if table == "futures/order": 305 | order = self._update_order(data) 306 | if self._order_update_callback: 307 | await asyncio.get_event_loop().create_task(self._order_update_callback(order)) 308 | 309 | elif table == "futures/position": 310 | self._update_position(data) 311 | if self._position_update_callback: 312 | await asyncio.get_event_loop().create_task(self._position_update_callback(self.position)) 313 | 314 | async def create_order(self, action, price, quantity, order_type=ORDER_TYPE_LIMIT): 315 | """ 创建订单 316 | @param action 交易方向 BUY/SELL 317 | @param price 委托价格 318 | @param quantity 委托数量(可以是正数,也可以是复数) 319 | @param order_type 委托类型 limit/market 320 | """ 321 | if int(quantity) > 0: 322 | if action == ORDER_ACTION_BUY: 323 | trade_type = "1" 324 | else: 325 | trade_type = "3" 326 | else: 327 | if action == ORDER_ACTION_BUY: 328 | trade_type = "4" 329 | else: 330 | trade_type = "2" 331 | quantity = abs(int(quantity)) 332 | result, error = await self._rest_api.create_order(self._symbol, trade_type, price, quantity) 333 | if error: 334 | return None, error 335 | return result["order_id"], None 336 | 337 | async def revoke_order(self, *order_nos): 338 | """ 撤销订单 339 | @param order_nos 订单号,可传入任意多个,如果不传入,那么就撤销所有订单 340 | * NOTE: 单次调用最多只能撤销100个订单,如果订单超过100个,请多次调用 341 | """ 342 | # 如果传入order_nos为空,即撤销全部委托单 343 | if len(order_nos) == 0: 344 | result, error = await self._rest_api.get_order_list(self._symbol, 6) 345 | if error: 346 | return False, error 347 | for order_info in result["order_info"]: 348 | order_no = order_info["order_id"] 349 | _, error = await self._rest_api.revoke_order(self._symbol, order_no) 350 | if error: 351 | return False, error 352 | return True, None 353 | 354 | # 如果传入order_nos为一个委托单号,那么只撤销一个委托单 355 | if len(order_nos) == 1: 356 | success, error = await self._rest_api.revoke_order(self._symbol, order_nos[0]) 357 | if error: 358 | return order_nos[0], error 359 | else: 360 | return order_nos[0], None 361 | 362 | # 如果传入order_nos数量大于1,那么就批量撤销传入的委托单 363 | if len(order_nos) > 1: 364 | success, error = [], [] 365 | for order_no in order_nos: 366 | _, e = await self._rest_api.revoke_order(self._symbol, order_no) 367 | if e: 368 | error.append((order_no, e)) 369 | else: 370 | success.append(order_no) 371 | return success, error 372 | 373 | async def get_open_order_nos(self): 374 | """ 获取未完全成交订单号列表 375 | """ 376 | success, error = await self._rest_api.get_order_list(self._symbol, 6) 377 | if error: 378 | return None, error 379 | else: 380 | order_nos = [] 381 | for order_info in success["order_info"]: 382 | order_nos.append(order_info["order_id"]) 383 | return order_nos, None 384 | 385 | def _update_order(self, order_info): 386 | """ 更新订单信息 387 | @param order_info 订单信息 388 | """ 389 | order_no = str(order_info["order_id"]) 390 | state = order_info["state"] 391 | remain = int(order_info["size"]) - int(order_info["filled_qty"]) 392 | ctime = tools.utctime_str_to_mts(order_info["timestamp"]) 393 | if state == "-2": 394 | status = ORDER_STATUS_FAILED 395 | elif state == "-1": 396 | status = ORDER_STATUS_CANCELED 397 | elif state == "0": 398 | status = ORDER_STATUS_SUBMITTED 399 | elif state == "1": 400 | status = ORDER_STATUS_PARTIAL_FILLED 401 | elif state == "2": 402 | status = ORDER_STATUS_FILLED 403 | else: 404 | return None 405 | 406 | order = self._orders.get(order_no) 407 | if not order: 408 | info = { 409 | "platform": self._platform, 410 | "account": self._account, 411 | "strategy": self._strategy, 412 | "order_no": order_no, 413 | "action": ORDER_ACTION_BUY if order_info["type"] in ["1", "4"] else ORDER_ACTION_SELL, 414 | "symbol": self._symbol, 415 | "price": order_info["price"], 416 | "quantity": order_info["size"], 417 | "trade_type": int(order_info["type"]) 418 | } 419 | order = Order(**info) 420 | order.remain = remain 421 | order.status = status 422 | order.avg_price = order_info["price_avg"] 423 | order.ctime = ctime 424 | order.utime = ctime 425 | self._orders[order_no] = order 426 | if state in ["-1", "2"]: 427 | self._orders.pop(order_no) 428 | return order 429 | 430 | def _update_position(self, position_info): 431 | """ 更新持仓信息 432 | @param position_info 持仓信息 433 | """ 434 | self._position.long_quantity = int(position_info["long_qty"]) 435 | self._position.long_avg_price = position_info["long_avg_cost"] 436 | self._position.short_quantity = int(position_info["short_qty"]) 437 | self._position.short_avg_price = position_info["short_avg_cost"] 438 | self._position.liquid_price = position_info["liquidation_price"] 439 | self._position.utime = tools.utctime_str_to_mts(position_info["updated_at"]) 440 | -------------------------------------------------------------------------------- /quant/event.py: -------------------------------------------------------------------------------- 1 | # -*— coding:utf-8 -*- 2 | 3 | """ 4 | 事件处理中心 5 | 6 | Author: HuangTao 7 | Date: 2018/05/04 8 | Update: 2018/09/26 1. 优化回调函数由exchange和routing_key确定; 9 | 2018/11/23 1. 事件增加批量订阅消息类型; 10 | 2018/11/28 1. 增加断线重连机制; 11 | """ 12 | 13 | import json 14 | import asyncio 15 | 16 | import aioamqp 17 | 18 | from quant.utils import tools 19 | from quant.utils import logger 20 | from quant.config import config 21 | from quant.tasks import LoopRunTask, SingleTask 22 | from quant.utils.decorator import async_method_locker 23 | from quant.market import Orderbook, Trade, Kline 24 | 25 | 26 | __all__ = ("EventCenter", "EventConfig", "EventHeartbeat", "EventAsset", "EventOrder", "EventKline", "EventKline5Min", 27 | "EventKline15Min", "EventOrderbook", "EventTrade") 28 | 29 | 30 | class Event: 31 | """ 事件 32 | """ 33 | 34 | def __init__(self, name=None, exchange=None, queue=None, routing_key=None, pre_fetch_count=1, data=None): 35 | """ 初始化 36 | @param name 事件名 37 | @param exchange 事件被投放的RabbitMQ交换机 38 | @param queue 事件被路由的RabbitMQ队列 39 | @param routing_key 路由规则 40 | @param pre_fetch_count 每次从消息队列里获取处理的消息条数,越多处理效率越高,但同时消耗内存越大,对进程压力也越大 41 | @param data 待发布事件的数据 42 | """ 43 | self._name = name 44 | self._exchange = exchange 45 | self._queue = queue 46 | self._routing_key = routing_key 47 | self._pre_fetch_count = pre_fetch_count 48 | self._data = data 49 | self._callback = None # 事件回调函数 50 | 51 | @property 52 | def name(self): 53 | return self._name 54 | 55 | @property 56 | def exchange(self): 57 | return self._exchange 58 | 59 | @property 60 | def queue(self): 61 | return self._queue 62 | 63 | @property 64 | def routing_key(self): 65 | return self._routing_key 66 | 67 | @property 68 | def prefetch_count(self): 69 | return self._pre_fetch_count 70 | 71 | @property 72 | def data(self): 73 | return self._data 74 | 75 | def dumps(self): 76 | """ 导出Json格式的数据 77 | """ 78 | d = { 79 | "n": self.name, 80 | "d": self.data 81 | } 82 | return json.dumps(d) 83 | 84 | def loads(self, b): 85 | """ 加载Json格式的bytes数据 86 | @param b bytes类型的数据 87 | """ 88 | d = json.loads(b) 89 | self._name = d.get("n") 90 | self._data = d.get("d") 91 | return d 92 | 93 | def parse(self): 94 | """ 解析self._data数据 95 | """ 96 | raise NotImplemented 97 | 98 | def subscribe(self, callback, multi=False): 99 | """ 订阅此事件 100 | @param callback 回调函数 101 | @param multi 是否批量订阅消息,即routing_key为批量匹配 102 | """ 103 | from quant.quant import quant 104 | self._callback = callback 105 | SingleTask.run(quant.event_center.subscribe, self, self.callback, multi) 106 | 107 | def publish(self): 108 | """ 发布此事件 109 | """ 110 | from quant.quant import quant 111 | SingleTask.run(quant.event_center.publish, self) 112 | 113 | async def callback(self, exchange, routing_key, body): 114 | """ 事件回调 115 | @param exchange 事件被投放的RabbitMQ交换机 116 | @param routing_key 路由规则 117 | @param body 从RabbitMQ接收到的bytes类型数据 118 | """ 119 | self._exchange = exchange 120 | self._routing_key = routing_key 121 | self.loads(body) 122 | o = self.parse() 123 | await self._callback(o) 124 | 125 | def __str__(self): 126 | info = "EVENT: name={n}, exchange={e}, queue={q}, routing_key={r}, data={d}".format( 127 | e=self.exchange, q=self.queue, r=self.routing_key, n=self.name, d=self.data) 128 | return info 129 | 130 | def __repr__(self): 131 | return str(self) 132 | 133 | 134 | class EventConfig(Event): 135 | """ 配置更新事件 136 | * NOTE: 137 | 订阅:配置模块 138 | 发布:管理工具 139 | """ 140 | EXCHANGE = "config" 141 | QUEUE = None 142 | NAME = "EVENT_CONFIG" 143 | 144 | def __init__(self, server_id=None, params=None): 145 | """ 初始化 146 | """ 147 | routing_key = "{server_id}".format(server_id=server_id) 148 | self.ROUTING_KEY = routing_key 149 | self.server_id = server_id 150 | self.params = params 151 | data = { 152 | "server_id": server_id, 153 | "params": params 154 | } 155 | super(EventConfig, self).__init__(data) 156 | 157 | def parse(self): 158 | """ 解析self._data数据 159 | """ 160 | self.server_id = self._data.get("server_id") 161 | self.params = self._data.get("params") 162 | 163 | 164 | class EventHeartbeat(Event): 165 | """ 服务心跳事件 166 | * NOTE: 167 | 订阅:监控模块 168 | 发布:业务服务进程 169 | """ 170 | EXCHANGE = "heartbeat" 171 | QUEUE = None 172 | NAME = "EVENT_HEARTBEAT" 173 | 174 | def __init__(self, server_id=None, count=None): 175 | """ 初始化 176 | @param server_id 服务进程id 177 | @param count 心跳次数 178 | """ 179 | self.server_id = server_id 180 | self.count = count 181 | data = { 182 | "server_id": server_id, 183 | "count": count 184 | } 185 | super(EventHeartbeat, self).__init__(data) 186 | 187 | def parse(self): 188 | """ 解析self._data数据 189 | """ 190 | self.server_id = self._data.get("server_id") 191 | self.count = self._data.get("count") 192 | 193 | 194 | class EventAsset(Event): 195 | """ 资产更新事件 196 | * NOTE: 197 | 订阅:业务模块 198 | 发布:Asset资产服务器 199 | """ 200 | EXCHANGE = "asset" 201 | QUEUE = None 202 | NAME = "EVENT_ASSET" 203 | 204 | def __init__(self, platform=None, account=None, assets=None, timestamp=None): 205 | """ 初始化 206 | """ 207 | timestamp = timestamp or tools.get_cur_timestamp_ms() 208 | self.ROUTING_KEY = "{platform}.{account}".format(platform=platform, account=account) 209 | self.platform = platform 210 | self.account = account 211 | self.assets = assets 212 | self.timestamp = timestamp 213 | data = { 214 | "platform": platform, 215 | "account": account, 216 | "assets": assets, 217 | "timestamp": timestamp 218 | } 219 | super(EventAsset, self).__init__(data) 220 | 221 | def parse(self): 222 | """ 解析self._data数据 223 | """ 224 | self.platform = self._data.get("platform") 225 | self.account = self._data.get("account") 226 | self.assets = self._data.get("assets") 227 | self.timestamp = self._data.get("timestamp") 228 | 229 | 230 | class EventOrder(Event): 231 | """ 委托单事件 232 | * NOTE: 233 | 订阅:订单管理器 234 | 发布:业务服务器 235 | """ 236 | EXCHANGE = "order" 237 | QUEUE = None 238 | NAME = "ORDER" 239 | 240 | def __init__(self, platform=None, account=None, strategy=None, order_no=None, symbol=None, action=None, price=None, 241 | quantity=None, status=None, order_type=None, timestamp=None): 242 | """ 初始化 243 | @param platform 交易平台名称 244 | @param account 交易账户 245 | @param strategy 策略名 246 | @param order_no 订单号 247 | @param symbol 交易对 248 | @param action 操作类型 249 | @param price 限价单价格 250 | @param quantity 限价单数量 251 | @param status 订单状态 252 | @param order_type 订单类型 253 | @param timestamp 时间戳(毫秒) 254 | """ 255 | data = { 256 | "platform": platform, 257 | "account": account, 258 | "strategy": strategy, 259 | "order_no": order_no, 260 | "symbol": symbol, 261 | "action": action, 262 | "price": price, 263 | "quantity": quantity, 264 | "status": status, 265 | "order_type": order_type, 266 | "timestamp": timestamp 267 | } 268 | super(EventOrder, self).__init__(data) 269 | 270 | def parse(self): 271 | """ 解析self._data数据 272 | """ 273 | self.platform = self._data.get("platform") 274 | self.account = self._data.get("account") 275 | self.strategy = self._data.get("strategy") 276 | self.order_no = self._data.get("order_no") 277 | self.symbol = self._data.get("symbol") 278 | self.action = self._data.get("action") 279 | self.price = self._data.get("price") 280 | self.quantity = self._data.get("quantity") 281 | self.status = self._data.get("status") 282 | self.order_type = self._data.get("order_type") 283 | self.timestamp = self._data.get("timestamp") 284 | 285 | 286 | class EventKline(Event): 287 | """ K线更新事件 1分钟 288 | """ 289 | EXCHANGE = "kline" 290 | QUEUE = None 291 | NAME = "EVENT_KLINE" 292 | PRE_FETCH_COUNT = 20 293 | 294 | def __init__(self, platform=None, symbol=None, open=None, high=None, low=None, close=None, volume=None, 295 | timestamp=None): 296 | """ 初始化 297 | @param platform 平台 298 | @param symbol 交易对 299 | @param open 开盘价 300 | @param high 最高价 301 | @param low 最低价 302 | @param close 收盘价 303 | @param volume 成交量 304 | @param timestamp 时间戳 305 | """ 306 | routing_key = "{platform}.{symbol}".format(platform=platform, symbol=symbol) 307 | data = { 308 | "platform": platform, 309 | "symbol": symbol, 310 | "open": open, 311 | "high": high, 312 | "low": low, 313 | "close": close, 314 | "volume": volume, 315 | "timestamp": timestamp 316 | } 317 | super(EventKline, self).__init__(name="EVENT_KLINE", exchange="Kline", routing_key=routing_key, data=data) 318 | 319 | def parse(self): 320 | """ 解析self._data数据 321 | """ 322 | kline = Kline(**self.data) 323 | return kline 324 | 325 | 326 | class EventKline5Min(Event): 327 | """ K线更新事件 5分钟 328 | """ 329 | EXCHANGE = "kline.5min" 330 | QUEUE = None 331 | NAME = "EVENT_KLINE_5MIN" 332 | PRE_FETCH_COUNT = 20 333 | 334 | def __init__(self, platform=None, symbol=None, open=None, high=None, low=None, close=None, volume=None, 335 | timestamp=None): 336 | """ 初始化 337 | @param platform 平台 比如: bitfinex 338 | @param symbol 交易对 339 | @param open 开盘价 340 | @param high 最高价 341 | @param low 最低价 342 | @param close 收盘价 343 | @param volume 成交量 344 | @param timestamp 时间戳 345 | """ 346 | routing_key = "{platform}.{symbol}".format(platform=platform, symbol=symbol) 347 | self.ROUTING_KEY = routing_key 348 | self.platform = platform 349 | self.symbol = symbol 350 | self.open = open 351 | self.high = high 352 | self.low = low 353 | self.close = close 354 | self.volume = volume 355 | self.timestamp = timestamp 356 | data = [platform, symbol, open, high, low, close, volume, timestamp] 357 | super(EventKline5Min, self).__init__(data) 358 | 359 | def parse(self): 360 | """ 解析self._data数据 361 | """ 362 | self.platform = self._data[0] 363 | self.symbol = self._data[1] 364 | self.open = self._data[2] 365 | self.high = self._data[3] 366 | self.low = self._data[4] 367 | self.close = self._data[5] 368 | self.volume = self._data[6] 369 | self.timestamp = self._data[7] 370 | 371 | 372 | class EventKline15Min(Event): 373 | """ K线更新事件 5分钟 374 | """ 375 | EXCHANGE = "kline.15min" 376 | QUEUE = None 377 | NAME = "EVENT_KLINE_15MIN" 378 | PRE_FETCH_COUNT = 20 379 | 380 | def __init__(self, platform=None, symbol=None, open=None, high=None, low=None, close=None, volume=None, 381 | timestamp=None): 382 | """ 初始化 383 | @param platform 平台 比如: bitfinex 384 | @param symbol 交易对 385 | @param open 开盘价 386 | @param high 最高价 387 | @param low 最低价 388 | @param close 收盘价 389 | @param volume 成交量 390 | @param timestamp 时间戳 391 | """ 392 | routing_key = "{platform}.{symbol}".format(platform=platform, symbol=symbol) 393 | self.ROUTING_KEY = routing_key 394 | self.platform = platform 395 | self.symbol = symbol 396 | self.open = open 397 | self.high = high 398 | self.low = low 399 | self.close = close 400 | self.volume = volume 401 | self.timestamp = timestamp 402 | data = [platform, symbol, open, high, low, close, volume, timestamp] 403 | super(EventKline15Min, self).__init__(data) 404 | 405 | def parse(self): 406 | """ 解析self._data数据 407 | """ 408 | self.platform = self._data[0] 409 | self.symbol = self._data[1] 410 | self.open = self._data[2] 411 | self.high = self._data[3] 412 | self.low = self._data[4] 413 | self.close = self._data[5] 414 | self.volume = self._data[6] 415 | self.timestamp = self._data[7] 416 | 417 | 418 | class EventOrderbook(Event): 419 | """ 订单薄事件 420 | * NOTE: 421 | 订阅:业务模块 422 | 发布:行情服务 423 | """ 424 | 425 | def __init__(self, platform=None, symbol=None, asks=None, bids=None, timestamp=None): 426 | """ 初始化 427 | """ 428 | routing_key = "{platform}.{symbol}".format(platform=platform, symbol=symbol) 429 | data = { 430 | "platform": platform, 431 | "symbol": symbol, 432 | "asks": asks, 433 | "bids": bids, 434 | "timestamp": timestamp 435 | } 436 | super(EventOrderbook, self).__init__(name="EVENT_ORDERBOOK", exchange="Orderbook", routing_key=routing_key, 437 | data=data) 438 | 439 | def parse(self): 440 | """ 解析self._data数据 441 | """ 442 | orderbook = Orderbook(**self.data) 443 | return orderbook 444 | 445 | 446 | class EventTrade(Event): 447 | """ 交易事件 448 | * NOTE: 449 | 订阅:业务模块 450 | 发布:行情服务 451 | """ 452 | 453 | def __init__(self, platform=None, symbol=None, action=None, price=None, quantity=None, timestamp=None): 454 | """ 初始化 455 | """ 456 | routing_key = "{platform}.{symbol}".format(platform=platform, symbol=symbol) 457 | data = { 458 | "platform": platform, 459 | "symbol": symbol, 460 | "action": action, 461 | "price": price, 462 | "quantity": quantity, 463 | "timestamp": timestamp 464 | } 465 | super(EventTrade, self).__init__(name="EVENT_TRADE", exchange="Trade", routing_key=routing_key, data=data) 466 | 467 | def parse(self): 468 | """ 解析self._data数据 469 | """ 470 | trade = Trade(**self.data) 471 | return trade 472 | 473 | 474 | class EventCenter: 475 | """ 事件处理中心 476 | """ 477 | 478 | def __init__(self): 479 | self._host = config.rabbitmq.get("host", "localhost") 480 | self._port = config.rabbitmq.get("port", 5672) 481 | self._username = config.rabbitmq.get("username", "guest") 482 | self._password = config.rabbitmq.get("password", "guest") 483 | self._protocol = None 484 | self._channel = None # 连接通道 485 | self._connected = False # 是否连接成功 486 | self._subscribers = [] # 订阅者 [(event, callback, multi), ...] 487 | self._event_handler = {} # 事件对应的处理函数 {"exchange:routing_key": [callback_function, ...]} 488 | 489 | LoopRunTask.register(self._check_connection, 10) # 检查连接是否正常 490 | 491 | def initialize(self): 492 | """ 初始化 493 | """ 494 | asyncio.get_event_loop().run_until_complete(self.connect()) 495 | 496 | @async_method_locker("EventCenter.subscribe") 497 | async def subscribe(self, event: Event, callback=None, multi=False): 498 | """ 注册事件 499 | @param event 事件 500 | @param callback 回调函数 501 | @param multi 是否批量订阅消息,即routing_key为批量匹配 502 | """ 503 | logger.info("NAME:", event.name, "EXCHANGE:", event.exchange, "QUEUE:", event.queue, "ROUTING_KEY:", 504 | event.routing_key, caller=self) 505 | self._subscribers.append((event, callback, multi)) 506 | 507 | async def publish(self, event): 508 | """ 发布消息 509 | @param event 发布的事件对象 510 | """ 511 | if not self._connected: 512 | logger.warn("RabbitMQ not ready right now!", caller=self) 513 | return 514 | data = event.dumps() 515 | await self._channel.basic_publish(payload=data, exchange_name=event.exchange, routing_key=event.routing_key) 516 | 517 | async def connect(self, reconnect=False): 518 | """ 建立TCP连接 519 | @param reconnect 是否是断线重连 520 | """ 521 | logger.info("host:", self._host, "port:", self._port, caller=self) 522 | if self._connected: 523 | return 524 | 525 | # 建立连接 526 | try: 527 | transport, protocol = await aioamqp.connect(host=self._host, port=self._port, login=self._username, 528 | password=self._password) 529 | except Exception as e: 530 | logger.error("connection error:", e, caller=self) 531 | return 532 | finally: 533 | # 如果已经有连接已经建立好,那么直接返回(此情况在连续发送了多个连接请求后,若干个连接建立好了连接) 534 | if self._connected: 535 | return 536 | channel = await protocol.channel() 537 | self._protocol = protocol 538 | self._channel = channel 539 | self._connected = True 540 | logger.info("Rabbitmq initialize success!", caller=self) 541 | 542 | # 创建默认的交换机 543 | exchanges = ["Orderbook", "Trade", "Kline", ] 544 | for name in exchanges: 545 | await self._channel.exchange_declare(exchange_name=name, type_name="topic") 546 | logger.info("create default exchanges success!", caller=self) 547 | 548 | # 如果是断线重连,那么直接绑定队列并开始消费数据,如果是首次连接,那么等待5秒再绑定消费(等待程序各个模块初始化完成) 549 | if reconnect: 550 | self._bind_and_consume() 551 | else: 552 | asyncio.get_event_loop().call_later(5, self._bind_and_consume) 553 | 554 | def _bind_and_consume(self): 555 | """ 绑定并开始消费事件消息 556 | """ 557 | async def do_them(): 558 | for event, callback, multi in self._subscribers: 559 | await self._initialize(event, callback, multi) 560 | SingleTask.run(do_them) 561 | 562 | async def _initialize(self, event: Event, callback=None, multi=False): 563 | """ 创建/绑定交易所相关消息队列 564 | @param event 订阅的事件 565 | @param callback 回调函数 566 | @param multi 是否批量订阅消息,即routing_key为批量匹配 567 | """ 568 | if event.queue: 569 | await self._channel.queue_declare(queue_name=event.queue) 570 | queue_name = event.queue 571 | else: 572 | result = await self._channel.queue_declare(exclusive=True) 573 | queue_name = result["queue"] 574 | await self._channel.queue_bind(queue_name=queue_name, exchange_name=event.exchange, 575 | routing_key=event.routing_key) 576 | await self._channel.basic_qos(prefetch_count=event.prefetch_count) # 消息窗口大小,越大,消息推送越快,但也需要处理越快 577 | if callback: 578 | if multi: 579 | # 消费队列,routing_key为批量匹配,无需ack 580 | await self._channel.basic_consume(callback=callback, queue_name=queue_name, no_ack=True) 581 | logger.info("multi message queue:", queue_name, "callback:", callback, caller=self) 582 | else: 583 | # 消费队列,routing_key唯一确定,需要ack确定 584 | await self._channel.basic_consume(self._on_consume_event_msg, queue_name=queue_name) 585 | logger.info("queue:", queue_name, caller=self) 586 | self._add_event_handler(event, callback) 587 | 588 | async def _on_consume_event_msg(self, channel, body, envelope, properties): 589 | """ 收到订阅的事件消息 590 | @param channel 消息队列通道 591 | @param body 接收到的消息 592 | @param envelope 路由规则 593 | @param properties 消息属性 594 | """ 595 | # logger.debug("exchange:", envelope.exchange_name, "routing_key:", envelope.routing_key, 596 | # "body:", body, caller=self) 597 | try: 598 | key = "{exchange}:{routing_key}".format(exchange=envelope.exchange_name, routing_key=envelope.routing_key) 599 | # 执行事件回调函数 600 | funcs = self._event_handler[key] 601 | for func in funcs: 602 | SingleTask.run(func, envelope.exchange_name, envelope.routing_key, body) 603 | except: 604 | logger.error("event handle error! body:", body, caller=self) 605 | return 606 | finally: 607 | await self._channel.basic_client_ack(delivery_tag=envelope.delivery_tag) # response ack 608 | 609 | def _add_event_handler(self, event: Event, callback): 610 | """ 增加事件处理回调函数 611 | * NOTE: {"exchange:routing_key": [callback_function, ...]} 612 | """ 613 | key = "{exchange}:{routing_key}".format(exchange=event.exchange, routing_key=event.routing_key) 614 | if key in self._event_handler: 615 | self._event_handler[key].append(callback) 616 | else: 617 | self._event_handler[key] = [callback] 618 | logger.info("event handlers:", self._event_handler.keys(), caller=self) 619 | 620 | async def _check_connection(self, *args, **kwargs): 621 | """ 检查连接是否正常,如果连接已经断开,那么立即发起连接 622 | """ 623 | if self._connected and self._channel and self._channel.is_open: 624 | logger.debug("RabbitMQ connection ok.", caller=self) 625 | return 626 | logger.error("CONNECTION LOSE! START RECONNECT RIGHT NOW!", caller=self) 627 | self._connected = False 628 | self._protocol = None 629 | self._channel = None 630 | self._event_handler = {} 631 | SingleTask.run(self.connect, reconnect=True) 632 | --------------------------------------------------------------------------------