This commit is contained in:
gcarq 2017-09-01 20:06:11 +02:00
commit 5f07e8aaff
8 changed files with 63 additions and 49 deletions

2
.pylintrc Normal file
View File

@ -0,0 +1,2 @@
[BASIC]
good-names=logger

9
Dockerfile Normal file
View File

@ -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

View File

@ -51,3 +51,11 @@ $ source .env/bin/activate
$ pip install -r requirements.txt $ pip install -r requirements.txt
$ ./main.py $ ./main.py
``` ```
#### Docker
```
$ cd freqtrade
$ docker build -t freqtrade .
$ docker run --rm -it freqtrade
```

View File

@ -1,7 +1,7 @@
import time import time
from datetime import timedelta from datetime import timedelta
import arrow
import logging import logging
import arrow
import requests import requests
from pandas.io.json import json_normalize from pandas.io.json import json_normalize
from stockstats import StockDataFrame from stockstats import StockDataFrame
@ -89,7 +89,7 @@ def get_buy_signal(pair):
return False return False
signal = latest['underpriced'] == 1 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 return signal
@ -107,8 +107,8 @@ def plot_dataframe(dataframe, pair):
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
# Three subplots sharing x axe # Three subplots sharing x axe
f, (ax1, ax2, ax3) = plt.subplots(3, sharex=True) fig, (ax1, ax2, ax3) = plt.subplots(3, sharex=True)
f.suptitle(pair, fontsize=14, fontweight='bold') fig.suptitle(pair, fontsize=14, fontweight='bold')
ax1.plot(dataframe.index.values, dataframe['close'], label='close') 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_30_ema'], label='EMA(60)')
ax1.plot(dataframe.index.values, dataframe['close_90_ema'], label='EMA(120)') 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 # Fine-tune figure; make subplots close to each other and hide x ticks for
# all but bottom plot. # all but bottom plot.
f.subplots_adjust(hspace=0) fig.subplots_adjust(hspace=0)
plt.setp([a.get_xticklabels() for a in f.axes[:-1]], visible=False) plt.setp([a.get_xticklabels() for a in fig.axes[:-1]], visible=False)
plt.show() plt.show()

45
main.py
View File

@ -1,5 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python
import json
import logging import logging
import threading import threading
import time import time
@ -7,20 +6,16 @@ import traceback
from datetime import datetime from datetime import datetime
from json import JSONDecodeError from json import JSONDecodeError
from requests import ConnectionError from requests import ConnectionError
from wrapt import synchronized from wrapt import synchronized
from analyze import get_buy_signal 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 persistence import Trade, Session
from exchange import get_exchange_api from exchange import get_exchange_api
from rpc.telegram import TelegramHandler from rpc.telegram import TelegramHandler
from utils import get_conf 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" __author__ = "gcarq"
__copyright__ = "gcarq 2017" __copyright__ = "gcarq 2017"
@ -28,8 +23,8 @@ __license__ = "GPLv3"
__version__ = "0.8.0" __version__ = "0.8.0"
conf = get_conf() CONFIG = get_conf()
api_wrapper = get_exchange_api(conf) api_wrapper = get_exchange_api(CONFIG)
@synchronized @synchronized
@ -64,13 +59,13 @@ class TradeThread(threading.Thread):
while not _should_stop: while not _should_stop:
try: try:
self._process() self._process()
except (ConnectionError, JSONDecodeError, ValueError) as e: except (ConnectionError, JSONDecodeError, ValueError) as error:
msg = 'Got {} during _process()'.format(e.__class__.__name__) msg = 'Got {} during _process()'.format(error.__class__.__name__)
logger.exception(msg) logger.exception(msg)
finally: finally:
Session.flush() Session.flush()
time.sleep(25) 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())) TelegramHandler.send_msg('*Status:* Got RuntimeError: ```\n{}\n```'.format(traceback.format_exc()))
logger.exception('RuntimeError. Stopping trader ...') logger.exception('RuntimeError. Stopping trader ...')
finally: finally:
@ -85,10 +80,10 @@ class TradeThread(threading.Thread):
""" """
# Query trades from persistence layer # Query trades from persistence layer
trades = Trade.query.filter(Trade.is_open.is_(True)).all() 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 # Create entity and execute trade
try: 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: except ValueError:
logger.exception('ValueError during trade creation') logger.exception('ValueError during trade creation')
for trade in trades: for trade in trades:
@ -110,7 +105,7 @@ class TradeThread(threading.Thread):
trade.open_order_id = None trade.open_order_id = None
# Check if this trade can be marked as closed # Check if this trade can be marked as closed
if close_trade_if_fulfilled(trade): 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 continue
# Check if we can sell our current pair # Check if we can sell our current pair
@ -145,7 +140,7 @@ def handle_trade(trade):
if not trade.is_open: if not trade.is_open:
raise ValueError('attempt to handle closed trade: {}'.format(trade)) 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 # Get current rate
current_rate = api_wrapper.get_ticker(trade.pair)['bid'] current_rate = api_wrapper.get_ticker(trade.pair)['bid']
current_profit = 100 * ((current_rate - trade.open_rate) / trade.open_rate) current_profit = 100 * ((current_rate - trade.open_rate) / trade.open_rate)
@ -154,7 +149,7 @@ def handle_trade(trade):
currency = trade.pair.split('_')[1] currency = trade.pair.split('_')[1]
balance = api_wrapper.get_balance(currency) 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) duration, threshold = float(duration), float(threshold)
# Check if time matches and current rate is above threshold # Check if time matches and current rate is above threshold
time_diff = (datetime.utcnow() - trade.open_date).total_seconds() / 60 time_diff = (datetime.utcnow() - trade.open_date).total_seconds() / 60
@ -172,7 +167,7 @@ def handle_trade(trade):
TelegramHandler.send_msg(message) TelegramHandler.send_msg(message)
return return
else: 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: except ValueError:
logger.exception('Unable to handle open order') 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 stake_amount: amount of btc to spend
:param exchange: exchange to use :param exchange: exchange to use
""" """
logger.info('Creating new trade with stake_amount: {} ...'.format(stake_amount)) logger.info('Creating new trade with stake_amount: %f ...', stake_amount)
whitelist = conf[exchange.name.lower()]['pair_whitelist'] whitelist = CONFIG[exchange.name.lower()]['pair_whitelist']
# Check if btc_amount is fulfilled # Check if btc_amount is fulfilled
if api_wrapper.get_balance(conf['stake_currency']) < stake_amount: if api_wrapper.get_balance(CONFIG['stake_currency']) < stake_amount:
raise ValueError('stake amount is not fulfilled (currency={}'.format(conf['stake_currency'])) raise ValueError('stake amount is not fulfilled (currency={}'.format(CONFIG['stake_currency']))
# Remove currently opened and latest pairs from whitelist # Remove currently opened and latest pairs from whitelist
trades = Trade.query.filter(Trade.is_open.is_(True)).all() 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: for trade in trades:
if trade.pair in whitelist: if trade.pair in whitelist:
whitelist.remove(trade.pair) 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: if not whitelist:
raise ValueError('No pair in whitelist') raise ValueError('No pair in whitelist')
@ -232,7 +227,7 @@ def create_trade(stake_amount: float, exchange):
if __name__ == '__main__': if __name__ == '__main__':
logger.info('Starting freqtrade {}'.format(__version__)) logger.info('Starting freqtrade %s', __version__)
TelegramHandler.listen() TelegramHandler.listen()
while True: while True:
time.sleep(0.5) time.sleep(0.5)

View File

@ -1,10 +1,10 @@
-e git+https://github.com/s4w3d0ff/python-poloniex.git#egg=Poloniex -e git+https://github.com/s4w3d0ff/python-poloniex.git#egg=Poloniex
-e git+https://github.com/ericsomdahl/python-bittrex.git#egg=python-bittrex -e git+https://github.com/ericsomdahl/python-bittrex.git#egg=python-bittrex
SQLAlchemy==1.1.9 SQLAlchemy==1.1.9
python-telegram-bot==6.1b2 python-telegram-bot==7.0.1
arrow==0.10.0 arrow==0.10.0
requests==2.17.3 requests==2.18.4
urllib3==1.21.1 urllib3==1.22
wrapt==1.10.10 wrapt==1.10.10
pandas==0.20.1 pandas==0.20.1
matplotlib==2.0.0 matplotlib==2.0.0

View File

@ -36,10 +36,10 @@ def authorized_only(command_handler):
chat_id = int(conf['telegram']['chat_id']) chat_id = int(conf['telegram']['chat_id'])
if int(update.message.chat_id) == 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) return command_handler(*args, **kwargs)
else: 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 return wrapper
@ -80,7 +80,7 @@ class TelegramHandler(object):
*Close Profit:* `{close_profit}` *Close Profit:* `{close_profit}`
*Current Profit:* `{current_profit}%` *Current Profit:* `{current_profit}%`
*Open Order:* `{open_order}` *Open Order:* `{open_order}`
""".format( """.format(
trade_id=trade.id, trade_id=trade.id,
pair=trade.pair, pair=trade.pair,
market_url=api_wrapper.get_pair_detail_url(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}` *Latest Trade opened:* `{latest_trade_date}`
*Avg. Duration:* `{avg_duration}` *Avg. Duration:* `{avg_duration}`
*Best Performing:* `{best_pair}: {best_rate}%` *Best Performing:* `{best_pair}: {best_rate}%`
""".format( """.format(
profit_btc=round(sum(profit_amounts), 8), profit_btc=round(sum(profit_amounts), 8),
profit=round(sum(profits), 2), profit=round(sum(profits), 2),
trade_count=len(trades), trade_count=len(trades),
@ -309,10 +309,10 @@ class TelegramHandler(object):
bot = bot or TelegramHandler.get_updater(conf).bot bot = bot or TelegramHandler.get_updater(conf).bot
try: try:
bot.send_message(conf['telegram']['chat_id'], msg, parse_mode=parse_mode) 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, # Sometimes the telegram server resets the current connection,
# if this is the case we send the message again. # 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) bot.send_message(conf['telegram']['chat_id'], msg, parse_mode=parse_mode)
except Exception: except Exception:
logger.exception('Exception occurred within Telegram API') logger.exception('Exception occurred within Telegram API')

View File

@ -17,8 +17,8 @@ def get_conf(filename='config.json'):
""" """
global _cur_conf global _cur_conf
if not _cur_conf: if not _cur_conf:
with open(filename) as fp: with open(filename) as file:
_cur_conf = json.load(fp) _cur_conf = json.load(file)
validate_conf(_cur_conf) validate_conf(_cur_conf)
return _cur_conf return _cur_conf
@ -40,11 +40,11 @@ def validate_conf(conf):
if not isinstance(conf.get('minimal_roi'), dict): if not isinstance(conf.get('minimal_roi'), dict):
raise ValueError('minimal_roi must be a 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): 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): 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'): if conf.get('telegram'):
telegram = conf.get('telegram') telegram = conf.get('telegram')
@ -95,7 +95,7 @@ def validate_bittrex_pairs(pairs):
data = Bittrex(None, None).get_markets() data = Bittrex(None, None).get_markets()
if not data['success']: if not data['success']:
raise RuntimeError('BITTREX: {}'.format(data['message'])) raise RuntimeError('BITTREX: {}'.format(data['message']))
available_markets = [m['MarketName'].replace('-', '_')for m in data['result']] available_markets = [market['MarketName'].replace('-', '_')for market in data['result']]
for p in pairs: for pair in pairs:
if p not in available_markets: if pair not in available_markets:
raise ValueError('Invalid pair: {}'.format(p)) raise ValueError('Invalid pair: {}'.format(pair))