├── purequant ├── __init__.py ├── exchange │ ├── binance │ │ ├── __init__.py │ │ ├── binance_futures.py │ │ ├── binance_swap.py │ │ └── binance_spot.py │ ├── huobi │ │ ├── __init__.py │ │ ├── util.py │ │ └── websocket.py │ └── okex │ │ ├── __init__.py │ │ ├── index_api.py │ │ ├── system_api.py │ │ ├── exceptions.py │ │ ├── utils.py │ │ ├── information_api.py │ │ ├── client.py │ │ ├── account_api.py │ │ ├── lever_api.py │ │ ├── consts.py │ │ ├── option_api.py │ │ ├── spot_api.py │ │ └── swap_api.py ├── example │ ├── plot_signal │ │ ├── __init__.py │ │ ├── ATR.png │ │ ├── EMA.png │ │ ├── MACD.png │ │ ├── 双均线.png │ │ ├── 布林带.png │ │ ├── STOCHRSI.png │ │ ├── 最高价、最低价.png │ │ ├── plot_signal.py │ │ └── config.json │ ├── boll_breakthrough_strategy │ │ ├── __init__.py │ │ ├── config.json │ │ └── boll_breakthrough_strategy.py │ └── double_moving_average_strategy │ │ ├── __init__.py │ │ ├── config.json │ │ ├── spot_double_moving_average_strategy.py │ │ ├── usd_futures_double_ma_strategy.py │ │ ├── usdt_futures_double_moving_average_strategy.py │ │ ├── fastly_backtest_doubale_ma_strategy.py │ │ └── multi-parameter_fast_backtest.py ├── const.py ├── exceptions.py ├── subscribe.py ├── market.py ├── push.py ├── logger.py ├── time.py ├── position.py ├── config.py ├── synchronize.py └── monitor.py ├── .gitattributes ├── talib安装文件windows64Bit python3.7 ├── TA_Lib-0.4.18-cp37-cp37m-win_amd64.whl └── talib库安装指南.md ├── LICENSE.txt ├── setup.py └── 版本更新记录.md /purequant/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /purequant/exchange/binance/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /purequant/exchange/huobi/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /purequant/example/plot_signal/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /purequant/example/boll_breakthrough_strategy/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /purequant/example/double_moving_average_strategy/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /purequant/example/plot_signal/ATR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenyanping/PureQuant/HEAD/purequant/example/plot_signal/ATR.png -------------------------------------------------------------------------------- /purequant/example/plot_signal/EMA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenyanping/PureQuant/HEAD/purequant/example/plot_signal/EMA.png -------------------------------------------------------------------------------- /purequant/example/plot_signal/MACD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenyanping/PureQuant/HEAD/purequant/example/plot_signal/MACD.png -------------------------------------------------------------------------------- /purequant/example/plot_signal/双均线.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenyanping/PureQuant/HEAD/purequant/example/plot_signal/双均线.png -------------------------------------------------------------------------------- /purequant/example/plot_signal/布林带.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenyanping/PureQuant/HEAD/purequant/example/plot_signal/布林带.png -------------------------------------------------------------------------------- /purequant/example/plot_signal/STOCHRSI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenyanping/PureQuant/HEAD/purequant/example/plot_signal/STOCHRSI.png -------------------------------------------------------------------------------- /purequant/example/plot_signal/最高价、最低价.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenyanping/PureQuant/HEAD/purequant/example/plot_signal/最高价、最低价.png -------------------------------------------------------------------------------- /purequant/exchange/okex/__init__.py: -------------------------------------------------------------------------------- 1 | """An unofficial Python wrapper for the OKEx exchange API v3 2 | 3 | .. moduleauthor:: gx_wind 4 | 5 | """ 6 | -------------------------------------------------------------------------------- /talib安装文件windows64Bit python3.7/TA_Lib-0.4.18-cp37-cp37m-win_amd64.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenyanping/PureQuant/HEAD/talib安装文件windows64Bit python3.7/TA_Lib-0.4.18-cp37-cp37m-win_amd64.whl -------------------------------------------------------------------------------- /purequant/const.py: -------------------------------------------------------------------------------- 1 | """ 2 | COLOR 3 | """ 4 | 5 | WHITE = "#FFFFFF" # 白色 6 | RED = "#FF0000" # 红色 7 | GREEN = "#00FF00" # 绿色 8 | BLUE = "#0000FF" # 蓝色 9 | PEONYRED = "#FF00FF" # 牡丹红 10 | CYANBLUE = "#00FFFF" # 青色 11 | YELLOW = "#FFFF00" # 黄色 12 | BLACK = "#000000" # 黑色 13 | SEABLUE = "#70DB93" # 海蓝 14 | CHOCOLATE = "#5C3317" # 巧克力色 15 | GOLDEN = "#CD7F32" # 金色 16 | ORANGE = "#FF7F00" # 橙色 17 | SILVER = "#E6E8FA" # 银色 18 | SKYBLUE = "#3299CC" # 天蓝 19 | PINK = "#FF1CAE" # 粉红 20 | -------------------------------------------------------------------------------- /purequant/exchange/okex/index_api.py: -------------------------------------------------------------------------------- 1 | from .client import Client 2 | from .consts import * 3 | 4 | 5 | class IndexAPI(Client): 6 | 7 | def __init__(self, api_key, api_secret_key, passphrase, use_server_time=False): 8 | Client.__init__(self, api_key, api_secret_key, passphrase, use_server_time) 9 | 10 | # get index constituents 11 | def get_index_constituents(self, instrument_id): 12 | return self._request_without_params(GET, INDEX_GET_CONSTITUENTS + str(instrument_id) + '/constituents') 13 | -------------------------------------------------------------------------------- /purequant/exchange/okex/system_api.py: -------------------------------------------------------------------------------- 1 | from .client import Client 2 | from .consts import * 3 | 4 | 5 | class SystemAPI(Client): 6 | 7 | def __init__(self, api_key, api_secret_key, passphrase, use_server_time=False): 8 | Client.__init__(self, api_key, api_secret_key, passphrase, use_server_time) 9 | 10 | # get system status 11 | def get_system_status(self, status=''): 12 | params = {} 13 | if status: 14 | params['status'] = status 15 | return self._request_with_params(GET, SYSTEM_STATUS, params) 16 | -------------------------------------------------------------------------------- /talib安装文件windows64Bit python3.7/talib库安装指南.md: -------------------------------------------------------------------------------- 1 | # talib库安装指南 2 | 3 | ```python 4 | pip install talib 5 | ``` 6 | 7 | 直接安装 不成功,因为python pip源对应的ta-lib是32位的,不能安装在64位的系统上。 8 | 9 | 所以需要使用如下方法进行安装: 10 | 11 | 在当前文件夹下,shift + 鼠标右键,选择“在此处打开PowerShell窗口”进入命令行,输入: 12 | 13 | ```python 14 | pip install TA_Lib-0.4.18-cp37-cp37m-win_amd64.whl 15 | ``` 16 | 17 | 按回车键 18 | 19 | ```python 20 | Processing c:\users\administrator\downloads\ta_lib-0.4.18-cp37-cp37m-win_amd64.whl 21 | Installing collected packages: TA-Lib 22 | Successfully installed TA-Lib-0.4.18 23 | ``` 24 | 25 | 如显示“Successfully installed TA-Lib-0.4.18”则安装成功。 -------------------------------------------------------------------------------- /purequant/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Error/Exception definition. 3 | 4 | Author: Gary-Hertel 5 | Date: 2020/08/12 6 | Email: interstella.ranger2020@gmail.com 7 | """ 8 | 9 | class CunstomException(Exception): 10 | """自定义异常类""" 11 | defaul_msg = "一个错误发生了!" 12 | 13 | def __init__(self, msg=None): 14 | self.msg = msg if msg is not None else self.defaul_msg 15 | 16 | def __str__(self): 17 | str_msg = "{msg}".format(msg=self.msg) 18 | return str_msg 19 | 20 | class ExchangeError(CunstomException): 21 | defaul_msg = "交易所设置错误!" 22 | 23 | class KlineError(CunstomException): 24 | defaul_msg = "k线周期错误,只支持【1min 3min 5min 15min 30min 1hour 2hour 4hour 6hour 12hour 1day】!" 25 | 26 | class SymbolError(CunstomException): 27 | defaul_msg = "合约ID错误,只可输入当季或者次季合约ID!" 28 | 29 | class DataBankError(CunstomException): 30 | defaul_msg = "数据库设置错误,必须是mysql或mongodb!" 31 | 32 | class MatchError(CunstomException): 33 | defaul_msg = "策略持仓与账户持仓比较失败!" 34 | 35 | class SendOrderError(CunstomException): 36 | defaul_msg = "下单失败!" 37 | 38 | class GetOrderError(CunstomException): 39 | defaul_msg = "查询订单失败!" -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /purequant/example/plot_signal/plot_signal.py: -------------------------------------------------------------------------------- 1 | from purequant.signalize import SIGNALIZE 2 | from purequant.trade import OKEXFUTURES, OKEXSPOT, OKEXSWAP, HUOBISPOT, HUOBIFUTURES, HUOBISWAP 3 | from purequant.const import * 4 | from purequant.config import config 5 | 6 | config.loads('config.json') 7 | symbol = "ETC-USDT-201225" 8 | time_frame = "1d" 9 | okex = OKEXFUTURES("", "", "", symbol) 10 | # huobi = HUOBIFUTURES("", "", symbol) # 火币需填入api 11 | 12 | signalize = SIGNALIZE(okex, symbol, time_frame) # 实例化 13 | signalize.plot_last(color=GOLDEN) 14 | signalize.plot_ma(60, color=PINK) 15 | signalize.plot_ma(90, color=SKYBLUE) 16 | # signalize.plot_atr(14) 17 | # signalize.plot_boll(14) 18 | # signalize.plot_highest(30) 19 | # signalize.plot_macd(12, 26, 9) 20 | # signalize.plot_ema(20) 21 | # signalize.plot_kama(10, color=PINK) 22 | # signalize.plot_kdj(9, 3, 3) 23 | # signalize.plot_lowest(30) 24 | # signalize.plot_obv() 25 | # signalize.plot_rsi(20) 26 | # signalize.plot_roc(20) 27 | # signalize.plot_stochrsi(14, 14, 3) 28 | # signalize.plot_sar() 29 | # signalize.plot_stddev(20) 30 | # signalize.plot_trix(20) 31 | # signalize.plot_volume() 32 | 33 | signalize.show() -------------------------------------------------------------------------------- /purequant/exchange/okex/exceptions.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import json 3 | 4 | class OkexAPIException(Exception): 5 | 6 | def __init__(self, response): 7 | # print(response.text + ', ' + str(response.status_code)) 8 | self.code = 0 9 | try: 10 | json_res = response.json() 11 | except ValueError: 12 | self.message = 'Invalid JSON error message from Okex: {}'.format(response.text) 13 | else: 14 | if "error_code" in json_res.keys() and "error_message" in json_res.keys(): 15 | self.code = json_res['error_code'] 16 | self.message = json_res['error_message'] 17 | else: 18 | self.code = 'None' 19 | self.message = 'System error' 20 | 21 | self.status_code = response.status_code 22 | self.response = response 23 | self.request = getattr(response, 'request', None) 24 | 25 | def __str__(self): # pragma: no cover 26 | return 'API Request Error(error_code=%s): %s' % (self.code, self.message) 27 | 28 | 29 | class OkexRequestException(Exception): 30 | 31 | def __init__(self, message): 32 | self.message = message 33 | 34 | def __str__(self): 35 | return 'OkexRequestException: %s' % self.message 36 | 37 | 38 | class OkexParamsException(Exception): 39 | 40 | def __init__(self, message): 41 | self.message = message 42 | 43 | def __str__(self): 44 | return 'OkexParamsException: %s' % self.message 45 | -------------------------------------------------------------------------------- /purequant/exchange/okex/utils.py: -------------------------------------------------------------------------------- 1 | import hmac 2 | import base64 3 | import datetime 4 | from . import consts as c 5 | 6 | 7 | def sign(message, secret_key): 8 | mac = hmac.new(bytes(secret_key, encoding='utf8'), bytes(message, encoding='utf-8'), digestmod='sha256') 9 | d = mac.digest() 10 | return base64.b64encode(d) 11 | 12 | 13 | def pre_hash(timestamp, method, request_path, body): 14 | return str(timestamp) + str.upper(method) + request_path + body 15 | 16 | 17 | def get_header(api_key, sign, timestamp, passphrase): 18 | header = dict() 19 | header[c.CONTENT_TYPE] = c.APPLICATION_JSON 20 | header[c.OK_ACCESS_KEY] = api_key 21 | header[c.OK_ACCESS_SIGN] = sign 22 | header[c.OK_ACCESS_TIMESTAMP] = str(timestamp) 23 | header[c.OK_ACCESS_PASSPHRASE] = passphrase 24 | 25 | return header 26 | 27 | 28 | def parse_params_to_str(params): 29 | url = '?' 30 | for key, value in params.items(): 31 | url = url + str(key) + '=' + str(value) + '&' 32 | 33 | return url[0:-1] 34 | 35 | 36 | def get_timestamp(): 37 | now = datetime.datetime.utcnow() 38 | t = now.isoformat("T", "milliseconds") 39 | return t + "Z" 40 | 41 | def signature(timestamp, method, request_path, body, secret_key): 42 | if str(body) == '{}' or str(body) == 'None': 43 | body = '' 44 | message = str(timestamp) + str.upper(method) + request_path + str(body) 45 | mac = hmac.new(bytes(secret_key, encoding='utf8'), bytes(message, encoding='utf-8'), digestmod='sha256') 46 | d = mac.digest() 47 | return base64.b64encode(d) 48 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | try: 4 | from setuptools import setup, find_packages 5 | except ImportError: 6 | from distutils.core import setup 7 | 8 | 9 | setup( 10 | name="purequant", 11 | version="0.1.17", 12 | packages=[ 13 | "purequant", 14 | "purequant/exchange/huobi", 15 | "purequant/exchange/okex", 16 | "purequant/exchange/binance", 17 | "purequant/example/double_moving_average_strategy", 18 | "purequant/example/plot_signal", 19 | "purequant/example/boll_breakthrough_strategy" 20 | ], 21 | platforms="any", 22 | description="数字货币程序化交易开源框架,助力中小投资者快速搭建程序化交易系统。", 23 | url="https://github.com/Gary-Hertel/PureQuant", 24 | author="Gary-Hertel", 25 | author_email="interstella.ranger2020@gmail.com", 26 | license="MIT", 27 | keywords=[ 28 | "purequant", "quant", "framework", "okex", "trade", "btc" 29 | ], 30 | install_requires=[ 31 | "chardet>=3.0.4", 32 | "certifi>=2020.4.5.1", 33 | "finplot>=0.9.0", 34 | "idna>=2.9", 35 | "mysql>=0.0.2", 36 | "mysqlclient>=2.0.1", 37 | "mysql-connector-python>=8.0.21", 38 | "numpy>=1.19.1", 39 | "pymongo>=3.10.1", 40 | "requests>=2.23.0", 41 | "six>=1.14.0", 42 | "twilio>=6.44.0", 43 | "urllib3>=1.25.8", 44 | "websockets>=8.1", 45 | "concurrent-log-handler>=0.9.17", 46 | "colorlog>=4.2.1" 47 | ], 48 | package_data={ 49 | "purequant/example/double_moving_average_strategy": ["*.json"], 50 | "purequant/example/plot_signal": ["*.json"], 51 | "purequant/example/boll_breakthrough_strategy": ["*.json"], 52 | }, 53 | ) 54 | -------------------------------------------------------------------------------- /purequant/subscribe.py: -------------------------------------------------------------------------------- 1 | from purequant.config import config 2 | from purequant.exchange.huobi.websocket import subscribe, handle_ws_data 3 | from purequant.exchange.okex.websocket import subscribe_without_login 4 | from purequant.exceptions import * 5 | import asyncio, uuid 6 | 7 | def markets_update(): 8 | print("行情服务器已启动,数据正获取并保存至MongoDB数据库中!") 9 | if config.markets_server_platform == "okex": 10 | try: 11 | url = 'wss://real.okex.com:8443/ws/v3' 12 | channels_list = config.markets_channels_list 13 | task_list = [] 14 | for item in channels_list: 15 | task_list.append(subscribe_without_login(url, item)) 16 | asyncio.run(asyncio.wait(task_list)) 17 | except Exception as e: 18 | print(e) 19 | if config.markets_server_platform == "huobi_futures": 20 | try: 21 | market_url = 'wss://www.hbdm.vn/ws' 22 | access_key = "" 23 | secret_key = "" 24 | channels_list = config.markets_channels_list 25 | market_subs = [] 26 | for item in channels_list: 27 | market_subs.append({"sub": item[0], "id": str(uuid.uuid1())}) 28 | asyncio.run(subscribe(market_url, access_key, secret_key, market_subs, handle_ws_data, auth=False)) 29 | except Exception as e: 30 | print(e) 31 | if config.markets_server_platform == "huobi_swap": 32 | try: 33 | market_url = 'wss://api.hbdm.vn/swap-ws' 34 | access_key = "" 35 | secret_key = "" 36 | channels_list = config.markets_channels_list 37 | market_subs = [] 38 | for item in channels_list: 39 | market_subs.append({"sub": item[0], "id": str(uuid.uuid1())}) 40 | asyncio.run(subscribe(market_url, access_key, secret_key, market_subs, handle_ws_data, auth=False)) 41 | except Exception as e: 42 | print(e) 43 | else: 44 | raise ExchangeError("配置文件中markets sever的platform设置错误!") -------------------------------------------------------------------------------- /purequant/exchange/okex/information_api.py: -------------------------------------------------------------------------------- 1 | from .client import Client 2 | from .consts import * 3 | 4 | 5 | class InformationAPI(Client): 6 | 7 | def __init__(self, api_key, api_secret_key, passphrase, use_server_time=False): 8 | Client.__init__(self, api_key, api_secret_key, passphrase, use_server_time) 9 | 10 | def get_long_short_ratio(self, currency, start='', end='', granularity=''): 11 | params = {} 12 | if start: 13 | params['start'] = start 14 | if end: 15 | params['end'] = end 16 | if granularity: 17 | params['granularity'] = granularity 18 | 19 | return self._request_with_params(GET, INFORMATION + str(currency) + '/long_short_ratio', params) 20 | 21 | def get_volume(self, currency, start='', end='', granularity=''): 22 | params = {} 23 | if start: 24 | params['start'] = start 25 | if end: 26 | params['end'] = end 27 | if granularity: 28 | params['granularity'] = granularity 29 | 30 | return self._request_with_params(GET, INFORMATION + str(currency) + '/volume', params) 31 | 32 | def get_taker(self, currency, start='', end='', granularity=''): 33 | params = {} 34 | if start: 35 | params['start'] = start 36 | if end: 37 | params['end'] = end 38 | if granularity: 39 | params['granularity'] = granularity 40 | 41 | return self._request_with_params(GET, INFORMATION + str(currency) + '/taker', params) 42 | 43 | def get_sentiment(self, currency, start='', end='', granularity=''): 44 | params = {} 45 | if start: 46 | params['start'] = start 47 | if end: 48 | params['end'] = end 49 | if granularity: 50 | params['granularity'] = granularity 51 | 52 | return self._request_with_params(GET, INFORMATION + str(currency) + '/sentiment', params) 53 | 54 | def get_margin(self, currency, start='', end='', granularity=''): 55 | params = {} 56 | if start: 57 | params['start'] = start 58 | if end: 59 | params['end'] = end 60 | if granularity: 61 | params['granularity'] = granularity 62 | 63 | return self._request_with_params(GET, INFORMATION + str(currency) + '/margin', params) 64 | -------------------------------------------------------------------------------- /版本更新记录.md: -------------------------------------------------------------------------------- 1 | # 版本更新记录 2 | 3 | ``` 4 | 0.1.1 5 | 2020-08-24 6 | ~~~~~~~~~~~~~~~~~~ 7 | 1.增加回测功能,一键切换回测与实盘模式。 8 | ``` 9 | 10 | ``` 11 | 0.1.2 12 | 2020-08-25 13 | ~~~~~~~~~~~~~~~~~~ 14 | 1.修复trade模块中订单状态为部分成交然后进行重发时委托数量为原下单数量减去已成交数量。 15 | 2.修正了storage模块中一个拼写错误。 16 | ``` 17 | 18 | ``` 19 | 0.1.5 20 | 2020-08-27 21 | ~~~~~~~~~~~~~~~~~~ 22 | 1.修正示例策略。 23 | 2.修正trade模块中sell和sellshort函数的时间撤单以及撤单后超价下单的设置。 24 | ``` 25 | 26 | ``` 27 | 0.1.8 28 | 2020-08-29 29 | ~~~~~~~~~~~~~~~~~~ 30 | 1.优化了storage模块中的kline_save()函数,调整了参数位置,以与其他函数的使用方式统一。 31 | 2.增加了一个手动记录持仓信息从而加快回测速度的双均线示例策略。 32 | ``` 33 | 34 | ``` 35 | 0.1.9 36 | 2020-08-31 37 | ~~~~~~~~~~~~~~~~~~ 38 | 1.增加币安交易所的现货、交割、永续合约rest api。 39 | 2.修复并优化trade模块的下单函数的撤单重发功能。 40 | 3.双均线示例策略增加了初始化方法中读取数据库中的总利润的功能。 41 | 4.market模块的获取开、高、低、收价格函数,增加了回测时可以传入参数以获取历史k线上的开高低收价格的功能。 42 | 5.增加了一个回测兼实盘的布林强盗突破策略。 43 | 6.exceptions模块新增下单失败的异常抛出派生类。 44 | ``` 45 | 46 | ``` 47 | 0.1.10 48 | 2020-09-03 49 | ~~~~~~~~~~~~~~~~~~ 50 | 1.trade模块中下单结果优化,现在返回的是一个字典,可以从中取出例如"成交均价": 51 | info = exchange.buy(price, amount) 52 | avg_price = info["【交易提醒】下单结果"]["成交均价"] 53 | 54 | info = exchange.BUY(平空价格,平空数量,开多价格,开多数量) 55 | avg_price = info["平仓结果"]["【交易提醒】下单结果"]["成交均价"] 56 | 来计算实际的交易利润。 57 | 2.增加okex现货的双均线示例策略以及币本位交割合约双均线示例策略。 58 | 3.优化trade模块中的火币现货下单和查询订单失败时的异常抛出。 59 | ``` 60 | 61 | ``` 62 | 0.1.11 63 | 2020-09-04 64 | ~~~~~~~~~~~~~~~~~~ 65 | 1.修复exchange下的binance sdk 66 | 2.优化trade模块中的各个交易所合约ID传入方式,现在完全统一。例如: 67 | 交割合约:"BTC-USD-201225", "BTC-USDT-201225" 68 | 永续合约:"BTC-USD-SWAP", "BTC-USDT-SWAP" 69 | ``` 70 | 71 | ``` 72 | 0.1.12 73 | 2020-09-04 74 | ~~~~~~~~~~~~~~~~~~ 75 | 1.修复trade模块中合约面值为小数时数据类型转换失败的问题。 76 | ``` 77 | 78 | ``` 79 | 0.1.13 80 | 2020-09-05 81 | ~~~~~~~~~~~~~~~~~~ 82 | 1.storage模块中增加存储币安k线的函数。 83 | 2.新增一个多参数回测的双均线示例策略。 84 | ``` 85 | 86 | ``` 87 | 0.1.15 88 | 2020-09-06 89 | ~~~~~~~~~~~~~~~~~~ 90 | 1.优化logger模块,避免多进程访问同一个日志文件时报错;并且增加了控制台输出日志时不同级别的日志具有不同的颜色;当前路径下没有logs文件夹时会自动创建;logger模块的 使用方式简化,不用再进行初始化操作: 91 | from purequant.logger import logger 92 | logger.debug("我是一条日志") 93 | ``` 94 | 95 | ``` 96 | 0.1.16 97 | 2020-09-07 98 | ~~~~~~~~~~~~~~~~~~ 99 | 1.优化logger模块,在使用logger记录异常信息时,直接使用logger.error(),可以是任何级别,不必传入任何参数,记录的日志包含了异常发生的准确地点,方便定位程序运行过程中产生的问题。 100 | ``` 101 | 102 | ``` 103 | 0.1.17 104 | 2020-09-07 105 | ~~~~~~~~~~~~~~~~~~ 106 | 1.优化logger模块,减少冗余。 107 | 2.优化保存和读取txt文件的函数。 108 | ``` 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /purequant/example/plot_signal/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "STATUS": { 3 | "first_run": "true" 4 | }, 5 | "MODE": { 6 | "backtest": "enabled" 7 | }, 8 | "ASSISTANT": { 9 | "automatic_cancellation": "true", 10 | "reissue_order": "0.1%", 11 | "price_cancellation": "true", 12 | "amplitude": "0.5%", 13 | "time_cancellation": "true", 14 | "seconds": 10 15 | }, 16 | "EXCHANGE": { 17 | "access_key": "your access key", 18 | "secret_key": "your secret key", 19 | "passphrase": "your passphrase" 20 | }, 21 | "MYSQL": { 22 | "authorization": "disabled", 23 | "user_name": "root", 24 | "password": "" 25 | }, 26 | "MONGODB": { 27 | "authorization": "disabled", 28 | "user_name": "root", 29 | "password": "" 30 | }, 31 | "PUSH": { 32 | "sendmail": "false", 33 | "dingtalk": "true", 34 | "twilio": "false" 35 | }, 36 | "SYNCHRONIZE": { 37 | "overprice": { 38 | "range": "0.1%" 39 | } 40 | }, 41 | "LOG": { 42 | "level": "debug", 43 | "handler": "stream" 44 | }, 45 | "MARKETS_SERVER": { 46 | "platform": { 47 | "huobi": { 48 | "console": "false", 49 | "database": "trade", 50 | "collection": "hbdata", 51 | "channels": [ 52 | ["market.BTC_NW.depth.step6"], ["market.BTC_CQ.kline.1min"] 53 | ] 54 | } 55 | } 56 | }, 57 | "POSITION_SERVER": { 58 | "platform": { 59 | "okex": { 60 | "futures_usd": "false", 61 | "futures_usdt": "true", 62 | "delivery_date": "201225", 63 | "swap_usd": "false", 64 | "swap_usdt": "false", 65 | "spot": "false", 66 | "margin": "false" 67 | } 68 | } 69 | }, 70 | "DINGTALK": { 71 | "ding_talk_api": "https://oapi.dingtalk.com/robot/send?access_token=d3a36823cccca302e51a1c9e83dbd79d74b453" 72 | }, 73 | "TWILIO": { 74 | "accountSID" : "AC97a11fcd39061ad140f", 75 | "authToken" : "3616b6ced82fa4aa559", 76 | "myNumber" : "+8615312345678", 77 | "twilio_Number" : "+12058958974" 78 | }, 79 | "SENDMAIL": { 80 | "from_addr" : "123456789@qq.com", 81 | "password" : "xxeqsfqwcjgj", 82 | "to_addr" : "iloveyou@icloud.com", 83 | "smtp_server" : "smtp.qq.com", 84 | "port": 587 85 | } 86 | } -------------------------------------------------------------------------------- /purequant/example/boll_breakthrough_strategy/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "STATUS": { 3 | "first_run": "true" 4 | }, 5 | "MODE": { 6 | "backtest": "enabled" 7 | }, 8 | "ASSISTANT": { 9 | "automatic_cancellation": "true", 10 | "reissue_order": "0.1%", 11 | "price_cancellation": "true", 12 | "amplitude": "0.5%", 13 | "time_cancellation": "true", 14 | "seconds": 10 15 | }, 16 | "EXCHANGE": { 17 | "access_key": "your access key", 18 | "secret_key": "your secret key", 19 | "passphrase": "your passphrase" 20 | }, 21 | "MYSQL": { 22 | "authorization": "disabled", 23 | "user_name": "root", 24 | "password": "" 25 | }, 26 | "MONGODB": { 27 | "authorization": "disabled", 28 | "user_name": "root", 29 | "password": "" 30 | }, 31 | "PUSH": { 32 | "sendmail": "false", 33 | "dingtalk": "true", 34 | "twilio": "false" 35 | }, 36 | "SYNCHRONIZE": { 37 | "overprice": { 38 | "range": "0.1%" 39 | } 40 | }, 41 | "LOG": { 42 | "level": "debug", 43 | "handler": "stream" 44 | }, 45 | "MARKETS_SERVER": { 46 | "platform": { 47 | "huobi": { 48 | "console": "false", 49 | "database": "trade", 50 | "collection": "hbdata", 51 | "channels": [ 52 | ["market.BTC_NW.depth.step6"], ["market.BTC_CQ.kline.1min"] 53 | ] 54 | } 55 | } 56 | }, 57 | "POSITION_SERVER": { 58 | "platform": { 59 | "okex": { 60 | "futures_usd": "false", 61 | "futures_usdt": "true", 62 | "delivery_date": "201225", 63 | "swap_usd": "false", 64 | "swap_usdt": "false", 65 | "spot": "false", 66 | "margin": "false" 67 | } 68 | } 69 | }, 70 | "DINGTALK": { 71 | "ding_talk_api": "https://oapi.dingtalk.com/robot/send?access_token=d3a36823cccca302e51a1c9e83dbd79d74b453" 72 | }, 73 | "TWILIO": { 74 | "accountSID" : "AC97a11fcd39061ad140f", 75 | "authToken" : "3616b6ced82fa4aa559", 76 | "myNumber" : "+8615312345678", 77 | "twilio_Number" : "+12058958974" 78 | }, 79 | "SENDMAIL": { 80 | "from_addr" : "123456789@qq.com", 81 | "password" : "xxeqsfqwcjgj", 82 | "to_addr" : "iloveyou@icloud.com", 83 | "smtp_server" : "smtp.qq.com", 84 | "port": 587 85 | } 86 | } -------------------------------------------------------------------------------- /purequant/example/double_moving_average_strategy/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "STATUS": { 3 | "first_run": "true" 4 | }, 5 | "MODE": { 6 | "backtest": "enabled" 7 | }, 8 | "ASSISTANT": { 9 | "automatic_cancellation": "true", 10 | "reissue_order": "0.1%", 11 | "price_cancellation": "true", 12 | "amplitude": "0.5%", 13 | "time_cancellation": "true", 14 | "seconds": 10 15 | }, 16 | "EXCHANGE": { 17 | "access_key": "your access key", 18 | "secret_key": "your secret key", 19 | "passphrase": "your passphrase" 20 | }, 21 | "MYSQL": { 22 | "authorization": "disabled", 23 | "user_name": "root", 24 | "password": "" 25 | }, 26 | "MONGODB": { 27 | "authorization": "disabled", 28 | "user_name": "root", 29 | "password": "" 30 | }, 31 | "PUSH": { 32 | "sendmail": "false", 33 | "dingtalk": "true", 34 | "twilio": "false" 35 | }, 36 | "SYNCHRONIZE": { 37 | "overprice": { 38 | "range": "0.1%" 39 | } 40 | }, 41 | "LOG": { 42 | "level": "debug", 43 | "handler": "stream" 44 | }, 45 | "MARKETS_SERVER": { 46 | "platform": { 47 | "huobi": { 48 | "console": "false", 49 | "database": "trade", 50 | "collection": "hbdata", 51 | "channels": [ 52 | ["market.BTC_NW.depth.step6"], ["market.BTC_CQ.kline.1min"] 53 | ] 54 | } 55 | } 56 | }, 57 | "POSITION_SERVER": { 58 | "platform": { 59 | "okex": { 60 | "futures_usd": "false", 61 | "futures_usdt": "true", 62 | "delivery_date": "201225", 63 | "swap_usd": "false", 64 | "swap_usdt": "false", 65 | "spot": "false", 66 | "margin": "false" 67 | } 68 | } 69 | }, 70 | "DINGTALK": { 71 | "ding_talk_api": "https://oapi.dingtalk.com/robot/send?access_token=d3a36823cccca302e51a1c9e83dbd79d74b453" 72 | }, 73 | "TWILIO": { 74 | "accountSID" : "AC97a11fcd39061ad140f", 75 | "authToken" : "3616b6ced82fa4aa559", 76 | "myNumber" : "+8615312345678", 77 | "twilio_Number" : "+12058958974" 78 | }, 79 | "SENDMAIL": { 80 | "from_addr" : "123456789@qq.com", 81 | "password" : "xxeqsfqwcjgj", 82 | "to_addr" : "iloveyou@icloud.com", 83 | "smtp_server" : "smtp.qq.com", 84 | "port": 587 85 | } 86 | } -------------------------------------------------------------------------------- /purequant/market.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | 市场行情模块 5 | 6 | Author: Gary-Hertel 7 | Date: 2020/07/11 8 | email: interstella.ranger2020@gmail.com 9 | """ 10 | 11 | from purequant.config import config 12 | 13 | class MARKET: 14 | 15 | def __init__(self, platform, instrument_id, time_frame): 16 | self.__platform = platform 17 | self.__instrument_id = instrument_id 18 | self.__time_frame = time_frame 19 | 20 | def last(self): 21 | """获取交易对的最新成交价""" 22 | result = float(self.__platform.get_ticker()['last']) 23 | return result 24 | 25 | def open(self, param, kline=None): 26 | """ 27 | 获取交易对当前k线上的开盘价 28 | :param param: 参数,如-1是获取当根k线上的开盘价,-2是上一根k线上的开盘价 29 | :param kline: 回测时传入指定k线数据 30 | :return: 31 | """ 32 | if config.backtest == "enabled": # 回测模式 33 | return float(kline[param][1]) 34 | else: # 实盘模式 35 | records = self.__platform.get_kline(self.__time_frame) 36 | records.reverse() 37 | result = float((records)[param][1]) 38 | return result 39 | 40 | def high(self, param, kline=None): 41 | """ 42 | 获取交易对当前k线上的最高价 43 | :param param: 参数,如-1是获取当根k线上的最高价,-2是上一根k线上的最高价 44 | :param kline: 回测时传入指定k线数据 45 | :return: 46 | """ 47 | if config.backtest == "enabled": 48 | return float(kline[param][2]) 49 | else: 50 | records = self.__platform.get_kline(self.__time_frame) 51 | records.reverse() 52 | result = float((records)[param][2]) 53 | return result 54 | 55 | def low(self, param, kline=None): 56 | """ 57 | 获取交易对当前k线上的最低价 58 | :param param: 参数,如-1是获取当根k线上的最低价,-2是上一根k线上的最低价 59 | :param kline: 回测时传入指定k线数据 60 | :return: 61 | """ 62 | if config.backtest == "enabled": 63 | return float(kline[param][3]) 64 | else: 65 | records = self.__platform.get_kline(self.__time_frame) 66 | records.reverse() 67 | result = float((records)[param][3]) 68 | return result 69 | 70 | def close(self, param, kline=None): 71 | """ 72 | 获取交易对当前k线上的收盘价 73 | :param param: 参数,如-1是获取当根k线上的收盘价,-2是上一根k线上的收盘价 74 | :param kline: 回测时传入指定k线数据 75 | :return: 76 | """ 77 | if config.backtest == "enabled": 78 | return float(kline[param][4]) 79 | else: 80 | records = self.__platform.get_kline(self.__time_frame) 81 | records.reverse() 82 | result = float((records)[param][4]) 83 | return result 84 | 85 | def contract_value(self): 86 | """获取合约面值""" 87 | contract_value = float(self.__platform.get_contract_value()['{}'.format(self.__instrument_id)]) 88 | return contract_value -------------------------------------------------------------------------------- /purequant/push.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | 智能渠道推送工具包 5 | 6 | Author: Gary-Hertel 7 | Date: 2020/07/09 8 | email: interstella.ranger2020@gmail.com 9 | """ 10 | from purequant.config import config 11 | import requests, json, smtplib 12 | from email.header import Header 13 | from email.mime.text import MIMEText 14 | from email.utils import parseaddr, formataddr 15 | from twilio.rest import Client 16 | from purequant.storage import storage 17 | from purequant.time import get_localtime 18 | 19 | def __dingtalk(text): 20 | """ 21 | 推送钉钉消息。 22 | :param data: 要推送的数据内容,字符串格式 23 | :return: 24 | """ 25 | json_text = { 26 | "msgtype": "text", 27 | "at": { 28 | "atMobiles": [ 29 | "" 30 | ], 31 | "isAtAll": True 32 | }, 33 | "text": { 34 | "content": text 35 | } 36 | } 37 | 38 | headers = {'Content-Type': 'application/json;charset=utf-8'} 39 | api_url = config.ding_talk_api 40 | dingtalk_result = requests.post(api_url, json.dumps(json_text), headers=headers).content # 发送钉钉消息并返回发送结果 41 | storage.text_save("时间:" + str(get_localtime()) + " 发送状态:" + str(dingtalk_result) + "发送内容:" + str(text), 42 | './dingtalk.txt') # 将发送时间、结果和具体发送内容保存至当前目录下text文件中 43 | 44 | def __sendmail(data): 45 | """ 46 | 推送邮件信息。 47 | :param data: 要推送的信息内容,字符串格式 48 | :return: 49 | """ 50 | from_addr = config.from_addr 51 | password = config.password 52 | to_addr = config.to_addr 53 | smtp_server = config.smtp_server 54 | port = config.mail_port 55 | 56 | msg = MIMEText(data, 'plain', 'utf-8') 57 | name, addr = parseaddr('Alert <%s>' % from_addr) 58 | msg['From'] = formataddr((Header(name, 'utf-8').encode(), addr)) 59 | name, addr = parseaddr('交易者 <%s>' % to_addr) 60 | msg['To'] = formataddr((Header(name, 'utf-8').encode(), addr)) 61 | msg['Subject'] = Header('交易提醒', 'utf-8').encode() 62 | 63 | server = smtplib.SMTP(smtp_server, port) 64 | server.login(from_addr, password) 65 | server.sendmail(from_addr, [to_addr], msg.as_string()) 66 | server.quit() 67 | 68 | def __twilio(message): 69 | """ 70 | 使用twilio推送信息到短信。 71 | :param message: 要推送的信息,字符串格式。 72 | :return: 73 | """ 74 | accountSID = config.accountSID 75 | authToken = config.authToken 76 | myNumber = config.myNumber 77 | twilio_Number = config.twilio_Number 78 | twilioCli = Client(accountSID, authToken) 79 | twilioCli.messages.create(body=message, from_=twilio_Number, to=myNumber) 80 | 81 | def push(message): 82 | """集成推送工具,配置模块中选择具体的推送渠道""" 83 | if config.backtest != "enabled": # 仅实盘模式时推送信息 84 | if config.sendmail == 'true': 85 | __sendmail(message) 86 | if config.dingtalk == 'true': 87 | __dingtalk(message) 88 | if config.twilio == 'true': 89 | __twilio(message) 90 | -------------------------------------------------------------------------------- /purequant/exchange/okex/client.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | from . import consts as c, utils, exceptions 4 | 5 | 6 | class Client(object): 7 | 8 | def __init__(self, api_key, api_secret_key, passphrase, use_server_time=False, test=False, first=False): 9 | 10 | self.API_KEY = api_key 11 | self.API_SECRET_KEY = api_secret_key 12 | self.PASSPHRASE = passphrase 13 | self.use_server_time = use_server_time 14 | self.first = first 15 | self.test = test 16 | 17 | def _request(self, method, request_path, params, cursor=False): 18 | if method == c.GET: 19 | request_path = request_path + utils.parse_params_to_str(params) 20 | # url 21 | url = c.API_URL + request_path 22 | 23 | # 获取本地时间 24 | timestamp = utils.get_timestamp() 25 | 26 | # sign & header 27 | if self.use_server_time: 28 | # 获取服务器时间 29 | timestamp = self._get_timestamp() 30 | 31 | body = json.dumps(params) if method == c.POST else "" 32 | sign = utils.sign(utils.pre_hash(timestamp, method, request_path, str(body)), self.API_SECRET_KEY) 33 | header = utils.get_header(self.API_KEY, sign, timestamp, self.PASSPHRASE) 34 | 35 | if self.test: 36 | header['x-simulated-trading'] = '1' 37 | if self.first: 38 | print("url:", url) 39 | self.first = False 40 | 41 | # print("url:", url) 42 | # print("headers:", header) 43 | # print("body:", body) 44 | 45 | # send request 46 | response = None 47 | if method == c.GET: 48 | response = requests.get(url, headers=header) 49 | elif method == c.POST: 50 | response = requests.post(url, data=body, headers=header) 51 | elif method == c.DELETE: 52 | response = requests.delete(url, headers=header) 53 | 54 | # exception handle 55 | if not str(response.status_code).startswith('2'): 56 | raise exceptions.OkexAPIException(response) 57 | try: 58 | res_header = response.headers 59 | if cursor: 60 | r = dict() 61 | try: 62 | r['before'] = res_header['OK-BEFORE'] 63 | r['after'] = res_header['OK-AFTER'] 64 | except: 65 | pass 66 | return response.json(), r 67 | else: 68 | return response.json() 69 | 70 | except ValueError: 71 | raise exceptions.OkexRequestException('Invalid Response: %s' % response.text) 72 | 73 | def _request_without_params(self, method, request_path): 74 | return self._request(method, request_path, {}) 75 | 76 | def _request_with_params(self, method, request_path, params, cursor=False): 77 | return self._request(method, request_path, params, cursor) 78 | 79 | def _get_timestamp(self): 80 | url = c.API_URL + c.SERVER_TIMESTAMP_URL 81 | response = requests.get(url) 82 | if response.status_code == 200: 83 | return response.json()['iso'] 84 | else: 85 | return "" 86 | -------------------------------------------------------------------------------- /purequant/logger.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | 日志输出 5 | 6 | Author: Gary-Hertel 7 | Date: 2020/07/09 8 | email: interstella.ranger2020@gmail.com 9 | """ 10 | 11 | import logging 12 | from logging import handlers 13 | from purequant.config import config 14 | from concurrent_log_handler import ConcurrentRotatingFileHandler 15 | import os 16 | import colorlog 17 | import traceback 18 | 19 | log_colors_config = { 20 | 'DEBUG': 'cyan', 21 | 'INFO': 'green', 22 | 'WARNING': 'blue', 23 | 'ERROR': 'red', 24 | 'CRITICAL': 'bold_red', 25 | } 26 | 27 | class __LOGGER: 28 | 29 | def __init__(self): 30 | if not os.path.exists("./logs"): # 如果logs文件夹不存在就自动创建 31 | os.makedirs("./logs") 32 | self.__path = './logs/purequant.log' 33 | self.__logger = logging.getLogger("purequant") 34 | 35 | formatter = logging.Formatter(fmt='[%(asctime)s] -> [%(levelname)s] : %(message)s') 36 | # 文件输出按照时间分割 37 | self.time_rotating_file_handler = handlers.TimedRotatingFileHandler(filename=self.__path, when='MIDNIGHT', 38 | interval=1, backupCount=10) 39 | self.time_rotating_file_handler.setFormatter(formatter) 40 | self.time_rotating_file_handler.suffix = "%Y%m%d-%H%M%S.log" 41 | 42 | # 控制台输出 43 | console_formatter = colorlog.ColoredFormatter( 44 | fmt='%(log_color)s[%(asctime)s] -> [%(levelname)s] : %(message)s', 45 | datefmt='%Y-%m-%d %H:%M:%S', 46 | log_colors=log_colors_config 47 | ) 48 | self.stream_handler = logging.StreamHandler() 49 | self.stream_handler.setFormatter(console_formatter) 50 | 51 | # 文件输出按照大小分割 52 | self.rotatingHandler = ConcurrentRotatingFileHandler(self.__path, "a", 1024 * 1024, 10) # a为追加模式,按1M大小分割,保留最近10个文件 53 | self.rotatingHandler.setFormatter(formatter) 54 | 55 | def __initialize(self): 56 | # 根据配置文件中的设置来设置日志级别 57 | if config.level == "debug": 58 | level = logging.DEBUG 59 | elif config.level == "info": 60 | level = logging.INFO 61 | elif config.level == "warning": 62 | level = logging.WARNING 63 | elif config.level == "error": 64 | level = logging.ERROR 65 | elif config.level == "critical": 66 | level = logging.CRITICAL 67 | else: 68 | level = logging.DEBUG 69 | self.__logger.setLevel(level=level) 70 | # 根据配置文件中的设置来设置日志输出方式 71 | if config.handler == "time": 72 | self.__logger.addHandler(self.time_rotating_file_handler) 73 | elif config.handler == "file": 74 | self.__logger.addHandler(self.rotatingHandler) 75 | else: 76 | self.__logger.addHandler(self.stream_handler) 77 | 78 | def debug(self, msg=None): 79 | self.__initialize() 80 | self.__logger.debug(msg) if msg is not None else self.__logger.debug(traceback.format_exc(limit=1)) 81 | 82 | def info(self, msg=None): 83 | self.__initialize() 84 | self.__logger.info(msg) if msg is not None else self.__logger.info(traceback.format_exc(limit=1)) 85 | 86 | def warning(self, msg=None): 87 | self.__initialize() 88 | self.__logger.warning(msg) if msg is not None else self.__logger.warning(traceback.format_exc(limit=1)) 89 | 90 | def error(self, msg=None): 91 | self.__initialize() 92 | self.__logger.error(msg) if msg is not None else self.__logger.error(traceback.format_exc(limit=1)) 93 | 94 | def critical(self, msg=None): 95 | self.__initialize() 96 | self.__logger.critical(msg) if msg is not None else self.__logger.critical(traceback.format_exc(limit=1)) 97 | 98 | 99 | logger = __LOGGER() 100 | 101 | -------------------------------------------------------------------------------- /purequant/exchange/huobi/util.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | 火币 5 | """ 6 | 7 | import base64 8 | import hmac 9 | import hashlib 10 | import json 11 | 12 | import urllib 13 | import datetime 14 | import requests 15 | #import urlparse # urllib.parse in python 3 16 | 17 | # timeout in 5 seconds: 18 | TIMEOUT = 5 19 | 20 | #各种请求,获取数据方式 21 | def http_get_request(url, params, add_to_headers=None): 22 | headers = { 23 | "Content-type": "application/x-www-form-urlencoded", 24 | 'User-Agent':'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:53.0) Gecko/20100101 Firefox/53.0' 25 | } 26 | if add_to_headers: 27 | headers.update(add_to_headers) 28 | postdata = urllib.parse.urlencode(params) 29 | try: 30 | response = requests.get(url, postdata, headers=headers, timeout=TIMEOUT) 31 | if response.status_code == 200: 32 | return response.json() 33 | else: 34 | return {"status":"fail"} 35 | except Exception as e: 36 | print("httpGet failed, detail is:%s" %e) 37 | return {"status":"fail","msg": "%s"%e} 38 | 39 | def http_post_request(url, params, add_to_headers=None): 40 | headers = { 41 | "Accept": "application/json", 42 | 'Content-Type': 'application/json', 43 | 'User-Agent':'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:53.0) Gecko/20100101 Firefox/53.0' 44 | } 45 | if add_to_headers: 46 | headers.update(add_to_headers) 47 | postdata = json.dumps(params) 48 | try: 49 | response = requests.post(url, postdata, headers=headers, timeout=TIMEOUT) 50 | if response.status_code == 200: 51 | return response.json() 52 | else: 53 | return response.json() 54 | except Exception as e: 55 | print("httpPost failed, detail is:%s" % e) 56 | return {"status":"fail","msg": "%s"%e} 57 | 58 | 59 | def api_key_get(url, request_path, params, ACCESS_KEY, SECRET_KEY): 60 | method = 'GET' 61 | timestamp = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S') 62 | params.update({'AccessKeyId': ACCESS_KEY, 63 | 'SignatureMethod': 'HmacSHA256', 64 | 'SignatureVersion': '2', 65 | 'Timestamp': timestamp}) 66 | 67 | host_name = host_url = url 68 | #host_name = urlparse.urlparse(host_url).hostname 69 | host_name = urllib.parse.urlparse(host_url).hostname 70 | host_name = host_name.lower() 71 | 72 | params['Signature'] = createSign(params, method, host_name, request_path, SECRET_KEY) 73 | url = host_url + request_path 74 | return http_get_request(url, params) 75 | 76 | 77 | def api_key_post(url, request_path, params, ACCESS_KEY, SECRET_KEY): 78 | method = 'POST' 79 | timestamp = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S') 80 | params_to_sign = {'AccessKeyId': ACCESS_KEY, 81 | 'SignatureMethod': 'HmacSHA256', 82 | 'SignatureVersion': '2', 83 | 'Timestamp': timestamp} 84 | 85 | host_url = url 86 | #host_name = urlparse.urlparse(host_url).hostname 87 | host_name = urllib.parse.urlparse(host_url).hostname 88 | host_name = host_name.lower() 89 | params_to_sign['Signature'] = createSign(params_to_sign, method, host_name, request_path, SECRET_KEY) 90 | url = host_url + request_path + '?' + urllib.parse.urlencode(params_to_sign) 91 | return http_post_request(url, params) 92 | 93 | 94 | def createSign(pParams, method, host_url, request_path, secret_key): 95 | sorted_params = sorted(pParams.items(), key=lambda d: d[0], reverse=False) 96 | encode_params = urllib.parse.urlencode(sorted_params) 97 | payload = [method, host_url, request_path, encode_params] 98 | payload = '\n'.join(payload) 99 | payload = payload.encode(encoding='UTF8') 100 | secret_key = secret_key.encode(encoding='UTF8') 101 | digest = hmac.new(secret_key, payload, digestmod=hashlib.sha256).digest() 102 | signature = base64.b64encode(digest) 103 | signature = signature.decode() 104 | return signature 105 | 106 | 107 | -------------------------------------------------------------------------------- /purequant/exchange/okex/account_api.py: -------------------------------------------------------------------------------- 1 | from .client import Client 2 | from .consts import * 3 | 4 | 5 | class AccountAPI(Client): 6 | 7 | def __init__(self, api_key, api_secret_key, passphrase, use_server_time=False, test=False, first=False): 8 | Client.__init__(self, api_key, api_secret_key, passphrase, use_server_time, test, first) 9 | 10 | # get all currencies list 11 | def get_currencies(self): 12 | return self._request_without_params(GET, CURRENCIES_INFO) 13 | 14 | # get wallet info 15 | def get_wallet(self): 16 | return self._request_without_params(GET, WALLET_INFO) 17 | 18 | # get specific currency info 19 | def get_currency(self, currency): 20 | return self._request_without_params(GET, CURRENCY_INFO + str(currency)) 21 | 22 | # coin withdraw 23 | def coin_withdraw(self, currency, amount, destination, to_address, trade_pwd, fee): 24 | params = {'currency': currency, 'amount': amount, 'destination': destination, 'to_address': to_address, 'trade_pwd': trade_pwd, 'fee': fee} 25 | return self._request_with_params(POST, COIN_WITHDRAW, params) 26 | 27 | # query the fee of coin withdraw 28 | def get_coin_fee(self, currency=''): 29 | params = {} 30 | if currency: 31 | params['currency'] = currency 32 | return self._request_with_params(GET, COIN_FEE, params) 33 | 34 | # query all recently coin withdraw record 35 | def get_coins_withdraw_record(self): 36 | return self._request_without_params(GET, COINS_WITHDRAW_RECORD) 37 | 38 | # query specific coin withdraw record 39 | def get_coin_withdraw_record(self, currency): 40 | return self._request_without_params(GET, COIN_WITHDRAW_RECORD + str(currency)) 41 | 42 | # query ledger record 43 | def get_ledger_record(self, currency='', after='', before='', limit='', type=''): 44 | params = {} 45 | if currency: 46 | params['currency'] = currency 47 | if after: 48 | params['after'] = after 49 | if before: 50 | params['before'] = before 51 | if limit: 52 | params['limit'] = limit 53 | if type: 54 | params['type'] = type 55 | return self._request_with_params(GET, LEDGER_RECORD, params, cursor=True) 56 | 57 | # query top up address 58 | def get_top_up_address(self, currency): 59 | params = {'currency': currency} 60 | return self._request_with_params(GET, TOP_UP_ADDRESS, params) 61 | 62 | def get_asset_valuation(self, account_type='', valuation_currency=''): 63 | params = {} 64 | if account_type: 65 | params['account_type'] = account_type 66 | if valuation_currency: 67 | params['valuation_currency'] = valuation_currency 68 | return self._request_with_params(GET, ASSET_VALUATION, params) 69 | 70 | def get_sub_account(self, sub_account): 71 | params = {'sub-account': sub_account} 72 | return self._request_with_params(GET, SUB_ACCOUNT, params) 73 | 74 | # query top up records 75 | def get_top_up_records(self): 76 | return self._request_without_params(GET, COIN_TOP_UP_RECORDS) 77 | 78 | # query top up record 79 | def get_top_up_record(self, currency, after='', before='', limit=''): 80 | params = {} 81 | if after: 82 | params['after'] = after 83 | if before: 84 | params['before'] = before 85 | if limit: 86 | params['limit'] = limit 87 | return self._request_without_params(GET, COIN_TOP_UP_RECORD + str(currency)) 88 | 89 | # coin transfer 90 | def coin_transfer(self, currency, amount, account_from, account_to, type='', sub_account='', instrument_id='', to_instrument_id=''): 91 | params = {'currency': currency, 'amount': amount, 'from': account_from, 'to': account_to} 92 | if type: 93 | params['type'] = type 94 | if sub_account: 95 | params['sub_account'] = sub_account 96 | if instrument_id: 97 | params['instrument_id'] = instrument_id 98 | if to_instrument_id: 99 | params['to_instrument_id'] = to_instrument_id 100 | return self._request_with_params('POST', COIN_TRANSFER, params) 101 | -------------------------------------------------------------------------------- /purequant/time.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | 时间工具包 5 | """ 6 | 7 | import time 8 | import decimal 9 | import datetime 10 | 11 | 12 | def get_cur_timestamp(): 13 | """ 获取当前时间戳(秒) 14 | """ 15 | ts = int(time.time()) 16 | return ts 17 | 18 | def ts_to_utc_str(ts=None, fmt='%Y-%m-%dT%H:%M:%S.000z'): 19 | """ 将时间戳转换为UTC时间格式,'2020-07-25T03:05:00.000z' 20 | @param ts 时间戳,默认None即为当前时间戳 21 | @param fmt 返回的UTC字符串格式 22 | """ 23 | if not ts: 24 | ts = get_cur_timestamp() 25 | dt = datetime.datetime.utcfromtimestamp(int(ts)) 26 | return dt.strftime(fmt) 27 | 28 | def get_cur_timestamp_ms(): 29 | """ 获取当前时间戳(毫秒) 30 | """ 31 | ts = int(time.time() * 1000) 32 | return ts 33 | 34 | 35 | def get_cur_datetime_m(fmt='%Y%m%d%H%M%S%f'): 36 | """ 获取当前日期时间字符串,包含 年 + 月 + 日 + 时 + 分 + 秒 + 微妙 37 | """ 38 | today = datetime.datetime.today() 39 | str_m = today.strftime(fmt) 40 | return str_m 41 | 42 | 43 | def get_datetime(fmt='%Y%m%d%H%M%S'): 44 | """ 获取日期时间字符串,包含 年 + 月 + 日 + 时 + 分 + 秒 45 | """ 46 | today = datetime.datetime.today() 47 | str_dt = today.strftime(fmt) 48 | return str_dt 49 | 50 | 51 | def get_date(fmt='%Y%m%d', delta_day=0): 52 | """ 获取日期字符串,包含 年 + 月 + 日 53 | @param fmt 返回的日期格式 54 | """ 55 | day = datetime.datetime.today() 56 | if delta_day: 57 | day += datetime.timedelta(days=delta_day) 58 | str_d = day.strftime(fmt) 59 | return str_d 60 | 61 | 62 | def date_str_to_dt(date_str=None, fmt='%Y%m%d', delta_day=0): 63 | """ 日期字符串转换到datetime对象 64 | @param date_str 日期字符串 65 | @param fmt 日期字符串格式 66 | @param delta_day 相对天数,<0减相对天数,>0加相对天数 67 | """ 68 | if not date_str: 69 | dt = datetime.datetime.today() 70 | else: 71 | dt = datetime.datetime.strptime(date_str, fmt) 72 | if delta_day: 73 | dt += datetime.timedelta(days=delta_day) 74 | return dt 75 | 76 | 77 | def dt_to_date_str(dt=None, fmt='%Y%m%d', delta_day=0): 78 | """ datetime对象转换到日期字符串 79 | @param dt datetime对象 80 | @param fmt 返回的日期字符串格式 81 | @param delta_day 相对天数,<0减相对天数,>0加相对天数 82 | """ 83 | if not dt: 84 | dt = datetime.datetime.today() 85 | if delta_day: 86 | dt += datetime.timedelta(days=delta_day) 87 | str_d = dt.strftime(fmt) 88 | return str_d 89 | 90 | 91 | def get_utc_time(): 92 | """ 获取当前utc时间 93 | """ 94 | utc_t = datetime.datetime.utcnow() 95 | return utc_t 96 | 97 | def get_localtime(): 98 | """ 获取本地时间""" 99 | localtime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) 100 | return localtime 101 | 102 | def ts_to_datetime_str(ts=None, fmt='%Y-%m-%d %H:%M:%S'): 103 | """ 将时间戳转换为日期时间格式,年-月-日 时:分:秒 104 | @param ts 时间戳,默认None即为当前时间戳 105 | @param fmt 返回的日期字符串格式 106 | """ 107 | if not ts: 108 | ts = get_cur_timestamp() 109 | dt = datetime.datetime.fromtimestamp(int(ts)) 110 | return dt.strftime(fmt) 111 | 112 | 113 | def datetime_str_to_ts(dt_str, fmt='%Y-%m-%d %H:%M:%S'): 114 | """ 将日期时间格式字符串转换成时间戳 115 | @param dt_str 日期时间字符串 116 | @param fmt 日期时间字符串格式 117 | """ 118 | ts = int(time.mktime(datetime.datetime.strptime(dt_str, fmt).timetuple())) 119 | return ts 120 | 121 | 122 | def datetime_to_timestamp(dt=None, tzinfo=None): 123 | """ 将datetime对象转换成时间戳 124 | @param dt datetime对象,如果为None,默认使用当前UTC时间 125 | @param tzinfo 时区对象,如果为None,默认使用timezone.utc 126 | @return ts 时间戳(秒) 127 | """ 128 | if not dt: 129 | dt = get_utc_time() 130 | if not tzinfo: 131 | tzinfo = datetime.timezone.utc 132 | ts = int(dt.replace(tzinfo=tzinfo).timestamp()) 133 | return ts 134 | 135 | 136 | def utctime_str_to_ts(utctime_str, fmt="%Y-%m-%dT%H:%M:%S.%fZ"): 137 | """ 将UTC日期时间格式字符串转换成时间戳 138 | @param utctime_str 日期时间字符串 eg: 2019-03-04T09:14:27.806Z 139 | @param fmt 日期时间字符串格式 140 | @return timestamp 时间戳(秒) 141 | """ 142 | dt = datetime.datetime.strptime(utctime_str, fmt) 143 | timestamp = int(dt.replace(tzinfo=datetime.timezone.utc).astimezone(tz=None).timestamp()) 144 | return timestamp 145 | 146 | 147 | def utctime_str_to_mts(utctime_str, fmt="%Y-%m-%dT%H:%M:%S.%fZ"): 148 | """ 将UTC日期时间格式字符串转换成时间戳(毫秒) 149 | @param utctime_str 日期时间字符串 eg: 2019-03-04T09:14:27.806Z 150 | @param fmt 日期时间字符串格式 151 | @return timestamp 时间戳(毫秒) 152 | """ 153 | dt = datetime.datetime.strptime(utctime_str, fmt) 154 | timestamp = int(dt.replace(tzinfo=datetime.timezone.utc).astimezone(tz=None).timestamp() * 1000) 155 | return timestamp 156 | 157 | def float_to_str(f, p=20): 158 | """ 将给定的float转换为字符串,而无需借助科学计数法。 159 | @param f 浮点数参数 160 | @param p 精读 161 | """ 162 | if type(f) == str: 163 | f = float(f) 164 | ctx = decimal.Context(p) 165 | d1 = ctx.create_decimal(repr(f)) 166 | return format(d1, 'f') 167 | 168 | -------------------------------------------------------------------------------- /purequant/position.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | 持仓信息模块 5 | 6 | Author: Gary-Hertel 7 | Date: 2020/07/09 8 | email: interstella.ranger2020@gmail.com 9 | """ 10 | from purequant.market import MARKET 11 | from purequant.config import config 12 | from purequant.storage import storage 13 | 14 | class POSITION: 15 | 16 | def __init__(self, platform, instrument_id, time_frame): 17 | self.__platform = platform 18 | self.__instrument_id = instrument_id 19 | self.__time_frame = time_frame 20 | self.__market = MARKET(self.__platform, self.__instrument_id, self.__time_frame) 21 | 22 | def direction(self): 23 | """获取当前持仓方向""" 24 | if config.backtest != "enabled": # 实盘模式下实时获取账户实际持仓方向 25 | result = self.__platform.get_position()['direction'] 26 | return result 27 | else: # 回测模式下从数据库中读取持仓方向 28 | result = storage.read_mysql_datas(0, "回测", self.__instrument_id.split("-")[0].lower() + "_" + self.__time_frame, "总资金", ">")[-1][6] 29 | return result 30 | 31 | def amount(self): 32 | """获取当前持仓数量""" 33 | if config.backtest != "enabled": # 实盘模式下实时获取账户实际持仓数量 34 | result = self.__platform.get_position()['amount'] 35 | return result 36 | else: # 回测模式下从数据库中读取持仓数量 37 | result = storage.read_mysql_datas(0, "回测", self.__instrument_id.split("-")[0].lower() + "_" + self.__time_frame, "总资金", ">")[-1][7] 38 | return result 39 | 40 | def price(self): 41 | """获取当前的持仓价格""" 42 | if config.backtest != "enabled": # 实盘模式下实时获取账户实际持仓价格 43 | result = self.__platform.get_position()['price'] 44 | return result 45 | else: # 回测模式下从数据库中读取持仓价格 46 | result = storage.read_mysql_datas(0, "回测", self.__instrument_id.split("-")[0].lower() + "_" + self.__time_frame, "总资金", ">")[-1][5] 47 | return result 48 | 49 | 50 | def coverlong_profit(self, market_type=None, last=None): 51 | """ 52 | 计算平多的单笔交易利润 53 | :param market_type: 默认是USDT合约,可填"usd_contract"(币本位合约)或者"spot"(现货) 54 | :param last: 回测模式可以传入最新成交价 55 | :return: 返回计算出的利润结果 56 | """ 57 | if market_type == "usd_contract": # 如果是币本位合约 58 | self.__value = self.__market.contract_value() # 合约面值 59 | if config.backtest == "enabled" and last is not None: # 如果是回测模式且传入了last最新成交价 60 | result = (last - self.price()) * ((self.amount() * self.__value) / self.price()) # 利润=价差*(合约张数*面值)/持仓价格 61 | else: # 如果是实盘模式 62 | result = (self.__market.last() - self.price()) * ((self.amount() * self.__value) / self.price()) # 利润=价差*(合约张数*面值)/持仓价格 63 | 64 | elif market_type == "spot": # 如果是现货 65 | if config.backtest == "enabled" and last is not None: # 如果是回测模式且传入了last最新成交价 66 | result = (last - self.price()) * self.amount() # 利润=价差*持仓数量 67 | else: # 如果是实盘模式 68 | result = (self.__market.last() - self.price()) * self.amount() 69 | 70 | else: # 默认是usdt合约 71 | self.__value = self.__market.contract_value() # 合约面值 72 | if config.backtest == "enabled" and last is not None: # 如果是回测模式且传入了last最新成交价 73 | result = (last - self.price()) * (self.amount() * self.__value) # 利润=价差*(持仓数量*面值) 74 | else: # 如果是实盘模式 75 | result = (self.__market.last() - self.price()) * (self.amount() * self.__value) 76 | return result 77 | 78 | def covershort_profit(self, market_type=None, last=None): 79 | """ 80 | 计算平空的单笔交易利润 81 | :param market_type: 默认是USDT合约,可填"usd_contract"(币本位合约)或者"spot"(现货) 82 | :param last: 回测模式可以传入最新成交价 83 | :return: 返回计算出的利润结果 84 | """ 85 | if market_type == "usd_contract": # 如果是币本位合约 86 | self.__value = self.__market.contract_value() # 合约面值 87 | if config.backtest == "enabled" and last is not None: # 如果是回测模式且传入了last最新成交价 88 | result = (self.price() - last) * ((self.amount() * self.__value) / self.price()) # 利润=价差*(合约张数*面值)/持仓价格 89 | else: # 如果是实盘模式 90 | result = (self.price() - self.__market.last()) * ( 91 | (self.amount() * self.__value) / self.price()) # 利润=价差*(合约张数*面值)/持仓价格 92 | 93 | elif market_type == "spot": # 如果是现货 94 | if config.backtest == "enabled" and last is not None: # 如果是回测模式且传入了last最新成交价 95 | result = (self.price() - last) * self.amount() # 利润=价差*持仓数量 96 | else: # 如果是实盘模式 97 | result = (self.price() - self.__market.last()) * self.amount() 98 | 99 | else: # 默认是usdt合约 100 | self.__value = self.__market.contract_value() # 合约面值 101 | if config.backtest == "enabled" and last is not None: # 如果是回测模式且传入了last最新成交价 102 | result = (self.price() - last) * (self.amount() * self.__value) # 利润=价差*(持仓数量*面值) 103 | else: # 如果是实盘模式 104 | result = (self.price() - self.__market.last()) * (self.amount() * self.__value) 105 | return result 106 | 107 | -------------------------------------------------------------------------------- /purequant/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | 服务配置 5 | 6 | Author: Gary-Hertel 7 | Date: 2020/07/09 8 | email: interstella.ranger2020@gmail.com 9 | """ 10 | 11 | import json 12 | 13 | class __Config: 14 | """服务配置""" 15 | 16 | def __init__(self): 17 | pass 18 | 19 | 20 | def loads(self, config_file=None): 21 | """ 22 | 加载配置。 23 | :param config_file:json配置文件 24 | :return: 25 | """ 26 | with open(config_file) as json_file: 27 | configures = json.load(json_file) 28 | # exchange 29 | self.access_key = configures['EXCHANGE']['access_key'] 30 | self.secret_key = configures['EXCHANGE']['secret_key'] 31 | self.passphrase = configures['EXCHANGE']['passphrase'] 32 | # push 33 | self.ding_talk_api = configures['DINGTALK']['ding_talk_api'] 34 | self.accountSID = configures['TWILIO']['accountSID'] 35 | self.authToken = configures['TWILIO']['authToken'] 36 | self.myNumber = configures['TWILIO']['myNumber'] 37 | self.twilio_Number = configures['TWILIO']['twilio_Number'] 38 | self.from_addr = configures['SENDMAIL']['from_addr'] 39 | self.password = configures['SENDMAIL']['password'] 40 | self.to_addr = configures['SENDMAIL']['to_addr'] 41 | self.smtp_server = configures['SENDMAIL']['smtp_server'] 42 | self.mail_port = configures['SENDMAIL']['port'] 43 | self.sendmail = configures['PUSH']['sendmail'] 44 | self.dingtalk = configures['PUSH']['dingtalk'] 45 | self.twilio = configures['PUSH']['twilio'] 46 | # logger 47 | self.level = configures['LOG']['level'] 48 | self.handler = configures['LOG']['handler'] 49 | # markets server 50 | markets_server_list = [] 51 | for item in configures["MARKETS_SERVER"]["platform"]: 52 | markets_server_list.append(item) 53 | self.markets_server_platform = markets_server_list[0] 54 | self.markets_channels_list = configures["MARKETS_SERVER"]["platform"][self.markets_server_platform]["channels"] 55 | self.mongodb_database = configures["MARKETS_SERVER"]["platform"][self.markets_server_platform]["database"] 56 | self.mongodb_collection = configures["MARKETS_SERVER"]["platform"][self.markets_server_platform]["collection"] 57 | self.mongodb_console = configures["MARKETS_SERVER"]["platform"][self.markets_server_platform]["console"] 58 | # position server 59 | position_server_list = [] 60 | for item in configures["POSITION_SERVER"]["platform"]: 61 | position_server_list.append(item) 62 | self.position_server_platform = position_server_list[0] 63 | if self.position_server_platform == "okex": 64 | self.delivery_date = configures['POSITION_SERVER']["platform"][self.position_server_platform]['delivery_date'] 65 | self.okex_futures_usd = configures['POSITION_SERVER']["platform"][self.position_server_platform]['futures_usd'] 66 | self.okex_futures_usdt = configures['POSITION_SERVER']["platform"][self.position_server_platform]['futures_usdt'] 67 | self.okex_swap_usd = configures['POSITION_SERVER']["platform"][self.position_server_platform]['swap_usd'] 68 | self.okex_swap_usdt = configures['POSITION_SERVER']["platform"][self.position_server_platform]['swap_usdt'] 69 | self.okex_spot = configures['POSITION_SERVER']["platform"][self.position_server_platform]['spot'] 70 | self.okex_margin = configures['POSITION_SERVER']["platform"][self.position_server_platform]['margin'] 71 | if self.position_server_platform == "huobi": 72 | self.huobi_futures = configures['POSITION_SERVER']["platform"][self.position_server_platform]['futures'] 73 | self.huobi_swap = configures['POSITION_SERVER']["platform"][self.position_server_platform]['swap'] 74 | # synchronize 75 | overprice_range_str = configures["SYNCHRONIZE"]["overprice"]["range"] 76 | self.overprice_range = float((overprice_range_str.split("%"))[0]) / 100 # 超价幅度,浮点数类型 77 | # first_run 78 | self.first_run = configures["STATUS"]["first_run"] 79 | # ASSISTANT 80 | price_cancellation_amplitude_str = configures["ASSISTANT"]["amplitude"] 81 | self.price_cancellation_amplitude = float((price_cancellation_amplitude_str.split("%"))[0]) / 100 82 | self.time_cancellation = configures["ASSISTANT"]["time_cancellation"] 83 | self.time_cancellation_seconds = configures["ASSISTANT"]["seconds"] 84 | self.price_cancellation = configures["ASSISTANT"]["price_cancellation"] 85 | reissue_order_overprice_range_str = configures["ASSISTANT"]["reissue_order"] 86 | self.reissue_order = float((reissue_order_overprice_range_str.split("%"))[0]) / 100 87 | self.automatic_cancellation = configures["ASSISTANT"]["automatic_cancellation"] 88 | # MONGODB AUTHORIZATION 89 | self.mongodb_authorization = configures["MONGODB"]["authorization"] 90 | self.mongodb_user_name = configures["MONGODB"]["user_name"] 91 | self.mongodb_password = configures["MONGODB"]["password"] 92 | # MYSQL AUTHORIZATION 93 | self.mysql_authorization = configures["MYSQL"]["authorization"] 94 | self.mysql_user_name = configures["MYSQL"]["user_name"] 95 | self.mysql_password = configures["MYSQL"]["password"] 96 | # BACKTEST 97 | self.backtest = configures["MODE"]["backtest"] 98 | 99 | def update_config(self, config_file, config_content): 100 | """ 101 | 更新配置文件 102 | :param config_file: 配置文件路径及名称,如"config.json" 103 | :param config_content: 配置文件中的具体字典内容,将以当前content内容替换掉原配置文件中的内容 104 | :return: 打印"配置文件已更新!" 105 | """ 106 | with open(config_file, 'w') as json_file: 107 | json.dump(config_content, json_file, indent=4) 108 | print("配置文件已更新!") 109 | 110 | config = __Config() -------------------------------------------------------------------------------- /purequant/exchange/okex/lever_api.py: -------------------------------------------------------------------------------- 1 | from .client import Client 2 | from .consts import * 3 | 4 | 5 | class LeverAPI(Client): 6 | 7 | def __init__(self, api_key, api_secret_key, passphrase, use_server_time=False, test=False): 8 | Client.__init__(self, api_key, api_secret_key, passphrase, use_server_time, test) 9 | 10 | # query lever account info 11 | def get_account_info(self): 12 | return self._request_without_params(GET, LEVER_ACCOUNT) 13 | 14 | # query specific account info 15 | def get_specific_account(self, instrument_id): 16 | return self._request_without_params(GET, LEVER_COIN_ACCOUNT + str(instrument_id)) 17 | 18 | # query ledger record 19 | def get_ledger_record(self, instrument_id, after='', before='', limit='', type=''): 20 | params = {} 21 | if after: 22 | params['after'] = after 23 | if before: 24 | params['before'] = before 25 | if limit: 26 | params['limit'] = limit 27 | if type: 28 | params['type'] = type 29 | return self._request_with_params(GET, LEVER_LEDGER_RECORD + str(instrument_id) + '/ledger', params, cursor=True) 30 | 31 | # query lever config info 32 | def get_config_info(self): 33 | return self._request_without_params(GET, LEVER_CONFIG) 34 | 35 | # query specific config info 36 | def get_specific_config_info(self, instrument_id): 37 | return self._request_without_params(GET, LEVER_SPECIFIC_CONFIG + str(instrument_id) + '/availability') 38 | 39 | def get_borrow_coin(self, status='', after='', before='', limit=''): 40 | params = {'status': status, 'after': after, 'before': before, 'limit': limit} 41 | return self._request_with_params(GET, LEVER_BORROW_RECORD, params, cursor=True) 42 | 43 | def get_specific_borrow_coin(self, instrument_id, status='', after='', before='', limit=''): 44 | params = {'status': status, 'after': after, 'before': before, 'limit': limit} 45 | return self._request_with_params(GET, LEVER_SPECIFIC_CONFIG + str(instrument_id) + '/borrowed', params, cursor=True) 46 | 47 | # borrow coin 48 | def borrow_coin(self, instrument_id, currency, amount, client_oid=''): 49 | params = {'instrument_id': instrument_id, 'currency': currency, 'amount': amount} 50 | if client_oid: 51 | params['client_oid'] = client_oid 52 | return self._request_with_params(POST, LEVER_BORROW_COIN, params) 53 | 54 | # repayment coin 55 | def repayment_coin(self, instrument_id, currency, amount, borrow_id='', client_oid=''): 56 | params = {'instrument_id': instrument_id, 'currency': currency, 'amount': amount} 57 | if borrow_id: 58 | params['borrow_id'] = borrow_id 59 | if client_oid: 60 | params['client_oid'] = client_oid 61 | return self._request_with_params(POST, LEVER_REPAYMENT_COIN, params) 62 | 63 | # take order 64 | def take_order(self, instrument_id, side, margin_trading, client_oid='', type='', order_type='0', price='', size='', notional=''): 65 | params = {'instrument_id': instrument_id, 'side': side, 'margin_trading': margin_trading, 'client_oid': client_oid, 'type': type, 'order_type': order_type, 'price': price, 'size': size, 'notional': notional} 66 | return self._request_with_params(POST, LEVER_ORDER, params) 67 | 68 | def take_orders(self, params): 69 | return self._request_with_params(POST, LEVER_ORDERS, params) 70 | 71 | # revoke order 72 | def revoke_order(self, instrument_id, order_id='', client_oid=''): 73 | params = {'instrument_id': instrument_id} 74 | if order_id: 75 | return self._request_with_params(POST, LEVER_REVOKE_ORDER + str(order_id), params) 76 | elif client_oid: 77 | return self._request_with_params(POST, LEVER_REVOKE_ORDER + str(client_oid), params) 78 | 79 | def revoke_orders(self, params): 80 | return self._request_with_params(POST, LEVER_REVOKE_ORDERS, params) 81 | 82 | # query order list 83 | def get_order_list(self, instrument_id, state, after='', before='', limit=''): 84 | params = {'instrument_id': instrument_id, 'state': state, 'after': after, 'before': before, 'limit': limit} 85 | return self._request_with_params(GET, LEVER_ORDER_LIST, params, cursor=True) 86 | 87 | def get_order_pending(self, instrument_id, after='', before='', limit=''): 88 | params = {'instrument_id': instrument_id} 89 | if after: 90 | params['after'] = after 91 | if before: 92 | params['before'] = before 93 | if limit: 94 | params['limit'] = limit 95 | return self._request_with_params(GET, LEVEL_ORDERS_PENDING, params, cursor=True) 96 | 97 | # query order info 98 | def get_order_info(self, instrument_id, order_id='', client_oid=''): 99 | params = {'instrument_id': instrument_id} 100 | if order_id: 101 | return self._request_with_params(GET, LEVER_ORDER_INFO + str(order_id), params) 102 | elif client_oid: 103 | return self._request_with_params(GET, LEVER_ORDER_INFO + str(client_oid), params) 104 | 105 | def get_fills(self, instrument_id, order_id='', after='', before='', limit=''): 106 | params = {'instrument_id': instrument_id} 107 | if order_id: 108 | params['order_id'] = order_id 109 | if after: 110 | params['after'] = after 111 | if before: 112 | params['before'] = before 113 | if limit: 114 | params['limit'] = limit 115 | return self._request_with_params(GET, LEVER_FILLS, params, cursor=True) 116 | 117 | def get_leverage(self, instrument_id): 118 | return self._request_without_params(GET, LEVER_LEDGER_RECORD + str(instrument_id) + '/leverage') 119 | 120 | def set_leverage(self, instrument_id, leverage): 121 | params = {'leverage': leverage} 122 | return self._request_with_params(POST, LEVER_LEDGER_RECORD + str(instrument_id) + '/leverage', params) 123 | 124 | def get_mark_price(self, instrument_id): 125 | return self._request_without_params(GET, LEVER_MARK_PRICE + str(instrument_id) + '/mark_price') 126 | -------------------------------------------------------------------------------- /purequant/synchronize.py: -------------------------------------------------------------------------------- 1 | from purequant.position import POSITION 2 | from purequant.storage import storage 3 | from purequant.time import get_localtime 4 | from purequant.market import MARKET 5 | from purequant.config import config 6 | from purequant.exceptions import * 7 | 8 | class SYNCHRONIZE: 9 | """持仓同步""" 10 | 11 | def __init__(self, databank, database, data_sheet, exchange, instrument_id, time_frame): 12 | print("{} {} 持仓同步功能已启动!".format(get_localtime(), instrument_id)) 13 | self.__databank = databank 14 | self.__database = database 15 | self.__datasheet = data_sheet 16 | self.__exchange = exchange 17 | self.__instrument_id = instrument_id 18 | self.__time_frame = time_frame 19 | self.__position = POSITION(self.__exchange, self.__instrument_id, self.__time_frame) 20 | self.__market = MARKET(self.__exchange, self.__instrument_id, self.__time_frame) 21 | self.__overprice_range = config.overprice_range 22 | 23 | def save_strategy_position(self, strategy_direction, strategy_amount): 24 | """下单后将仓位信息保存至数据库.""" 25 | if self.__databank == "mysql": 26 | storage.mysql_save_strategy_position(self.__database, self.__datasheet, strategy_direction, 27 | strategy_amount) 28 | elif self.__databank == "mongodb": 29 | storage.mongodb_save(data={"时间": get_localtime(), "strategy_direction": strategy_direction, "strategy_amount": strategy_amount}, database=self.__database, collection=self.__datasheet) 30 | else: 31 | raise DataBankError 32 | 33 | def match(self): 34 | # 获取当前账户持仓信息 35 | account_direction = self.__position.direction() 36 | account_amount = self.__position.amount() 37 | 38 | # 获取当前策略应持仓位信息 39 | if self.__databank == "mysql": 40 | strategy_direction = storage.read_mysql_datas(0, self.__database, self.__datasheet, "amount", ">=")[-1][-2] 41 | strategy_amount = storage.read_mysql_datas(0, self.__database, self.__datasheet, "amount", ">=")[-1][-1] 42 | elif self.__databank == "mongodb": 43 | strategy_direction = storage.mongodb_read_data(self.__database, self.__datasheet)[-1][0]["strategy_direction"] 44 | strategy_amount = int(storage.mongodb_read_data(self.__database, self.__datasheet)[-1][0]["strategy_amount"]) 45 | else: 46 | strategy_direction = None 47 | strategy_amount = None 48 | raise DataBankError 49 | 50 | # 比较账户持仓与策略持仓,如不匹配则同步之 51 | if strategy_direction == "long" and account_direction == "long": 52 | if account_amount < strategy_amount: 53 | receipt = self.__exchange.buy(self.__market.last() * (1 + self.__overprice_range), strategy_amount - account_amount, 0) 54 | return "当前持多,当前实际持仓小于策略应持仓位数量,自动同步结果:{}".format(receipt) 55 | elif account_amount > strategy_amount: 56 | receipt = self.__exchange.sell(self.__market.last() * (1 - self.__overprice_range), account_amount - strategy_amount, 0) 57 | return "当前持多,当前实际持仓大于策略应持仓位数量,自动同步结果:{}".format(receipt) 58 | if strategy_direction == "short" and account_direction == "short": # 策略与账户均持空时 59 | if account_amount < strategy_amount: 60 | receipt = self.__exchange.sellshort(self.__market.last() * (1 - self.__overprice_range), strategy_amount - account_amount, 0) 61 | return "当前持空,当前实际持仓小于策略应持仓位数量,自动同步结果:{}".format(receipt) 62 | elif account_amount > strategy_amount: 63 | receipt = self.__exchange.buytocover(self.__market.last() * (1 + self.__overprice_range), account_amount - strategy_amount, 0) 64 | return "当前持空,当前实际持仓大于策略应持仓位数量,自动同步结果:{}".format(receipt) 65 | if strategy_direction == "long" and account_direction == "short": # 策略持多,账户却持空时 66 | receipt1 = self.__exchange.buytocover(self.__market.last() * (1 + self.__overprice_range), account_amount, 0) 67 | if "完全成交" not in receipt1: 68 | return "策略应持多,当前实际持空,自动同步结果:{}".format(receipt1) 69 | else: 70 | receipt2 = self.__exchange.buy(self.__market.last() * (1 + self.__overprice_range), strategy_amount, 0) 71 | return "策略应持多,当前实际持空,自动同步结果:{}".format(receipt1 + receipt2) 72 | if strategy_direction == "short" and account_direction == "long": # 策略持空,账户却持多时 73 | receipt1 = self.__exchange.sell(self.__market.last() * (1 - self.__overprice_range), account_amount, 0) 74 | if "完全成交" not in receipt1: 75 | return "策略应持空,当前实际持多,自动同步结果:{}".format(receipt1) 76 | else: 77 | receipt2 = self.__exchange.sellshort(self.__market.last() * (1 - self.__overprice_range), strategy_amount, 0) 78 | return "策略应持空,当前实际持多,自动同步结果:{}".format(receipt1 + receipt2) 79 | if strategy_direction == "none" and account_direction == "long": # 策略无持仓,账户却持多时 80 | receipt = self.__exchange.sell(self.__market.last() * (1 - self.__overprice_range), account_amount, 0) 81 | return "策略应无持仓,当前实际持多,自动同步结果:{}".format(receipt) 82 | if strategy_direction == "none" and account_direction == "short": # 策略无持仓,账户却持空时 83 | receipt = self.__exchange.buytocover(self.__market.last() * (1 + self.__overprice_range), account_amount, 0) 84 | return "策略应无持仓,当前实际持空,自动同步结果:{}".format(receipt) 85 | if account_direction == "none" and strategy_direction == "long": # 账户无持仓,策略却应持多时 86 | receipt = self.__exchange.buy(self.__market.last() * (1 + self.__overprice_range), strategy_amount, 0) 87 | return "策略应持多仓,当前实际无持仓,自动同步结果:{}".format(receipt) 88 | if account_direction == "none" and strategy_direction == "short": # 账户无持仓,策略却应持空 89 | receipt = self.__exchange.sellshort(self.__market.last() * (1 - self.__overprice_range), strategy_amount, 0) 90 | return "策略应持空仓,当前实际无持仓,自动同步结果:{}".format(receipt) 91 | if account_amount == strategy_amount and account_direction == strategy_direction: 92 | dict = {"策略持仓方向": strategy_direction, "策略持仓数量": strategy_amount, "账户实际持仓方向": account_direction, 93 | "账户实际持仓数量": account_amount} 94 | return "策略持仓与账户持仓匹配! {}".format(dict) 95 | else: 96 | raise MatchError 97 | 98 | -------------------------------------------------------------------------------- /purequant/exchange/binance/binance_futures.py: -------------------------------------------------------------------------------- 1 | import hmac 2 | import hashlib 3 | import logging 4 | import requests 5 | import time 6 | from purequant.time import get_cur_timestamp_ms 7 | try: 8 | from urllib import urlencode 9 | 10 | # for python3 11 | except ImportError: 12 | from urllib.parse import urlencode 13 | 14 | 15 | ENDPOINT = "https://www.binance.com" 16 | 17 | BUY = "BUY" 18 | SELL = "SELL" 19 | 20 | LIMIT = "LIMIT" 21 | MARKET = "MARKET" 22 | 23 | GTC = "GTC" 24 | IOC = "IOC" 25 | 26 | options = {} 27 | 28 | 29 | def set(apiKey, secret): 30 | """Set API key and secret. 31 | 32 | Must be called before any making any signed API calls. 33 | """ 34 | options["apiKey"] = apiKey 35 | options["secret"] = secret 36 | 37 | 38 | def depth(symbol, **kwargs): 39 | """Get order book. 40 | 41 | Args: 42 | symbol (str) 43 | limit (int, optional): Default 100. Must be one of 50, 20, 100, 500, 5, 44 | 200, 10. 45 | 46 | """ 47 | params = {"symbol": symbol} 48 | params.update(kwargs) 49 | data = request("GET", "/dapi/v1/depth", params) 50 | return { 51 | "bids": data["bids"], 52 | "asks": data["asks"] 53 | } 54 | 55 | 56 | def klines(symbol, interval, **kwargs): 57 | """Get kline/candlestick bars for a symbol. 58 | 59 | Klines are uniquely identified by their open time. If startTime and endTime 60 | are not sent, the most recent klines are returned. 61 | 62 | Args: 63 | symbol (str) 64 | interval (str) 65 | limit (int, optional): Default 500; max 500. 66 | startTime (int, optional) 67 | endTime (int, optional) 68 | 69 | """ 70 | params = {"symbol": symbol, "interval": interval} 71 | params.update(kwargs) 72 | data = request("GET", "/dapi/v1/klines", params) 73 | return data 74 | 75 | 76 | def position(): 77 | data = signedRequest("GET", "/dapi/v1/positionRisk", {}) 78 | if 'msg' in data: 79 | raise ValueError("Error from exchange: {}".format(data['msg'])) 80 | return data 81 | 82 | 83 | def order(symbol, side, quantity, price, orderType=LIMIT, positionSide=None, timeInForce=GTC, test=False, **kwargs): 84 | positionSide = "BOTH" if positionSide is None else positionSide 85 | params = { 86 | "symbol": symbol, 87 | "side": side, 88 | "type": orderType, 89 | "positionSide": positionSide, 90 | "timeInForce": timeInForce, 91 | "quantity": formatNumber(quantity), 92 | "price": formatNumber(price) 93 | } 94 | params.update(kwargs) 95 | path = "/dapi/v1/order" 96 | data = signedRequest("POST", path, params) 97 | return data 98 | 99 | 100 | def orderStatus(symbol, **kwargs): 101 | """Check an order's status. 102 | 103 | Args: 104 | symbol (str) 105 | orderId (int, optional) 106 | origClientOrderId (str, optional) 107 | recvWindow (int, optional) 108 | 109 | """ 110 | params = {"symbol": symbol} 111 | params.update(kwargs) 112 | data = signedRequest("GET", "/dapi/v1/order", params) 113 | return data 114 | 115 | 116 | def cancel(symbol, **kwargs): 117 | """Cancel an active order. 118 | 119 | Args: 120 | symbol (str) 121 | orderId (int, optional) 122 | origClientOrderId (str, optional) 123 | newClientOrderId (str, optional): Used to uniquely identify this 124 | cancel. Automatically generated by default. 125 | recvWindow (int, optional) 126 | 127 | """ 128 | params = {"symbol": symbol} 129 | params.update(kwargs) 130 | data = signedRequest("DELETE", "/dapi/v1/order", params) 131 | return data 132 | 133 | 134 | def openOrders(symbol, **kwargs): 135 | """Get all open orders on a symbol. 136 | 137 | Args: 138 | symbol (str) 139 | recvWindow (int, optional) 140 | 141 | """ 142 | params = {"symbol": symbol} 143 | params.update(kwargs) 144 | data = signedRequest("GET", "/dapi/v1/openOrder", params) 145 | return data 146 | 147 | 148 | def allOrders(symbol, **kwargs): 149 | """Get all account orders; active, canceled, or filled. 150 | 151 | If orderId is set, it will get orders >= that orderId. Otherwise most 152 | recent orders are returned. 153 | 154 | Args: 155 | symbol (str) 156 | orderId (int, optional) 157 | limit (int, optional): Default 500; max 500. 158 | recvWindow (int, optional) 159 | 160 | """ 161 | params = {"symbol": symbol} 162 | params.update(kwargs) 163 | data = signedRequest("GET", "/dapi/v1/openOrders", params) 164 | return data 165 | 166 | 167 | def myTrades(symbol, **kwargs): 168 | """Get trades for a specific account and symbol. 169 | 170 | Args: 171 | symbol (str) 172 | limit (int, optional): Default 500; max 500. 173 | fromId (int, optional): TradeId to fetch from. Default gets most recent 174 | trades. 175 | recvWindow (int, optional) 176 | 177 | """ 178 | params = {"symbol": symbol} 179 | params.update(kwargs) 180 | data = signedRequest("GET", "/dapi/v1/userTrades", params) 181 | return data 182 | 183 | 184 | def request(method, path, params=None): 185 | resp = requests.request(method, ENDPOINT + path, params=params) 186 | data = resp.json() 187 | if "msg" in data: 188 | logging.error(data['msg']) 189 | return data 190 | 191 | 192 | def signedRequest(method, path, params): 193 | if "apiKey" not in options or "secret" not in options: 194 | raise ValueError("Api key and secret must be set") 195 | 196 | query = urlencode(sorted(params.items())) 197 | query += "×tamp={}".format(get_cur_timestamp_ms() - 1000) 198 | secret = bytes(options["secret"].encode("utf-8")) 199 | signature = hmac.new(secret, query.encode("utf-8"), 200 | hashlib.sha256).hexdigest() 201 | query += "&signature={}".format(signature) 202 | resp = requests.request(method, 203 | ENDPOINT + path + "?" + query, 204 | headers={"X-MBX-APIKEY": options["apiKey"]}) 205 | data = resp.json() 206 | if "msg" in data: 207 | logging.error(data['msg']) 208 | return data 209 | 210 | 211 | def formatNumber(x): 212 | if isinstance(x, float): 213 | return "{:.8f}".format(x) 214 | else: 215 | return str(x) 216 | 217 | def get_ticker(symbol): 218 | params = {"symbol": symbol} 219 | data = request("GET", "/dapi/v1/ticker/price", params) 220 | return data 221 | 222 | 223 | def get_contract_value(symbol): 224 | result = None 225 | params = {} 226 | data = request("GET", "/dapi/v1/exchangeInfo", params) 227 | for item in data["symbols"]: 228 | if item["symbol"] == symbol: 229 | result = int(item["contractSize"]) 230 | return result 231 | -------------------------------------------------------------------------------- /purequant/exchange/binance/binance_swap.py: -------------------------------------------------------------------------------- 1 | import hmac 2 | import hashlib 3 | import logging 4 | import requests 5 | import time 6 | from purequant.time import get_cur_timestamp_ms 7 | try: 8 | from urllib import urlencode 9 | 10 | # for python3 11 | except ImportError: 12 | from urllib.parse import urlencode 13 | 14 | 15 | ENDPOINT = "https://www.binance.com" 16 | 17 | BUY = "BUY" 18 | SELL = "SELL" 19 | 20 | LIMIT = "LIMIT" 21 | MARKET = "MARKET" 22 | 23 | GTC = "GTC" 24 | IOC = "IOC" 25 | 26 | options = {} 27 | 28 | 29 | def set(apiKey, secret): 30 | """Set API key and secret. 31 | 32 | Must be called before any making any signed API calls. 33 | """ 34 | options["apiKey"] = apiKey 35 | options["secret"] = secret 36 | 37 | 38 | def depth(symbol, **kwargs): 39 | """Get order book. 40 | 41 | Args: 42 | symbol (str) 43 | limit (int, optional): Default 100. Must be one of 50, 20, 100, 500, 5, 44 | 200, 10. 45 | 46 | """ 47 | params = {"symbol": symbol} 48 | params.update(kwargs) 49 | data = request("GET", "/fapi/v1/depth", params) 50 | return { 51 | "bids": data["bids"], 52 | "asks": data["asks"] 53 | } 54 | 55 | 56 | def klines(symbol, interval, **kwargs): 57 | """Get kline/candlestick bars for a symbol. 58 | 59 | Klines are uniquely identified by their open time. If startTime and endTime 60 | are not sent, the most recent klines are returned. 61 | 62 | Args: 63 | symbol (str) 64 | interval (str) 65 | limit (int, optional): Default 500; max 500. 66 | startTime (int, optional) 67 | endTime (int, optional) 68 | 69 | """ 70 | params = {"symbol": symbol, "interval": interval} 71 | params.update(kwargs) 72 | data = request("GET", "/fapi/v1/klines", params) 73 | return data 74 | 75 | 76 | def position(): 77 | data = signedRequest("GET", "/fapi/v2/positionRisk", {}) 78 | if 'msg' in data: 79 | raise ValueError("Error from exchange: {}".format(data['msg'])) 80 | return data 81 | 82 | 83 | def order(symbol, side, quantity, price, orderType=LIMIT, positionSide=None, timeInForce=GTC, test=False, **kwargs): 84 | positionSide = "BOTH" if positionSide is None else positionSide 85 | params = { 86 | "symbol": symbol, 87 | "side": side, 88 | "type": orderType, 89 | "positionSide": positionSide, 90 | "timeInForce": timeInForce, 91 | "quantity": formatNumber(quantity), 92 | "price": formatNumber(price) 93 | } 94 | params.update(kwargs) 95 | path = "/fapi/v1/order" 96 | data = signedRequest("POST", path, params) 97 | return data 98 | 99 | 100 | def orderStatus(symbol, **kwargs): 101 | """Check an order's status. 102 | 103 | Args: 104 | symbol (str) 105 | orderId (int, optional) 106 | origClientOrderId (str, optional) 107 | recvWindow (int, optional) 108 | 109 | """ 110 | params = {"symbol": symbol} 111 | params.update(kwargs) 112 | data = signedRequest("GET", "/fapi/v1/order", params) 113 | return data 114 | 115 | 116 | def cancel(symbol, **kwargs): 117 | """Cancel an active order. 118 | 119 | Args: 120 | symbol (str) 121 | orderId (int, optional) 122 | origClientOrderId (str, optional) 123 | newClientOrderId (str, optional): Used to uniquely identify this 124 | cancel. Automatically generated by default. 125 | recvWindow (int, optional) 126 | 127 | """ 128 | params = {"symbol": symbol} 129 | params.update(kwargs) 130 | data = signedRequest("DELETE", "/fapi/v1/order", params) 131 | return data 132 | 133 | 134 | def openOrders(symbol, **kwargs): 135 | """Get all open orders on a symbol. 136 | 137 | Args: 138 | symbol (str) 139 | recvWindow (int, optional) 140 | 141 | """ 142 | params = {"symbol": symbol} 143 | params.update(kwargs) 144 | data = signedRequest("GET", "/fapi/v1/openOrder", params) 145 | return data 146 | 147 | 148 | def allOrders(symbol, **kwargs): 149 | """Get all account orders; active, canceled, or filled. 150 | 151 | If orderId is set, it will get orders >= that orderId. Otherwise most 152 | recent orders are returned. 153 | 154 | Args: 155 | symbol (str) 156 | orderId (int, optional) 157 | limit (int, optional): Default 500; max 500. 158 | recvWindow (int, optional) 159 | 160 | """ 161 | params = {"symbol": symbol} 162 | params.update(kwargs) 163 | data = signedRequest("GET", "/fapi/v1/openOrders", params) 164 | return data 165 | 166 | 167 | def myTrades(symbol, **kwargs): 168 | """Get trades for a specific account and symbol. 169 | 170 | Args: 171 | symbol (str) 172 | limit (int, optional): Default 500; max 500. 173 | fromId (int, optional): TradeId to fetch from. Default gets most recent 174 | trades. 175 | recvWindow (int, optional) 176 | 177 | """ 178 | params = {"symbol": symbol} 179 | params.update(kwargs) 180 | data = signedRequest("GET", "/fapi/v1/userTrades", params) 181 | return data 182 | 183 | 184 | def request(method, path, params=None): 185 | resp = requests.request(method, ENDPOINT + path, params=params) 186 | data = resp.json() 187 | if "msg" in data: 188 | logging.error(data['msg']) 189 | return data 190 | 191 | 192 | def signedRequest(method, path, params): 193 | if "apiKey" not in options or "secret" not in options: 194 | raise ValueError("Api key and secret must be set") 195 | 196 | query = urlencode(sorted(params.items())) 197 | query += "×tamp={}".format(get_cur_timestamp_ms() - 1000) 198 | secret = bytes(options["secret"].encode("utf-8")) 199 | signature = hmac.new(secret, query.encode("utf-8"), 200 | hashlib.sha256).hexdigest() 201 | query += "&signature={}".format(signature) 202 | resp = requests.request(method, 203 | ENDPOINT + path + "?" + query, 204 | headers={"X-MBX-APIKEY": options["apiKey"]}) 205 | data = resp.json() 206 | if "msg" in data: 207 | logging.error(data['msg']) 208 | return data 209 | 210 | 211 | def formatNumber(x): 212 | if isinstance(x, float): 213 | return "{:.8f}".format(x) 214 | else: 215 | return str(x) 216 | 217 | def get_ticker(symbol): 218 | params = {"symbol": symbol} 219 | data = request("GET", "/fapi/v1/ticker/price", params) 220 | return data 221 | 222 | 223 | def get_contract_value(symbol): 224 | result = None 225 | params = {} 226 | data = request("GET", "/fapi/v1/exchangeInfo", params)["symbols"] 227 | for item in data: 228 | if item["symbol"] == symbol: 229 | result = float(item["filters"][1]['stepSize']) 230 | return result 231 | -------------------------------------------------------------------------------- /purequant/exchange/okex/consts.py: -------------------------------------------------------------------------------- 1 | 2 | API_URL = 'https://www.okex.com' 3 | 4 | CONTENT_TYPE = 'Content-Type' 5 | OK_ACCESS_KEY = 'OK-ACCESS-KEY' 6 | OK_ACCESS_SIGN = 'OK-ACCESS-SIGN' 7 | OK_ACCESS_TIMESTAMP = 'OK-ACCESS-TIMESTAMP' 8 | OK_ACCESS_PASSPHRASE = 'OK-ACCESS-PASSPHRASE' 9 | 10 | ACEEPT = 'Accept' 11 | COOKIE = 'Cookie' 12 | LOCALE = 'Locale=' 13 | 14 | APPLICATION_JSON = 'application/json' 15 | 16 | GET = "GET" 17 | POST = "POST" 18 | DELETE = "DELETE" 19 | 20 | SERVER_TIMESTAMP_URL = '/api/general/v3/time' 21 | 22 | # account 23 | WALLET_INFO = '/api/account/v3/wallet' 24 | CURRENCY_INFO = '/api/account/v3/wallet/' 25 | COIN_TRANSFER = '/api/account/v3/transfer' 26 | COIN_WITHDRAW = '/api/account/v3/withdrawal' 27 | LEDGER_RECORD = '/api/account/v3/ledger' 28 | TOP_UP_ADDRESS = '/api/account/v3/deposit/address' 29 | ASSET_VALUATION = '/api/account/v3/asset-valuation' 30 | SUB_ACCOUNT = '/api/account/v3/sub-account' 31 | COINS_WITHDRAW_RECORD = '/api/account/v3/withdrawal/history' 32 | COIN_WITHDRAW_RECORD = '/api/account/v3/withdrawal/history/' 33 | COIN_TOP_UP_RECORDS = '/api/account/v3/deposit/history' 34 | COIN_TOP_UP_RECORD = '/api/account/v3/deposit/history/' 35 | CURRENCIES_INFO = '/api/account/v3/currencies' 36 | COIN_FEE = '/api/account/v3/withdrawal/fee' 37 | 38 | # spot 39 | SPOT_ACCOUNT_INFO = '/api/spot/v3/accounts' 40 | SPOT_COIN_ACCOUNT_INFO = '/api/spot/v3/accounts/' 41 | SPOT_LEDGER_RECORD = '/api/spot/v3/accounts/' 42 | SPOT_ORDER = '/api/spot/v3/orders' 43 | SPOT_ORDERS = '/api/spot/v3/batch_orders' 44 | SPOT_REVOKE_ORDER = '/api/spot/v3/cancel_orders/' 45 | SPOT_REVOKE_ORDERS = '/api/spot/v3/cancel_batch_orders/' 46 | SPOT_ORDERS_LIST = '/api/spot/v3/orders' 47 | SPOT_ORDERS_PENDING = '/api/spot/v3/orders_pending' 48 | SPOT_ORDER_INFO = '/api/spot/v3/orders/' 49 | SPOT_FILLS = '/api/spot/v3/fills' 50 | SPOT_ORDER_ALGO = '/api/spot/v3/order_algo' 51 | SPOT_CANCEL_ALGOS = '/api/spot/v3/cancel_batch_algos' 52 | SPOT_TRADE_FEE = '/api/spot/v3/trade_fee' 53 | SPOT_GET_ORDER_ALGOS = '/api/spot/v3/algo' 54 | SPOT_COIN_INFO = '/api/spot/v3/instruments' 55 | SPOT_DEPTH = '/api/spot/v3/instruments/' 56 | SPOT_TICKER = '/api/spot/v3/instruments/ticker' 57 | SPOT_SPECIFIC_TICKER = '/api/spot/v3/instruments/' 58 | SPOT_DEAL = '/api/spot/v3/instruments/' 59 | SPOT_KLINE = '/api/spot/v3/instruments/' 60 | 61 | # lever 62 | LEVER_ACCOUNT = '/api/margin/v3/accounts' 63 | LEVER_COIN_ACCOUNT = '/api/margin/v3/accounts/' 64 | LEVER_LEDGER_RECORD = '/api/margin/v3/accounts/' 65 | LEVER_CONFIG = '/api/margin/v3/accounts/availability' 66 | LEVER_SPECIFIC_CONFIG = '/api/margin/v3/accounts/' 67 | LEVER_BORROW_RECORD = '/api/margin/v3/accounts/borrowed' 68 | LEVER_SPECIFIC_BORROW_RECORD = '/api/margin/v3/accounts/' 69 | LEVER_BORROW_COIN = '/api/margin/v3/accounts/borrow' 70 | LEVER_REPAYMENT_COIN = '/api/margin/v3/accounts/repayment' 71 | LEVER_ORDER = '/api/margin/v3/orders' 72 | LEVER_ORDERS = '/api/margin/v3/batch_orders' 73 | LEVER_REVOKE_ORDER = '/api/margin/v3/cancel_orders/' 74 | LEVER_REVOKE_ORDERS = '/api/margin/v3/cancel_batch_orders' 75 | LEVER_ORDER_LIST = '/api/margin/v3/orders' 76 | LEVEL_ORDERS_PENDING = '/api/margin/v3/orders_pending' 77 | LEVER_ORDER_INFO = '/api/margin/v3/orders/' 78 | LEVER_FILLS = '/api/margin/v3/fills' 79 | LEVER_MARK_PRICE = '/api/margin/v3/instruments/' 80 | 81 | # future 82 | FUTURE_POSITION = '/api/futures/v3/position' 83 | FUTURE_SPECIFIC_POSITION = '/api/futures/v3/' 84 | FUTURE_ACCOUNTS = '/api/futures/v3/accounts' 85 | FUTURE_COIN_ACCOUNT = '/api/futures/v3/accounts/' 86 | FUTURE_GET_LEVERAGE = '/api/futures/v3/accounts/' 87 | FUTURE_SET_LEVERAGE = '/api/futures/v3/accounts/' 88 | FUTURE_LEDGER = '/api/futures/v3/accounts/' 89 | FUTURE_ORDER = '/api/futures/v3/order' 90 | FUTURE_ORDERS = '/api/futures/v3/orders' 91 | FUTURE_REVOKE_ORDER = '/api/futures/v3/cancel_order/' 92 | FUTURE_REVOKE_ORDERS = '/api/futures/v3/cancel_batch_orders/' 93 | FUTURE_ORDERS_LIST = '/api/futures/v3/orders/' 94 | FUTURE_ORDER_INFO = '/api/futures/v3/orders/' 95 | FUTURE_FILLS = '/api/futures/v3/fills' 96 | FUTURE_MARGIN_MODE = '/api/futures/v3/accounts/margin_mode' 97 | FUTURE_CLOSE_POSITION = '/api/futures/v3/close_position' 98 | FUTURE_CANCEL_ALL = '/api/futures/v3/cancel_all' 99 | HOLD_AMOUNT = '/api/futures/v3/accounts/' 100 | FUTURE_ORDER_ALGO = '/api/futures/v3/order_algo' 101 | FUTURE_CANCEL_ALGOS = '/api/futures/v3/cancel_algos' 102 | FUTURE_GET_ORDER_ALGOS = '/api/futures/v3/order_algo/' 103 | FUTURE_TRADE_FEE = '/api/futures/v3/trade_fee' 104 | FUTURE_PRODUCTS_INFO = '/api/futures/v3/instruments' 105 | FUTURE_DEPTH = '/api/futures/v3/instruments/' 106 | FUTURE_TICKER = '/api/futures/v3/instruments/ticker' 107 | FUTURE_SPECIFIC_TICKER = '/api/futures/v3/instruments/' 108 | FUTURE_TRADES = '/api/futures/v3/instruments/' 109 | FUTURE_KLINE = '/api/futures/v3/instruments/' 110 | FUTURE_INDEX = '/api/futures/v3/instruments/' 111 | FUTURE_RATE = '/api/futures/v3/rate' 112 | FUTURE_ESTIMAT_PRICE = '/api/futures/v3/instruments/' 113 | FUTURE_HOLDS = '/api/futures/v3/instruments/' 114 | FUTURE_LIMIT = '/api/futures/v3/instruments/' 115 | FUTURE_MARK = '/api/futures/v3/instruments/' 116 | FUTURE_LIQUIDATION = '/api/futures/v3/instruments/' 117 | FUTURE_AUTO_MARGIN = '/api/futures/v3/accounts/auto_margin' 118 | FUTURE_CHANGE_MARGIN = '/api/futures/v3/position/margin' 119 | FUTURE_HISTORY_SETTLEMENT = '/api/futures/v3/settlement/history' 120 | FUTURE_AMEND_ORDER = '/api/futures/v3/amend_order/' 121 | FUTURE_AMEND_BATCH_ORDERS = '/api/futures/v3/amend_batch_orders/' 122 | 123 | # swap 124 | SWAP_POSITIONS = '/api/swap/v3/position' 125 | SWAP_POSITION = '/api/swap/v3/' 126 | SWAP_ACCOUNTS = '/api/swap/v3/accounts' 127 | SWAP_ORDER_ALGO = '/api/swap/v3/order_algo' 128 | SWAP_CANCEL_ALGOS = '/api/swap/v3/cancel_algos' 129 | SWAP_GET_ORDER_ALGOS = '/api/swap/v3/order_algo/' 130 | SWAP_GET_TRADE_FEE = '/api/swap/v3/trade_fee' 131 | SWAP_ACCOUNT = '/api/swap/v3/' 132 | SWAP_ORDER = '/api/swap/v3/order' 133 | SWAP_ORDERS = '/api/swap/v3/orders' 134 | SWAP_CANCEL_ORDER = '/api/swap/v3/cancel_order/' 135 | SWAP_CANCEL_ORDERS = '/api/swap/v3/cancel_batch_orders/' 136 | SWAP_FILLS = '/api/swap/v3/fills' 137 | SWAP_INSTRUMENTS = '/api/swap/v3/instruments' 138 | SWAP_TICKETS = '/api/swap/v3/instruments/ticker' 139 | SWAP_RATE = '/api/swap/v3/rate' 140 | SWAP_CLOSE_POSITION = '/api/swap/v3/close_position' 141 | SWAP_CANCEL_ALL = '/api/swap/v3/cancel_all' 142 | SWAP_AMEND_ORDER = '/api/swap/v3/amend_order/' 143 | SWAP_AMEND_BATCH_ORDERS = '/api/swap/v3/amend_batch_orders/' 144 | SWAP_HISTORY_KLINE = '/api/swap/v3/instruments/' 145 | 146 | # information 147 | INFORMATION = '/api/information/v3/' 148 | 149 | # index 150 | INDEX_GET_CONSTITUENTS = '/api/index/v3/' 151 | 152 | # option 153 | OPTION_ORDER = '/api/option/v3/order' 154 | OPTION_ORDERS = '/api/option/v3/orders' 155 | OPTION_CANCEL_ORDER = '/api/option/v3/cancel_order/' 156 | OPTION_CANCEL_ORDERS = '/api/option/v3/cancel_batch_orders/' 157 | OPTION_AMEND_ORDER = '/api/option/v3/amend_order/' 158 | OPTION_AMEND_BATCH_ORDERS ='/api/option/v3/amend_batch_orders/' 159 | OPTION_FILLS = '/api/option/v3/fills/' 160 | OPTION_POSITION = '/api/option/v3/' 161 | OPTION_ACCOUNT = '/api/option/v3/accounts/' 162 | OPTION_TRADE_FEE = '/api/option/v3/trade_fee' 163 | OPTION_INDEX = '/api/option/v3/underlying' 164 | OPTION_INSTRUMENTS = '/api/option/v3/instruments/' 165 | OPTION_HISTORY_SETTLEMENT = '/api/option/v3/settlement/history/' 166 | 167 | # system 168 | SYSTEM_STATUS = '/api/system/v3/status' 169 | -------------------------------------------------------------------------------- /purequant/example/double_moving_average_strategy/spot_double_moving_average_strategy.py: -------------------------------------------------------------------------------- 1 | """ 2 | 双均线策略 3 | 此策略适用于OKEX的现货 4 | 如需用于其他类型合约或现货,可自行修改 5 | Author: Gary-Hertel 6 | Date: 2020/09/01 7 | email: interstella.ranger2020@gmail.com 8 | """ 9 | 10 | from purequant.indicators import INDICATORS 11 | from purequant.trade import OKEXSPOT 12 | from purequant.position import POSITION 13 | from purequant.market import MARKET 14 | from purequant.logger import logger 15 | from purequant.push import push 16 | from purequant.storage import storage 17 | from purequant.time import * 18 | from purequant.config import config 19 | 20 | class Strategy: 21 | 22 | def __init__(self, instrument_id, time_frame, fast_length, slow_length, long_stop, short_stop, start_asset, precision): 23 | try: 24 | print("{} {} 双均线多空策略已启动!".format(get_localtime(), instrument_id)) # 程序启动时打印提示信息 25 | config.loads('config.json') # 载入配置文件 26 | self.instrument_id = instrument_id # 合约ID 27 | self.time_frame = time_frame # k线周期 28 | self.precision = precision # 精度,即币对的最小交易数量 29 | self.exchange = OKEXSPOT(config.access_key, config.secret_key, config.passphrase, self.instrument_id) # 初始化交易所 30 | self.position = POSITION(self.exchange, self.instrument_id, self.time_frame) # 初始化potion 31 | self.market = MARKET(self.exchange, self.instrument_id, self.time_frame) # 初始化market 32 | self.indicators = INDICATORS(self.exchange, self.instrument_id, self.time_frame) # 初始化indicators 33 | # 在第一次运行程序时,将初始资金数据保存至数据库中 34 | self.database = "回测" # 回测时必须为"回测" 35 | self.datasheet = self.instrument_id.split("-")[0].lower() + "_" + time_frame 36 | if config.first_run == "true": 37 | storage.mysql_save_strategy_run_info(self.database, self.datasheet, get_localtime(), 38 | "none", 0, 0, 0, 0, "none", 0, 0, 0, start_asset) 39 | # 读取数据库中保存的总资金数据 40 | self.total_asset = storage.read_mysql_datas(0, self.database, self.datasheet, "总资金", ">")[-1][-1] 41 | self.total_profit = storage.read_mysql_datas(0, self.database, self.datasheet, "总资金", ">")[-1][-2] # 策略总盈亏 42 | self.counter = 0 # 计数器 43 | self.fast_length = fast_length # 短周期均线长度 44 | self.slow_length = slow_length # 长周期均线长度 45 | self.long_stop = long_stop # 多单止损幅度 46 | self.short_stop = short_stop # 空单止损幅度 47 | self.hold_price = 0 # 注意:okex的现货没有获取持仓均价的接口,故需实盘时需要手动记录入场价格。此种写法对于不同的交易所是通用的。 48 | # 此种写法,若策略重启,持仓价格会回归0 49 | except: 50 | logger.warning() 51 | 52 | def begin_trade(self, kline=None): 53 | try: 54 | if self.indicators.CurrentBar(kline=kline) < self.slow_length: # 如果k线数据不够长就返回 55 | return 56 | timestamp = ts_to_datetime_str(utctime_str_to_ts(kline[-1][0])) if kline else get_localtime() # 非回测模式下时间戳就是当前本地时间 57 | # 计算策略信号 58 | ma = self.indicators.MA(self.fast_length, self.slow_length, kline=kline) 59 | fast_ma = ma[0] 60 | slow_ma = ma[1] 61 | cross_over = fast_ma[-2] >= slow_ma[-2] and fast_ma[-3] < slow_ma[-3] # 不用当根k线上的ma来计算信号,防止信号闪烁 62 | cross_below = slow_ma[-2] >= fast_ma[-2] and slow_ma[-3] < fast_ma[-3] 63 | if self.indicators.BarUpdate(kline=kline): # 如果k线更新,计数器归零 64 | self.counter = 0 65 | if self.counter < 1: 66 | # 按照策略信号开平仓 67 | if cross_over and round(self.position.amount()) < self.precision: # 金叉时,若当前无持仓,则买入开多并推送下单结果。0.1这个数值根据每个币对的最小交易数量决定 68 | price = float(self.market.open(-1, kline=kline)) # 下单价格=此根k线开盘价 69 | self.hold_price = price # 记录开仓价格 70 | amount = float(self.total_asset / price) # 数量=总资金/价格 71 | info = self.exchange.buy(price, amount) 72 | push(info) 73 | storage.mysql_save_strategy_run_info(self.database, self.datasheet, timestamp, "买入开多", 74 | price, amount, amount * price, price, "long", amount, 75 | 0, self.total_profit, self.total_asset) # 将信息保存至数据库 76 | if cross_below and round(self.position.amount(), 1) >= self.precision: # 死叉时,如果当前持多就卖出平多。当前持仓数量根据币对的最小交易数量取小数 77 | price = float(self.market.open(-1, kline=kline)) 78 | amount = float(self.position.amount()) 79 | profit = (price - self.hold_price) * amount # 计算逻辑盈亏 80 | self.total_profit += profit 81 | self.total_asset += profit 82 | info = self.exchange.sell(price, amount) 83 | push(info) 84 | self.hold_price = 0 # 平多后记录持仓价格为0 85 | storage.mysql_save_strategy_run_info(self.database, self.datasheet, timestamp, "卖出平多", 86 | price, amount, amount * price, 0, "none", 0, 87 | profit, self.total_profit, self.total_asset) 88 | # 如果当前持多且最低价小于等于持仓均价*止损幅度,触发止损,卖出平多止损 89 | if round(self.position.amount(), 1) >= self.precision and self.market.low(-1, kline=kline) <= self.hold_price * self.long_stop: 90 | price = float(self.hold_price * self.long_stop) 91 | amount = float(self.position.amount()) 92 | profit = (price - self.hold_price) * amount # 计算逻辑盈亏 93 | self.total_profit += profit 94 | self.total_asset += profit 95 | info = self.exchange.sell(price, amount) 96 | push("此次盈亏:{} 当前总资金:{}".format(profit, self.total_asset) + info) 97 | self.hold_price = 0 # 平多后记录持仓价格为0 98 | storage.mysql_save_strategy_run_info(self.database, self.datasheet, timestamp, 99 | "卖出止损", price, amount, amount * price, 0, "none", 0, 100 | profit, self.total_profit, self.total_asset) 101 | self.counter += 1 # 计数器加1,控制此根k线上不再下单 102 | except Exception as e: 103 | logger.info() 104 | 105 | if __name__ == "__main__": 106 | 107 | # 实例化策略类 108 | instrument_id = "EOS-USDT" 109 | time_frame = "1m" 110 | strategy = Strategy(instrument_id=instrument_id, time_frame=time_frame, 111 | fast_length=5, slow_length=10, long_stop=0.98, short_stop=1.02, 112 | start_asset=4, precision=0.1) 113 | 114 | if config.backtest == "enabled": # 回测模式 115 | print("正在回测,可能需要一段时间,请稍后...") 116 | start_time = get_cur_timestamp() 117 | records = [] 118 | data = storage.read_purequant_server_datas(instrument_id.split("-")[0].lower() + "_" + time_frame) 119 | for k in data: 120 | records.append(k) 121 | strategy.begin_trade(kline=records) 122 | cost_time = get_cur_timestamp() - start_time 123 | print("回测用时{}秒,结果已保存至mysql数据库!".format(cost_time)) 124 | else: # 实盘模式 125 | while True: # 循环运行begin_trade函数 126 | strategy.begin_trade() 127 | time.sleep(3) # 休眠几秒 ,防止请求频率超限 -------------------------------------------------------------------------------- /purequant/monitor.py: -------------------------------------------------------------------------------- 1 | from purequant.exchange.okex.websocket import subscribe as okex_subscribe 2 | from purequant.exchange.huobi.websocket import subscribe as huobi_subscribe 3 | from purequant.exchange.huobi.websocket import huobi_swap_position_subscribe 4 | import asyncio, uuid 5 | from purequant.config import config 6 | from purequant.exchange.huobi.websocket import handle_ws_data 7 | from purequant.exceptions import * 8 | 9 | 10 | def okex_futures_usd(): 11 | print("Okex币本位交割合约持仓状态监控中...") 12 | url = 'wss://real.okex.com:8443/ws/v3' 13 | access_key = config.access_key 14 | secret_key = config.secret_key 15 | passphrase = config.passphrase 16 | delivery_date = config.delivery_date 17 | task1 = okex_subscribe(url, access_key, passphrase, secret_key, ["futures/position:BTC-USD-{}".format(delivery_date)]) 18 | task2 = okex_subscribe(url, access_key, passphrase, secret_key, ["futures/position:BCH-USD-{}".format(delivery_date)]) 19 | task3 = okex_subscribe(url, access_key, passphrase, secret_key, ["futures/position:BSV-USD-{}".format(delivery_date)]) 20 | task4 = okex_subscribe(url, access_key, passphrase, secret_key, ["futures/position:ETH-USD-{}".format(delivery_date)]) 21 | task5 = okex_subscribe(url, access_key, passphrase, secret_key, ["futures/position:ETC-USD-{}".format(delivery_date)]) 22 | task6 = okex_subscribe(url, access_key, passphrase, secret_key, ["futures/position:EOS-USD-{}".format(delivery_date)]) 23 | task7 = okex_subscribe(url, access_key, passphrase, secret_key, ["futures/position:LTC-USD-{}".format(delivery_date)]) 24 | task8 = okex_subscribe(url, access_key, passphrase, secret_key, ["futures/position:TRX-USD-{}".format(delivery_date)]) 25 | task9 = okex_subscribe(url, access_key, passphrase, secret_key, ["futures/position:XRP-USD-{}".format(delivery_date)]) 26 | task_list = [task1, task2, task3, task4, task5, task6, task7, task8, task9] 27 | asyncio.run(asyncio.wait(task_list)) 28 | 29 | def okex_futures_usdt(): 30 | print("Okex USDT本位交割合约持仓状态监控中...") 31 | url = 'wss://real.okex.com:8443/ws/v3' 32 | access_key = config.access_key 33 | secret_key = config.secret_key 34 | passphrase = config.passphrase 35 | delivery_date = config.delivery_date 36 | task1 = okex_subscribe(url, access_key, passphrase, secret_key, ["futures/position:BTC-USDT-{}".format(delivery_date)]) 37 | task2 = okex_subscribe(url, access_key, passphrase, secret_key, ["futures/position:BCH-USDT-{}".format(delivery_date)]) 38 | task3 = okex_subscribe(url, access_key, passphrase, secret_key, ["futures/position:BSV-USDT-{}".format(delivery_date)]) 39 | task4 = okex_subscribe(url, access_key, passphrase, secret_key, ["futures/position:ETH-USDT-{}".format(delivery_date)]) 40 | task5 = okex_subscribe(url, access_key, passphrase, secret_key, ["futures/position:ETC-USDT-{}".format(delivery_date)]) 41 | task6 = okex_subscribe(url, access_key, passphrase, secret_key, ["futures/position:EOS-USDT-{}".format(delivery_date)]) 42 | task7 = okex_subscribe(url, access_key, passphrase, secret_key, ["futures/position:LTC-USDT-{}".format(delivery_date)]) 43 | task8 = okex_subscribe(url, access_key, passphrase, secret_key, ["futures/position:TRX-USDT-{}".format(delivery_date)]) 44 | task9 = okex_subscribe(url, access_key, passphrase, secret_key, ["futures/position:XRP-USDT-{}".format(delivery_date)]) 45 | task_list = [task1, task2, task3, task4, task5, task6, task7, task8, task9] 46 | asyncio.run(asyncio.wait(task_list)) 47 | 48 | def okex_swap_usd(): 49 | print("Okex 永续合约持仓状态监控中...") 50 | url = 'wss://real.okex.com:8443/ws/v3' 51 | access_key = config.access_key 52 | secret_key = config.secret_key 53 | passphrase = config.passphrase 54 | task_list = [] 55 | symbol_list = ['BTC-USD-SWAP', 'lTC-USD-SWAP', 'ETH-USD-SWAP', 'ETC-USD-SWAP', 'XRP-USD-SWAP', 56 | 'EOS-USD-SWAP', 'BCH-USD-SWAP', 'BSV-USD-SWAP', 'TRX-USD-SWAP', ] 57 | for item in symbol_list: 58 | task_list.append(okex_subscribe(url, access_key, passphrase, secret_key, ["swap/position:{}".format(item)])) 59 | asyncio.run(asyncio.wait(task_list)) 60 | 61 | def okex_swap_usdt(): 62 | print("Okex 永续合约持仓状态监控中...") 63 | url = 'wss://real.okex.com:8443/ws/v3' 64 | access_key = config.access_key 65 | secret_key = config.secret_key 66 | passphrase = config.passphrase 67 | task_list = [] 68 | symbol_list = ['BTC-USDT-SWAP', 'lTC-USDT-SWAP', 'ETH-USDT-SWAP', 'ETC-USDT-SWAP', 'XRP-USDT-SWAP', 69 | 'EOS-USDT-SWAP', 'BCH-USDT-SWAP', 'BSV-USDT-SWAP', 'TRX-USDT-SWAP', ] 70 | for item in symbol_list: 71 | task_list.append(okex_subscribe(url, access_key, passphrase, secret_key, ["swap/position:{}".format(item)])) 72 | asyncio.run(asyncio.wait(task_list)) 73 | 74 | def okex_spot(): 75 | print("Okex 币币账户状态监控中...") 76 | url = 'wss://real.okex.com:8443/ws/v3' 77 | access_key = config.access_key 78 | secret_key = config.secret_key 79 | passphrase = config.passphrase 80 | task_list = [] 81 | symbol_list = ['BTC', 'lTC', 'ETH', 'ETC', 'XRP', 82 | 'EOS', 'BCH', 'BSV', 'TRX', 'USDT'] 83 | for item in symbol_list: 84 | task_list.append(okex_subscribe(url, access_key, passphrase, secret_key, ["spot/account:{}".format(item)])) 85 | asyncio.run(asyncio.wait(task_list)) 86 | 87 | def okex_margin(): 88 | print("Okex 币币杠杆账户状态监控中...") 89 | url = 'wss://real.okex.com:8443/ws/v3' 90 | access_key = config.access_key 91 | secret_key = config.secret_key 92 | passphrase = config.passphrase 93 | task_list = [] 94 | symbol_list = ['BTC-USDT', 'lTC-USDT', 'ETH-USDT', 'ETC-USDT', 'XRP-USDT', 95 | 'EOS-USDT', 'BCH-USDT', 'BSV-USDT', 'TRX-USDT'] 96 | for item in symbol_list: 97 | task_list.append(okex_subscribe(url, access_key, passphrase, secret_key, ["spot/margin_account:{}".format(item)])) 98 | asyncio.run(asyncio.wait(task_list)) 99 | 100 | def huobi_futures(): 101 | print("Huobi币本位交割合约持仓状态监控中...") 102 | url = 'wss://api.hbdm.vn/notification' 103 | access_key = config.access_key 104 | secret_key = config.secret_key 105 | position_subs = [ 106 | { 107 | "op": "sub", 108 | "cid": str(uuid.uuid1()), 109 | "topic": "positions.*" 110 | } 111 | ] 112 | asyncio.run(huobi_subscribe(url, access_key, secret_key, position_subs, handle_ws_data, auth=True)) 113 | 114 | def huobi_swap(): 115 | print("Huobi币本位永续合约持仓状态监控中...") 116 | url = 'wss://api.hbdm.vn/swap-notification' 117 | access_key = config.access_key 118 | secret_key = config.secret_key 119 | position_subs = [ 120 | { 121 | "op": "sub", 122 | "cid": str(uuid.uuid1()), 123 | "topic": "positions.*" 124 | } 125 | ] 126 | asyncio.run(huobi_swap_position_subscribe(url, access_key, secret_key, position_subs, handle_ws_data, auth=True)) 127 | 128 | 129 | def position_update(): 130 | """持仓状态更新自动推送""" 131 | if config.position_server_platform == "okex": 132 | if config.okex_futures_usd == "true": 133 | okex_futures_usd() 134 | if config.okex_futures_usdt == "true": 135 | okex_futures_usdt() 136 | if config.okex_swap_usd == "true": 137 | okex_swap_usd() 138 | if config.okex_swap_usdt == "true": 139 | okex_swap_usdt() 140 | if config.okex_spot == "true": 141 | okex_spot() 142 | if config.okex_margin == "true": 143 | okex_margin() 144 | if config.position_server_platform == "huobi": 145 | if config.huobi_futures == "true": 146 | huobi_futures() 147 | if config.huobi_swap == "true": 148 | huobi_swap() 149 | else: 150 | raise ExchangeError("配置文件中position sever的platform设置错误!") 151 | -------------------------------------------------------------------------------- /purequant/exchange/okex/option_api.py: -------------------------------------------------------------------------------- 1 | from .client import Client 2 | from .consts import * 3 | 4 | 5 | class OptionAPI(Client): 6 | 7 | def __init__(self, api_key, api_secret_key, passphrase, use_server_time=False, test=False): 8 | Client.__init__(self, api_key, api_secret_key, passphrase, use_server_time, test) 9 | 10 | def take_order(self, instrument_id, side, price, size, client_oid='', order_type='0', match_price='0'): 11 | params = {'instrument_id': instrument_id, 'side': side, 'price': price, 'size': size} 12 | if client_oid: 13 | params['client_oid'] = client_oid 14 | if order_type: 15 | params['order_type'] = order_type 16 | if match_price: 17 | params['match_price'] = match_price 18 | return self._request_with_params(POST, OPTION_ORDER, params) 19 | 20 | def take_orders(self, underlying, order_data): 21 | params = {'underlying': underlying, 'order_data': order_data} 22 | return self._request_with_params(POST, OPTION_ORDERS, params) 23 | 24 | def revoke_order(self, underlying, order_id='', client_oid=''): 25 | if order_id: 26 | return self._request_without_params(POST, OPTION_CANCEL_ORDER + str(underlying) + '/' + str(order_id)) 27 | elif client_oid: 28 | return self._request_without_params(POST, OPTION_CANCEL_ORDER + str(underlying) + '/' + str(client_oid)) 29 | 30 | def revoke_orders(self, underlying, order_ids='', client_oids=''): 31 | params = {} 32 | if order_ids: 33 | params = {'order_ids': order_ids} 34 | elif client_oids: 35 | params = {'client_oids': client_oids} 36 | return self._request_with_params(POST, OPTION_CANCEL_ORDERS + str(underlying), params) 37 | 38 | def amend_order(self, underlying, order_id='', client_oid='', request_id='', new_size='', new_price=''): 39 | params = {} 40 | if order_id: 41 | params['order_id'] = order_id 42 | elif client_oid: 43 | params['client_oid'] = client_oid 44 | if new_size: 45 | params['new_size'] = new_size 46 | if new_price: 47 | params['new_price'] = new_price 48 | if request_id: 49 | params['request_id'] = request_id 50 | return self._request_with_params(POST, OPTION_AMEND_ORDER + str(underlying), params) 51 | 52 | def amend_batch_orders(self, underlying, amend_data): 53 | params = {'amend_data': amend_data} 54 | return self._request_with_params(POST, OPTION_AMEND_BATCH_ORDERS + str(underlying), params) 55 | 56 | def get_order_info(self, underlying, order_id='', client_oid=''): 57 | if order_id: 58 | return self._request_without_params(GET, OPTION_ORDERS + '/' + str(underlying) + '/' + str(order_id)) 59 | elif client_oid: 60 | return self._request_without_params(GET, OPTION_ORDERS + '/' + str(underlying) + '/' + str(client_oid)) 61 | 62 | def get_order_list(self, underlying, state, instrument_id='', after='', before='', limit=''): 63 | params = {'state': state} 64 | if instrument_id: 65 | params['instrument_id'] = instrument_id 66 | if after: 67 | params['after'] = after 68 | if before: 69 | params['before'] = before 70 | if limit: 71 | params['limit'] = limit 72 | return self._request_with_params(GET, OPTION_ORDERS + '/' + str(underlying), params, cursor=True) 73 | 74 | def get_fills(self, underlying, order_id='', instrument_id='', after='', before='', limit=''): 75 | params = {} 76 | if order_id: 77 | params['order_id'] = order_id 78 | if instrument_id: 79 | params['instrument_id'] = instrument_id 80 | if after: 81 | params['after'] = after 82 | if before: 83 | params['before'] = before 84 | if limit: 85 | params['limit'] = limit 86 | return self._request_with_params(GET, OPTION_FILLS + str(underlying), params, cursor=True) 87 | 88 | def get_specific_position(self, underlying, instrument_id=''): 89 | params = {} 90 | if instrument_id: 91 | params['instrument_id'] = instrument_id 92 | return self._request_with_params(GET, OPTION_POSITION + str(underlying) + '/position', params) 93 | 94 | def get_underlying_account(self, underlying): 95 | return self._request_without_params(GET, OPTION_ACCOUNT + str(underlying)) 96 | 97 | def get_ledger(self, underlying, after='', before='', limit=''): 98 | params = {} 99 | if after: 100 | params['after'] = after 101 | if before: 102 | params['before'] = before 103 | if limit: 104 | params['limit'] = limit 105 | return self._request_with_params(GET, OPTION_ACCOUNT + str(underlying) + '/ledger', params, cursor=True) 106 | 107 | def get_trade_fee(self): 108 | return self._request_without_params(GET, OPTION_TRADE_FEE) 109 | 110 | def get_index(self): 111 | return self._request_without_params(GET, OPTION_INDEX) 112 | 113 | def get_instruments(self, underlying, delivery='', instrument_id=''): 114 | params = {} 115 | if delivery: 116 | params['delivery'] = delivery 117 | if instrument_id: 118 | params['instrument_id'] = instrument_id 119 | return self._request_with_params(GET, OPTION_INSTRUMENTS + str(underlying), params) 120 | 121 | def get_instruments_summary(self, underlying, delivery=''): 122 | params = {} 123 | if delivery: 124 | params['delivery'] = delivery 125 | return self._request_with_params(GET, OPTION_INSTRUMENTS + str(underlying) + '/summary', params) 126 | 127 | def get_option_instruments_summary(self, underlying, instrument_id): 128 | return self._request_without_params(GET, OPTION_INSTRUMENTS + str(underlying) + '/summary/' + str(instrument_id)) 129 | 130 | def get_depth(self, instrument_id, size=''): 131 | params = {} 132 | if size: 133 | params['size'] = size 134 | return self._request_with_params(GET, OPTION_INSTRUMENTS + str(instrument_id) + '/book', params) 135 | 136 | def get_trades(self, instrument_id, after='', before='', limit=''): 137 | params = {} 138 | if after: 139 | params['after'] = after 140 | if before: 141 | params['before'] = before 142 | if limit: 143 | params['limit'] = limit 144 | return self._request_with_params(GET, OPTION_INSTRUMENTS + str(instrument_id) + '/trades', params, cursor=True) 145 | 146 | def get_specific_ticker(self, instrument_id): 147 | return self._request_without_params(GET, OPTION_INSTRUMENTS + str(instrument_id) + '/ticker') 148 | 149 | def get_kline(self, instrument_id, start='', end='', granularity=''): 150 | params = {} 151 | if start: 152 | params['start'] = start 153 | if end: 154 | params['end'] = end 155 | if granularity: 156 | params['granularity'] = granularity 157 | # 按时间倒叙 即由结束时间到开始时间 158 | return self._request_with_params(GET, OPTION_INSTRUMENTS + str(instrument_id) + '/candles', params) 159 | 160 | # 按时间正序 即由开始时间到结束时间 161 | # data = self._request_with_params(GET, OPTION_INSTRUMENTS + str(instrument_id) + '/candles', params) 162 | # return list(reversed(data)) 163 | 164 | def get_history_settlement(self, instrument_id, start='', end='', limit=''): 165 | params = {} 166 | if start: 167 | params['start'] = start 168 | if end: 169 | params['end'] = end 170 | if limit: 171 | params['limit'] = limit 172 | return self._request_with_params(GET, OPTION_HISTORY_SETTLEMENT + str(instrument_id), params) 173 | -------------------------------------------------------------------------------- /purequant/exchange/binance/binance_spot.py: -------------------------------------------------------------------------------- 1 | import hmac 2 | import hashlib 3 | import logging 4 | import requests 5 | import time 6 | from purequant.time import ts_to_utc_str, get_cur_timestamp_ms 7 | try: 8 | from urllib import urlencode 9 | 10 | # for python3 11 | except ImportError: 12 | from urllib.parse import urlencode 13 | 14 | 15 | ENDPOINT = "https://www.binance.com" 16 | 17 | BUY = "BUY" 18 | SELL = "SELL" 19 | 20 | LIMIT = "LIMIT" 21 | MARKET = "MARKET" 22 | 23 | GTC = "GTC" 24 | IOC = "IOC" 25 | 26 | options = {} 27 | 28 | 29 | def set(apiKey, secret): 30 | """Set API key and secret. 31 | 32 | Must be called before any making any signed API calls. 33 | """ 34 | options["apiKey"] = apiKey 35 | options["secret"] = secret 36 | 37 | 38 | def tickers(): 39 | """Get best price/qty on the order book for all symbols.""" 40 | data = request("GET", "/api/v3/ticker/bookTicker") 41 | return {d["symbol"]: { 42 | "bid": d["bidPrice"], 43 | "ask": d["askPrice"], 44 | "bidQty": d["bidQty"], 45 | "askQty": d["askQty"], 46 | } for d in data} 47 | 48 | 49 | def depth(symbol, **kwargs): 50 | """Get order book. 51 | 52 | Args: 53 | symbol (str) 54 | limit (int, optional): Default 100. Must be one of 50, 20, 100, 500, 5, 55 | 200, 10. 56 | 57 | """ 58 | params = {"symbol": symbol} 59 | params.update(kwargs) 60 | data = request("GET", "/api/v3/depth", params) 61 | return { 62 | "bids": data['bids'], 63 | "asks": data['asks'] 64 | } 65 | 66 | 67 | def klines(symbol, interval, **kwargs): 68 | """Get kline/candlestick bars for a symbol. 69 | 70 | Klines are uniquely identified by their open time. If startTime and endTime 71 | are not sent, the most recent klines are returned. 72 | 73 | Args: 74 | symbol (str) 75 | interval (str) 76 | limit (int, optional): Default 500; max 500. 77 | startTime (int, optional) 78 | endTime (int, optional) 79 | 80 | """ 81 | params = {"symbol": symbol, "interval": interval} 82 | params.update(kwargs) 83 | data = request("GET", "/api/v3/klines", params) 84 | return data 85 | 86 | 87 | def balances(): 88 | """Get current balances for all symbols.""" 89 | data = signedRequest("GET", "/api/v3/account", {}) 90 | if 'msg' in data: 91 | raise ValueError("Error from exchange: {}".format(data['msg'])) 92 | 93 | return {d["asset"]: { 94 | "free": d["free"], 95 | "locked": d["locked"], 96 | } for d in data.get("balances", [])} 97 | 98 | 99 | def order(symbol, side, quantity, price, orderType=LIMIT, timeInForce=GTC, 100 | test=False, **kwargs): 101 | """Send in a new order. 102 | 103 | Args: 104 | symbol (str) 105 | side (str): BUY or SELL. 106 | quantity (float, str or decimal) 107 | price (float, str or decimal) 108 | orderType (str, optional): LIMIT or MARKET. 109 | timeInForce (str, optional): GTC or IOC. 110 | test (bool, optional): Creates and validates a new order but does not 111 | send it into the matching engine. Returns an empty dict if 112 | successful. 113 | newClientOrderId (str, optional): A unique id for the order. 114 | Automatically generated if not sent. 115 | stopPrice (float, str or decimal, optional): Used with stop orders. 116 | icebergQty (float, str or decimal, optional): Used with iceberg orders. 117 | 118 | """ 119 | params = { 120 | "symbol": symbol, 121 | "side": side, 122 | "type": orderType, 123 | "timeInForce": timeInForce, 124 | "quantity": formatNumber(quantity), 125 | "price": formatNumber(price), 126 | } 127 | params.update(kwargs) 128 | path = "/api/v3/order/test" if test else "/api/v3/order" 129 | data = signedRequest("POST", path, params) 130 | return data 131 | 132 | 133 | def orderStatus(symbol, **kwargs): 134 | """Check an order's status. 135 | 136 | Args: 137 | symbol (str) 138 | orderId (int, optional) 139 | origClientOrderId (str, optional) 140 | recvWindow (int, optional) 141 | 142 | """ 143 | params = {"symbol": symbol} 144 | params.update(kwargs) 145 | data = signedRequest("GET", "/api/v3/order", params) 146 | return data 147 | 148 | 149 | def cancel(symbol, **kwargs): 150 | """Cancel an active order. 151 | 152 | Args: 153 | symbol (str) 154 | orderId (int, optional) 155 | origClientOrderId (str, optional) 156 | newClientOrderId (str, optional): Used to uniquely identify this 157 | cancel. Automatically generated by default. 158 | recvWindow (int, optional) 159 | 160 | """ 161 | params = {"symbol": symbol} 162 | params.update(kwargs) 163 | data = signedRequest("DELETE", "/api/v3/order", params) 164 | return data 165 | 166 | 167 | def openOrders(symbol, **kwargs): 168 | """Get all open orders on a symbol. 169 | 170 | Args: 171 | symbol (str) 172 | recvWindow (int, optional) 173 | 174 | """ 175 | params = {"symbol": symbol} 176 | params.update(kwargs) 177 | data = signedRequest("GET", "/api/v3/openOrders", params) 178 | return data 179 | 180 | 181 | def allOrders(symbol, **kwargs): 182 | """Get all account orders; active, canceled, or filled. 183 | 184 | If orderId is set, it will get orders >= that orderId. Otherwise most 185 | recent orders are returned. 186 | 187 | Args: 188 | symbol (str) 189 | orderId (int, optional) 190 | limit (int, optional): Default 500; max 500. 191 | recvWindow (int, optional) 192 | 193 | """ 194 | params = {"symbol": symbol} 195 | params.update(kwargs) 196 | data = signedRequest("GET", "/api/v3/allOrders", params) 197 | return data 198 | 199 | 200 | def myTrades(symbol, **kwargs): 201 | """Get trades for a specific account and symbol. 202 | 203 | Args: 204 | symbol (str) 205 | limit (int, optional): Default 500; max 500. 206 | fromId (int, optional): TradeId to fetch from. Default gets most recent 207 | trades. 208 | recvWindow (int, optional) 209 | 210 | """ 211 | params = {"symbol": symbol} 212 | params.update(kwargs) 213 | data = signedRequest("GET", "/api/v3/myTrades", params) 214 | return data 215 | 216 | 217 | def request(method, path, params=None): 218 | resp = requests.request(method, ENDPOINT + path, params=params) 219 | data = resp.json() 220 | if "msg" in data: 221 | logging.error(data['msg']) 222 | return data 223 | 224 | 225 | def signedRequest(method, path, params): 226 | if "apiKey" not in options or "secret" not in options: 227 | raise ValueError("Api key and secret must be set") 228 | 229 | query = urlencode(sorted(params.items())) 230 | query += "×tamp={}".format(get_cur_timestamp_ms()-1000) 231 | secret = bytes(options["secret"].encode("utf-8")) 232 | signature = hmac.new(secret, query.encode("utf-8"), 233 | hashlib.sha256).hexdigest() 234 | query += "&signature={}".format(signature) 235 | resp = requests.request(method, 236 | ENDPOINT + path + "?" + query, 237 | headers={"X-MBX-APIKEY": options["apiKey"]}) 238 | data = resp.json() 239 | if "msg" in data: 240 | logging.error(data['msg']) 241 | return data 242 | 243 | 244 | def formatNumber(x): 245 | if isinstance(x, float): 246 | return "{:.8f}".format(x) 247 | else: 248 | return str(x) 249 | 250 | def get_ticker(symbol): 251 | params = {"symbol": symbol} 252 | data = request("GET", "/api/v3/ticker/price", params) 253 | return data 254 | 255 | def get_last_kline(symbol): 256 | """获取24hr 价格变动情况""" 257 | params = {"symbol": symbol} 258 | data = request("GET", "/api/v3/ticker/24hr", params) 259 | timestamp = ts_to_utc_str(float(data["closeTime"]) / 1000) 260 | open = data["openPrice"] 261 | high = data["highPrice"] 262 | low = data["lowPrice"] 263 | close = data["lastPrice"] 264 | volume = data["volume"] 265 | last_kline = [timestamp, open, high, low, close, volume] 266 | return last_kline -------------------------------------------------------------------------------- /purequant/exchange/okex/spot_api.py: -------------------------------------------------------------------------------- 1 | from .client import Client 2 | from .consts import * 3 | 4 | 5 | class SpotAPI(Client): 6 | 7 | def __init__(self, api_key, api_secret_key, passphrase, use_server_time=False, test=False, first=False): 8 | Client.__init__(self, api_key, api_secret_key, passphrase, use_server_time, test, first) 9 | 10 | # query spot account info 11 | def get_account_info(self): 12 | return self._request_without_params(GET, SPOT_ACCOUNT_INFO) 13 | 14 | # query specific coin account info 15 | def get_coin_account_info(self, currency): 16 | return self._request_without_params(GET, SPOT_COIN_ACCOUNT_INFO + str(currency)) 17 | 18 | # query ledger record not paging 19 | def get_ledger_record(self, currency, after='', before='', limit='', type=''): 20 | params = {} 21 | if after: 22 | params['after'] = after 23 | if before: 24 | params['before'] = before 25 | if limit: 26 | params['limit'] = limit 27 | if type: 28 | params['type'] = type 29 | return self._request_with_params(GET, SPOT_LEDGER_RECORD + str(currency) + '/ledger', params, cursor=True) 30 | 31 | # take order 32 | def take_order(self, instrument_id, side, client_oid='', type='', size='', price='', order_type='0', notional=''): 33 | params = {'instrument_id': instrument_id, 'side': side, 'client_oid': client_oid, 'type': type, 'size': size, 'price': price, 'order_type': order_type, 'notional': notional} 34 | return self._request_with_params(POST, SPOT_ORDER, params) 35 | 36 | def take_orders(self, params): 37 | return self._request_with_params(POST, SPOT_ORDERS, params) 38 | 39 | # revoke order 40 | def revoke_order(self, instrument_id, order_id='', client_oid=''): 41 | params = {'instrument_id': instrument_id} 42 | if order_id: 43 | return self._request_with_params(POST, SPOT_REVOKE_ORDER + str(order_id), params) 44 | elif client_oid: 45 | return self._request_with_params(POST, SPOT_REVOKE_ORDER + str(client_oid), params) 46 | 47 | def revoke_orders(self, params): 48 | return self._request_with_params(POST, SPOT_REVOKE_ORDERS, params) 49 | 50 | # query orders list v3 51 | def get_orders_list(self, instrument_id, state, after='', before='', limit=''): 52 | params = {'instrument_id': instrument_id, 'state': state} 53 | if after: 54 | params['after'] = after 55 | if before: 56 | params['before'] = before 57 | if limit: 58 | params['limit'] = limit 59 | return self._request_with_params(GET, SPOT_ORDERS_LIST, params, cursor=True) 60 | 61 | # query order info 62 | def get_order_info(self, instrument_id, order_id='', client_oid=''): 63 | params = {'instrument_id': instrument_id} 64 | if order_id: 65 | return self._request_with_params(GET, SPOT_ORDER_INFO + str(order_id), params) 66 | elif client_oid: 67 | return self._request_with_params(GET, SPOT_ORDER_INFO + str(client_oid), params) 68 | 69 | def get_orders_pending(self, instrument_id, after='', before='', limit=''): 70 | params = {'instrument_id': instrument_id} 71 | if after: 72 | params['after'] = after 73 | if before: 74 | params['before'] = before 75 | if limit: 76 | params['limit'] = limit 77 | return self._request_with_params(GET, SPOT_ORDERS_PENDING, params, cursor=True) 78 | 79 | def get_fills(self, instrument_id, order_id='', after='', before='', limit=''): 80 | params = {'instrument_id': instrument_id} 81 | if order_id: 82 | params['order_id'] = order_id 83 | if after: 84 | params['after'] = after 85 | if before: 86 | params['before'] = before 87 | if limit: 88 | params['limit'] = limit 89 | return self._request_with_params(GET, SPOT_FILLS, params, cursor=True) 90 | 91 | # take order_algo 92 | def take_order_algo(self, instrument_id, mode, order_type, size, side, trigger_price='', algo_price='', algo_type='', 93 | callback_rate='', algo_variance='', avg_amount='', limit_price='', sweep_range='', 94 | sweep_ratio='', single_limit='', time_interval=''): 95 | params = {'instrument_id': instrument_id, 'mode': mode, 'order_type': order_type, 'size': size, 'side': side} 96 | if order_type == '1': # 止盈止损参数 97 | params['trigger_price'] = trigger_price 98 | params['algo_price'] = algo_price 99 | if algo_type: 100 | params['algo_type'] = algo_type 101 | elif order_type == '2': # 跟踪委托参数 102 | params['callback_rate'] = callback_rate 103 | params['trigger_price'] = trigger_price 104 | elif order_type == '3': # 冰山委托参数(最多同时存在6单) 105 | params['algo_variance'] = algo_variance 106 | params['avg_amount'] = avg_amount 107 | params['limit_price'] = limit_price 108 | elif order_type == '4': # 时间加权参数(最多同时存在6单) 109 | params['sweep_range'] = sweep_range 110 | params['sweep_ratio'] = sweep_ratio 111 | params['single_limit'] = single_limit 112 | params['limit_price'] = limit_price 113 | params['time_interval'] = time_interval 114 | return self._request_with_params(POST, SPOT_ORDER_ALGO, params) 115 | 116 | # cancel_algos 117 | def cancel_algos(self, instrument_id, algo_ids, order_type): 118 | params = {'instrument_id': instrument_id, 'algo_ids': algo_ids, 'order_type': order_type} 119 | return self._request_with_params(POST, SPOT_CANCEL_ALGOS, params) 120 | 121 | def get_trade_fee(self): 122 | return self._request_without_params(GET, SPOT_TRADE_FEE) 123 | 124 | # get order_algos 125 | def get_order_algos(self, instrument_id, order_type, status='', algo_id='', before='', after='', limit=''): 126 | params = {'instrument_id': instrument_id, 'order_type': order_type} 127 | if status: 128 | params['status'] = status 129 | elif algo_id: 130 | params['algo_id'] = algo_id 131 | if before: 132 | params['before'] = before 133 | if after: 134 | params['after'] = after 135 | if limit: 136 | params['limit'] = limit 137 | return self._request_with_params(GET, SPOT_GET_ORDER_ALGOS, params) 138 | 139 | # query spot coin info 140 | def get_coin_info(self): 141 | return self._request_without_params(GET, SPOT_COIN_INFO) 142 | 143 | # query depth 144 | def get_depth(self, instrument_id, size='', depth=''): 145 | params = {} 146 | if size: 147 | params['size'] = size 148 | if depth: 149 | params['depth'] = depth 150 | return self._request_with_params(GET, SPOT_DEPTH + str(instrument_id) + '/book', params) 151 | 152 | # query ticker info 153 | def get_ticker(self): 154 | return self._request_without_params(GET, SPOT_TICKER) 155 | 156 | # query specific ticker 157 | def get_specific_ticker(self, instrument_id): 158 | return self._request_without_params(GET, SPOT_SPECIFIC_TICKER + str(instrument_id) + '/ticker') 159 | 160 | def get_deal(self, instrument_id, limit=''): 161 | params = {} 162 | if limit: 163 | params['limit'] = limit 164 | return self._request_with_params(GET, SPOT_DEAL + str(instrument_id) + '/trades', params) 165 | 166 | # query k-line info 167 | def get_kline(self, instrument_id, start='', end='', granularity=''): 168 | params = {} 169 | if start: 170 | params['start'] = start 171 | if end: 172 | params['end'] = end 173 | if granularity: 174 | params['granularity'] = granularity 175 | # 按时间倒叙 即由结束时间到开始时间 176 | return self._request_with_params(GET, SPOT_KLINE + str(instrument_id) + '/candles', params) 177 | 178 | # 按时间正序 即由开始时间到结束时间 179 | # data = self._request_with_params(GET, SPOT_KLINE + str(instrument_id) + '/candles', params) 180 | # return list(reversed(data)) 181 | 182 | def get_history_kline(self, instrument_id, start='', end='', granularity=''): 183 | params = {} 184 | if start: 185 | params['start'] = start 186 | if end: 187 | params['end'] = end 188 | if granularity: 189 | params['granularity'] = granularity 190 | return self._request_with_params(GET, SPOT_KLINE + str(instrument_id) + '/history' + '/candles', params) 191 | -------------------------------------------------------------------------------- /purequant/exchange/huobi/websocket.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import uuid 3 | import urllib 4 | import asyncio 5 | import websockets 6 | import json 7 | import hmac 8 | import base64 9 | import hashlib 10 | import gzip 11 | import traceback 12 | from purequant.storage import storage 13 | from purequant.config import config 14 | from purequant.push import push 15 | 16 | 17 | def generate_signature(host, method, params, request_path, secret_key): 18 | """Generate signature of huobi future. 19 | 20 | Args: 21 | host: api domain url.PS: colo user should set this host as 'api.hbdm.com',not colo domain. 22 | method: request method. 23 | params: request params. 24 | request_path: "/notification" 25 | secret_key: api secret_key 26 | Returns: 27 | singature string. 28 | """ 29 | host_url = urllib.parse.urlparse(host).hostname.lower() 30 | sorted_params = sorted(params.items(), key=lambda d: d[0], reverse=False) 31 | encode_params = urllib.parse.urlencode(sorted_params) 32 | payload = [method, host_url, request_path, encode_params] 33 | payload = "\n".join(payload) 34 | payload = payload.encode(encoding="UTF8") 35 | secret_key = secret_key.encode(encoding="utf8") 36 | digest = hmac.new(secret_key, payload, digestmod=hashlib.sha256).digest() 37 | signature = base64.b64encode(digest) 38 | signature = signature.decode() 39 | return signature 40 | 41 | 42 | async def subscribe(url, access_key, secret_key, subs, callback=None, auth=False): 43 | """ Huobi Future subscribe websockets. 44 | Args: 45 | url: the url to be signatured. 46 | access_key: API access_key. 47 | secret_key: API secret_key. 48 | subs: the data list to subscribe. 49 | callback: the callback function to handle the ws data received. 50 | auth: True: Need to be signatured. False: No need to be signatured. 51 | """ 52 | async with websockets.connect(url) as websocket: 53 | if auth: 54 | timestamp = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S") 55 | data = { 56 | "AccessKeyId": access_key, 57 | "SignatureMethod": "HmacSHA256", 58 | "SignatureVersion": "2", 59 | "Timestamp": timestamp 60 | } 61 | sign = generate_signature(url, "GET", data, "/notification", secret_key) 62 | data["op"] = "auth" 63 | data["type"] = "api" 64 | data["Signature"] = sign 65 | msg_str = json.dumps(data) 66 | await websocket.send(msg_str) 67 | print(f"send: {msg_str}") 68 | for sub in subs: 69 | sub_str = json.dumps(sub) 70 | await websocket.send(sub_str) 71 | print(f"send: {sub_str}") 72 | while True: 73 | rsp = await websocket.recv() 74 | data = json.loads(gzip.decompress(rsp).decode()) 75 | # print(f"recevie<--: {data}") 76 | if "op" in data and data.get("op") == "ping": 77 | pong_msg = {"op": "pong", "ts": data.get("ts")} 78 | await websocket.send(json.dumps(pong_msg)) 79 | # print(f"send: {pong_msg}") 80 | continue 81 | if "ping" in data: 82 | pong_msg = {"pong": data.get("ping")} 83 | await websocket.send(json.dumps(pong_msg)) 84 | # print(f"send: {pong_msg}") 85 | continue 86 | rsp = await callback(data) 87 | 88 | async def huobi_swap_position_subscribe(url, access_key, secret_key, subs, callback=None, auth=False): 89 | async with websockets.connect(url) as websocket: 90 | if auth: 91 | timestamp = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S") 92 | data = { 93 | "AccessKeyId": access_key, 94 | "SignatureMethod": "HmacSHA256", 95 | "SignatureVersion": "2", 96 | "Timestamp": timestamp 97 | } 98 | sign = generate_signature(url, "GET", data, "/swap-notification", secret_key) 99 | data["op"] = "auth" 100 | data["type"] = "api" 101 | data["Signature"] = sign 102 | msg_str = json.dumps(data) 103 | await websocket.send(msg_str) 104 | print(f"send: {msg_str}") 105 | for sub in subs: 106 | sub_str = json.dumps(sub) 107 | await websocket.send(sub_str) 108 | print(f"send: {sub_str}") 109 | while True: 110 | rsp = await websocket.recv() 111 | data = json.loads(gzip.decompress(rsp).decode()) 112 | # print(f"recevie<--: {data}") 113 | if "op" in data and data.get("op") == "ping": 114 | pong_msg = {"op": "pong", "ts": data.get("ts")} 115 | await websocket.send(json.dumps(pong_msg)) 116 | # print(f"send: {pong_msg}") 117 | continue 118 | if "ping" in data: 119 | pong_msg = {"pong": data.get("ping")} 120 | await websocket.send(json.dumps(pong_msg)) 121 | # print(f"send: {pong_msg}") 122 | continue 123 | rsp = await callback(data) 124 | 125 | async def huobi_swap_position_subscribe(url, access_key, secret_key, subs, callback=None, auth=False): 126 | async with websockets.connect(url) as websocket: 127 | if auth: 128 | timestamp = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S") 129 | data = { 130 | "AccessKeyId": access_key, 131 | "SignatureMethod": "HmacSHA256", 132 | "SignatureVersion": "2", 133 | "Timestamp": timestamp 134 | } 135 | sign = generate_signature(url, "GET", data, "/swap-notification", secret_key) 136 | data["op"] = "auth" 137 | data["type"] = "api" 138 | data["Signature"] = sign 139 | msg_str = json.dumps(data) 140 | await websocket.send(msg_str) 141 | print(f"send: {msg_str}") 142 | for sub in subs: 143 | sub_str = json.dumps(sub) 144 | await websocket.send(sub_str) 145 | print(f"send: {sub_str}") 146 | while True: 147 | rsp = await websocket.recv() 148 | data = json.loads(gzip.decompress(rsp).decode()) 149 | # print(f"recevie<--: {data}") 150 | if "op" in data and data.get("op") == "ping": 151 | pong_msg = {"op": "pong", "ts": data.get("ts")} 152 | await websocket.send(json.dumps(pong_msg)) 153 | # print(f"send: {pong_msg}") 154 | continue 155 | if "ping" in data: 156 | pong_msg = {"pong": data.get("ping")} 157 | await websocket.send(json.dumps(pong_msg)) 158 | # print(f"send: {pong_msg}") 159 | continue 160 | rsp = await callback(data) 161 | 162 | 163 | 164 | async def handle_ws_data(*args, **kwargs): 165 | """ callback function 166 | Args: 167 | args: values 168 | kwargs: key-values. 169 | """ 170 | try: 171 | dict = {"data":("callback param", *args)} 172 | # print(dict['data'][1]) 173 | if "topic" in dict['data'][1]: 174 | topic = dict['data'][1]['topic'] 175 | if "position" in topic: 176 | if 'data' in dict['data'][1]: 177 | data = dict['data'][1]['data'][0] 178 | if 'contract_code' in data: 179 | contract_code = data['contract_code'] 180 | amount = data['volume'] 181 | price = data['cost_hold'] 182 | direction = data['direction'] 183 | last = data['last_price'] 184 | leverage = data['lever_rate'] 185 | info = "【交易提醒】HUOBI交割合约持仓更新!合约ID:{} 持仓方向:{} 持仓量:{} 持仓均价:{} 最新成交价:{} 杠杆倍数:{}".format( 186 | contract_code, direction, amount, price, last, leverage) 187 | if dict['data'][1]['event'] == 'order.match' or dict['data'][1]['event'] == 'settlement' or dict['data'][1]['event'] == 'order.liquidation': 188 | push(info) 189 | else: 190 | if config.mongodb_console == "true": 191 | print("callback param", *args, **kwargs) 192 | dict = {"data":("callback param", *args)} 193 | storage.mongodb_save(dict, config.mongodb_database, config.mongodb_collection) 194 | except: 195 | pass 196 | 197 | 198 | if __name__ == "__main__": 199 | #### input your access_key and secret_key below: 200 | access_key = "" 201 | secret_key = "" 202 | 203 | market_url = 'wss://www.hbdm.vn/ws' 204 | order_url = 'wss://api.hbdm.vn/notification' 205 | 206 | market_subs = [ 207 | { 208 | "sub": "market.BTC_CQ.kline.1min", 209 | "id": str(uuid.uuid1()) 210 | }, 211 | { 212 | "sub": "market.BTC_NW.depth.step6", 213 | "id": str(uuid.uuid1()) 214 | }, 215 | { 216 | "sub": "market.BTC_CW.detail", 217 | "id": str(uuid.uuid1()) 218 | } 219 | ] 220 | order_subs = [ 221 | { 222 | "op": "sub", 223 | "cid": str(uuid.uuid1()), 224 | "topic": "positions.EOS" 225 | } 226 | ] 227 | 228 | while True: 229 | try: 230 | # asyncio.get_event_loop().run_until_complete( 231 | # subscribe(market_url, access_key, secret_key, market_subs, handle_ws_data, auth=False)) 232 | asyncio.get_event_loop().run_until_complete(subscribe(order_url, access_key, secret_key, order_subs, handle_ws_data, auth=True)) 233 | # except (websockets.exceptions.ConnectionClosed): 234 | except Exception as e: 235 | traceback.print_exc() 236 | print('websocket connection error. reconnect rightnow') -------------------------------------------------------------------------------- /purequant/example/boll_breakthrough_strategy/boll_breakthrough_strategy.py: -------------------------------------------------------------------------------- 1 | """ 2 | 布林强盗突破策略 策略规则详见:https://zhuanlan.zhihu.com/p/64238996 3 | 此策略适用于OKEX的USDT合约 4 | 如需用于其他类型合约或现货,可自行修改 5 | Author: Gary-Hertel 6 | Date: 2020/08/31 7 | email: interstella.ranger2020@gmail.com 8 | 9 | 鉴于回测模式与实盘模式策略运行机制的不同,故此策略的设置是当根k线开仓后当根k线不平仓 10 | """ 11 | 12 | from purequant.trade import OKEXFUTURES 13 | from purequant.market import MARKET 14 | from purequant.position import POSITION 15 | from purequant.indicators import INDICATORS 16 | from purequant.logger import logger 17 | from purequant.config import config 18 | from purequant.time import * 19 | from purequant.storage import storage 20 | from purequant.push import push 21 | 22 | class Strategy: 23 | """布林强盗策略""" 24 | 25 | def __init__(self, instrument_id, time_frame, bollinger_lengths, filter_length, start_asset): 26 | try: 27 | # 策略启动时控制台输出提示信息 28 | print("{} {} 布林强盗突破策略已启动!".format(get_localtime(), instrument_id)) # 程序启动时打印提示信息 29 | config.loads("config.json") # 载入配置文件 30 | # 初始化 31 | self.instrument_id = instrument_id # 合约ID 32 | self.time_frame = time_frame # k线周期 33 | self.exchange = OKEXFUTURES(config.access_key, config.secret_key, config.passphrase, self.instrument_id) # 交易所 34 | self.market = MARKET(self.exchange, self.instrument_id, self.time_frame) # 行情 35 | self.position = POSITION(self.exchange, self.instrument_id, self.time_frame) # 持仓 36 | self.indicators = INDICATORS(self.exchange, self.instrument_id, self.time_frame) # 指标 37 | # 在第一次运行程序时,将初始资金、总盈亏等数据保存至数据库中 38 | self.database = "回测" # 数据库,回测时必须为"回测" 39 | self.datasheet = self.instrument_id.split("-")[0].lower() + "_" + time_frame # 数据表 40 | if config.first_run == "true": 41 | storage.mysql_save_strategy_run_info(self.database, self.datasheet, get_localtime(), 42 | "none", 0, 0, 0, 0, "none", 0, 0, 0, start_asset) 43 | # 读取数据库中保存的总资金数据 44 | self.total_asset = storage.read_mysql_datas(0, self.database, self.datasheet, "总资金", ">")[-1][-1] 45 | self.total_profit = storage.read_mysql_datas(0, self.database, self.datasheet, "总资金", ">")[-1][-2] # 策略总盈亏 46 | # 策略参数 47 | self.contract_value = self.market.contract_value() # 合约面值 48 | self.counter = 0 # 计数器 49 | self.bollinger_lengths = bollinger_lengths # 布林通道参数 50 | self.filter_length = filter_length # 过滤器参数 51 | self.out_day = self.bollinger_lengths + 1 # 自适应出场ma的初始值,设为51,因为策略启动时k线更新函数会起作用,其值会减去1 52 | except: 53 | logger.warning() 54 | 55 | def begin_trade(self, kline=None): 56 | try: # 异常处理 57 | if self.indicators.CurrentBar(kline=kline) < self.bollinger_lengths: # 如果k线数据不够长就返回 58 | return 59 | 60 | timestamp = ts_to_datetime_str(utctime_str_to_ts(kline[-1][0])) if kline else get_localtime() # 非回测模式下时间戳就是当前本地时间 61 | 62 | if self.indicators.BarUpdate(kline=kline): 63 | self.counter = 0 # k线更新时还原计数器 64 | if self.out_day > 10: # 计算MA的天数最小递减到10。如果达到10,则不再递减。 65 | self.out_day -= 1 # 自适应出场ma的长度参数根据持仓周期递减,持有头寸的时间每多一天,计算MA的天数减1 66 | 67 | deviation = float(self.indicators.STDDEV(self.bollinger_lengths, nbdev=2, kline=kline)[-1]) # 标准差 68 | middleband = float(self.indicators.BOLL(self.bollinger_lengths, kline=kline)['middleband'][-1]) # 布林通道中轨 69 | upperband = float(middleband + deviation) # 布林通道上轨 70 | lowerband = float(middleband - deviation) # 布林通道下轨 71 | filter = float(self.market.close(-1, kline=kline) - self.market.close((self.filter_length * -1) - 1, kline=kline)) # 过滤器:当日收盘价减去30日前的收盘价 72 | ma = float(self.indicators.MA(self.out_day, kline=kline)[-1]) # 自适应移动出场平均线 73 | 74 | # 策略主体 75 | # 若k线数据足够长,且满足过滤条件,且当根k线最高价大于等于布林通道上轨,买入开多。 76 | # 开仓处也设置计数器过滤,是为了防止没有启用交易助手的情况下挂单未成交,仓位为零时当根k线一直满足开仓条件,会重复挂单。 77 | if self.indicators.CurrentBar(kline=kline) >= self.bollinger_lengths and filter > 0 and self.market.high(-1, kline=kline) > upperband and self.counter < 1: 78 | if self.position.amount() == 0: # 若当前无持仓 79 | price = upperband # 开多价格为布林通道上轨的值 80 | amount = round(self.total_asset/upperband/self.contract_value) # 合约张数取整 81 | info = self.exchange.buy(price, amount) # 买入开多,并将返回的信息赋值给变量info 82 | push(info) # 推送信息 83 | storage.mysql_save_strategy_run_info(self.database, self.datasheet, timestamp, "买入开多", 84 | price, amount, amount * price * self.contract_value, price, 85 | "long", amount, 0, self.total_profit, 86 | self.total_asset) # 将信息保存至数据库 87 | self.counter += 1 # 此策略是在盘中开仓,而在回测时,每根bar只会运行一次,每根bar上的价格不分时间先后,故此处开仓后计数器加1,也就是当根k线不平仓 88 | # 因为实盘时每个ticker进来策略就会运行一次。注意回测和实盘策略运行机制的不同。 89 | # 开空 90 | if self.indicators.CurrentBar(kline=kline) >= self.bollinger_lengths and filter < 0 and self.market.low(-1, kline=kline) < lowerband and self.counter < 1: 91 | if self.position.amount() == 0: 92 | price = lowerband 93 | amount = round(self.total_asset/upperband/self.contract_value) 94 | info = self.exchange.sellshort(price, amount) 95 | push(info) 96 | storage.mysql_save_strategy_run_info(self.database, self.datasheet, timestamp, "卖出开空", 97 | price, amount, amount * price * self.contract_value, price, 98 | "short", amount, 0, self.total_profit, self.total_asset) 99 | self.counter += 1 100 | # 如果当前持多,且当根k线最低价小于等于中轨值,触发保护性止损,就平多止损 101 | # 因为回测是一根k线上运行整个策略一次,所以要实现当根k线开仓后当根k线不平仓,需要将self.counter < 1的条件加在平仓的地方 102 | if self.position.direction() == "long" and self.market.low(-1, kline=kline) < middleband and self.counter < 1: 103 | profit = self.position.coverlong_profit(last=middleband) # 此处计算平多利润时,传入最新价last为中轨值,也就是触发止损价格的那个值。 104 | self.total_profit += profit # 计算经过本次盈亏后的总利润 105 | self.total_asset += profit # 计算经过本次盈亏后的总资金 106 | price = middleband # 平多价格为中轨值 107 | amount = self.position.amount() # 平仓数量为当前持仓数量 108 | info = self.exchange.sell(price, amount) 109 | push(info) 110 | self.counter += 1 111 | storage.mysql_save_strategy_run_info(self.database, self.datasheet, timestamp, "卖出止损", 112 | price, amount, price * amount * self.contract_value, 113 | 0, "none", 0, profit, self.total_profit, self.total_asset) 114 | if self.position.direction() == "short" and self.market.high(-1, kline=kline) > middleband and self.counter < 1: 115 | profit = self.position.covershort_profit(last=middleband) 116 | self.total_profit += profit 117 | self.total_asset += profit 118 | price = middleband 119 | amount = self.position.amount() 120 | info = self.exchange.buytocover(price, amount) 121 | push(info) 122 | self.counter += 1 123 | storage.mysql_save_strategy_run_info(self.database, self.datasheet, timestamp, 124 | "买入止损", price, amount, amount * price * self.contract_value, 125 | 0, "none", 0, profit, self.total_profit, self.total_asset) 126 | # 平多 127 | if self.position.direction() == "long" and upperband > ma > self.market.low(-1, kline=kline) and self.counter < 1: 128 | profit = self.position.coverlong_profit(last=ma) 129 | self.total_profit += profit 130 | self.total_asset += profit 131 | price = ma # 平仓价格为自适应出场均线的值 132 | amount = self.position.amount() 133 | info = self.exchange.sell(price, amount) 134 | push(info) 135 | self.counter += 1 136 | storage.mysql_save_strategy_run_info(self.database, self.datasheet, timestamp, "卖出平多", 137 | price, amount, price * amount * self.contract_value, 138 | 0, "none", 0, profit, self.total_profit, self.total_asset) 139 | # 平空 140 | if self.position.direction() == "short" and lowerband < ma < self.market.high(-1, kline=kline) and self.counter < 1: 141 | profit = self.position.covershort_profit(last=ma) 142 | self.total_profit += profit 143 | self.total_asset += profit 144 | price = ma 145 | amount = self.position.amount() 146 | info = self.exchange.buytocover(price, amount) 147 | push(info) 148 | self.counter += 1 149 | storage.mysql_save_strategy_run_info(self.database, self.datasheet, timestamp, 150 | "买入平空", price, amount, amount * price * self.contract_value, 151 | 0, "none", 0, profit, self.total_profit, self.total_asset) 152 | except: 153 | logger.error() 154 | 155 | if __name__ == "__main__": 156 | 157 | instrument_id = "ETH-USDT-201225" 158 | time_frame = "1d" 159 | strategy = Strategy(instrument_id=instrument_id, time_frame=time_frame, bollinger_lengths=50, filter_length=30, start_asset=1000) # 实例化策略类 160 | 161 | if config.backtest == "enabled": # 回测模式 162 | print("正在回测,可能需要一段时间,请稍后...") 163 | start_time = get_cur_timestamp() 164 | records = [] 165 | data = storage.read_purequant_server_datas(instrument_id.split("-")[0].lower() + "_" + time_frame) 166 | for k in data: 167 | records.append(k) 168 | strategy.begin_trade(kline=records) 169 | cost_time = get_cur_timestamp() - start_time 170 | print("回测用时{}秒,结果已保存至mysql数据库!".format(cost_time)) 171 | else: # 实盘模式 172 | while True: # 循环运行begin_trade函数 173 | strategy.begin_trade() 174 | -------------------------------------------------------------------------------- /purequant/example/double_moving_average_strategy/usd_futures_double_ma_strategy.py: -------------------------------------------------------------------------------- 1 | """ 2 | 双均线策略 3 | 此策略适用于OKEX的币本位合约 4 | 如需用于其他类型合约或现货,可自行修改 5 | Author: Gary-Hertel 6 | Date: 2020/09/02 7 | email: interstella.ranger2020@gmail.com 8 | """ 9 | 10 | from purequant.indicators import INDICATORS 11 | from purequant.trade import OKEXFUTURES 12 | from purequant.position import POSITION 13 | from purequant.market import MARKET 14 | from purequant.logger import logger 15 | from purequant.push import push 16 | from purequant.storage import storage 17 | from purequant.time import * 18 | from purequant.config import config 19 | 20 | class Strategy: 21 | 22 | def __init__(self, instrument_id, time_frame, fast_length, slow_length, long_stop, short_stop, start_asset): 23 | try: 24 | print("{} {} 双均线多空策略已启动!".format(get_localtime(), instrument_id)) # 程序启动时打印提示信息 25 | config.loads('config.json') # 载入配置文件 26 | self.instrument_id = instrument_id # 合约ID 27 | self.time_frame = time_frame # k线周期 28 | self.exchange = OKEXFUTURES(config.access_key, config.secret_key, config.passphrase, self.instrument_id) # 初始化交易所 29 | self.position = POSITION(self.exchange, self.instrument_id, self.time_frame) # 初始化potion 30 | self.market = MARKET(self.exchange, self.instrument_id, self.time_frame) # 初始化market 31 | self.indicators = INDICATORS(self.exchange, self.instrument_id, self.time_frame) # 初始化indicators 32 | # 在第一次运行程序时,将初始资金数据保存至数据库中 33 | self.database = "回测" # 回测时必须为"回测" 34 | self.datasheet = self.instrument_id.split("-")[0].lower() + "_" + time_frame 35 | if config.first_run == "true": 36 | storage.mysql_save_strategy_run_info(self.database, self.datasheet, get_localtime(), 37 | "none", 0, 0, 0, 0, "none", 0, 0, 0, start_asset) 38 | # 读取数据库中保存的总资金数据 39 | self.total_asset = storage.read_mysql_datas(0, self.database, self.datasheet, "总资金", ">")[-1][-1] 40 | self.total_profit = storage.read_mysql_datas(0, self.database, self.datasheet, "总资金", ">")[-1][-2] # 策略总盈亏 41 | self.counter = 0 # 计数器 42 | self.fast_length = fast_length # 短周期均线长度 43 | self.slow_length = slow_length # 长周期均线长度 44 | self.long_stop = long_stop # 多单止损幅度 45 | self.short_stop = short_stop # 空单止损幅度 46 | self.contract_value = self.market.contract_value() # 合约面值,每次获取需发起网络请求,故于此处声明变量,优化性能 47 | except: 48 | logger.warning() 49 | 50 | def begin_trade(self, kline=None): 51 | try: 52 | if self.indicators.CurrentBar(kline=kline) < self.slow_length: # 如果k线数据不够长就返回 53 | return 54 | timestamp = ts_to_datetime_str(utctime_str_to_ts(kline[-1][0])) if kline else get_localtime() # 非回测模式下时间戳就是当前本地时间 55 | # 计算策略信号 56 | ma = self.indicators.MA(self.fast_length, self.slow_length, kline=kline) 57 | fast_ma = ma[0] 58 | slow_ma = ma[1] 59 | cross_over = fast_ma[-2] >= slow_ma[-2] and fast_ma[-3] < slow_ma[-3] # 不用当根k线上的ma来计算信号,防止信号闪烁 60 | cross_below = slow_ma[-2] >= fast_ma[-2] and slow_ma[-3] < fast_ma[-3] 61 | if self.indicators.BarUpdate(kline=kline): # 如果k线更新,计数器归零 62 | self.counter = 0 63 | if self.counter < 1: 64 | # 按照策略信号开平仓 65 | if cross_over: # 金叉时 66 | if self.position.amount() == 0: # 若当前无持仓,则买入开多并推送下单结果 67 | price = self.market.open(-1, kline=kline) # 下单价格=此根k线收盘价 68 | amount = round(self.total_asset / self.contract_value) # 数量=总资金/价格/合约面值 69 | info = self.exchange.buy(price, amount) 70 | push(info) 71 | storage.mysql_save_strategy_run_info(self.database, self.datasheet, timestamp, "买入开多", 72 | price, amount, amount * self.contract_value, price, 73 | "long", amount, 0, self.total_profit, self.total_asset) # 将信息保存至数据库 74 | if self.position.direction() == 'short': # 若当前持空头,先平空再开多 75 | profit = self.position.covershort_profit(market_type="usd_contract", last=self.market.open(-1, kline=kline)) # 在平空前先计算逻辑盈亏,当前最新成交价为开盘价 76 | self.total_profit += profit 77 | self.total_asset += profit # 计算此次盈亏后的总资金 78 | cover_short_price = self.market.open(-1, kline=kline) 79 | cover_short_amount = self.position.amount() 80 | open_long_price = self.market.open(-1, kline=kline) 81 | open_long_amount = round(self.total_asset / self.contract_value) 82 | info = self.exchange.BUY(cover_short_price, cover_short_amount, open_long_price, open_long_amount) 83 | push("此次盈亏:{} 当前总资金:{}".format(profit, self.total_asset) + str(info)) # 需将返回的下单结果info转换为字符串后进行拼接 84 | storage.mysql_save_strategy_run_info(self.database, self.datasheet, timestamp, "平空开多", 85 | open_long_price, open_long_amount, open_long_amount * self.contract_value, 86 | open_long_price, "long", open_long_amount, profit, self.total_profit, self.total_asset) 87 | if cross_below: # 死叉时 88 | if self.position.amount() == 0: 89 | price = self.market.open(-1, kline=kline) 90 | amount = round(self.total_asset / self.contract_value) 91 | info = self.exchange.sellshort(price, amount) 92 | push(info) 93 | storage.mysql_save_strategy_run_info(self.database, self.datasheet, timestamp, "卖出开空", 94 | price, amount, amount * self.contract_value, price, 95 | "short", amount, 0, self.total_profit, self.total_asset) 96 | if self.position.direction() == 'long': 97 | profit = self.position.coverlong_profit(market_type="usd_contract", last=self.market.open(-1, kline=kline)) # 在平多前先计算逻辑盈亏,当前最新成交价为开盘价 98 | self.total_profit += profit 99 | self.total_asset += profit 100 | cover_long_price = self.market.open(-1, kline=kline) 101 | cover_long_amount = self.position.amount() 102 | open_short_price = self.market.open(-1, kline=kline) 103 | open_short_amount = round(self.total_asset / self.contract_value) 104 | info = self.exchange.SELL(cover_long_price, 105 | cover_long_amount, 106 | open_short_price, 107 | open_short_amount) 108 | push("此次盈亏:{} 当前总资金:{}".format(profit, self.total_asset) + str(info)) 109 | storage.mysql_save_strategy_run_info(self.database, self.datasheet, timestamp, "平多开空", 110 | open_short_price, open_short_amount, 111 | open_short_amount * self.contract_value, 112 | open_short_price, "short", open_short_amount, profit, self.total_profit, 113 | self.total_asset) 114 | # 止损 115 | if self.position.amount() > 0: 116 | if self.position.direction() == 'long' and self.market.low(-1, kline=kline) <= self.position.price() * self.long_stop: # 多单止损 117 | profit = self.position.coverlong_profit(market_type="usd_contract", last=self.position.price() * self.long_stop) # 在平多前先计算逻辑盈亏,当前最新成交价为止损价 118 | self.total_profit += profit 119 | self.total_asset += profit 120 | price = self.position.price() * self.long_stop 121 | amount = self.position.amount() 122 | info = self.exchange.sell(price, amount) 123 | push("此次盈亏:{} 当前总资金:{}".format(profit, self.total_asset) + str(info)) 124 | storage.mysql_save_strategy_run_info(self.database, self.datasheet, timestamp, 125 | "卖出止损", price, amount, 126 | amount * self.contract_value, 127 | 0, "none", 0, profit, self.total_profit, 128 | self.total_asset) 129 | self.counter += 1 # 计数器加1,控制此根k线上不再下单 130 | 131 | if self.position.direction() == 'short' and self.market.high(-1, kline=kline) >= self.position.price() * self.short_stop: # 空头止损 132 | profit = self.position.covershort_profit(market_type="usd_contract", last=self.position.price() * self.short_stop) 133 | self.total_profit += profit 134 | self.total_asset += profit 135 | price = self.position.price() * self.short_stop 136 | amount = self.position.amount() 137 | info = self.exchange.buytocover(price, amount) 138 | push("此次盈亏:{} 当前总资金:{}".format(profit, self.total_asset) + str(info)) 139 | storage.mysql_save_strategy_run_info(self.database, self.datasheet, timestamp, 140 | "买入止损", price, amount, 141 | amount * self.contract_value, 142 | 0, "none", 0, profit, self.total_profit, 143 | self.total_asset) 144 | self.counter += 1 145 | except: 146 | logger.info() 147 | 148 | if __name__ == "__main__": 149 | 150 | # 实例化策略类 151 | instrument_id = "EOS-USD-201225" 152 | time_frame = "1m" 153 | strategy = Strategy(instrument_id=instrument_id, time_frame=time_frame, fast_length=5, slow_length=10, long_stop=0.98, short_stop=1.02, start_asset=30) 154 | 155 | if config.backtest == "enabled": # 回测模式 156 | print("正在回测,可能需要一段时间,请稍后...") 157 | start_time = get_cur_timestamp() 158 | records = [] 159 | data = storage.read_purequant_server_datas(instrument_id.split("-")[0].lower() + "_" + time_frame) 160 | for k in data: 161 | records.append(k) 162 | strategy.begin_trade(kline=records) 163 | cost_time = get_cur_timestamp() - start_time 164 | print("回测用时{}秒,结果已保存至mysql数据库!".format(cost_time)) 165 | else: # 实盘模式 166 | while True: # 循环运行begin_trade函数 167 | strategy.begin_trade() 168 | time.sleep(3) # 休眠几秒 ,防止请求频率超限 -------------------------------------------------------------------------------- /purequant/example/double_moving_average_strategy/usdt_futures_double_moving_average_strategy.py: -------------------------------------------------------------------------------- 1 | """ 2 | 双均线策略 3 | 此策略适用于OKEX的USDT合约 4 | 如需用于其他类型合约或现货,可自行修改 5 | Author: Gary-Hertel 6 | Date: 2020/08/21 7 | email: interstella.ranger2020@gmail.com 8 | """ 9 | 10 | from purequant.indicators import INDICATORS 11 | from purequant.trade import OKEXFUTURES 12 | from purequant.position import POSITION 13 | from purequant.market import MARKET 14 | from purequant.logger import logger 15 | from purequant.push import push 16 | from purequant.storage import storage 17 | from purequant.time import * 18 | from purequant.config import config 19 | 20 | class Strategy: 21 | 22 | def __init__(self, instrument_id, time_frame, fast_length, slow_length, long_stop, short_stop, start_asset): 23 | try: 24 | print("{} {} 双均线多空策略已启动!".format(get_localtime(), instrument_id)) # 程序启动时打印提示信息 25 | config.loads('config.json') # 载入配置文件 26 | self.instrument_id = instrument_id # 合约ID 27 | self.time_frame = time_frame # k线周期 28 | self.exchange = OKEXFUTURES(config.access_key, config.secret_key, config.passphrase, self.instrument_id) # 初始化交易所 29 | self.position = POSITION(self.exchange, self.instrument_id, self.time_frame) # 初始化potion 30 | self.market = MARKET(self.exchange, self.instrument_id, self.time_frame) # 初始化market 31 | self.indicators = INDICATORS(self.exchange, self.instrument_id, self.time_frame) # 初始化indicators 32 | # 在第一次运行程序时,将初始资金数据保存至数据库中 33 | self.database = "回测" # 回测时必须为"回测" 34 | self.datasheet = self.instrument_id.split("-")[0].lower() + "_" + time_frame 35 | if config.first_run == "true": 36 | storage.mysql_save_strategy_run_info(self.database, self.datasheet, get_localtime(), 37 | "none", 0, 0, 0, 0, "none", 0, 0, 0, start_asset) 38 | # 读取数据库中保存的总资金数据 39 | self.total_asset = storage.read_mysql_datas(0, self.database, self.datasheet, "总资金", ">")[-1][-1] 40 | self.total_profit = storage.read_mysql_datas(0, self.database, self.datasheet, "总资金", ">")[-1][-2] # 策略总盈亏 41 | self.counter = 0 # 计数器 42 | self.fast_length = fast_length # 短周期均线长度 43 | self.slow_length = slow_length # 长周期均线长度 44 | self.long_stop = long_stop # 多单止损幅度 45 | self.short_stop = short_stop # 空单止损幅度 46 | self.contract_value = self.market.contract_value() # 合约面值,每次获取需发起网络请求,故于此处声明变量,优化性能 47 | except: 48 | logger.warning() 49 | 50 | def begin_trade(self, kline=None): 51 | try: 52 | if self.indicators.CurrentBar(kline=kline) < self.slow_length: # 如果k线数据不够长就返回 53 | return 54 | timestamp = ts_to_datetime_str(utctime_str_to_ts(kline[-1][0])) if kline else get_localtime() # 非回测模式下时间戳就是当前本地时间 55 | # 计算策略信号 56 | ma = self.indicators.MA(self.fast_length, self.slow_length, kline=kline) 57 | fast_ma = ma[0] 58 | slow_ma = ma[1] 59 | cross_over = fast_ma[-2] >= slow_ma[-2] and fast_ma[-3] < slow_ma[-3] # 不用当根k线上的ma来计算信号,防止信号闪烁 60 | cross_below = slow_ma[-2] >= fast_ma[-2] and slow_ma[-3] < fast_ma[-3] 61 | if self.indicators.BarUpdate(kline=kline): # 如果k线更新,计数器归零 62 | self.counter = 0 63 | if self.counter < 1: 64 | # 按照策略信号开平仓 65 | if cross_over: # 金叉时 66 | if self.position.amount() == 0: # 若当前无持仓,则买入开多并推送下单结果 67 | price = self.market.open(-1, kline=kline) # 下单价格=此根k线收盘价 68 | amount = round(self.total_asset / price / self.contract_value) # 数量=总资金/价格/合约面值 69 | info = self.exchange.buy(price, amount) 70 | push(info) 71 | storage.mysql_save_strategy_run_info(self.database, self.datasheet, timestamp, "买入开多", 72 | price, amount, amount*price*self.contract_value, price, 73 | "long", amount, 0, self.total_profit, self.total_asset) # 将信息保存至数据库 74 | if self.position.direction() == 'short': # 若当前持空头,先平空再开多 75 | profit = self.position.covershort_profit(last=self.market.open(-1, kline=kline)) # 在平空前先计算逻辑盈亏,当前最新成交价为开盘价 76 | self.total_profit += profit 77 | self.total_asset += profit # 计算此次盈亏后的总资金 78 | cover_short_price = self.market.open(-1, kline=kline) 79 | cover_short_amount = self.position.amount() 80 | open_long_price = self.market.open(-1, kline=kline) 81 | open_long_amount = round(self.total_asset / self.market.open(-1, kline=kline) / self.contract_value) 82 | info = self.exchange.BUY(cover_short_price, cover_short_amount, open_long_price, open_long_amount) 83 | push("此次盈亏:{} 当前总资金:{}".format(profit, self.total_asset) + info) 84 | storage.mysql_save_strategy_run_info(self.database, self.datasheet, timestamp, "平空开多", 85 | open_long_price, open_long_amount, open_long_amount * open_long_price * self.contract_value, 86 | open_long_price, "long", open_long_amount, profit, self.total_profit, self.total_asset) 87 | if cross_below: # 死叉时 88 | if self.position.amount() == 0: 89 | price = self.market.open(-1, kline=kline) 90 | amount = round(self.total_asset / price / self.contract_value) 91 | info = self.exchange.sellshort(price, amount) 92 | push(info) 93 | storage.mysql_save_strategy_run_info(self.database, self.datasheet, timestamp, "卖出开空", 94 | price, amount, amount * price * self.contract_value, price, 95 | "short", amount, 0, self.total_profit, self.total_asset) 96 | if self.position.direction() == 'long': 97 | profit = self.position.coverlong_profit(last=self.market.open(-1, kline=kline)) # 在平多前先计算逻辑盈亏,当前最新成交价为开盘价 98 | self.total_profit += profit 99 | self.total_asset += profit 100 | cover_long_price = self.market.open(-1, kline=kline) 101 | cover_long_amount = self.position.amount() 102 | open_short_price = self.market.open(-1, kline=kline) 103 | open_short_amount = round(self.total_asset / self.market.open(-1, kline=kline) / self.contract_value) 104 | info = self.exchange.SELL(cover_long_price, 105 | cover_long_amount, 106 | open_short_price, 107 | open_short_amount) 108 | push("此次盈亏:{} 当前总资金:{}".format(profit, self.total_asset) + info) 109 | storage.mysql_save_strategy_run_info(self.database, self.datasheet, timestamp, "平多开空", 110 | open_short_price, open_short_amount, 111 | open_short_price * open_short_amount * self.contract_value, 112 | open_short_price, "short", open_short_amount, profit, self.total_profit, 113 | self.total_asset) 114 | # 止损 115 | if self.position.amount() > 0: 116 | if self.position.direction() == 'long' and self.market.low(-1, kline=kline) <= self.position.price() * self.long_stop: # 多单止损 117 | profit = self.position.coverlong_profit(last=self.position.price() * self.long_stop) # 在平多前先计算逻辑盈亏,当前最新成交价为止损价 118 | self.total_profit += profit 119 | self.total_asset += profit 120 | price = self.position.price() * self.long_stop 121 | amount = self.position.amount() 122 | info = self.exchange.sell(price, amount) 123 | push("此次盈亏:{} 当前总资金:{}".format(profit, self.total_asset) + info) 124 | storage.mysql_save_strategy_run_info(self.database, self.datasheet, timestamp, 125 | "卖出止损", price, amount, 126 | amount * price * self.contract_value, 127 | 0, "none", 0, profit, self.total_profit, 128 | self.total_asset) 129 | self.counter += 1 # 计数器加1,控制此根k线上不再下单 130 | 131 | if self.position.direction() == 'short' and self.market.high(-1, kline=kline) >= self.position.price() * self.short_stop: # 空头止损 132 | profit = self.position.covershort_profit(last=self.position.price() * self.short_stop) 133 | self.total_profit += profit 134 | self.total_asset += profit 135 | price = self.position.price() * self.short_stop 136 | amount = self.position.amount() 137 | info = self.exchange.buytocover(price, amount) 138 | push("此次盈亏:{} 当前总资金:{}".format(profit, self.total_asset) + info) 139 | storage.mysql_save_strategy_run_info(self.database, self.datasheet, timestamp, 140 | "买入止损", price, amount, 141 | amount * price * self.contract_value, 142 | 0, "none", 0, profit, self.total_profit, 143 | self.total_asset) 144 | self.counter += 1 145 | except: 146 | logger.debug() 147 | 148 | if __name__ == "__main__": 149 | 150 | # 实例化策略类 151 | instrument_id = "ETH-USDT-201225" 152 | time_frame = "1d" 153 | strategy = Strategy(instrument_id=instrument_id, time_frame=time_frame, fast_length=5, slow_length=10, long_stop=0.98, short_stop=1.02, start_asset=30) 154 | 155 | if config.backtest == "enabled": # 回测模式 156 | print("正在回测,可能需要一段时间,请稍后...") 157 | start_time = get_cur_timestamp() 158 | records = [] 159 | data = storage.read_purequant_server_datas(instrument_id.split("-")[0].lower() + "_" + time_frame) 160 | for k in data: 161 | records.append(k) 162 | strategy.begin_trade(kline=records) 163 | cost_time = get_cur_timestamp() - start_time 164 | print("回测用时{}秒,结果已保存至mysql数据库!".format(cost_time)) 165 | else: # 实盘模式 166 | while True: # 循环运行begin_trade函数 167 | strategy.begin_trade() 168 | time.sleep(3) # 休眠几秒 ,防止请求频率超限 -------------------------------------------------------------------------------- /purequant/exchange/okex/swap_api.py: -------------------------------------------------------------------------------- 1 | from .client import Client 2 | from .consts import * 3 | 4 | 5 | class SwapAPI(Client): 6 | 7 | def __init__(self, api_key, api_secret_key, passphrase, use_server_time=False, test=False, first=False): 8 | Client.__init__(self, api_key, api_secret_key, passphrase, use_server_time, test, first) 9 | 10 | def get_position(self): 11 | return self._request_without_params(GET, SWAP_POSITIONS) 12 | 13 | def get_specific_position(self, instrument_id): 14 | return self._request_without_params(GET, SWAP_POSITION + str(instrument_id) + '/position') 15 | 16 | def get_accounts(self): 17 | return self._request_without_params(GET, SWAP_ACCOUNTS) 18 | 19 | def get_coin_account(self, instrument_id): 20 | return self._request_without_params(GET, SWAP_ACCOUNT + str(instrument_id) + '/accounts') 21 | 22 | def get_settings(self, instrument_id): 23 | return self._request_without_params(GET, SWAP_ACCOUNTS + '/' + str(instrument_id) + '/settings') 24 | 25 | def set_leverage(self, instrument_id, leverage, side): 26 | params = {'leverage': leverage, 'side': side} 27 | return self._request_with_params(POST, SWAP_ACCOUNTS + '/' + str(instrument_id) + '/leverage', params) 28 | 29 | def get_ledger(self, instrument_id, after='', before='', limit='', type=''): 30 | params = {} 31 | if after: 32 | params['after'] = after 33 | if before: 34 | params['before'] = before 35 | if limit: 36 | params['limit'] = limit 37 | if type: 38 | params['type'] = type 39 | return self._request_with_params(GET, SWAP_ACCOUNTS + '/' + str(instrument_id) + '/ledger', params, cursor=True) 40 | 41 | def take_order(self, instrument_id, type, price, size, client_oid='', order_type='0', match_price='0'): 42 | params = {'instrument_id': instrument_id, 'type': type, 'size': size, 'price': price} 43 | if client_oid: 44 | params['client_oid'] = client_oid 45 | if order_type: 46 | params['order_type'] = order_type 47 | if match_price: 48 | params['match_price'] = match_price 49 | return self._request_with_params(POST, SWAP_ORDER, params) 50 | 51 | def take_orders(self, instrument_id, order_data): 52 | params = {'instrument_id': instrument_id, 'order_data': order_data} 53 | return self._request_with_params(POST, SWAP_ORDERS, params) 54 | 55 | def revoke_order(self, instrument_id, order_id='', client_oid=''): 56 | if order_id: 57 | return self._request_without_params(POST, SWAP_CANCEL_ORDER + str(instrument_id) + '/' + str(order_id)) 58 | elif client_oid: 59 | return self._request_without_params(POST, SWAP_CANCEL_ORDER + str(instrument_id) + '/' + str(client_oid)) 60 | 61 | def revoke_orders(self, instrument_id, ids='', client_oids=''): 62 | params = {} 63 | if ids: 64 | params = {'ids': ids} 65 | elif client_oids: 66 | params = {'client_oids': client_oids} 67 | return self._request_with_params(POST, SWAP_CANCEL_ORDERS + str(instrument_id), params) 68 | 69 | def amend_order(self, instrument_id, cancel_on_fail, order_id='', client_oid='', request_id='', new_size='', new_price=''): 70 | params = {'cancel_on_fail': cancel_on_fail} 71 | if order_id: 72 | params['order_id'] = order_id 73 | if client_oid: 74 | params['client_oid'] = client_oid 75 | if request_id: 76 | params['request_id'] = request_id 77 | if new_size: 78 | params['new_size'] = new_size 79 | if new_price: 80 | params['new_price'] = new_price 81 | return self._request_with_params(POST, SWAP_AMEND_ORDER + str(instrument_id), params) 82 | 83 | def amend_batch_orders(self, instrument_id, amend_data): 84 | params = {'amend_data': amend_data} 85 | return self._request_with_params(POST, SWAP_AMEND_BATCH_ORDERS + str(instrument_id), params) 86 | 87 | def get_order_list(self, instrument_id, state, after='', before='', limit=''): 88 | params = {'state': state} 89 | if after: 90 | params['after'] = after 91 | if before: 92 | params['before'] = before 93 | if limit: 94 | params['limit'] = limit 95 | return self._request_with_params(GET, SWAP_ORDERS + '/' + str(instrument_id), params, cursor=True) 96 | 97 | def get_order_info(self, instrument_id, order_id='', client_oid=''): 98 | if order_id: 99 | return self._request_without_params(GET, SWAP_ORDERS + '/' + str(instrument_id) + '/' + str(order_id)) 100 | elif client_oid: 101 | return self._request_without_params(GET, SWAP_ORDERS + '/' + str(instrument_id) + '/' + str(client_oid)) 102 | 103 | def get_fills(self, instrument_id, order_id='', after='', before='', limit=''): 104 | params = {'instrument_id': instrument_id} 105 | if order_id: 106 | params['order_id'] = order_id 107 | if after: 108 | params['after'] = after 109 | if before: 110 | params['before'] = before 111 | if limit: 112 | params['limit'] = limit 113 | return self._request_with_params(GET, SWAP_FILLS, params, cursor=True) 114 | 115 | def close_position(self, instrument_id, direction): 116 | params = {'instrument_id': instrument_id, 'direction': direction} 117 | return self._request_with_params(POST, SWAP_CLOSE_POSITION, params) 118 | 119 | def cancel_all(self, instrument_id, direction): 120 | params = {'instrument_id': instrument_id, 'direction': direction} 121 | return self._request_with_params(POST, SWAP_CANCEL_ALL, params) 122 | 123 | def get_instruments(self): 124 | return self._request_without_params(GET, SWAP_INSTRUMENTS) 125 | 126 | def get_depth(self, instrument_id, size='', depth=''): 127 | params = {} 128 | if size: 129 | params['size'] = size 130 | if depth: 131 | params['depth'] = depth 132 | return self._request_with_params(GET, SWAP_INSTRUMENTS + '/' + str(instrument_id) + '/depth', params) 133 | 134 | def get_ticker(self): 135 | return self._request_without_params(GET, SWAP_TICKETS) 136 | 137 | def get_specific_ticker(self, instrument_id): 138 | return self._request_without_params(GET, SWAP_INSTRUMENTS + '/' + str(instrument_id) + '/ticker') 139 | 140 | def get_trades(self, instrument_id, after='', before='', limit=''): 141 | params = {} 142 | if after: 143 | params['after'] = after 144 | if before: 145 | params['before'] = before 146 | if limit: 147 | params['limit'] = limit 148 | return self._request_with_params(GET, SWAP_INSTRUMENTS + '/' + str(instrument_id) + '/trades', params, cursor=True) 149 | 150 | def get_kline(self, instrument_id, start='', end='', granularity=''): 151 | params = {} 152 | if start: 153 | params['start'] = start 154 | if end: 155 | params['end'] = end 156 | if granularity: 157 | params['granularity'] = granularity 158 | # 按时间倒叙 即由结束时间到开始时间 159 | return self._request_with_params(GET, SWAP_INSTRUMENTS + '/' + str(instrument_id) + '/candles', params) 160 | 161 | # 按时间正序 即由开始时间到结束时间 162 | # data = self._request_with_params(GET, SWAP_INSTRUMENTS + '/' + str(instrument_id) + '/candles', params) 163 | # return list(reversed(data)) 164 | 165 | def get_index(self, instrument_id): 166 | return self._request_without_params(GET, SWAP_INSTRUMENTS + '/' + str(instrument_id) + '/index') 167 | 168 | def get_rate(self): 169 | return self._request_without_params(GET, SWAP_RATE) 170 | 171 | def get_holds(self, instrument_id): 172 | return self._request_without_params(GET, SWAP_INSTRUMENTS + '/' + str(instrument_id) + '/open_interest') 173 | 174 | def get_limit(self, instrument_id): 175 | return self._request_without_params(GET, SWAP_INSTRUMENTS + '/' + str(instrument_id) + '/price_limit') 176 | 177 | def get_liquidation(self, instrument_id, status, froms='', to='', limit=''): 178 | params = {'status': status} 179 | if froms: 180 | params['from'] = froms 181 | if to: 182 | params['to'] = to 183 | if limit: 184 | params['limit'] = limit 185 | return self._request_with_params(GET, SWAP_INSTRUMENTS + '/' + str(instrument_id) + '/liquidation', params) 186 | 187 | def get_holds_amount(self, instrument_id): 188 | return self._request_without_params(GET, SWAP_ACCOUNTS + '/' + str(instrument_id) + '/holds') 189 | 190 | # take order_algo 191 | def take_order_algo(self, instrument_id, type, order_type, size, trigger_price='', algo_price='', algo_type='', 192 | callback_rate='', algo_variance='', avg_amount='', price_limit='', sweep_range='', 193 | sweep_ratio='', single_limit='', time_interval=''): 194 | params = {'instrument_id': instrument_id, 'type': type, 'order_type': order_type, 'size': size} 195 | if order_type == '1': # 止盈止损参数(最多同时存在10单) 196 | params['trigger_price'] = trigger_price 197 | params['algo_price'] = algo_price 198 | if algo_type: 199 | params['algo_type'] = algo_type 200 | elif order_type == '2': # 跟踪委托参数(最多同时存在10单) 201 | params['callback_rate'] = callback_rate 202 | params['trigger_price'] = trigger_price 203 | elif order_type == '3': # 冰山委托参数(最多同时存在6单) 204 | params['algo_variance'] = algo_variance 205 | params['avg_amount'] = avg_amount 206 | params['price_limit'] = price_limit 207 | elif order_type == '4': # 时间加权参数(最多同时存在6单) 208 | params['sweep_range'] = sweep_range 209 | params['sweep_ratio'] = sweep_ratio 210 | params['single_limit'] = single_limit 211 | params['price_limit'] = price_limit 212 | params['time_interval'] = time_interval 213 | return self._request_with_params(POST, SWAP_ORDER_ALGO, params) 214 | 215 | # cancel_algos 216 | def cancel_algos(self, instrument_id, algo_ids, order_type): 217 | params = {'instrument_id': instrument_id, 'algo_ids': algo_ids, 'order_type': order_type} 218 | return self._request_with_params(POST, SWAP_CANCEL_ALGOS, params) 219 | 220 | # get order_algos 221 | def get_order_algos(self, instrument_id, order_type, status='', algo_id='', before='', after='', limit=''): 222 | params = {'order_type': order_type} 223 | if status: 224 | params['status'] = status 225 | elif algo_id: 226 | params['algo_id'] = algo_id 227 | if before: 228 | params['before'] = before 229 | if after: 230 | params['after'] = after 231 | if limit: 232 | params['limit'] = limit 233 | return self._request_with_params(GET, SWAP_GET_ORDER_ALGOS + str(instrument_id), params) 234 | 235 | # get_trade_fee 236 | def get_trade_fee(self): 237 | return self._request_without_params(GET, SWAP_GET_TRADE_FEE) 238 | 239 | def get_funding_time(self, instrument_id): 240 | return self._request_without_params(GET, SWAP_INSTRUMENTS + '/' + str(instrument_id) + '/funding_time') 241 | 242 | def get_mark_price(self, instrument_id): 243 | return self._request_without_params(GET, SWAP_INSTRUMENTS + '/' + str(instrument_id) + '/mark_price') 244 | 245 | def get_historical_funding_rate(self, instrument_id, limit=''): 246 | params = {} 247 | if limit: 248 | params['limit'] = limit 249 | return self._request_with_params(GET, SWAP_INSTRUMENTS + '/' + str(instrument_id) + '/historical_funding_rate', params) 250 | 251 | def get_history_kline(self, instrument_id, start='', end='', granularity=''): 252 | params = {} 253 | if start: 254 | params['start'] = start 255 | if end: 256 | params['end'] = end 257 | if granularity: 258 | params['granularity'] = granularity 259 | return self._request_with_params(GET, SWAP_HISTORY_KLINE + str(instrument_id) + '/history' + '/candles', params) 260 | -------------------------------------------------------------------------------- /purequant/example/double_moving_average_strategy/fastly_backtest_doubale_ma_strategy.py: -------------------------------------------------------------------------------- 1 | """ 2 | 双均线策略 3 | 此种策略的写法是手动记录持仓信息,而非读取数据库中的持仓信息(回测模式)或实时从交易所获取账户实际持仓信息(实盘模式) 4 | 此策略也可兼容回测与实盘 5 | 优点:由于回测时不再反复读取数据库中的持仓信息,回测速度大大提升 6 | 缺点:策略实盘时,如果策略重启,记录的持仓信息将回归初始值 7 | 8 | 此策略适用于OKEX的USDT合约 9 | 如需用于其他类型合约或现货,可自行修改 10 | Author: Gary-Hertel 11 | Date: 2020/08/21 12 | email: interstella.ranger2020@gmail.com 13 | """ 14 | 15 | from purequant.indicators import INDICATORS 16 | from purequant.trade import OKEXFUTURES 17 | from purequant.position import POSITION 18 | from purequant.market import MARKET 19 | from purequant.logger import logger 20 | from purequant.push import push 21 | from purequant.storage import storage 22 | from purequant.time import * 23 | from purequant.config import config 24 | 25 | class Strategy: 26 | 27 | def __init__(self, instrument_id, time_frame, fast_length, slow_length, long_stop, short_stop, start_asset): 28 | try: 29 | config.loads('config.json') # 载入配置文件 30 | print("{} {} 双均线多空策略已启动!".format(get_localtime(), instrument_id)) # 程序启动时打印提示信息 31 | self.instrument_id = instrument_id # 合约ID 32 | self.time_frame = time_frame # k线周期 33 | self.exchange = OKEXFUTURES(config.access_key, config.secret_key, config.passphrase, self.instrument_id) # 初始化交易所 34 | self.position = POSITION(self.exchange, self.instrument_id, self.time_frame) # 初始化potion 35 | self.market = MARKET(self.exchange, self.instrument_id, self.time_frame) # 初始化market 36 | self.indicators = INDICATORS(self.exchange, self.instrument_id, self.time_frame) # 初始化indicators 37 | # 在第一次运行程序时,将初始资金数据保存至数据库中 38 | self.database = "回测" # 无论实盘或回测,此处database名称可以任意命名 39 | self.datasheet = self.instrument_id.split("-")[0].lower() + "_" + time_frame 40 | if config.first_run == "true": 41 | storage.mysql_save_strategy_run_info(self.database, self.datasheet, get_localtime(), 42 | "none", 0, 0, 0, 0, "none", 0, 0, 0, start_asset) 43 | # 读取数据库中保存的总资金数据 44 | self.total_asset = storage.read_mysql_datas(0, self.database, self.datasheet, "总资金", ">")[-1][-1] 45 | self.total_profit = storage.read_mysql_datas(0, self.database, self.datasheet, "总资金", ">")[-1][-2] # 策略总盈亏 46 | self.counter = 0 # 计数器 47 | self.fast_length = fast_length # 短周期均线长度 48 | self.slow_length = slow_length # 长周期均线长度 49 | self.long_stop = long_stop # 多单止损幅度 50 | self.short_stop = short_stop # 空单止损幅度 51 | self.contract_value = self.market.contract_value() # 合约面值,每次获取需发起网络请求,故于此处声明变量,优化性能 52 | # 声明持仓方向、数量与价格变量,每次开平仓后手动重新赋值 53 | self.hold_direction = "none" 54 | self.hold_amount = 0 55 | self.hold_price = 0 56 | except: 57 | logger.warning() 58 | 59 | 60 | def begin_trade(self, kline=None): 61 | try: 62 | if self.indicators.CurrentBar(kline=kline) < self.slow_length: # 如果k线数据不够长就返回 63 | return 64 | # 非回测模式下时间戳就是当前本地时间 65 | timestamp = ts_to_datetime_str(utctime_str_to_ts(kline[-1][0])) if kline else get_localtime() 66 | # 计算策略信号 67 | ma = self.indicators.MA(self.fast_length, self.slow_length, kline=kline) 68 | fast_ma = ma[0] 69 | slow_ma = ma[1] 70 | cross_over = fast_ma[-2] >= slow_ma[-2] and fast_ma[-3] < slow_ma[-3] # 不用当根k线上的ma来计算信号,防止信号闪烁 71 | cross_below = slow_ma[-2] >= fast_ma[-2] and slow_ma[-3] < fast_ma[-3] 72 | if self.indicators.BarUpdate(kline=kline): # 如果k线更新,计数器归零 73 | self.counter = 0 74 | if self.counter < 1: 75 | # 按照策略信号开平仓 76 | if cross_over: # 金叉时 77 | if self.hold_amount == 0: # 若当前无持仓,则买入开多并推送下单结果 78 | price = self.market.open(-1, kline=kline) # 下单价格=此根k线收盘价 79 | amount = round(self.total_asset / price / self.contract_value) # 数量=总资金/价格/合约面值 80 | info = self.exchange.buy(price, amount) 81 | push(info) 82 | self.hold_direction = "long" 83 | self.hold_amount = amount 84 | self.hold_price = price 85 | storage.mysql_save_strategy_run_info(self.database, self.datasheet, timestamp, "买入开多", 86 | price, amount, amount*price*self.contract_value, price, 87 | "long", amount, 0, self.total_profit, self.total_asset) # 将信息保存至数据库 88 | if self.hold_direction == 'short': # 若当前持空头,先平空再开多 89 | profit = self.position.covershort_profit(last=self.market.open(-1, kline=kline)) # 在平空前先计算逻辑盈亏,当前最新成交价为开盘价 90 | self.total_profit += profit 91 | self.total_asset += profit # 计算此次盈亏后的总资金 92 | cover_short_price = self.market.open(-1, kline=kline) 93 | cover_short_amount = self.hold_amount 94 | open_long_price = self.market.open(-1, kline=kline) 95 | open_long_amount = round(self.total_asset / self.market.open(-1, kline=kline) / self.contract_value) 96 | info = self.exchange.BUY(cover_short_price, cover_short_amount, open_long_price, open_long_amount) 97 | push("此次盈亏:{} 当前总资金:{}".format(profit, self.total_asset) + info) 98 | self.hold_direction = "long" 99 | self.hold_amount = open_long_amount 100 | self.hold_price = open_long_price 101 | storage.mysql_save_strategy_run_info(self.database, self.datasheet, timestamp, "平空开多", 102 | open_long_price, open_long_amount, open_long_amount * open_long_price * self.contract_value, 103 | open_long_price, "long", open_long_amount, profit, self.total_profit, self.total_asset) 104 | if cross_below: # 死叉时 105 | if self.hold_amount == 0: 106 | price = self.market.open(-1, kline=kline) 107 | amount = round(self.total_asset / price / self.contract_value) 108 | info = self.exchange.sellshort(price, amount) 109 | push(info) 110 | self.hold_direction = "short" 111 | self.hold_amount = amount 112 | self.hold_price = price 113 | storage.mysql_save_strategy_run_info(self.database, self.datasheet, timestamp, "卖出开空", 114 | price, amount, amount * price * self.contract_value, price, 115 | "short", amount, 0, self.total_profit, self.total_asset) 116 | if self.hold_direction == 'long': 117 | profit = self.position.coverlong_profit(last=self.market.open(-1, kline=kline)) # 在平多前先计算逻辑盈亏,当前最新成交价为开盘价 118 | self.total_profit += profit 119 | self.total_asset += profit 120 | cover_long_price = self.market.open(-1, kline=kline) 121 | cover_long_amount = self.hold_amount 122 | open_short_price = self.market.open(-1, kline=kline) 123 | open_short_amount = round(self.total_asset / self.market.open(-1, kline=kline) / self.contract_value) 124 | info = self.exchange.SELL(cover_long_price, 125 | cover_long_amount, 126 | open_short_price, 127 | open_short_amount) 128 | push("此次盈亏:{} 当前总资金:{}".format(profit, self.total_asset) + info) 129 | self.hold_direction = "short" 130 | self.hold_amount = open_short_amount 131 | self.hold_price = open_short_price 132 | storage.mysql_save_strategy_run_info(self.database, self.datasheet, timestamp, "平多开空", 133 | open_short_price, open_short_amount, 134 | open_short_price * open_short_amount * self.contract_value, 135 | open_short_price, "short", open_short_amount, profit, self.total_profit, 136 | self.total_asset) 137 | # 止损 138 | if self.hold_amount > 0: 139 | if self.hold_direction == 'long' and self.market.low(-1, kline=kline) <= self.hold_price * self.long_stop: # 多单止损 140 | profit = self.position.coverlong_profit(last=self.hold_price * self.long_stop) # 在平多前先计算逻辑盈亏,当前最新成交价为止损价 141 | self.total_profit += profit 142 | self.total_asset += profit 143 | price = self.hold_price * self.long_stop 144 | amount = self.hold_amount 145 | info = self.exchange.sell(price, amount) 146 | push("此次盈亏:{} 当前总资金:{}".format(profit, self.total_asset) + info) 147 | self.hold_direction = "none" 148 | self.hold_amount = 0 149 | self.hold_price = 0 150 | storage.mysql_save_strategy_run_info(self.database, self.datasheet, timestamp, 151 | "卖出止损", price, amount, 152 | amount * price * self.contract_value, 153 | 0, "none", 0, profit, self.total_profit, 154 | self.total_asset) 155 | self.counter += 1 # 计数器加1,控制此根k线上不再下单 156 | 157 | if self.hold_direction == 'short' and self.market.high(-1, kline=kline) >= self.hold_price * self.short_stop: # 空头止损 158 | profit = self.position.covershort_profit(last=self.hold_price * self.short_stop) 159 | self.total_profit += profit 160 | self.total_asset += profit 161 | price = self.hold_price * self.short_stop 162 | amount = self.hold_amount 163 | info = self.exchange.buytocover(price, amount) 164 | push("此次盈亏:{} 当前总资金:{}".format(profit, self.total_asset) + info) 165 | self.hold_direction = "none" 166 | self.hold_amount = 0 167 | self.hold_price = 0 168 | storage.mysql_save_strategy_run_info(self.database, self.datasheet, timestamp, 169 | "买入止损", price, amount, 170 | amount * price * self.contract_value, 171 | 0, "none", 0, profit, self.total_profit, 172 | self.total_asset) 173 | self.counter += 1 174 | except: 175 | logger.info() 176 | 177 | if __name__ == "__main__": 178 | 179 | # 实例化策略类 180 | instrument_id = "ETH-USDT-201225" 181 | time_frame = "1d" 182 | strategy = Strategy(instrument_id=instrument_id, time_frame=time_frame, fast_length=5, slow_length=10, long_stop=0.9, short_stop=1.1, start_asset=1000) 183 | 184 | if config.backtest == "enabled": # 回测模式 185 | print("正在回测,可能需要一段时间,请稍后...") 186 | start_time = get_cur_timestamp() 187 | records = [] 188 | data = storage.read_purequant_server_datas(instrument_id.split("-")[0].lower() + "_" + time_frame) 189 | for k in data: 190 | records.append(k) 191 | strategy.begin_trade(kline=records) 192 | cost_time = get_cur_timestamp() - start_time 193 | print("回测用时{}秒,结果已保存至mysql数据库!".format(cost_time)) 194 | else: # 实盘模式 195 | while True: # 循环运行begin_trade函数 196 | strategy.begin_trade() -------------------------------------------------------------------------------- /purequant/example/double_moving_average_strategy/multi-parameter_fast_backtest.py: -------------------------------------------------------------------------------- 1 | """ 2 | 双均线策略 3 | 此种策略的写法是手动记录持仓信息,而非读取数据库中的持仓信息(回测模式)或实时从交易所获取账户实际持仓信息(实盘模式) 4 | 此策略也可兼容回测与实盘 5 | 优点:由于回测时不再反复读取数据库中的持仓信息,回测速度大大提升 6 | 缺点:策略实盘时,如果策略重启,记录的持仓信息将回归初始值 7 | 8 | 此策略适用于OKEX的USDT合约 9 | 如需用于其他类型合约或现货,可自行修改 10 | Author: Gary-Hertel 11 | Date: 2020/08/21 12 | email: interstella.ranger2020@gmail.com 13 | """ 14 | 15 | from purequant.indicators import INDICATORS 16 | from purequant.trade import OKEXFUTURES 17 | from purequant.position import POSITION 18 | from purequant.market import MARKET 19 | from purequant.logger import logger 20 | from purequant.push import push 21 | from purequant.storage import storage 22 | from purequant.time import * 23 | from purequant.config import config 24 | 25 | class Strategy: 26 | 27 | def __init__(self, instrument_id, time_frame, fast_length, slow_length, long_stop, short_stop, start_asset): 28 | print("{} {} 双均线多空策略已启动!".format(get_localtime(), instrument_id)) # 程序启动时打印提示信息 29 | config.loads('config.json') # 载入配置文件 30 | self.instrument_id = instrument_id # 合约ID 31 | self.time_frame = time_frame # k线周期 32 | self.exchange = OKEXFUTURES(config.access_key, config.secret_key, config.passphrase, self.instrument_id) # 初始化交易所 33 | self.position = POSITION(self.exchange, self.instrument_id, self.time_frame) # 初始化potion 34 | self.market = MARKET(self.exchange, self.instrument_id, self.time_frame) # 初始化market 35 | self.indicators = INDICATORS(self.exchange, self.instrument_id, self.time_frame) # 初始化indicators 36 | # 在第一次运行程序时,将初始资金数据保存至数据库中 37 | self.database = "回测" # 无论实盘或回测,此处database名称可以任意命名 38 | self.datasheet = self.instrument_id.split("-")[0].lower() + "_" + time_frame 39 | if config.first_run == "true": 40 | storage.mysql_save_strategy_run_info(self.database, self.datasheet, "策略参数为" + str(fast_length) + "&" + str(slow_length), 41 | "none", 0, 0, 0, 0, "none", 0, 0, 0, start_asset) 42 | # 读取数据库中保存的总资金数据 43 | self.total_asset = storage.read_mysql_datas(0, self.database, self.datasheet, "总资金", ">")[-1][-1] 44 | self.counter = 0 # 计数器 45 | self.fast_length = fast_length # 短周期均线长度 46 | self.slow_length = slow_length # 长周期均线长度 47 | self.long_stop = long_stop # 多单止损幅度 48 | self.short_stop = short_stop # 空单止损幅度 49 | self.total_profit = 0 50 | self.contract_value = self.market.contract_value() # 合约面值,每次获取需发起网络请求,故于此处声明变量,优化性能 51 | # 声明持仓方向、数量与价格变量,每次开平仓后手动重新赋值 52 | self.hold_direction = "none" 53 | self.hold_amount = 0 54 | self.hold_price = 0 55 | 56 | 57 | def begin_trade(self, kline=None): 58 | try: 59 | if self.indicators.CurrentBar(kline=kline) < self.slow_length: # 如果k线数据不够长就返回 60 | return 61 | # 非回测模式下时间戳就是当前本地时间 62 | timestamp = ts_to_datetime_str(utctime_str_to_ts(kline[-1][0])) if kline else get_localtime() 63 | # 计算策略信号 64 | ma = self.indicators.MA(self.fast_length, self.slow_length, kline=kline) 65 | fast_ma = ma[0] 66 | slow_ma = ma[1] 67 | cross_over = fast_ma[-2] >= slow_ma[-2] and fast_ma[-3] < slow_ma[-3] # 不用当根k线上的ma来计算信号,防止信号闪烁 68 | cross_below = slow_ma[-2] >= fast_ma[-2] and slow_ma[-3] < fast_ma[-3] 69 | if self.indicators.BarUpdate(kline=kline): # 如果k线更新,计数器归零 70 | self.counter = 0 71 | if self.counter < 1: 72 | # 按照策略信号开平仓 73 | if cross_over: # 金叉时 74 | if self.hold_amount == 0: # 若当前无持仓,则买入开多并推送下单结果 75 | price = self.market.open(-1, kline=kline) # 下单价格=此根k线收盘价 76 | amount = round(self.total_asset / price / self.contract_value) # 数量=总资金/价格/合约面值 77 | info = self.exchange.buy(price, amount) 78 | push(info) 79 | self.hold_direction = "long" 80 | self.hold_amount = amount 81 | self.hold_price = price 82 | storage.mysql_save_strategy_run_info(self.database, self.datasheet, timestamp, "买入开多", 83 | price, amount, amount*price*self.contract_value, price, 84 | "long", amount, 0, self.total_profit, self.total_asset) # 将信息保存至数据库 85 | if self.hold_direction == 'short': # 若当前持空头,先平空再开多 86 | profit = self.position.covershort_profit(last=self.market.open(-1, kline=kline)) # 在平空前先计算逻辑盈亏,当前最新成交价为开盘价 87 | self.total_profit += profit 88 | self.total_asset += profit # 计算此次盈亏后的总资金 89 | cover_short_price = self.market.open(-1, kline=kline) 90 | cover_short_amount = self.hold_amount 91 | open_long_price = self.market.open(-1, kline=kline) 92 | open_long_amount = round(self.total_asset / self.market.open(-1, kline=kline) / self.contract_value) 93 | info = self.exchange.BUY(cover_short_price, cover_short_amount, open_long_price, open_long_amount) 94 | push("此次盈亏:{} 当前总资金:{}".format(profit, self.total_asset) + info) 95 | self.hold_direction = "long" 96 | self.hold_amount = open_long_amount 97 | self.hold_price = open_long_price 98 | storage.mysql_save_strategy_run_info(self.database, self.datasheet, timestamp, "平空开多", 99 | open_long_price, open_long_amount, open_long_amount * open_long_price * self.contract_value, 100 | open_long_price, "long", open_long_amount, profit, self.total_profit, self.total_asset) 101 | if cross_below: # 死叉时 102 | if self.hold_amount == 0: 103 | price = self.market.open(-1, kline=kline) 104 | amount = round(self.total_asset / price / self.contract_value) 105 | info = self.exchange.sellshort(price, amount) 106 | push(info) 107 | self.hold_direction = "short" 108 | self.hold_amount = amount 109 | self.hold_price = price 110 | storage.mysql_save_strategy_run_info(self.database, self.datasheet, timestamp, "卖出开空", 111 | price, amount, amount * price * self.contract_value, price, 112 | "short", amount, 0, self.total_profit, self.total_asset) 113 | if self.hold_direction == 'long': 114 | profit = self.position.coverlong_profit(last=self.market.open(-1, kline=kline)) # 在平多前先计算逻辑盈亏,当前最新成交价为开盘价 115 | self.total_profit += profit 116 | self.total_asset += profit 117 | cover_long_price = self.market.open(-1, kline=kline) 118 | cover_long_amount = self.hold_amount 119 | open_short_price = self.market.open(-1, kline=kline) 120 | open_short_amount = round(self.total_asset / self.market.open(-1, kline=kline) / self.contract_value) 121 | info = self.exchange.SELL(cover_long_price, 122 | cover_long_amount, 123 | open_short_price, 124 | open_short_amount) 125 | push("此次盈亏:{} 当前总资金:{}".format(profit, self.total_asset) + info) 126 | self.hold_direction = "short" 127 | self.hold_amount = open_short_amount 128 | self.hold_price = open_short_price 129 | storage.mysql_save_strategy_run_info(self.database, self.datasheet, timestamp, "平多开空", 130 | open_short_price, open_short_amount, 131 | open_short_price * open_short_amount * self.contract_value, 132 | open_short_price, "short", open_short_amount, profit, self.total_profit, 133 | self.total_asset) 134 | # 止损 135 | if self.hold_amount > 0: 136 | if self.hold_direction == 'long' and self.market.low(-1, kline=kline) <= self.hold_price * self.long_stop: # 多单止损 137 | profit = self.position.coverlong_profit(last=self.hold_price * self.long_stop) # 在平多前先计算逻辑盈亏,当前最新成交价为止损价 138 | self.total_profit += profit 139 | self.total_asset += profit 140 | price = self.hold_price * self.long_stop 141 | amount = self.hold_amount 142 | info = self.exchange.sell(price, amount) 143 | push("此次盈亏:{} 当前总资金:{}".format(profit, self.total_asset) + info) 144 | self.hold_direction = "none" 145 | self.hold_amount = 0 146 | self.hold_price = 0 147 | storage.mysql_save_strategy_run_info(self.database, self.datasheet, timestamp, 148 | "卖出止损", price, amount, 149 | amount * price * self.contract_value, 150 | 0, "none", 0, profit, self.total_profit, 151 | self.total_asset) 152 | self.counter += 1 # 计数器加1,控制此根k线上不再下单 153 | 154 | if self.hold_direction == 'short' and self.market.high(-1, kline=kline) >= self.hold_price * self.short_stop: # 空头止损 155 | profit = self.position.covershort_profit(last=self.hold_price * self.short_stop) 156 | self.total_profit += profit 157 | self.total_asset += profit 158 | price = self.hold_price * self.short_stop 159 | amount = self.hold_amount 160 | info = self.exchange.buytocover(price, amount) 161 | push("此次盈亏:{} 当前总资金:{}".format(profit, self.total_asset) + info) 162 | self.hold_direction = "none" 163 | self.hold_amount = 0 164 | self.hold_price = 0 165 | storage.mysql_save_strategy_run_info(self.database, self.datasheet, timestamp, 166 | "买入止损", price, amount, 167 | amount * price * self.contract_value, 168 | 0, "none", 0, profit, self.total_profit, 169 | self.total_asset) 170 | self.counter += 1 171 | except: 172 | logger.info() 173 | 174 | if __name__ == "__main__": 175 | 176 | config.loads('config.json') 177 | if config.backtest == "enabled": # 回测模式 178 | instrument_id = "LTC-USDT-201225" 179 | time_frame = "1d" 180 | fast_length_list = range(5, 20, 2) 181 | slow_length_list = range(10, 30, 2) 182 | start_time = get_cur_timestamp() 183 | for i in fast_length_list: 184 | fast_length = i 185 | for j in slow_length_list: 186 | slow_length = j 187 | print("策略参数为{}和{},正在回测,可能需要一段时间,请稍后...".format(fast_length, slow_length)) 188 | strategy = Strategy(instrument_id=instrument_id, time_frame=time_frame, 189 | fast_length=fast_length, slow_length=slow_length, 190 | long_stop=0.95, short_stop=1.05, start_asset=1000) 191 | records = [] 192 | data = storage.read_purequant_server_datas(instrument_id.split("-")[0].lower() + "_" + time_frame) 193 | for k in data: 194 | records.append(k) 195 | strategy.begin_trade(kline=records) 196 | cost_time = get_cur_timestamp() - start_time 197 | print("回测用时{}秒,结果已保存至mysql数据库!".format(cost_time)) 198 | else: # 实盘模式 199 | instrument_id = "LTC-USDT-201225" 200 | time_frame = "1d" 201 | fast_length = 5 202 | slow_length = 10 203 | strategy = Strategy(instrument_id=instrument_id, time_frame=time_frame, 204 | fast_length=fast_length, slow_length=slow_length, 205 | long_stop=0.95, short_stop=1.05, start_asset=1000) 206 | while True: # 循环运行begin_trade函数 207 | strategy.begin_trade() --------------------------------------------------------------------------------