diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..9f4726bfb --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +.git +.gitignore +Dockerfile +.dockerignore +config.json* +*.sqlite diff --git a/Dockerfile b/Dockerfile index bd303769d..75929a5e8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,20 +1,23 @@ -FROM python:3.6.2 - -RUN apt-get update -RUN apt-get -y install build-essential +FROM python:3.6.2 # Install TA-lib -RUN wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz -RUN tar zxvf ta-lib-0.4.0-src.tar.gz -RUN cd ta-lib && ./configure && make && make install +RUN apt-get update && apt-get -y install build-essential && apt-get clean +RUN curl -L http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz | \ + tar xzvf - && \ + cd ta-lib && \ + ./configure && make && make install && \ + cd .. && rm -rf ta-lib ENV LD_LIBRARY_PATH /usr/local/lib # Prepare environment RUN mkdir /freqtrade -COPY . /freqtrade/ WORKDIR /freqtrade -# Install dependencies and execute +# Install dependencies +COPY requirements.txt /freqtrade/ RUN pip install -r requirements.txt + +# Install and execute +COPY . /freqtrade/ RUN pip install -e . CMD ["freqtrade"] diff --git a/README.md b/README.md index d9eb017ff..4388f00e3 100644 --- a/README.md +++ b/README.md @@ -30,15 +30,14 @@ in minutes and the value is the minimum ROI in percent. See the example below: ``` "minimal_roi": { - "2880": 0.005, # Sell after 48 hours if there is at least 0.5% profit - "1440": 0.01, # Sell after 24 hours if there is at least 1% profit - "720": 0.02, # Sell after 12 hours if there is at least 2% profit - "360": 0.02, # Sell after 6 hours if there is at least 2% profit - "0": 0.025 # Sell immediately if there is at least 2.5% profit + "50": 0.0, # Sell after 30 minutes if the profit is not negative + "40": 0.01, # Sell after 25 minutes if there is at least 1% profit + "30": 0.02, # Sell after 15 minutes if there is at least 2% profit + "0": 0.045 # Sell immediately if there is at least 4.5% profit }, ``` -`stoploss` is loss in percentage that should trigger a sale. +`stoploss` is loss in percentage that should trigger a sale. For example value `-0.10` will cause immediate sell if the profit dips below -10% for a given trade. This parameter is optional. @@ -47,7 +46,9 @@ Possible values are `running` or `stopped`. (default=`running`) If the value is `stopped` the bot has to be started with `/start` first. `ask_last_balance` sets the bidding price. Value `0.0` will use `ask` price, `1.0` will -use the `last` price and values between those interpolate between ask and last price. Using `ask` price will guarantee quick success in bid, but bot will also end up paying more then would probably have been necessary. +use the `last` price and values between those interpolate between ask and last +price. Using `ask` price will guarantee quick success in bid, but bot will also +end up paying more then would probably have been necessary. The other values should be self-explanatory, if not feel free to raise a github issue. @@ -84,16 +85,57 @@ $ pytest This will by default skip the slow running backtest set. To run backtest set: ``` -$ BACKTEST=true pytest +$ BACKTEST=true pytest -s freqtrade/tests/test_backtesting.py ``` #### Docker + +Building the image: + ``` $ cd freqtrade $ docker build -t freqtrade . -$ docker run --rm -it freqtrade ``` +For security reasons, your configuration file will not be included in the +image, you will need to bind mount it. It is also advised to bind mount +a SQLite database file (see second example) to keep it between updates. + +You can run a one-off container that is immediately deleted upon exiting with +the following command (config.json must be in the current working directory): + +``` +$ docker run --rm -v `pwd`/config.json:/freqtrade/config.json -it freqtrade +``` + +To run a restartable instance in the background (feel free to place your +configuration and database files wherever it feels comfortable on your +filesystem): + +``` +$ cd ~/.freq +$ touch tradesv2.sqlite +$ docker run -d \ + --name freqtrade \ + -v ~/.freq/config.json:/freqtrade/config.json \ + -v ~/.freq/tradesv2.sqlite:/freqtrade/tradesv2.sqlite \ + freqtrade +``` +If you are using `dry_run=True` you need to bind `tradesv2.dry_run.sqlite` instead of `tradesv2.sqlite`. + +You can then use the following commands to monitor and manage your container: + +``` +$ docker logs freqtrade +$ docker logs -f freqtrade +$ docker restart freqtrade +$ docker stop freqtrade +$ docker start freqtrade +``` + +You do not need to rebuild the image for configuration +changes, it will suffice to edit `config.json` and restart the container. + #### Contributing Feel like our bot is missing a feature? We welcome your pull requests! Few pointers for contributions: diff --git a/config.json.example b/config.json.example index d2ae679db..685189087 100644 --- a/config.json.example +++ b/config.json.example @@ -4,10 +4,10 @@ "stake_amount": 0.05, "dry_run": false, "minimal_roi": { - "60": 0.0, - "40": 0.01, - "20": 0.02, - "0": 0.03 + "50": 0.0, + "40": 0.01, + "30": 0.02, + "0": 0.045 }, "stoploss": -0.40, "bid_strategy": { diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index db6fd3728..44a21be08 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,3 +1,3 @@ -__version__ = '0.11.0' +__version__ = '0.12.0' from . import main diff --git a/freqtrade/analyze.py b/freqtrade/analyze.py index 159e1d137..310754754 100644 --- a/freqtrade/analyze.py +++ b/freqtrade/analyze.py @@ -6,7 +6,8 @@ import arrow import talib.abstract as ta from pandas import DataFrame -from freqtrade.exchange import get_ticker_history +from freqtrade import exchange +from freqtrade.exchange import Bittrex, get_ticker_history logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') @@ -23,23 +24,23 @@ def parse_ticker_dataframe(ticker: list, minimum_date: arrow.Arrow) -> DataFrame .drop('BV', 1) \ .rename(columns={'C':'close', 'V':'volume', 'O':'open', 'H':'high', 'L':'low', 'T':'date'}) \ .sort_values('date') - return df[df['date'].map(arrow.get) > minimum_date] + return df def populate_indicators(dataframe: DataFrame) -> DataFrame: """ Adds several different TA indicators to the given DataFrame """ - dataframe['sar'] = ta.SAR(dataframe, 0.02, 0.22) + dataframe['sar'] = ta.SAR(dataframe) dataframe['adx'] = ta.ADX(dataframe) stoch = ta.STOCHF(dataframe) dataframe['fastd'] = stoch['fastd'] dataframe['fastk'] = stoch['fastk'] dataframe['blower'] = ta.BBANDS(dataframe, nbdevup=2, nbdevdn=2)['lowerband'] - dataframe['cci'] = ta.CCI(dataframe, timeperiod=5) - dataframe['sma'] = ta.SMA(dataframe, timeperiod=100) - dataframe['tema'] = ta.TEMA(dataframe, timeperiod=4) + dataframe['sma'] = ta.SMA(dataframe, timeperiod=40) + dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9) dataframe['mfi'] = ta.MFI(dataframe) + dataframe['cci'] = ta.CCI(dataframe) return dataframe @@ -49,14 +50,12 @@ def populate_buy_trend(dataframe: DataFrame) -> DataFrame: :param dataframe: DataFrame :return: DataFrame with buy column """ - dataframe.loc[ (dataframe['close'] < dataframe['sma']) & - (dataframe['cci'] < -100) & (dataframe['tema'] <= dataframe['blower']) & - (dataframe['mfi'] < 30) & - (dataframe['fastd'] < 20) & - (dataframe['adx'] > 20), + (dataframe['mfi'] < 25) & + (dataframe['fastd'] < 25) & + (dataframe['adx'] > 30), 'buy'] = 1 dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close'] @@ -119,20 +118,26 @@ def plot_dataframe(dataframe: DataFrame, pair: str) -> None: import matplotlib.pyplot as plt # Two subplots sharing x axis - fig, (ax1, ax2) = plt.subplots(2, sharex=True) + fig, (ax1, ax2, ax3) = plt.subplots(3, sharex=True) fig.suptitle(pair, fontsize=14, fontweight='bold') - ax1.plot(dataframe.index.values, dataframe['sar'], 'g_', label='pSAR') ax1.plot(dataframe.index.values, dataframe['close'], label='close') # ax1.plot(dataframe.index.values, dataframe['sell'], 'ro', label='sell') ax1.plot(dataframe.index.values, dataframe['sma'], '--', label='SMA') + ax1.plot(dataframe.index.values, dataframe['tema'], ':', label='TEMA') + ax1.plot(dataframe.index.values, dataframe['blower'], '-.', label='BB low') ax1.plot(dataframe.index.values, dataframe['buy_price'], 'bo', label='buy') ax1.legend() - # ax2.plot(dataframe.index.values, dataframe['adx'], label='ADX') + ax2.plot(dataframe.index.values, dataframe['adx'], label='ADX') ax2.plot(dataframe.index.values, dataframe['mfi'], label='MFI') # ax2.plot(dataframe.index.values, [25] * len(dataframe.index.values)) ax2.legend() + ax3.plot(dataframe.index.values, dataframe['fastk'], label='k') + ax3.plot(dataframe.index.values, dataframe['fastd'], label='d') + ax3.plot(dataframe.index.values, [20] * len(dataframe.index.values)) + ax3.legend() + # Fine-tune figure; make subplots close to each other and hide x ticks for # all but bottom plot. fig.subplots_adjust(hspace=0) @@ -143,6 +148,7 @@ def plot_dataframe(dataframe: DataFrame, pair: str) -> None: if __name__ == '__main__': # Install PYQT5==5.9 manually if you want to test this helper function while True: + exchange.EXCHANGE = Bittrex({'key': '', 'secret': ''}) test_pair = 'BTC_ETH' # for pair in ['BTC_ANT', 'BTC_ETH', 'BTC_GNT', 'BTC_ETC']: # get_buy_signal(pair) diff --git a/freqtrade/exchange/bittrex.py b/freqtrade/exchange/bittrex.py index d2fd08ddc..cb85aaf87 100644 --- a/freqtrade/exchange/bittrex.py +++ b/freqtrade/exchange/bittrex.py @@ -26,10 +26,6 @@ class Bittrex(Exchange): # Sleep time to avoid rate limits, used in the main loop SLEEP_TIME: float = 25 - @property - def name(self) -> str: - return self.__class__.__name__ - @property def sleep_time(self) -> float: return self.SLEEP_TIME @@ -40,13 +36,6 @@ class Bittrex(Exchange): _EXCHANGE_CONF.update(config) _API = _Bittrex(api_key=_EXCHANGE_CONF['key'], api_secret=_EXCHANGE_CONF['secret']) - # Check if all pairs are available - markets = self.get_markets() - exchange_name = self.name - for pair in _EXCHANGE_CONF['pair_whitelist']: - if pair not in markets: - raise RuntimeError('Pair {} is not available at {}'.format(pair, exchange_name)) - def buy(self, pair: str, rate: float, amount: float) -> str: data = _API.buy_limit(pair.replace('_', '-'), amount, rate) if not data['success']: diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 5312125ed..4b320eb85 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -45,6 +45,7 @@ def init(config: dict) -> None: CommandHandler('stop', _stop), CommandHandler('forcesell', _forcesell), CommandHandler('performance', _performance), + CommandHandler('help', _help), ] for handle in handles: _updater.dispatcher.add_handler(handle) @@ -301,6 +302,27 @@ def _performance(bot: Bot, update: Update) -> None: send_msg(message, parse_mode=ParseMode.HTML) +@authorized_only +def _help(bot: Bot, update: Update) -> None: + """ + Handler for /help. + Show commands of the bot + :param bot: telegram bot + :param update: message update + :return: None + """ + message = """ +*/start:* `Starts the trader` +*/stop:* `Stops the trader` +*/status:* `Lists all open trades` +*/profit:* `Lists cumulative profit from all finished trades` +*/forcesell :* `Instantly sells the given trade, regardless of profit` +*/performance:* `Show performance of each finished trade grouped by pair` +*/help:* `This help message` + """ + send_msg(message, bot=bot) + + def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None: """ Send given markdown message diff --git a/freqtrade/tests/test_backtesting.py b/freqtrade/tests/test_backtesting.py index fb3b7e511..d008c4ae6 100644 --- a/freqtrade/tests/test_backtesting.py +++ b/freqtrade/tests/test_backtesting.py @@ -18,7 +18,7 @@ def print_results(results): len(results.index), results.profit.mean() * 100.0, results.profit.sum(), - results.duration.mean()*5 + results.duration.mean() * 5 )) @pytest.fixture @@ -30,10 +30,10 @@ def pairs(): def conf(): return { "minimal_roi": { - "60": 0.0, + "50": 0.0, "40": 0.01, - "20": 0.02, - "0": 0.03 + "30": 0.02, + "0": 0.045 }, "stoploss": -0.40 } diff --git a/freqtrade/tests/test_hyperopt.py b/freqtrade/tests/test_hyperopt.py new file mode 100644 index 000000000..167e0d9ab --- /dev/null +++ b/freqtrade/tests/test_hyperopt.py @@ -0,0 +1,166 @@ +# pragma pylint: disable=missing-docstring +import json +import logging +import os +from functools import reduce + +import pytest +import arrow +from pandas import DataFrame + +from hyperopt import fmin, tpe, hp + +from freqtrade.analyze import analyze_ticker +from freqtrade.main import should_sell +from freqtrade.persistence import Trade + +logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot + +def print_results(results): + print('Made {} buys. Average profit {:.2f}%. Total profit was {:.3f}. Average duration {:.1f} mins.'.format( + len(results.index), + results.profit.mean() * 100.0, + results.profit.sum(), + results.duration.mean() * 5 + )) + +@pytest.fixture +def pairs(): + return ['btc-neo', 'btc-eth', 'btc-omg', 'btc-edg', 'btc-pay', + 'btc-pivx', 'btc-qtum', 'btc-mtl', 'btc-etc', 'btc-ltc'] + +@pytest.fixture +def conf(): + return { + "minimal_roi": { + "40": 0.0, + "30": 0.01, + "20": 0.02, + "0": 0.04 + }, + "stoploss": -0.05 + } + + +def backtest(conf, pairs, mocker, buy_strategy): + trades = [] + mocker.patch.dict('freqtrade.main._CONF', conf) + for pair in pairs: + with open('freqtrade/tests/testdata/'+pair+'.json') as data_file: + data = json.load(data_file) + + mocker.patch('freqtrade.analyze.get_ticker_history', return_value=data) + mocker.patch('arrow.utcnow', return_value=arrow.get('2017-08-20T14:50:00')) + mocker.patch('freqtrade.analyze.populate_buy_trend', side_effect=buy_strategy) + ticker = analyze_ticker(pair) + # for each buy point + for index, row in ticker[ticker.buy == 1].iterrows(): + trade = Trade( + open_rate=row['close'], + open_date=arrow.get(row['date']).datetime, + amount=1, + ) + # calculate win/lose forwards from buy point + for index2, row2 in ticker[index:].iterrows(): + if should_sell(trade, row2['close'], arrow.get(row2['date']).datetime): + current_profit = (row2['close'] - trade.open_rate) / trade.open_rate + + trades.append((pair, current_profit, index2 - index)) + break + + labels = ['currency', 'profit', 'duration'] + results = DataFrame.from_records(trades, columns=labels) + + print_results(results) + + # set the value below to suit your number concurrent trades so its realistic to 20days of data + TARGET_TRADES = 1200 + if results.profit.sum() == 0 or results.profit.mean() == 0: + return 49999999999 # avoid division by zero, return huge value to discard result + return abs(len(results.index) - 1200.1) / (results.profit.sum() ** 2) * results.duration.mean() # the smaller the better + +def buy_strategy_generator(params): + print(params) + def populate_buy_trend(dataframe: DataFrame) -> DataFrame: + conditions = [] + # GUARDS AND TRENDS + if params['below_sma']['enabled']: + conditions.append(dataframe['close'] < dataframe['sma']) + if params['over_sma']['enabled']: + conditions.append(dataframe['close'] > dataframe['sma']) + if params['mfi']['enabled']: + conditions.append(dataframe['mfi'] < params['mfi']['value']) + if params['fastd']['enabled']: + conditions.append(dataframe['fastd'] < params['fastd']['value']) + if params['adx']['enabled']: + conditions.append(dataframe['adx'] > params['adx']['value']) + if params['cci']['enabled']: + conditions.append(dataframe['cci'] < params['cci']['value']) + if params['over_sar']['enabled']: + conditions.append(dataframe['close'] > dataframe['sar']) + if params['uptrend_sma']['enabled']: + prevsma = dataframe['sma'].shift(1) + conditions.append(dataframe['sma'] > prevsma) + + prev_fastd = dataframe['fastd'].shift(1) + # TRIGGERS + triggers = { + 'lower_bb': dataframe['tema'] <= dataframe['blower'], + 'faststoch10': (dataframe['fastd'] >= 10) & (prev_fastd < 10), + } + conditions.append(triggers.get(params['trigger']['type'])) + + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'buy'] = 1 + dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close'] + + return dataframe + return populate_buy_trend + +@pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set") +def test_hyperopt(conf, pairs, mocker): + + def optimizer(params): + return backtest(conf, pairs, mocker, buy_strategy_generator(params)) + + space = { + 'mfi': hp.choice('mfi', [ + {'enabled': False}, + {'enabled': True, 'value': hp.uniform('mfi-value', 2, 40)} + ]), + 'fastd': hp.choice('fastd', [ + {'enabled': False}, + {'enabled': True, 'value': hp.uniform('fastd-value', 2, 40)} + ]), + 'adx': hp.choice('adx', [ + {'enabled': False}, + {'enabled': True, 'value': hp.uniform('adx-value', 2, 40)} + ]), + 'cci': hp.choice('cci', [ + {'enabled': False}, + {'enabled': True, 'value': hp.uniform('cci-value', -200, -100)} + ]), + 'below_sma': hp.choice('below_sma', [ + {'enabled': False}, + {'enabled': True} + ]), + 'over_sma': hp.choice('over_sma', [ + {'enabled': False}, + {'enabled': True} + ]), + 'over_sar': hp.choice('over_sar', [ + {'enabled': False}, + {'enabled': True} + ]), + 'uptrend_sma': hp.choice('uptrend_sma', [ + {'enabled': False}, + {'enabled': True} + ]), + 'trigger': hp.choice('trigger', [ + {'type': 'lower_bb'}, + {'type': 'faststoch10'} + ]), + } + + print('Best parameters {}'.format(fmin(fn=optimizer, space=space, algo=tpe.suggest, max_evals=40))) diff --git a/freqtrade/tests/testdata/download_backtest_data.py b/freqtrade/tests/testdata/download_backtest_data.py new file mode 100644 index 000000000..1119aac42 --- /dev/null +++ b/freqtrade/tests/testdata/download_backtest_data.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 + +"""This script generate json data from bittrex""" + +from urllib.request import urlopen + +CURRENCIES = ["ok", "neo", "dash", "etc", "eth", "snt"] + +for cur in CURRENCIES: + url1 = 'https://bittrex.com/Api/v2.0/pub/market/GetTicks?marketName=BTC-' + url = url1+cur+'&tickInterval=fiveMin' + x = urlopen(url) + json_data = x.read() + json_str = str(json_data, 'utf-8') + with open('btc-'+cur+'.json', 'w') as file: + file.write(json_str) diff --git a/requirements.txt b/requirements.txt index b7bf943ad..69379b025 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ -e git+https://github.com/ericsomdahl/python-bittrex.git@d7033d0#egg=python-bittrex -SQLAlchemy==1.1.13 -python-telegram-bot==8.0 +SQLAlchemy==1.1.14 +python-telegram-bot==8.1.1 arrow==0.10.0 requests==2.18.4 urllib3==1.22 @@ -11,10 +11,13 @@ scipy==0.19.1 jsonschema==2.6.0 numpy==1.13.3 TA-Lib==0.4.10 -pytest==3.2.2 +pytest==3.2.3 pytest-mock==1.6.3 pytest-cov==2.5.1 +hyperopt==0.1 +# do not upgrade networkx before this is fixed https://github.com/hyperopt/hyperopt/issues/325 +networkx==1.11 # Required for plotting data -#matplotlib==2.0.2 +#matplotlib==2.1.0 #PYQT5==5.9 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index ea59030de..000000000 --- a/setup.cfg +++ /dev/null @@ -1,5 +0,0 @@ -[aliases] -test=pytest - -[tool:pytest] -addopts = --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/ diff --git a/setup.py b/setup.py index e89fd2eee..eced2b5b3 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ setup(name='freqtrade', install_requires=[ 'python-bittrex==0.1.3', 'SQLAlchemy==1.1.13', - 'python-telegram-bot==8.0', + 'python-telegram-bot==8.1.1', 'arrow==0.10.0', 'requests==2.18.4', 'urllib3==1.22',