initial commit
This commit is contained in:
		
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| config.json | ||||
| preprocessor.py | ||||
| *.sqlite | ||||
							
								
								
									
										35
									
								
								config.json.example
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								config.json.example
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| { | ||||
|     "stake_amount": 0.05, | ||||
|     "dry_run": false, | ||||
|     "trade_thresholds": { | ||||
|         "2880": 0.005, | ||||
|         "1440": 0.01, | ||||
|         "720":  0.03, | ||||
|         "360":  0.05, | ||||
|         "0":    0.10 | ||||
|     }, | ||||
|     "poloniex": { | ||||
|         "enabled": false, | ||||
|         "key": "key", | ||||
|         "secret": "secret", | ||||
|         "pair_whitelist": [] | ||||
|     }, | ||||
|     "bittrex": { | ||||
|         "enabled": true, | ||||
|         "key": "key", | ||||
|         "secret": "secret", | ||||
|         "pair_whitelist": [ | ||||
|             "BTC_MLN", | ||||
|             "BTC_TRST", | ||||
|             "BTC_TIME", | ||||
|             "BTC_NXS", | ||||
|             "BTC_GBYTE", | ||||
|             "BTC_SNGLS" | ||||
|         ] | ||||
|     }, | ||||
|     "telegram": { | ||||
|         "enabled": true, | ||||
|         "token": "token", | ||||
|         "chat_id": "chat_id" | ||||
|     } | ||||
| } | ||||
							
								
								
									
										152
									
								
								exchange.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								exchange.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,152 @@ | ||||
| import enum | ||||
| import threading | ||||
|  | ||||
| from bittrex.bittrex import Bittrex | ||||
| from poloniex import Poloniex | ||||
|  | ||||
|  | ||||
| _lock = threading.Condition() | ||||
| _exchange_api = None | ||||
|  | ||||
|  | ||||
| def get_exchange_api(conf): | ||||
|     """ | ||||
|     Returns the current exchange api or instantiates a new one | ||||
|     :return: exchange.ApiWrapper | ||||
|     """ | ||||
|     global _exchange_api | ||||
|     _lock.acquire() | ||||
|     if not _exchange_api: | ||||
|         _exchange_api = ApiWrapper(conf) | ||||
|     _lock.release() | ||||
|     return _exchange_api | ||||
|  | ||||
|  | ||||
| class Exchange(enum.Enum): | ||||
|     POLONIEX = 0 | ||||
|     BITTREX = 1 | ||||
|  | ||||
|  | ||||
| class ApiWrapper(object): | ||||
|     """ | ||||
|     Wrapper for exchanges. | ||||
|     Currently implemented: | ||||
|         * Bittrex | ||||
|         * Poloniex (partly) | ||||
|     """ | ||||
|     def __init__(self, config): | ||||
|         """ | ||||
|         Initializes the ApiWrapper with the given config, it does not validate those values. | ||||
|         :param config: dict | ||||
|         """ | ||||
|         self.dry_run = config['dry_run'] | ||||
|  | ||||
|         use_poloniex = config.get('poloniex', {}).get('enabled', False) | ||||
|         use_bittrex = config.get('bittrex', {}).get('enabled', False) | ||||
|  | ||||
|         if use_poloniex: | ||||
|             self.exchange = Exchange.POLONIEX | ||||
|             self.api = Poloniex( | ||||
|                 key=config['poloniex']['key'], | ||||
|                 secret=config['poloniex']['secret'] | ||||
|             ) | ||||
|         elif use_bittrex: | ||||
|             self.exchange = Exchange.BITTREX | ||||
|             self.api = Bittrex( | ||||
|                 api_key=config['bittrex']['key'], | ||||
|                 api_secret=config['bittrex']['secret'] | ||||
|             ) | ||||
|         else: | ||||
|             self.api = None | ||||
|  | ||||
|     def buy(self, pair, rate, amount): | ||||
|         """ | ||||
|         Places a limit buy order. | ||||
|         :param pair: Pair as str, format: BTC_ETH | ||||
|         :param rate: Rate limit for order | ||||
|         :param amount: The amount to purchase | ||||
|         :return: None | ||||
|         """ | ||||
|         if self.dry_run: | ||||
|             pass | ||||
|         elif self.exchange == Exchange.POLONIEX: | ||||
|             self.api.buy(pair, rate, amount) | ||||
|         elif self.exchange == Exchange.BITTREX: | ||||
|             data = self.api.buy_limit(pair.replace('_', '-'), amount, rate) | ||||
|             if not data['success']: | ||||
|                 raise RuntimeError('BITTREX: {}'.format(data['message'])) | ||||
|  | ||||
|     def sell(self, pair, rate, amount): | ||||
|         """ | ||||
|         Places a limit sell order. | ||||
|         :param pair: Pair as str, format: BTC_ETH | ||||
|         :param rate: Rate limit for order | ||||
|         :param amount: The amount to sell | ||||
|         :return: None | ||||
|         """ | ||||
|         if self.dry_run: | ||||
|             pass | ||||
|         elif self.exchange == Exchange.POLONIEX: | ||||
|             self.api.sell(pair, rate, amount) | ||||
|         elif self.exchange == Exchange.BITTREX: | ||||
|             data = self.api.sell_limit(pair.replace('_', '-'), amount, rate) | ||||
|             if not data['success']: | ||||
|                 raise RuntimeError('BITTREX: {}'.format(data['message'])) | ||||
|  | ||||
|     def get_balance(self, currency): | ||||
|         """ | ||||
|         Get account balance. | ||||
|         :param currency: currency as str, format: BTC | ||||
|         :return: float | ||||
|         """ | ||||
|         if self.exchange == Exchange.POLONIEX: | ||||
|             data = self.api.returnBalances() | ||||
|             return float(data[currency]) | ||||
|         elif self.exchange == Exchange.BITTREX: | ||||
|             data = self.api.get_balance(currency) | ||||
|             if not data['success']: | ||||
|                 raise RuntimeError('BITTREX: {}'.format(data['message'])) | ||||
|             return float(data['result']['Balance'] or 0.0) | ||||
|  | ||||
|     def get_ticker(self, pair): | ||||
|         """ | ||||
|         Get Ticker for given pair. | ||||
|         :param pair: Pair as str, format: BTC_ETC | ||||
|         :return: dict | ||||
|         """ | ||||
|         if self.exchange == Exchange.POLONIEX: | ||||
|             data = self.api.returnTicker() | ||||
|             return { | ||||
|                 'bid': float(data[pair]['highestBid']), | ||||
|                 'ask': float(data[pair]['lowestAsk']), | ||||
|                 'last': float(data[pair]['last']) | ||||
|             } | ||||
|         elif self.exchange == Exchange.BITTREX: | ||||
|             data = self.api.get_ticker(pair.replace('_', '-')) | ||||
|             if not data['success']: | ||||
|                 raise RuntimeError('BITTREX: {}'.format(data['message'])) | ||||
|             return { | ||||
|                 'bid': float(data['result']['Bid']), | ||||
|                 'ask': float(data['result']['Ask']), | ||||
|                 'last': float(data['result']['Last']), | ||||
|             } | ||||
|  | ||||
|     def get_open_orders(self, pair): | ||||
|         """ | ||||
|         Get all open orders for given pair. | ||||
|         :param pair: Pair as str, format: BTC_ETC | ||||
|         :return: list of dicts | ||||
|         """ | ||||
|         if self.exchange == Exchange.POLONIEX: | ||||
|             raise NotImplemented('Not implemented') | ||||
|         elif self.exchange == Exchange.BITTREX: | ||||
|             data = self.api.get_open_orders(pair.replace('_', '-')) | ||||
|             if not data['success']: | ||||
|                 raise RuntimeError('BITTREX: {}'.format(data['message'])) | ||||
|             return [{ | ||||
|                 'type': entry['OrderType'], | ||||
|                 'opened': entry['Opened'], | ||||
|                 'rate': entry['PricePerUnit'], | ||||
|                 'amount': entry['Quantity'], | ||||
|                 'remaining': entry['QuantityRemaining'], | ||||
|             } for entry in data['result']] | ||||
							
								
								
									
										207
									
								
								main.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										207
									
								
								main.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,207 @@ | ||||
| #!/usr/bin/env python | ||||
|  | ||||
| import logging | ||||
| import random | ||||
| import threading | ||||
| import time | ||||
| import traceback | ||||
| from datetime import datetime | ||||
|  | ||||
| logging.basicConfig(level=logging.DEBUG, | ||||
|                     format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') | ||||
| logger = logging.getLogger(__name__) | ||||
|  | ||||
| from persistence import Trade, Session | ||||
| from exchange import get_exchange_api | ||||
| from rpc.telegram import TelegramHandler | ||||
| from utils import get_conf | ||||
|  | ||||
| __author__ = "gcarq" | ||||
| __copyright__ = "gcarq 2017" | ||||
| __license__ = "custom" | ||||
| __version__ = "0.4" | ||||
|  | ||||
|  | ||||
| conf = get_conf() | ||||
| api_wrapper = get_exchange_api(conf) | ||||
|  | ||||
| _lock = threading.Condition() | ||||
| _instance = None | ||||
| _should_stop = False | ||||
|  | ||||
|  | ||||
| class TradeThread(threading.Thread): | ||||
|     @staticmethod | ||||
|     def get_instance(recreate=False): | ||||
|         """ | ||||
|         Get the current instance of this thread. This is a singleton. | ||||
|         :param recreate: Must be True if you want to start the instance | ||||
|         :return: TradeThread instance | ||||
|         """ | ||||
|         global _instance, _should_stop | ||||
|         _lock.acquire() | ||||
|         if _instance is None or (not _instance.is_alive() and recreate): | ||||
|             _should_stop = False | ||||
|             _instance = TradeThread() | ||||
|         _lock.release() | ||||
|         return _instance | ||||
|  | ||||
|     @staticmethod | ||||
|     def stop(): | ||||
|         """ | ||||
|         Sets stop signal for the current instance | ||||
|         :return: None | ||||
|         """ | ||||
|         global _should_stop | ||||
|         _lock.acquire() | ||||
|         _should_stop = True | ||||
|         _lock.release() | ||||
|  | ||||
|     def run(self): | ||||
|         """ | ||||
|         Threaded main function | ||||
|         :return: None | ||||
|         """ | ||||
|         try: | ||||
|             TelegramHandler.send_msg('*Status:* `trader started`') | ||||
|             while not _should_stop: | ||||
|                 try: | ||||
|                     # Query trades from persistence layer | ||||
|                     trade = Trade.query.filter(Trade.is_open.is_(True)).first() | ||||
|                     if trade: | ||||
|                         # Check if there is already an open order for this pair | ||||
|                         open_orders = api_wrapper.get_open_orders(trade.pair) | ||||
|                         if open_orders: | ||||
|                             msg = 'There is already an open order for this trade. (total: {}, remaining: {}, type: {})'\ | ||||
|                                 .format( | ||||
|                                     round(open_orders[0]['amount'], 8), | ||||
|                                     round(open_orders[0]['remaining'], 8), | ||||
|                                     open_orders[0]['type'] | ||||
|                                 ) | ||||
|                             logger.info(msg) | ||||
|                         elif close_trade_if_fulfilled(trade): | ||||
|                             logger.info('No open orders found and close values are set. Marking trade as closed ...') | ||||
|                         else: | ||||
|                             # Maybe sell with current rate | ||||
|                             handle_trade(trade) | ||||
|                     else: | ||||
|                         # Prepare entity and execute trade | ||||
|                         Session.add(create_trade(float(conf['stake_amount']), api_wrapper.exchange)) | ||||
|                 except ValueError: | ||||
|                     logger.exception('ValueError') | ||||
|                 except RuntimeError: | ||||
|                     TelegramHandler.send_msg('RuntimeError. Stopping trader ...'.format(traceback.format_exc())) | ||||
|                     logger.exception('RuntimeError. Stopping trader ...') | ||||
|                     Session.flush() | ||||
|                     return | ||||
|                 finally: | ||||
|                     Session.flush() | ||||
|                     time.sleep(25) | ||||
|         finally: | ||||
|             TelegramHandler.send_msg('*Status:* `trader has stopped`') | ||||
|  | ||||
|  | ||||
| def close_trade_if_fulfilled(trade): | ||||
|     """ | ||||
|     Checks if the trade is closable, and if so it is being closed. | ||||
|     :param trade: Trade | ||||
|     :return: True if trade has been closed else False | ||||
|     """ | ||||
|     # If we don't have an open order and the close rate is already set, | ||||
|     # we can close this trade. | ||||
|     if trade.close_profit and trade.close_date and trade.close_rate: | ||||
|         trade.is_open = False | ||||
|         return True | ||||
|     return False | ||||
|  | ||||
|  | ||||
| def handle_trade(trade): | ||||
|     """ | ||||
|     Sells the current pair if the threshold is reached | ||||
|     and updates the trade record. | ||||
|     :return: current instance | ||||
|     """ | ||||
|     try: | ||||
|         if not trade.is_open: | ||||
|             raise ValueError('attempt to handle closed trade: {}'.format(trade)) | ||||
|  | ||||
|         logger.debug('Handling open trade {} ...'.format(trade)) | ||||
|         # Get current rate | ||||
|         current_rate = api_wrapper.get_ticker(trade.pair)['last'] | ||||
|         current_profit = 100 * ((current_rate - trade.open_rate) / trade.open_rate) | ||||
|  | ||||
|         # Get available balance | ||||
|         currency = trade.pair.split('_')[1] | ||||
|         balance = api_wrapper.get_balance(currency) | ||||
|  | ||||
|         for duration, threshold in sorted(conf['trade_thresholds'].items()): | ||||
|             duration = float(duration) | ||||
|             threshold = float(threshold) | ||||
|             # Check if time matches and current rate is above threshold | ||||
|             time_diff = (datetime.utcnow() - trade.open_date).total_seconds() / 60 | ||||
|             if time_diff > duration and current_rate > (1 + threshold) * trade.open_rate: | ||||
|  | ||||
|                 # Execute sell and update trade record | ||||
|                 api_wrapper.sell(trade.pair, current_rate, balance) | ||||
|                 trade.close_rate = current_rate | ||||
|                 trade.close_profit = current_profit | ||||
|                 trade.close_date = datetime.utcnow() | ||||
|  | ||||
|                 message = '*{}:* Selling {} at rate `{:f} (profit: {}%)`'.format( | ||||
|                     trade.exchange.name, | ||||
|                     trade.pair.replace('_', '/'), | ||||
|                     trade.close_rate, | ||||
|                     round(current_profit, 2) | ||||
|                 ) | ||||
|                 logger.info(message) | ||||
|                 TelegramHandler.send_msg(message) | ||||
|                 return | ||||
|         else: | ||||
|             logger.debug('Threshold not reached. (cur_profit: {}%)'.format(round(current_profit, 2))) | ||||
|     except ValueError: | ||||
|         logger.exception('Unable to handle open order') | ||||
|  | ||||
|  | ||||
| def create_trade(stake_amount: float, exchange): | ||||
|     """ | ||||
|     Creates a new trade record with a random pair | ||||
|     :param stake_amount: amount of btc to spend | ||||
|     :param exchange: exchange to use | ||||
|     """ | ||||
|     # Whitelist sanity check | ||||
|     whitelist = conf[exchange.name.lower()]['pair_whitelist'] | ||||
|     if not whitelist or not isinstance(whitelist, list): | ||||
|         raise ValueError('No usable pair in whitelist.') | ||||
|     # Check if btc_amount is fulfilled | ||||
|     if api_wrapper.get_balance('BTC') < stake_amount: | ||||
|         raise ValueError('BTC amount is not fulfilled.') | ||||
|     # Pick random pair and execute trade | ||||
|     idx = random.randint(0, len(whitelist) - 1) | ||||
|     pair = whitelist[idx] | ||||
|     open_rate = api_wrapper.get_ticker(pair)['last'] | ||||
|     amount = stake_amount / open_rate | ||||
|     exchange = exchange | ||||
|     api_wrapper.buy(pair, open_rate, amount) | ||||
|  | ||||
|     trade = Trade( | ||||
|         pair=pair, | ||||
|         btc_amount=stake_amount, | ||||
|         open_rate=open_rate, | ||||
|         amount=amount, | ||||
|         exchange=exchange, | ||||
|     ) | ||||
|     message = '*{}:* Buying {} at rate `{:f}`'.format( | ||||
|         trade.exchange.name, | ||||
|         trade.pair.replace('_', '/'), | ||||
|         trade.open_rate | ||||
|     ) | ||||
|     logger.info(message) | ||||
|     TelegramHandler.send_msg(message) | ||||
|     return trade | ||||
|  | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     logger.info('Starting marginbot {}'.format(__version__)) | ||||
|     TelegramHandler.listen() | ||||
|     while True: | ||||
|         time.sleep(0.1) | ||||
							
								
								
									
										49
									
								
								persistence.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								persistence.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| from datetime import datetime | ||||
|  | ||||
| from sqlalchemy import Boolean, Column, DateTime, Float, Integer, String, create_engine | ||||
| from sqlalchemy.ext.declarative import declarative_base | ||||
| from sqlalchemy.orm import scoped_session, sessionmaker | ||||
| from sqlalchemy.types import Enum | ||||
|  | ||||
| from exchange import Exchange | ||||
|  | ||||
|  | ||||
| def create_session(base, filename): | ||||
|     """ | ||||
|     Creates sqlite database and setup tables. | ||||
|     :return: sqlalchemy Session | ||||
|     """ | ||||
|     engine = create_engine(filename, echo=False) | ||||
|     base.metadata.create_all(engine) | ||||
|     return scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True)) | ||||
|  | ||||
|  | ||||
| Base = declarative_base() | ||||
| Session = create_session(Base, filename='sqlite:///tradesv2.sqlite') | ||||
|  | ||||
|  | ||||
| class Trade(Base): | ||||
|     __tablename__ = 'trades' | ||||
|  | ||||
|     query = Session.query_property() | ||||
|  | ||||
|     id = Column(Integer, primary_key=True) | ||||
|     exchange = Column(Enum(Exchange), nullable=False) | ||||
|     pair = Column(String, nullable=False) | ||||
|     is_open = Column(Boolean, nullable=False, default=True) | ||||
|     open_rate = Column(Float, nullable=False) | ||||
|     close_rate = Column(Float) | ||||
|     close_profit = Column(Float) | ||||
|     btc_amount = Column(Float, nullable=False) | ||||
|     amount = Column(Float, nullable=False) | ||||
|     open_date = Column(DateTime, nullable=False, default=datetime.utcnow) | ||||
|     close_date = Column(DateTime) | ||||
|  | ||||
|     def __repr__(self): | ||||
|         return 'Trade(id={}, pair={}, amount={}, open_rate={}, open_since={})'.format( | ||||
|             self.id, | ||||
|             self.pair, | ||||
|             self.amount, | ||||
|             self.open_rate, | ||||
|             'closed' if not self.is_open else round((datetime.utcnow() - self.open_date).total_seconds() / 60, 2) | ||||
|         ) | ||||
							
								
								
									
										6
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| -e git+https://github.com/s4w3d0ff/python-poloniex.git#egg=Poloniex | ||||
| -e git+https://github.com/ericsomdahl/python-bittrex.git#egg=python-bittrex | ||||
| SQLAlchemy==1.1.9 | ||||
| python-telegram-bot==5.3.1 | ||||
| arrow==0.10.0 | ||||
| requests==2.14.2 | ||||
							
								
								
									
										0
									
								
								rpc/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								rpc/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										
											BIN
										
									
								
								rpc/__pycache__/__init__.cpython-36.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								rpc/__pycache__/__init__.cpython-36.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								rpc/__pycache__/telegram.cpython-36.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								rpc/__pycache__/telegram.cpython-36.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										202
									
								
								rpc/telegram.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										202
									
								
								rpc/telegram.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,202 @@ | ||||
| import threading | ||||
|  | ||||
| import arrow | ||||
| from datetime import timedelta | ||||
|  | ||||
| import logging | ||||
| from telegram.ext import CommandHandler, Updater | ||||
| from telegram import ParseMode | ||||
|  | ||||
| from persistence import Trade | ||||
| from exchange import get_exchange_api | ||||
| from utils import get_conf | ||||
|  | ||||
| logger = logging.getLogger(__name__) | ||||
|  | ||||
| _lock = threading.Condition() | ||||
| _updater = None | ||||
|  | ||||
| conf = get_conf() | ||||
| api_wrapper = get_exchange_api(conf) | ||||
|  | ||||
|  | ||||
| class TelegramHandler(object): | ||||
|     @staticmethod | ||||
|     def get_updater(conf): | ||||
|         """ | ||||
|         Returns the current telegram updater instantiates a new one | ||||
|         :param conf: | ||||
|         :return: telegram.ext.Updater | ||||
|         """ | ||||
|         global _updater | ||||
|         _lock.acquire() | ||||
|         if not _updater: | ||||
|             _updater = Updater(token=conf['telegram']['token'], workers=0) | ||||
|         _lock.release() | ||||
|         return _updater | ||||
|  | ||||
|     @staticmethod | ||||
|     def listen(): | ||||
|         """ | ||||
|         Registers all known command handlers and starts polling for message updates | ||||
|         :return: None | ||||
|         """ | ||||
|         # Register command handler and start telegram message polling | ||||
|         handles = [CommandHandler('status', TelegramHandler._status), | ||||
|                    CommandHandler('profit', TelegramHandler._profit), | ||||
|                    CommandHandler('start', TelegramHandler._start), | ||||
|                    CommandHandler('stop', TelegramHandler._stop)] | ||||
|         for handle in handles: | ||||
|             TelegramHandler.get_updater(conf).dispatcher.add_handler(handle) | ||||
|         TelegramHandler.get_updater(conf).start_polling() | ||||
|  | ||||
|     @staticmethod | ||||
|     def _is_correct_scope(update): | ||||
|         """ | ||||
|         Checks if it is save to process the given update | ||||
|         :param update:  | ||||
|         :return: True if valid else False | ||||
|         """ | ||||
|         # Only answer to our chat | ||||
|         return int(update.message.chat_id) == int(conf['telegram']['chat_id']) | ||||
|  | ||||
|     @staticmethod | ||||
|     def send_msg(markdown_message, bot=None): | ||||
|         """ | ||||
|         Send given markdown message | ||||
|         :param markdown_message: message | ||||
|         :param bot: alternative bot | ||||
|         :return: None | ||||
|         """ | ||||
|         if conf['telegram'].get('enabled', False): | ||||
|             try: | ||||
|                 bot = bot or TelegramHandler.get_updater(conf).bot | ||||
|                 bot.send_message( | ||||
|                     chat_id=conf['telegram']['chat_id'], | ||||
|                     text=markdown_message, | ||||
|                     parse_mode=ParseMode.MARKDOWN, | ||||
|                 ) | ||||
|             except Exception: | ||||
|                 logger.exception('Exception occurred within telegram api') | ||||
|  | ||||
|     @staticmethod | ||||
|     def _status(bot, update): | ||||
|         """ | ||||
|         Handler for /status | ||||
|         :param bot: telegram bot | ||||
|         :param update: message update | ||||
|         :return: None | ||||
|         """ | ||||
|         if not TelegramHandler._is_correct_scope(update): | ||||
|             return | ||||
|  | ||||
|         # Fetch open trade | ||||
|         trade = Trade.query.filter(Trade.is_open.is_(True)).first() | ||||
|  | ||||
|         from main import TradeThread | ||||
|         if not TradeThread.get_instance().is_alive(): | ||||
|             message = '*Status:* `trader stopped`' | ||||
|         elif not trade: | ||||
|             message = '*Status:* `no active order`' | ||||
|         else: | ||||
|             # calculate profit and send message to user | ||||
|             current_rate = api_wrapper.get_ticker(trade.pair)['last'] | ||||
|             current_profit = 100 * ((current_rate - trade.open_rate) / trade.open_rate) | ||||
|             open_orders = api_wrapper.get_open_orders(trade.pair) | ||||
|             order = open_orders[0] if open_orders else None | ||||
|             message = """ | ||||
| *Current Pair:* [{pair}](https://bittrex.com/Market/Index?MarketName={pair}) | ||||
| *Open Since:* `{date}` | ||||
| *Amount:* `{amount}` | ||||
| *Open Rate:* `{open_rate}` | ||||
| *Close Rate:* `{close_rate}` | ||||
| *Current Rate:* `{current_rate}` | ||||
| *Close Profit:* `{close_profit}%` | ||||
| *Current Profit:* `{current_profit}%` | ||||
| *Open Order:* `{open_order}` | ||||
|                     """.format( | ||||
|                 pair=trade.pair.replace('_', '-'), | ||||
|                 date=arrow.get(trade.open_date).humanize(), | ||||
|                 open_rate=trade.open_rate, | ||||
|                 close_rate=trade.close_rate, | ||||
|                 current_rate=current_rate, | ||||
|                 amount=round(trade.amount, 8), | ||||
|                 close_profit=round(trade.close_profit, 2) if trade.close_profit else 'None', | ||||
|                 current_profit=round(current_profit, 2), | ||||
|                 open_order='{} ({})'.format( | ||||
|                     order['remaining'], | ||||
|                     order['type'] | ||||
|                 ) if order else None, | ||||
|             ) | ||||
|         TelegramHandler.send_msg(message, bot=bot) | ||||
|  | ||||
|     @staticmethod | ||||
|     def _profit(bot, update): | ||||
|         """ | ||||
|         Handler for /profit | ||||
|         :param bot: telegram bot | ||||
|         :param update: message update | ||||
|         :return: None | ||||
|         """ | ||||
|         if not TelegramHandler._is_correct_scope(update): | ||||
|             return | ||||
|         trades = Trade.query.filter(Trade.is_open.is_(False)).all() | ||||
|         trade_count = len(trades) | ||||
|         profit_amount = sum((t.close_profit / 100) * t.btc_amount for t in trades) | ||||
|         profit = sum(t.close_profit for t in trades) | ||||
|         avg_stake_amount = sum(t.btc_amount for t in trades) / float(trade_count) | ||||
|         durations_hours = [(t.close_date - t.open_date).total_seconds() / 3600.0 for t in trades] | ||||
|         avg_duration = sum(durations_hours) / float(len(durations_hours)) | ||||
|  | ||||
|         markdown_msg = """ | ||||
| *Total Balance:* `{total_amount} BTC` | ||||
| *Total Profit:* `{profit_btc} BTC ({profit}%)` | ||||
| *Trade Count:* `{trade_count}` | ||||
| *First Action:* `{first_trade_date}` | ||||
| *Latest Action:* `{latest_trade_date}` | ||||
| *Avg. Stake Amount:* `{avg_open_amount} BTC` | ||||
| *Avg. Duration:* `{avg_duration}`  | ||||
|     """.format( | ||||
|             total_amount=round(api_wrapper.get_balance('BTC'), 8), | ||||
|             profit_btc=round(profit_amount, 8), | ||||
|             profit=round(profit, 2), | ||||
|             trade_count=trade_count, | ||||
|             first_trade_date=arrow.get(trades[0].open_date).humanize(), | ||||
|             latest_trade_date=arrow.get(trades[-1].open_date).humanize(), | ||||
|             avg_open_amount=round(avg_stake_amount, 8), | ||||
|             avg_duration=str(timedelta(hours=avg_duration)).split('.')[0], | ||||
|         ) | ||||
|         TelegramHandler.send_msg(markdown_msg, bot=bot) | ||||
|  | ||||
|     @staticmethod | ||||
|     def _start(bot, update): | ||||
|         """ | ||||
|         Handler for /start | ||||
|         :param bot: telegram bot | ||||
|         :param update: message update | ||||
|         :return: None | ||||
|         """ | ||||
|         if not TelegramHandler._is_correct_scope(update): | ||||
|             return | ||||
|         from main import TradeThread | ||||
|         if TradeThread.get_instance().is_alive(): | ||||
|             TelegramHandler.send_msg('*Status:* `already running`', bot=bot) | ||||
|             return | ||||
|         else: | ||||
|             TradeThread.get_instance(recreate=True).start() | ||||
|  | ||||
|     @staticmethod | ||||
|     def _stop(bot, update): | ||||
|         """ | ||||
|         Handler for /stop | ||||
|         :param bot: telegram bot | ||||
|         :param update: message update | ||||
|         :return: None | ||||
|         """ | ||||
|         if not TelegramHandler._is_correct_scope(update): | ||||
|             return | ||||
|         from main import TradeThread | ||||
|         if TradeThread.get_instance().is_alive(): | ||||
|             TradeThread.stop() | ||||
|         else: | ||||
|             TelegramHandler.send_msg('*Status:* `already stopped`', bot=bot) | ||||
							
								
								
									
										72
									
								
								utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								utils.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | ||||
| import json | ||||
| import logging | ||||
|  | ||||
| logger = logging.getLogger(__name__) | ||||
|  | ||||
| _CUR_CONF = None | ||||
|  | ||||
|  | ||||
| def get_conf(): | ||||
|     """ | ||||
|     Loads the config into memory and returns the instance of it | ||||
|     :return: dict | ||||
|     """ | ||||
|     global _CUR_CONF | ||||
|     if not _CUR_CONF: | ||||
|         with open('config.json') as fp: | ||||
|             _CUR_CONF = json.load(fp) | ||||
|             validate_conf(_CUR_CONF) | ||||
|     return _CUR_CONF | ||||
|  | ||||
|  | ||||
| def validate_conf(conf): | ||||
|     """ | ||||
|     Validates if the minimal possible config is provided | ||||
|     :param conf: config as dict | ||||
|     :return: None, raises ValueError if something is wrong | ||||
|     """ | ||||
|     if not isinstance(conf.get('stake_amount'), float): | ||||
|         raise ValueError('stake_amount  must be a float') | ||||
|     if not isinstance(conf.get('dry_run'), bool): | ||||
|         raise ValueError('dry_run must be a boolean') | ||||
|     if not isinstance(conf.get('trade_thresholds'), dict): | ||||
|         raise ValueError('trade_thresholds must be a dict') | ||||
|     if not isinstance(conf.get('trade_thresholds'), dict): | ||||
|         raise ValueError('trade_thresholds must be a dict') | ||||
|  | ||||
|     for i, (minutes, threshold) in enumerate(conf.get('trade_thresholds').items()): | ||||
|         if not isinstance(minutes, str): | ||||
|             raise ValueError('trade_thresholds[{}].key must be a string'.format(i)) | ||||
|         if not isinstance(threshold, float): | ||||
|             raise ValueError('trade_thresholds[{}].value must be a float'.format(i)) | ||||
|  | ||||
|     if conf.get('telegram'): | ||||
|         telegram = conf.get('telegram') | ||||
|         if not isinstance(telegram.get('token'), str): | ||||
|             raise ValueError('telegram.token must be a string') | ||||
|         if not isinstance(telegram.get('chat_id'), str): | ||||
|             raise ValueError('telegram.chat_id must be a string') | ||||
|  | ||||
|     if conf.get('poloniex'): | ||||
|         poloniex = conf.get('poloniex') | ||||
|         if not isinstance(poloniex.get('key'), str): | ||||
|             raise ValueError('poloniex.key must be a string') | ||||
|         if not isinstance(poloniex.get('secret'), str): | ||||
|             raise ValueError('poloniex.secret must be a string') | ||||
|         if not isinstance(poloniex.get('pair_whitelist'), list): | ||||
|             raise ValueError('poloniex.pair_whitelist must be a list') | ||||
|  | ||||
|     if conf.get('bittrex'): | ||||
|         bittrex = conf.get('bittrex') | ||||
|         if not isinstance(bittrex.get('key'), str): | ||||
|             raise ValueError('bittrex.key must be a string') | ||||
|         if not isinstance(bittrex.get('secret'), str): | ||||
|             raise ValueError('bittrex.secret must be a string') | ||||
|         if not isinstance(bittrex.get('pair_whitelist'), list): | ||||
|             raise ValueError('bittrex.pair_whitelist must be a list') | ||||
|  | ||||
|     if conf.get('poloniex', {}).get('enabled', False) \ | ||||
|             and conf.get('bittrex', {}).get('enabled', False): | ||||
|         raise ValueError('Cannot use poloniex and bittrex at the same time') | ||||
|  | ||||
|     logger.info('Config is valid ...') | ||||
		Reference in New Issue
	
	Block a user