diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 000000000..eddcbf7a5 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,2 @@ +[BASIC] +good-names=logger diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..f43f12e16 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3.6.2 + +RUN mkdir -p /freqtrade +WORKDIR /freqtrade + +ADD ./requirements.txt /freqtrade/requirements.txt +RUN pip install -r requirements.txt +ADD . /freqtrade +CMD python main.py diff --git a/README.md b/README.md index 73a4ce644..6479bf8ea 100644 --- a/README.md +++ b/README.md @@ -51,3 +51,11 @@ $ source .env/bin/activate $ pip install -r requirements.txt $ ./main.py ``` + + +#### Docker +``` +$ cd freqtrade +$ docker build -t freqtrade . +$ docker run --rm -it freqtrade +``` diff --git a/analyze.py b/analyze.py index 2e5945b54..68b7fa1c6 100644 --- a/analyze.py +++ b/analyze.py @@ -1,7 +1,7 @@ import time from datetime import timedelta -import arrow import logging +import arrow import requests from pandas.io.json import json_normalize from stockstats import StockDataFrame @@ -89,7 +89,7 @@ def get_buy_signal(pair): return False signal = latest['underpriced'] == 1 - logger.debug('buy_trigger: {} (pair={}, signal={})'.format(latest['date'], pair, signal)) + logger.debug('buy_trigger: %s (pair=%s, signal=%s)', latest['date'], pair, signal) return signal @@ -107,8 +107,8 @@ def plot_dataframe(dataframe, pair): import matplotlib.pyplot as plt # Three subplots sharing x axe - f, (ax1, ax2, ax3) = plt.subplots(3, sharex=True) - f.suptitle(pair, fontsize=14, fontweight='bold') + fig, (ax1, ax2, ax3) = plt.subplots(3, sharex=True) + fig.suptitle(pair, fontsize=14, fontweight='bold') ax1.plot(dataframe.index.values, dataframe['close'], label='close') ax1.plot(dataframe.index.values, dataframe['close_30_ema'], label='EMA(60)') ax1.plot(dataframe.index.values, dataframe['close_90_ema'], label='EMA(120)') @@ -129,8 +129,8 @@ def plot_dataframe(dataframe, pair): # Fine-tune figure; make subplots close to each other and hide x ticks for # all but bottom plot. - f.subplots_adjust(hspace=0) - plt.setp([a.get_xticklabels() for a in f.axes[:-1]], visible=False) + fig.subplots_adjust(hspace=0) + plt.setp([a.get_xticklabels() for a in fig.axes[:-1]], visible=False) plt.show() diff --git a/main.py b/main.py index 7ee13f031..81f16607a 100755 --- a/main.py +++ b/main.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -import json import logging import threading import time @@ -7,20 +6,16 @@ import traceback from datetime import datetime from json import JSONDecodeError from requests import ConnectionError - from wrapt import synchronized - from analyze import get_buy_signal - -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 +logging.basicConfig(level=logging.DEBUG, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) __author__ = "gcarq" __copyright__ = "gcarq 2017" @@ -28,8 +23,8 @@ __license__ = "GPLv3" __version__ = "0.8.0" -conf = get_conf() -api_wrapper = get_exchange_api(conf) +CONFIG = get_conf() +api_wrapper = get_exchange_api(CONFIG) @synchronized @@ -64,13 +59,13 @@ class TradeThread(threading.Thread): while not _should_stop: try: self._process() - except (ConnectionError, JSONDecodeError, ValueError) as e: - msg = 'Got {} during _process()'.format(e.__class__.__name__) + except (ConnectionError, JSONDecodeError, ValueError) as error: + msg = 'Got {} during _process()'.format(error.__class__.__name__) logger.exception(msg) finally: Session.flush() time.sleep(25) - except (RuntimeError, json.decoder.JSONDecodeError) as e: + except (RuntimeError, JSONDecodeError) as e: TelegramHandler.send_msg('*Status:* Got RuntimeError: ```\n{}\n```'.format(traceback.format_exc())) logger.exception('RuntimeError. Stopping trader ...') finally: @@ -85,10 +80,10 @@ class TradeThread(threading.Thread): """ # Query trades from persistence layer trades = Trade.query.filter(Trade.is_open.is_(True)).all() - if len(trades) < conf['max_open_trades']: + if len(trades) < CONFIG['max_open_trades']: # Create entity and execute trade try: - Session.add(create_trade(float(conf['stake_amount']), api_wrapper.exchange)) + Session.add(create_trade(float(CONFIG['stake_amount']), api_wrapper.exchange)) except ValueError: logger.exception('ValueError during trade creation') for trade in trades: @@ -110,7 +105,7 @@ class TradeThread(threading.Thread): trade.open_order_id = None # Check if this trade can be marked as closed if close_trade_if_fulfilled(trade): - logger.info('No open orders found and trade is fulfilled. Marking {} as closed ...'.format(trade)) + logger.info('No open orders found and trade is fulfilled. Marking %s as closed ...', trade) continue # Check if we can sell our current pair @@ -145,7 +140,7 @@ def handle_trade(trade): if not trade.is_open: raise ValueError('attempt to handle closed trade: {}'.format(trade)) - logger.debug('Handling open trade {} ...'.format(trade)) + logger.debug('Handling open trade %s ...', trade) # Get current rate current_rate = api_wrapper.get_ticker(trade.pair)['bid'] current_profit = 100 * ((current_rate - trade.open_rate) / trade.open_rate) @@ -154,7 +149,7 @@ def handle_trade(trade): currency = trade.pair.split('_')[1] balance = api_wrapper.get_balance(currency) - for duration, threshold in sorted(conf['minimal_roi'].items()): + for duration, threshold in sorted(CONFIG['minimal_roi'].items()): duration, threshold = float(duration), float(threshold) # Check if time matches and current rate is above threshold time_diff = (datetime.utcnow() - trade.open_date).total_seconds() / 60 @@ -172,7 +167,7 @@ def handle_trade(trade): TelegramHandler.send_msg(message) return else: - logger.debug('Threshold not reached. (cur_profit: {}%)'.format(round(current_profit, 2))) + logger.debug('Threshold not reached. (cur_profit: %1.2f%%)', current_profit) except ValueError: logger.exception('Unable to handle open order') @@ -183,11 +178,11 @@ def create_trade(stake_amount: float, exchange): :param stake_amount: amount of btc to spend :param exchange: exchange to use """ - logger.info('Creating new trade with stake_amount: {} ...'.format(stake_amount)) - whitelist = conf[exchange.name.lower()]['pair_whitelist'] + logger.info('Creating new trade with stake_amount: %f ...', stake_amount) + whitelist = CONFIG[exchange.name.lower()]['pair_whitelist'] # Check if btc_amount is fulfilled - if api_wrapper.get_balance(conf['stake_currency']) < stake_amount: - raise ValueError('stake amount is not fulfilled (currency={}'.format(conf['stake_currency'])) + if api_wrapper.get_balance(CONFIG['stake_currency']) < stake_amount: + raise ValueError('stake amount is not fulfilled (currency={}'.format(CONFIG['stake_currency'])) # Remove currently opened and latest pairs from whitelist trades = Trade.query.filter(Trade.is_open.is_(True)).all() @@ -197,7 +192,7 @@ def create_trade(stake_amount: float, exchange): for trade in trades: if trade.pair in whitelist: whitelist.remove(trade.pair) - logger.debug('Ignoring {} in pair whitelist'.format(trade.pair)) + logger.debug('Ignoring %s in pair whitelist', trade.pair) if not whitelist: raise ValueError('No pair in whitelist') @@ -232,7 +227,7 @@ def create_trade(stake_amount: float, exchange): if __name__ == '__main__': - logger.info('Starting freqtrade {}'.format(__version__)) + logger.info('Starting freqtrade %s', __version__) TelegramHandler.listen() while True: time.sleep(0.5) diff --git a/requirements.txt b/requirements.txt index 5c66e3142..a15c0f3f2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ -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==6.1b2 +python-telegram-bot==7.0.1 arrow==0.10.0 -requests==2.17.3 -urllib3==1.21.1 +requests==2.18.4 +urllib3==1.22 wrapt==1.10.10 pandas==0.20.1 matplotlib==2.0.0 diff --git a/rpc/telegram.py b/rpc/telegram.py index 64d6da866..815ff7c37 100644 --- a/rpc/telegram.py +++ b/rpc/telegram.py @@ -36,10 +36,10 @@ def authorized_only(command_handler): chat_id = int(conf['telegram']['chat_id']) if int(update.message.chat_id) == chat_id: - logger.info('Executing handler: {} for chat_id: {}'.format(command_handler.__name__, chat_id)) + logger.info('Executing handler: %s for chat_id: %s', command_handler.__name__, chat_id) return command_handler(*args, **kwargs) else: - logger.info('Rejected unauthorized message from: {}'.format(update.message.chat_id)) + logger.info('Rejected unauthorized message from: %s', update.message.chat_id) return wrapper @@ -80,7 +80,7 @@ class TelegramHandler(object): *Close Profit:* `{close_profit}` *Current Profit:* `{current_profit}%` *Open Order:* `{open_order}` - """.format( + """.format( trade_id=trade.id, pair=trade.pair, market_url=api_wrapper.get_pair_detail_url(trade.pair), @@ -136,7 +136,7 @@ class TelegramHandler(object): *Latest Trade opened:* `{latest_trade_date}` *Avg. Duration:* `{avg_duration}` *Best Performing:* `{best_pair}: {best_rate}%` - """.format( + """.format( profit_btc=round(sum(profit_amounts), 8), profit=round(sum(profits), 2), trade_count=len(trades), @@ -309,10 +309,10 @@ class TelegramHandler(object): bot = bot or TelegramHandler.get_updater(conf).bot try: bot.send_message(conf['telegram']['chat_id'], msg, parse_mode=parse_mode) - except NetworkError as e: + except NetworkError as error: # Sometimes the telegram server resets the current connection, # if this is the case we send the message again. - logger.warning('Got Telegram NetworkError: {}! trying one more time'.format(e.message)) + logger.warning('Got Telegram NetworkError: %s! Trying one more time.', error.message) bot.send_message(conf['telegram']['chat_id'], msg, parse_mode=parse_mode) except Exception: logger.exception('Exception occurred within Telegram API') diff --git a/utils.py b/utils.py index 304c55e1e..e87119b5b 100644 --- a/utils.py +++ b/utils.py @@ -17,8 +17,8 @@ def get_conf(filename='config.json'): """ global _cur_conf if not _cur_conf: - with open(filename) as fp: - _cur_conf = json.load(fp) + with open(filename) as file: + _cur_conf = json.load(file) validate_conf(_cur_conf) return _cur_conf @@ -40,11 +40,11 @@ def validate_conf(conf): if not isinstance(conf.get('minimal_roi'), dict): raise ValueError('minimal_roi must be a dict') - for i, (minutes, threshold) in enumerate(conf.get('minimal_roi').items()): + for index, (minutes, threshold) in enumerate(conf.get('minimal_roi').items()): if not isinstance(minutes, str): - raise ValueError('minimal_roi[{}].key must be a string'.format(i)) + raise ValueError('minimal_roi[{}].key must be a string'.format(index)) if not isinstance(threshold, float): - raise ValueError('minimal_roi[{}].value must be a float'.format(i)) + raise ValueError('minimal_roi[{}].value must be a float'.format(index)) if conf.get('telegram'): telegram = conf.get('telegram') @@ -95,7 +95,7 @@ def validate_bittrex_pairs(pairs): data = Bittrex(None, None).get_markets() if not data['success']: raise RuntimeError('BITTREX: {}'.format(data['message'])) - available_markets = [m['MarketName'].replace('-', '_')for m in data['result']] - for p in pairs: - if p not in available_markets: - raise ValueError('Invalid pair: {}'.format(p)) + available_markets = [market['MarketName'].replace('-', '_')for market in data['result']] + for pair in pairs: + if pair not in available_markets: + raise ValueError('Invalid pair: {}'.format(pair))