├── .gitignore ├── LICENSE ├── README.md ├── app ├── __init__.py ├── fake_run.py ├── run.py ├── service │ ├── __init__.py │ ├── kline_handler.py │ ├── mailagent.py │ └── websocket.py └── settings.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Bing 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 火币网自动化交易工具 2 | 3 | ## 目录 4 | 5 | * 目前实现的功能 6 | * 即将实现的功能 7 | * 使用方法 8 | 9 | ## 目前实现的功能 10 | 11 | 1. 通过websocket获取K线图消息 12 | 1. 支持将K线图信息保存到MongoDB数据库 13 | 1. 通过计算N分钟内M的货币的平均变化趋势,判断火币网大盘的涨跌 14 | 1. 支持涨跌幅度超过限度时邮件通知 15 | 1. 通过读取数据库重现一段时间的交易,便于快速验证 16 | 1. 添加测试脚本模拟买入/卖出,便于根据结果修改参数 17 | 18 | ## 即将实现的功能 19 | 20 | 1. 根据需要添加更多的数据到数据库中 21 | 1. 编写价格预测算法 22 | 1. 添加WSGI服务器,实现通过REST API查询当前模拟价格 23 | 1. 添加交易功能,实现自动化交易 24 | 25 | ## 使用方法 26 | 27 | ### Linux系统 28 | 29 | 进入huobi-autotrading目录 30 | 31 | ```bash 32 | pip install -r requirements.txt 33 | ``` 34 | 35 | 阅读并根据app/settings.py中的提示进行配置 36 | 37 | 运行程序 38 | 39 | ```bash 40 | python3 -m app.run 41 | ``` 42 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baibinghere/huobi-autotrading/94b61ae1239d73637bb06307b57c479a046b8999/app/__init__.py -------------------------------------------------------------------------------- /app/fake_run.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from app import settings 3 | from app.service import kline_handler 4 | from app.service import mongodb 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | # 不通过websocket,直接通过读取到的数据库的值调用kline_handler 9 | if __name__ == "__main__": 10 | index_dict = {} 11 | currency_dict = {} 12 | if mongodb: 13 | for currency in settings.COINS.keys(): 14 | index_dict["market_%susdt_kline_1min" % currency.lower()] = 0 15 | 16 | for collection_name in index_dict.keys(): 17 | collection = mongodb.get_collection(collection_name) 18 | currency_dict[collection_name] = collection.find_one({}, skip=index_dict[collection_name]) 19 | 20 | while True: 21 | # 从currency_dict中找到ts最小的key,更新该key 22 | min_ts_document_key = min(currency_dict.items(), key=lambda x: x[1]['_id'])[0] 23 | collection = mongodb.get_collection(min_ts_document_key) 24 | index_dict[min_ts_document_key] += 1 25 | kline_handler.handle_raw_message(currency_dict[min_ts_document_key]) 26 | currency_dict[min_ts_document_key] = collection.find_one( 27 | {'ts': {'$gt': 1000 * int(settings.SIMULATE_START.timestamp()), 28 | '$lt': 1000 * int(settings.SIMULATE_END.timestamp())}}, skip=index_dict[min_ts_document_key]) 29 | if currency_dict[min_ts_document_key] is None: 30 | break 31 | print("已完成处理") 32 | -------------------------------------------------------------------------------- /app/run.py: -------------------------------------------------------------------------------- 1 | from app.service import websocket 2 | 3 | if __name__ == "__main__": 4 | websocket.start() 5 | -------------------------------------------------------------------------------- /app/service/__init__.py: -------------------------------------------------------------------------------- 1 | from app import settings 2 | from pymongo import MongoClient 3 | from app.service.mailagent import MailAgent 4 | 5 | mongodb = None 6 | ma = None 7 | 8 | # set MongoDB configuration 9 | if settings.DATABASE_SERVER_ADDRESS and settings.DATABASE_SERVER_PORT: 10 | mongo = MongoClient(settings.DATABASE_SERVER_ADDRESS, settings.DATABASE_SERVER_PORT) 11 | if settings.DATABASE_SERVER_USERNAME and settings.DATABASE_SERVER_PASSWORD: 12 | db_auth = mongo.admin 13 | db_auth.authenticate(settings.DATABASE_SERVER_USERNAME, settings.DATABASE_SERVER_PASSWORD) 14 | mongodb = mongo.get_database(settings.DATABASE_NAME) 15 | 16 | # set MailAgent configuration 17 | if settings.MAIL_ACCOUNT and settings.MAIL_AUTH_CODE and len(settings.MAIL_RECEIPIENTS) > 0: 18 | ma = MailAgent(settings.MAIL_ACCOUNT, settings.MAIL_AUTH_CODE) 19 | -------------------------------------------------------------------------------- /app/service/kline_handler.py: -------------------------------------------------------------------------------- 1 | import json 2 | import numpy 3 | import logging 4 | import time 5 | import datetime 6 | import http.client 7 | import statistics 8 | 9 | from app import settings 10 | from app.service import ma 11 | from collections import deque 12 | 13 | ### 14 | # 本文件对传入的价格信息进行处理 15 | ### 16 | 17 | # 记录每种虚拟货币的每笔交易 18 | transaction_dict = {} 19 | # 记录每种虚拟货币在1分钟内的分析数据,队列长度设置为10,即10分钟内的数据 20 | analyzed_queue_dict = {} 21 | # 记录10分钟内的"价格"变化,这里的"价格"目前直接用close的差计算,以后考虑通过交易量,标准差等计算 22 | price_change_dict = {} 23 | 24 | logger = logging.getLogger(__name__) 25 | 26 | # 记录上一次邮件发送的时间 27 | last_mail_datetime = None 28 | 29 | 30 | def get_usdt_sell_price(): 31 | conn = http.client.HTTPSConnection("api-otc.huobi.pro") 32 | conn.request("GET", "/v1/otc/trade/list/public?coinId=2&tradeType=0¤tPage=1&payWay=&country=") 33 | res = conn.getresponse() 34 | try: 35 | data = json.loads(res.read().decode("utf-8"))['data'] 36 | return statistics.mean(list(map(lambda x: x['price'], data))) 37 | except Exception as exp: 38 | logger.error("无法获得USDT交易卖出价:" + str(exp)) 39 | return "失败" 40 | 41 | 42 | def send_mail(title, content): 43 | if not ma: 44 | return 45 | global last_mail_datetime 46 | now = datetime.datetime.now() 47 | if last_mail_datetime and now - last_mail_datetime < datetime.timedelta( 48 | minutes=settings.N_MINUTES_STATE): 49 | return 50 | last_mail_datetime = now 51 | with ma.SMTP() as s: 52 | s.send(settings.MAIL_RECEIPIENTS, content, title) 53 | 54 | 55 | def get_current_wealth(): 56 | total = 0 57 | for key, value in settings.COINS.items(): 58 | total += transaction_dict["market.%susdt.kline.1min" % key.lower()][-1]['tick']['close'] * value['AMOUNT'] 59 | total += settings.USDT_CURRENCY 60 | return total 61 | 62 | 63 | def trigger_price_increase_action(total_price_change): 64 | content = "价格上升:%.4f" % total_price_change 65 | logger.warning(content) 66 | 67 | # 60%购买当前平均10分钟内涨幅 * WEIGHT最高的, 40%购买第2高的 68 | if settings.USDT_CURRENCY > 0: 69 | sorted_prices = sorted(price_change_dict.items(), key=lambda x: x[1] * settings.COINS[x[0].split(".")[1].replace(settings.SYMBOL.lower(), "").upper()]['WEIGHT'], reverse=True) 70 | settings.COINS[sorted_prices[0][0].split(".")[1].replace(settings.SYMBOL.lower(), "").upper()]['AMOUNT'] = 0.998 * 0.6 * settings.USDT_CURRENCY / transaction_dict[sorted_prices[0][0]][-1]['tick']['close'] 71 | settings.COINS[sorted_prices[1][0].split(".")[1].replace(settings.SYMBOL.lower(), "").upper()]['AMOUNT'] = 0.998 * 0.4 * settings.USDT_CURRENCY / transaction_dict[sorted_prices[1][0]][-1]['tick']['close'] 72 | settings.USDT_CURRENCY = 0 73 | logger.info("总财富USDT: %.2f; 不折腾: %.2f; 时间: %s" % (get_current_wealth(), settings.ORIGINAL_WEALTH, time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(transaction_dict['market.etcusdt.kline.1min'][-1]['ts'] / 1000)))) 74 | send_mail("火币网价格上升", content) 75 | 76 | def trigger_price_decrease_action(total_price_change): 77 | content = "价格下降:%.4f" % total_price_change 78 | logger.warning(content) 79 | 80 | # 查找当前价格涨幅最高的两个,如果我们已购买的货币在这里面,则不卖出 81 | sorted_prices = sorted(price_change_dict.items(), key=lambda x: x[1] / settings.COINS[x[0].split(".")[1].replace(settings.SYMBOL.lower(), "").upper()]['WEIGHT'], reverse=True) 82 | skip_list = [sorted_prices[0][0], sorted_prices[1][0]] 83 | for key, value in price_change_dict.items(): 84 | if key in skip_list: continue 85 | coin = key.split(".")[1].replace(settings.SYMBOL.lower(), "").upper() 86 | # 全部卖掉 87 | if settings.COINS[coin]['AMOUNT'] > 0: 88 | settings.USDT_CURRENCY += 0.998 * settings.COINS[coin]['AMOUNT'] * transaction_dict[key][-1]['tick']['close'] 89 | settings.COINS[coin]['AMOUNT'] = 0 90 | logger.info("总财富USDT: %.2f; 不折腾: %.2f; 时间: %s" % (get_current_wealth(), settings.ORIGINAL_WEALTH, time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(transaction_dict['market.etcusdt.kline.1min'][-1]['ts'] / 1000)))) 91 | send_mail("火币网价格下降", content) 92 | 93 | def predict_and_notify(total_price_change): 94 | if total_price_change >= settings.PRICE_ALERT_INCREASE_POINT: 95 | trigger_price_increase_action(total_price_change) 96 | if total_price_change <= settings.PRICE_ALERT_DECREASE_POINT: 97 | trigger_price_decrease_action(total_price_change) 98 | logger.info("价格变动:%.4f;USDT当前售价:%.2f" % (total_price_change, get_usdt_sell_price())) 99 | 100 | 101 | def perform_calculation(): 102 | total_price = 0 103 | total_price_change = 0 104 | # 当收集满所有货币的10分钟内交易额后,计算才有意义 105 | if not len(price_change_dict) == len(settings.COINS): 106 | return 107 | # 取出price_change_dict中的所有数据,并根据settings中设置的WEIGHT决定是否购买 108 | for channel, price in price_change_dict.items(): 109 | currency = channel.split(".")[1].replace(settings.SYMBOL.lower(), "").upper() 110 | weight = settings.COINS[currency]["WEIGHT"] 111 | total_price += settings.COINS[currency].get("AMOUNT", 0) * transaction_dict[channel][-1]['tick']['close'] 112 | total_price_change += price * weight 113 | total_price_change /= sum(list(map(lambda x: x["WEIGHT"], settings.COINS.values()))) 114 | predict_and_notify(total_price_change) 115 | 116 | 117 | # 火币的交易量相关信息每60秒重置一次 118 | def update_data(channel): 119 | # 从transaction_dict[channel]中获取 120 | # 1. 1分钟内的涨跌幅 121 | # 2. 价格变化幅度(标准差) 122 | # 3. 1分钟内的交易额 123 | # 4. 1分钟最后一笔交易的价格 124 | if channel not in analyzed_queue_dict: 125 | analyzed_queue_dict[channel] = deque("", settings.N_MINUTES_STATE) 126 | data = { 127 | 'change': transaction_dict[channel][-1]['tick']['close'] - transaction_dict[channel][0]['tick']['close'], 128 | 'vol': transaction_dict[channel][-1]['tick']['vol'], 129 | 'mean': numpy.std(list(map(lambda x: x['tick']['close'], transaction_dict[channel]))), 130 | 'close': transaction_dict[channel][-1]['tick']['close'], 131 | } 132 | analyzed_queue_dict[channel].append(data) 133 | logger.debug("updated: " + channel) 134 | if len(analyzed_queue_dict[channel]) == settings.N_MINUTES_STATE: 135 | # 10分钟以内的"价格"变化 136 | price_change_dict[channel] = (analyzed_queue_dict[channel][-1]['close'] - analyzed_queue_dict[channel][0][ 137 | 'close']) * 100 / analyzed_queue_dict[channel][0]['close'] 138 | perform_calculation() 139 | 140 | 141 | def handle_raw_message(msg_dict): 142 | channel = msg_dict['ch'] 143 | if len(transaction_dict) == len(settings.COINS): 144 | total = 0 145 | for key, value in settings.ORIGINAL_COINS.items(): 146 | total += transaction_dict["market.%susdt.kline.1min" % key.lower()][-1]['tick']['close'] * value['AMOUNT'] 147 | settings.ORIGINAL_WEALTH = settings.ORIGINAL_USDT_CURRENCY + total 148 | if channel not in transaction_dict: 149 | transaction_dict[channel] = [msg_dict] 150 | else: 151 | if transaction_dict[channel][-1]['tick']['count'] > msg_dict['tick']['count']: 152 | # 每60秒计算一次已有数据,然后重置该channel 153 | update_data(channel) 154 | transaction_dict[channel] = [msg_dict] 155 | else: 156 | transaction_dict[channel].append(msg_dict) 157 | -------------------------------------------------------------------------------- /app/service/mailagent.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | p = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 5 | if p not in sys.path: 6 | sys.path.insert(0, p) 7 | 8 | import time 9 | import smtplib 10 | import imaplib 11 | from email.encoders import encode_base64 12 | from email.mime.multipart import MIMEMultipart 13 | from email.mime.base import MIMEBase 14 | from email.mime.text import MIMEText 15 | from email.header import decode_header 16 | from email import message_from_bytes 17 | 18 | SERVER_LIB = { 19 | 'sample.com': { 20 | 'smtp': 'smtp.sample.com', 21 | 'imap': 'imap.sample.com', 22 | 'smtp_port': 0, 23 | 'imap_port': 0, 24 | 'use_ssl': True 25 | } 26 | } 27 | 28 | 29 | class MailAgent(object): 30 | def __init__(self, account, auth_code, name='', **config): 31 | account_name, server_name = account.split('@') 32 | 33 | self.smtp = 'smtp.' + server_name 34 | self.imap = 'imap.' + server_name 35 | self.smtp_port = 0 36 | self.imap_port = 0 37 | self.use_ssl = True 38 | 39 | self.__dict__.update(SERVER_LIB.get(server_name, {})) 40 | self.__dict__.update(config) 41 | 42 | self.name = '%s <%s>' % (name or account_name, account) 43 | self.account = account 44 | self.auth_code = auth_code 45 | 46 | st_SMTP = smtplib.SMTP_SSL if self.use_ssl else smtplib.SMTP 47 | st_IMAP = imaplib.IMAP4_SSL if self.use_ssl else imaplib.IMAP4 48 | 49 | if self.smtp_port: 50 | self.st_SMTP = lambda: st_SMTP(self.smtp, self.smtp_port) 51 | else: 52 | self.st_SMTP = lambda: st_SMTP(self.smtp) 53 | 54 | if self.imap_port: 55 | self.st_IMAP = lambda: st_IMAP(self.imap, self.imap_port) 56 | else: 57 | self.st_IMAP = lambda: st_IMAP(self.imap) 58 | 59 | self.SMTP = lambda: SMTP(self) 60 | self.IMAP = lambda: IMAP(self) 61 | 62 | 63 | class SMTP(object): 64 | def __init__(self, mail_agent): 65 | self.name, self.account = mail_agent.name, mail_agent.account 66 | self.server = mail_agent.st_SMTP() 67 | try: 68 | self.server.login(mail_agent.account, mail_agent.auth_code) 69 | except: 70 | self.close() 71 | raise 72 | 73 | def close(self): 74 | try: 75 | return self.server.quit() 76 | except: 77 | pass 78 | 79 | def __enter__(self): 80 | return self 81 | 82 | def __exit__(self, exc_type, exc_value, traceback): 83 | self.close() 84 | 85 | def send(self, to_addr, html='', subject='', to_name='', png_content=''): 86 | subject = subject or 'No subject' 87 | to_name = to_name or "; ".join(to_addr) 88 | html = '
%s' % html 89 | if html: 90 | html = html.replace('{{png}}', '