├── .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}}', '') 91 | 92 | msg = MIMEMultipart() 93 | msg.attach(MIMEText(html, 'html', 'utf8')) 94 | msg['From'] = self.name 95 | msg['To'] = '%s <%s>' % (to_name, to_addr) 96 | msg['Subject'] = subject 97 | 98 | if png_content: 99 | m = MIMEBase('image', 'png', filename='x.png') 100 | m.add_header('Content-Disposition', 'attachment', filename='x.png') 101 | m.add_header('Content-ID', '<0>') 102 | m.add_header('X-Attachment-Id', '0') 103 | m.set_payload(png_content) 104 | encode_base64(m) 105 | msg.attach(m) 106 | 107 | self.server.sendmail(self.account, to_addr, msg.as_string()) 108 | 109 | 110 | class IMAP(object): 111 | def __init__(self, mail_agent): 112 | self.name, self.account = mail_agent.name, mail_agent.account 113 | self.conn = mail_agent.st_IMAP() 114 | try: 115 | self.conn.login(mail_agent.account, mail_agent.auth_code) 116 | self.conn.select('INBOX') 117 | except: 118 | self.close() 119 | raise 120 | 121 | def __enter__(self): 122 | return self 123 | 124 | def __exit__(self, exc_type, exc_value, traceback): 125 | self.close() 126 | 127 | def close(self): 128 | try: 129 | return self.conn.close() 130 | except: 131 | pass 132 | 133 | def getSubject(self, i): 134 | conn = self.conn 135 | id_list = conn.search(None, '(UNSEEN)')[1][0].split() 136 | try: 137 | email_id = id_list[i] 138 | except IndexError: 139 | return None, -1 140 | data = conn.fetch(email_id, 'BODY.PEEK[HEADER.FIELDS (SUBJECT)]')[1] 141 | 142 | msg = message_from_bytes(data[0][1]) 143 | s, encoding = decode_header(msg['Subject'])[0] 144 | subject = s if type(s) is str else s.decode(encoding or 'utf-8') 145 | return subject 146 | 147 | 148 | if __name__ == '__main__': 149 | from app import settings 150 | 151 | if settings.MAIL_ACCOUNT and settings.MAIL_AUTH_CODE: 152 | ma = MailAgent(settings.MAIL_ACCOUNT, settings.MAIL_AUTH_CODE) 153 | 154 | with ma.SMTP() as s: 155 | s.send(settings.MAIL_RECEIPIENTS, '测试邮件发送', '测试') 156 | print("发送成功") 157 | 158 | time.sleep(5) 159 | 160 | with ma.IMAP() as i: 161 | subject = i.getSubject(-1) 162 | print('最新邮件: ' + subject) 163 | print('接收成功') 164 | -------------------------------------------------------------------------------- /app/service/websocket.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from app import settings 4 | from app.service import kline_handler 5 | from app.service import mongodb 6 | import gzip 7 | import json 8 | import logging 9 | 10 | import websocket 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | ### 16 | # 本文件通过websocket与火币网实现通信 17 | ### 18 | 19 | def save_data(msg): 20 | if settings.DATABASE_RECORD and mongodb: 21 | try: 22 | collection = mongodb.get_collection(msg['ch'].replace('.', '_')) 23 | collection.insert_one(msg) 24 | except Exception as exp: 25 | logger.error("无法保存到数据库:" + str(exp)) 26 | 27 | 28 | def send_message(ws, msg_dict): 29 | data = json.dumps(msg_dict).encode() 30 | logger.debug("发送消息:" + str(msg_dict)) 31 | ws.send(data) 32 | 33 | 34 | def on_message(ws, message): 35 | unzipped_data = gzip.decompress(message).decode() 36 | msg_dict = json.loads(unzipped_data) 37 | if 'ping' in msg_dict: 38 | data = { 39 | "pong": msg_dict['ping'] 40 | } 41 | logger.debug("收到ping消息: " + str(msg_dict)) 42 | send_message(ws, data) 43 | elif 'subbed' in msg_dict: 44 | logger.debug("收到订阅状态消息:" + str(msg_dict)) 45 | else: 46 | save_data(msg_dict) 47 | logger.debug("收到消息: " + str(msg_dict)) 48 | kline_handler.handle_raw_message(msg_dict) 49 | 50 | 51 | def on_error(ws, error): 52 | # error = gzip.decompress(error).decode() 53 | logger.error(str(error)) 54 | 55 | 56 | def on_close(ws): 57 | logger.info("已断开连接") 58 | logger.info("等待5秒后重新尝试连接") 59 | time.sleep(5) 60 | start() 61 | 62 | 63 | def on_open(ws): 64 | # 遍历settings中的货币对象 65 | for currency in settings.COINS.keys(): 66 | subscribe = "market.{0}{1}.kline.{2}".format(currency, settings.SYMBOL, settings.PERIOD).lower() 67 | data = { 68 | "sub": subscribe, 69 | "id": currency 70 | } 71 | # 订阅K线图 72 | send_message(ws, data) 73 | 74 | 75 | def start(): 76 | ws = websocket.WebSocketApp( 77 | # 似乎 www.huobi.com 在翻墙的情况下和可用 78 | # "wss://www.huobi.com/-/s/pro/ws", 79 | # www.huobi.br.com 目前在不翻墙的情况下可用 80 | "wss://www.huobi.br.com/-/s/pro/ws", 81 | on_open=on_open, 82 | on_message=on_message, 83 | on_error=on_error, 84 | on_close=on_close 85 | ) 86 | ws.run_forever() 87 | -------------------------------------------------------------------------------- /app/settings.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import sys 3 | import logging 4 | 5 | # 日志配置文件 6 | from copy import deepcopy 7 | 8 | _filename = None 9 | _format = "%(asctime)-15s [%(levelname)s] [%(name)s] %(message)s" 10 | _datefmt = "%Y/%m/%d %H:%M:%S" 11 | _level = logging.INFO 12 | 13 | if _filename: 14 | handlers = [logging.StreamHandler(sys.stdout), logging.FileHandler(_filename)] 15 | else: 16 | handlers = [logging.StreamHandler(sys.stdout)] 17 | 18 | logging.basicConfig(format=_format, datefmt=_datefmt, level=_level, handlers=handlers) 19 | 20 | # 目前查询的$symbol均为:<货币>USTD (可选:{ USDT, BTC }) 21 | # 时间间隔均为1min(可选:{ 1min, 5min, 15min, 30min, 60min, 1day, 1mon, 1week, 1year }) 22 | SYMBOL = "USDT" 23 | PERIOD = "1min" 24 | 25 | # 计算n分钟内的涨跌幅 26 | N_MINUTES_STATE = 10 27 | 28 | # 当达到以下值,发出警报通知,触发下一步动作 29 | PRICE_ALERT_INCREASE_POINT = 1.25 30 | PRICE_ALERT_DECREASE_POINT = -1.25 31 | 32 | # 买入和卖出策略目前写死在代码里 33 | # 卖出策略 - 达到PRICE_ALERT_DECREASE_POINT时 34 | # 以当时的成交价格卖出,如果这种货币为降幅最小两个之一,则等待不卖出 35 | 36 | # 买入策略 - 达到PRICE_ALERT_INCREASE_POINT时 37 | # 以60%和40%的比例分别买涨价最高/次高的 38 | 39 | # 设定参考货币的类型,权重及初始默认拥有的货币数量 40 | COINS = { 41 | "BTC": { 42 | "WEIGHT": 1, 43 | "AMOUNT": 0 44 | }, 45 | "BCH": { 46 | "WEIGHT": 1, 47 | "AMOUNT": 0 48 | }, 49 | "ETH": { 50 | "WEIGHT": 1, 51 | "AMOUNT": 0 52 | }, 53 | "LTC": { 54 | "WEIGHT": 1, 55 | "AMOUNT": 0 56 | }, 57 | "XRP": { 58 | "WEIGHT": 1, 59 | "AMOUNT": 0 60 | }, 61 | "DASH": { 62 | "WEIGHT": 1, 63 | "AMOUNT": 0 64 | }, 65 | "ETC": { 66 | "WEIGHT": 1, 67 | "AMOUNT": 0 68 | }, 69 | "EOS": { 70 | "WEIGHT": 1, 71 | "AMOUNT": 0 72 | }, 73 | "OMG": { 74 | "WEIGHT": 1, 75 | "AMOUNT": 0 76 | } 77 | } 78 | 79 | # 用户当前USDT账户余额 80 | USDT_CURRENCY = 0 81 | 82 | # 备份原始COIN/CURRENCY数据,作对比用 83 | ORIGINAL_USDT_CURRENCY = USDT_CURRENCY 84 | ORIGINAL_COINS = deepcopy(COINS) 85 | ORIGINAL_WEALTH = None 86 | 87 | # 将从火币上获取到的交易信息保存到数据库(mongodb) 88 | DATABASE_RECORD = False 89 | 90 | # 配置以下项目以初始化数据库 91 | DATABASE_SERVER_ADDRESS = None 92 | DATABASE_SERVER_PORT = 27017 93 | DATABASE_NAME = "huobi_exchange" 94 | 95 | # 这里的模拟起止仅用于数据库交易模拟分析 96 | SIMULATE_START = datetime.datetime(2017, 12, 1, 0, 0, 0) 97 | SIMULATE_END = datetime.datetime.now() 98 | 99 | # 如果数据库有用户名/密码,则定义如下 100 | DATABASE_SERVER_USERNAME = None 101 | DATABASE_SERVER_PASSWORD = None 102 | 103 | # 邮件通知,配置SMTP,获取其AuthCode即可发送邮件 104 | MAIL_ACCOUNT = None 105 | MAIL_AUTH_CODE = None 106 | MAIL_RECEIPIENTS = [] 107 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | pymongo 3 | websocket 4 | websocket-client --------------------------------------------------------------------------------