From 55a69e4a4599f681cd8296c13021f95c917ce8b3 Mon Sep 17 00:00:00 2001 From: gcarq Date: Mon, 20 Nov 2017 22:15:19 +0100 Subject: [PATCH 01/13] use normal program flow to handle interrupts --- freqtrade/__init__.py | 14 ++++++- freqtrade/exchange/__init__.py | 10 +++-- freqtrade/exchange/bittrex.py | 23 ++++++----- freqtrade/main.py | 69 +++++++++++++++++--------------- freqtrade/misc.py | 4 -- freqtrade/tests/test_exchange.py | 5 ++- freqtrade/tests/test_main.py | 13 +++--- 7 files changed, 78 insertions(+), 60 deletions(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 8ece07269..a190ca117 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,4 +1,16 @@ """ FreqTrade bot """ __version__ = '0.14.3' -from . import main + +class DependencyException(BaseException): + """ + Indicates that a assumed dependency is not met. + This could happen when there is currently not enough money on the account. + """ + + +class OperationalException(BaseException): + """ + Requires manual intervention. + This happens when an exchange returns an unexpected error during runtime. + """ diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 98ade43a0..a1f039820 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -11,6 +11,7 @@ from cachetools import cached, TTLCache from freqtrade.exchange.bittrex import Bittrex from freqtrade.exchange.interface import Exchange +from freqtrade import OperationalException logger = logging.getLogger(__name__) @@ -51,7 +52,7 @@ def init(config: dict) -> None: try: exchange_class = Exchanges[name.upper()].value except KeyError: - raise RuntimeError('Exchange {} is not supported'.format(name)) + raise OperationalException('Exchange {} is not supported'.format(name)) _API = exchange_class(exchange_config) @@ -62,7 +63,7 @@ def init(config: dict) -> None: def validate_pairs(pairs: List[str]) -> None: """ Checks if all given pairs are tradable on the current exchange. - Raises RuntimeError if one pair is not available. + Raises OperationalException if one pair is not available. :param pairs: list of pairs :return: None """ @@ -75,11 +76,12 @@ def validate_pairs(pairs: List[str]) -> None: stake_cur = _CONF['stake_currency'] for pair in pairs: if not pair.startswith(stake_cur): - raise RuntimeError( + raise OperationalException( 'Pair {} not compatible with stake_currency: {}'.format(pair, stake_cur) ) if pair not in markets: - raise RuntimeError('Pair {} is not available at {}'.format(pair, _API.name.lower())) + raise OperationalException( + 'Pair {} is not available at {}'.format(pair, _API.name.lower())) def buy(pair: str, rate: float, amount: float) -> str: diff --git a/freqtrade/exchange/bittrex.py b/freqtrade/exchange/bittrex.py index 37793530f..32e29184b 100644 --- a/freqtrade/exchange/bittrex.py +++ b/freqtrade/exchange/bittrex.py @@ -5,6 +5,7 @@ from bittrex.bittrex import Bittrex as _Bittrex, API_V2_0, API_V1_1 from requests.exceptions import ContentDecodingError from freqtrade.exchange.interface import Exchange +from freqtrade import OperationalException logger = logging.getLogger(__name__) @@ -46,7 +47,7 @@ class Bittrex(Exchange): def buy(self, pair: str, rate: float, amount: float) -> str: data = _API.buy_limit(pair.replace('_', '-'), amount, rate) if not data['success']: - raise RuntimeError('{message} params=({pair}, {rate}, {amount})'.format( + raise OperationalException('{message} params=({pair}, {rate}, {amount})'.format( message=data['message'], pair=pair, rate=rate, @@ -56,7 +57,7 @@ class Bittrex(Exchange): def sell(self, pair: str, rate: float, amount: float) -> str: data = _API.sell_limit(pair.replace('_', '-'), amount, rate) if not data['success']: - raise RuntimeError('{message} params=({pair}, {rate}, {amount})'.format( + raise OperationalException('{message} params=({pair}, {rate}, {amount})'.format( message=data['message'], pair=pair, rate=rate, @@ -66,7 +67,7 @@ class Bittrex(Exchange): def get_balance(self, currency: str) -> float: data = _API.get_balance(currency) if not data['success']: - raise RuntimeError('{message} params=({currency})'.format( + raise OperationalException('{message} params=({currency})'.format( message=data['message'], currency=currency)) return float(data['result']['Balance'] or 0.0) @@ -74,13 +75,13 @@ class Bittrex(Exchange): def get_balances(self): data = _API.get_balances() if not data['success']: - raise RuntimeError('{message}'.format(message=data['message'])) + raise OperationalException('{message}'.format(message=data['message'])) return data['result'] def get_ticker(self, pair: str) -> dict: data = _API.get_ticker(pair.replace('_', '-')) if not data['success']: - raise RuntimeError('{message} params=({pair})'.format( + raise OperationalException('{message} params=({pair})'.format( message=data['message'], pair=pair)) @@ -121,7 +122,7 @@ class Bittrex(Exchange): pair=pair)) if not data['success']: - raise RuntimeError('{message} params=({pair})'.format( + raise OperationalException('{message} params=({pair})'.format( message=data['message'], pair=pair)) @@ -130,7 +131,7 @@ class Bittrex(Exchange): def get_order(self, order_id: str) -> Dict: data = _API.get_order(order_id) if not data['success']: - raise RuntimeError('{message} params=({order_id})'.format( + raise OperationalException('{message} params=({order_id})'.format( message=data['message'], order_id=order_id)) data = data['result'] @@ -148,7 +149,7 @@ class Bittrex(Exchange): def cancel_order(self, order_id: str) -> None: data = _API.cancel(order_id) if not data['success']: - raise RuntimeError('{message} params=({order_id})'.format( + raise OperationalException('{message} params=({order_id})'.format( message=data['message'], order_id=order_id)) @@ -158,19 +159,19 @@ class Bittrex(Exchange): def get_markets(self) -> List[str]: data = _API.get_markets() if not data['success']: - raise RuntimeError('{message}'.format(message=data['message'])) + raise OperationalException('{message}'.format(message=data['message'])) return [m['MarketName'].replace('-', '_') for m in data['result']] def get_market_summaries(self) -> List[Dict]: data = _API.get_market_summaries() if not data['success']: - raise RuntimeError('{message}'.format(message=data['message'])) + raise OperationalException('{message}'.format(message=data['message'])) return data['result'] def get_wallet_health(self) -> List[Dict]: data = _API_V2.get_wallet_health() if not data['success']: - raise RuntimeError('{message}'.format(message=data['message'])) + raise OperationalException('{message}'.format(message=data['message'])) return [{ 'Currency': entry['Health']['Currency'], 'IsActive': entry['Health']['IsActive'], diff --git a/freqtrade/main.py b/freqtrade/main.py index dc66540cb..1c2137569 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -6,16 +6,16 @@ import sys import time import traceback from datetime import datetime -from signal import signal, SIGINT, SIGABRT, SIGTERM from typing import Dict, Optional, List import requests from cachetools import cached, TTLCache -from freqtrade import __version__, exchange, persistence, rpc +from freqtrade import __version__, exchange, persistence, rpc, DependencyException, \ + OperationalException from freqtrade.analyze import get_signal, SignalType from freqtrade.misc import State, get_state, update_state, parse_args, throttle, \ - load_config, FreqtradeException + load_config from freqtrade.persistence import Trade logger = logging.getLogger('freqtrade') @@ -76,7 +76,7 @@ def _process(dynamic_whitelist: Optional[bool] = False) -> bool: 'Checked all whitelisted currencies. ' 'Found no suitable entry positions for buying. Will keep looking ...' ) - except FreqtradeException as e: + except DependencyException as e: logger.warning('Unable to create trade: %s', e) for trade in trades: @@ -97,12 +97,12 @@ def _process(dynamic_whitelist: Optional[bool] = False) -> bool: error ) time.sleep(30) - except RuntimeError: - rpc.send_msg('*Status:* Got RuntimeError:\n```\n{traceback}```{hint}'.format( + except OperationalException: + rpc.send_msg('*Status:* Got OperationalException:\n```\n{traceback}```{hint}'.format( traceback=traceback.format_exc(), hint='Issue `/start` if you think it is safe to restart.' )) - logger.exception('Got RuntimeError. Stopping trader ...') + logger.exception('Got OperationalException. Stopping trader ...') update_state(State.STOPPED) return state_changed @@ -185,7 +185,7 @@ def create_trade(stake_amount: float) -> Optional[Trade]: whitelist = copy.deepcopy(_CONF['exchange']['pair_whitelist']) # Check if stake_amount is fulfilled if exchange.get_balance(_CONF['stake_currency']) < stake_amount: - raise FreqtradeException( + raise DependencyException( 'stake amount is not fulfilled (currency={})'.format(_CONF['stake_currency']) ) @@ -195,7 +195,7 @@ def create_trade(stake_amount: float) -> Optional[Trade]: whitelist.remove(trade.pair) logger.debug('Ignoring %s in pair whitelist', trade.pair) if not whitelist: - raise FreqtradeException('No pair in whitelist') + raise DependencyException('No pair in whitelist') # Pick pair based on StochRSI buy signals for _pair in whitelist: @@ -248,10 +248,6 @@ def init(config: dict, db_url: Optional[str] = None) -> None: else: update_state(State.STOPPED) - # Register signal handlers - for sig in (SIGINT, SIGTERM, SIGABRT): - signal(sig, cleanup) - @cached(TTLCache(maxsize=1, ttl=1800)) def gen_pair_whitelist(base_currency: str, topn: int = 20, key: str = 'BaseVolume') -> List[str]: @@ -270,7 +266,7 @@ def gen_pair_whitelist(base_currency: str, topn: int = 20, key: str = 'BaseVolum return [s['MarketName'].replace('-', '_') for s in summaries[:topn]] -def cleanup(*args, **kwargs) -> None: +def cleanup() -> None: """ Cleanup the application state und finish all pending tasks :return: None @@ -283,7 +279,7 @@ def cleanup(*args, **kwargs) -> None: exit(0) -def main(): +def main() -> None: """ Loads and validates the config and handles the main loop :return: None @@ -311,24 +307,33 @@ def main(): # Initialize all modules and start main loop if args.dynamic_whitelist: logger.info('Using dynamically generated whitelist. (--dynamic-whitelist detected)') - init(_CONF) - old_state = None - while True: - new_state = get_state() - # Log state transition - if new_state != old_state: - rpc.send_msg('*Status:* `{}`'.format(new_state.name.lower())) - logger.info('Changing state to: %s', new_state.name) - if new_state == State.STOPPED: - time.sleep(1) - elif new_state == State.RUNNING: - throttle( - _process, - min_secs=_CONF['internals'].get('process_throttle_secs', 10), - dynamic_whitelist=args.dynamic_whitelist, - ) - old_state = new_state + try: + init(_CONF) + old_state = None + while True: + new_state = get_state() + # Log state transition + if new_state != old_state: + rpc.send_msg('*Status:* `{}`'.format(new_state.name.lower())) + logger.info('Changing state to: %s', new_state.name) + + if new_state == State.STOPPED: + time.sleep(1) + elif new_state == State.RUNNING: + throttle( + _process, + min_secs=_CONF['internals'].get('process_throttle_secs', 10), + dynamic_whitelist=args.dynamic_whitelist, + ) + old_state = new_state + + except RuntimeError: + logger.exception('Got fatal exception!') + except KeyboardInterrupt: + logger.info('Got SIGINT, aborting ...') + finally: + cleanup() if __name__ == '__main__': diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 2bd983137..abd9581e2 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -15,10 +15,6 @@ from freqtrade import __version__ logger = logging.getLogger(__name__) -class FreqtradeException(BaseException): - pass - - class State(enum.Enum): RUNNING = 0 STOPPED = 1 diff --git a/freqtrade/tests/test_exchange.py b/freqtrade/tests/test_exchange.py index d0083af6a..93a394de1 100644 --- a/freqtrade/tests/test_exchange.py +++ b/freqtrade/tests/test_exchange.py @@ -4,6 +4,7 @@ from unittest.mock import MagicMock import pytest from freqtrade.exchange import validate_pairs +from freqtrade.misc import OperationalException def test_validate_pairs(default_conf, mocker): @@ -21,7 +22,7 @@ def test_validate_pairs_not_available(default_conf, mocker): api_mock.get_markets = MagicMock(return_value=[]) mocker.patch('freqtrade.exchange._API', api_mock) mocker.patch.dict('freqtrade.exchange._CONF', default_conf) - with pytest.raises(RuntimeError, match=r'not available'): + with pytest.raises(OperationalException, match=r'not available'): validate_pairs(default_conf['exchange']['pair_whitelist']) @@ -31,5 +32,5 @@ def test_validate_pairs_not_compatible(default_conf, mocker): default_conf['stake_currency'] = 'ETH' mocker.patch('freqtrade.exchange._API', api_mock) mocker.patch.dict('freqtrade.exchange._CONF', default_conf) - with pytest.raises(RuntimeError, match=r'not compatible'): + with pytest.raises(OperationalException, match=r'not compatible'): validate_pairs(default_conf['exchange']['pair_whitelist']) diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index 3de35ff46..6154c45d4 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -6,11 +6,12 @@ import pytest import requests from sqlalchemy import create_engine +from freqtrade import DependencyException, OperationalException from freqtrade.exchange import Exchanges from freqtrade.analyze import SignalType from freqtrade.main import create_trade, handle_trade, init, \ get_target_bid, _process -from freqtrade.misc import get_state, State, FreqtradeException +from freqtrade.misc import get_state, State from freqtrade.persistence import Trade @@ -59,7 +60,7 @@ def test_process_exchange_failures(default_conf, ticker, health, mocker): assert sleep_mock.has_calls() -def test_process_runtime_error(default_conf, ticker, health, mocker): +def test_process_operational_exception(default_conf, ticker, health, mocker): msg_mock = MagicMock() mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=msg_mock) @@ -68,14 +69,14 @@ def test_process_runtime_error(default_conf, ticker, health, mocker): validate_pairs=MagicMock(), get_ticker=ticker, get_wallet_health=health, - buy=MagicMock(side_effect=RuntimeError)) + buy=MagicMock(side_effect=OperationalException)) init(default_conf, create_engine('sqlite://')) assert get_state() == State.RUNNING result = _process() assert result is False assert get_state() == State.STOPPED - assert 'RuntimeError' in msg_mock.call_args_list[-1][0][0] + assert 'OperationalException' in msg_mock.call_args_list[-1][0][0] def test_process_trade_handling(default_conf, ticker, limit_buy_order, health, mocker): @@ -141,7 +142,7 @@ def test_create_trade_no_stake_amount(default_conf, ticker, mocker): get_ticker=ticker, buy=MagicMock(return_value='mocked_limit_buy'), get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5)) - with pytest.raises(FreqtradeException, match=r'.*stake amount.*'): + with pytest.raises(DependencyException, match=r'.*stake amount.*'): create_trade(default_conf['stake_amount']) @@ -154,7 +155,7 @@ def test_create_trade_no_pairs(default_conf, ticker, mocker): get_ticker=ticker, buy=MagicMock(return_value='mocked_limit_buy')) - with pytest.raises(FreqtradeException, match=r'.*No pair in whitelist.*'): + with pytest.raises(DependencyException, match=r'.*No pair in whitelist.*'): conf = copy.deepcopy(default_conf) conf['exchange']['pair_whitelist'] = [] mocker.patch.dict('freqtrade.main._CONF', conf) From 788cda4925b7e1cdf18d09242f0e733f8f6a71ef Mon Sep 17 00:00:00 2001 From: gcarq Date: Mon, 20 Nov 2017 22:26:32 +0100 Subject: [PATCH 02/13] add missing import --- freqtrade/analyze.py | 2 +- freqtrade/exchange/__init__.py | 2 +- freqtrade/exchange/bittrex.py | 2 +- freqtrade/rpc/telegram.py | 4 ++-- freqtrade/tests/test_analyze.py | 1 + freqtrade/tests/test_exchange.py | 2 +- freqtrade/tests/test_main.py | 2 +- freqtrade/tests/test_rpc.py | 3 +-- freqtrade/vendor/qtpylib/indicators.py | 8 ++++---- 9 files changed, 13 insertions(+), 13 deletions(-) diff --git a/freqtrade/analyze.py b/freqtrade/analyze.py index de880c9e6..743e8eebd 100644 --- a/freqtrade/analyze.py +++ b/freqtrade/analyze.py @@ -1,9 +1,9 @@ """ Functions to analyze ticker data with indicators and produce buy and sell signals """ -from enum import Enum import logging from datetime import timedelta +from enum import Enum import arrow import talib.abstract as ta diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index a1f039820..7b5c0c753 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -9,9 +9,9 @@ import arrow import requests from cachetools import cached, TTLCache +from freqtrade import OperationalException from freqtrade.exchange.bittrex import Bittrex from freqtrade.exchange.interface import Exchange -from freqtrade import OperationalException logger = logging.getLogger(__name__) diff --git a/freqtrade/exchange/bittrex.py b/freqtrade/exchange/bittrex.py index 32e29184b..aa74cba80 100644 --- a/freqtrade/exchange/bittrex.py +++ b/freqtrade/exchange/bittrex.py @@ -4,8 +4,8 @@ from typing import List, Dict from bittrex.bittrex import Bittrex as _Bittrex, API_V2_0, API_V1_1 from requests.exceptions import ContentDecodingError -from freqtrade.exchange.interface import Exchange from freqtrade import OperationalException +from freqtrade.exchange.interface import Exchange logger = logging.getLogger(__name__) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index cce871b70..b3bf9e025 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -2,11 +2,11 @@ import logging import re from datetime import timedelta from typing import Callable, Any -from pandas import DataFrame -from tabulate import tabulate import arrow +from pandas import DataFrame from sqlalchemy import and_, func, text +from tabulate import tabulate from telegram import ParseMode, Bot, Update from telegram.error import NetworkError, TelegramError from telegram.ext import CommandHandler, Updater diff --git a/freqtrade/tests/test_analyze.py b/freqtrade/tests/test_analyze.py index a2ff1aec2..c62639997 100644 --- a/freqtrade/tests/test_analyze.py +++ b/freqtrade/tests/test_analyze.py @@ -1,5 +1,6 @@ # pragma pylint: disable=missing-docstring,W0621 import json + import arrow import pytest from pandas import DataFrame diff --git a/freqtrade/tests/test_exchange.py b/freqtrade/tests/test_exchange.py index 93a394de1..78bb89bfc 100644 --- a/freqtrade/tests/test_exchange.py +++ b/freqtrade/tests/test_exchange.py @@ -3,8 +3,8 @@ from unittest.mock import MagicMock import pytest +from freqtrade import OperationalException from freqtrade.exchange import validate_pairs -from freqtrade.misc import OperationalException def test_validate_pairs(default_conf, mocker): diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index 6154c45d4..494e50e43 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -7,8 +7,8 @@ import requests from sqlalchemy import create_engine from freqtrade import DependencyException, OperationalException -from freqtrade.exchange import Exchanges from freqtrade.analyze import SignalType +from freqtrade.exchange import Exchanges from freqtrade.main import create_trade, handle_trade, init, \ get_target_bid, _process from freqtrade.misc import get_state, State diff --git a/freqtrade/tests/test_rpc.py b/freqtrade/tests/test_rpc.py index 2dcb1d300..9235cc674 100644 --- a/freqtrade/tests/test_rpc.py +++ b/freqtrade/tests/test_rpc.py @@ -1,7 +1,6 @@ # pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, C0103 -from unittest.mock import MagicMock - from copy import deepcopy +from unittest.mock import MagicMock from freqtrade.rpc import init, cleanup, send_msg diff --git a/freqtrade/vendor/qtpylib/indicators.py b/freqtrade/vendor/qtpylib/indicators.py index d3c1b89af..ee1f14e1f 100644 --- a/freqtrade/vendor/qtpylib/indicators.py +++ b/freqtrade/vendor/qtpylib/indicators.py @@ -19,12 +19,12 @@ # limitations under the License. # +import sys +import warnings +from datetime import datetime, timedelta + import numpy as np import pandas as pd -import warnings -import sys - -from datetime import datetime, timedelta from pandas.core.base import PandasObject # ============================================= From 383a9f6eeb54882fc2b72b4c3d530ca8b3a64d4e Mon Sep 17 00:00:00 2001 From: gcarq Date: Tue, 21 Nov 2017 20:24:52 +0100 Subject: [PATCH 03/13] catch BaseException to force stdout flush when process dies --- freqtrade/main.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index 1c2137569..602ec67b2 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -327,11 +327,10 @@ def main() -> None: dynamic_whitelist=args.dynamic_whitelist, ) old_state = new_state - - except RuntimeError: - logger.exception('Got fatal exception!') except KeyboardInterrupt: logger.info('Got SIGINT, aborting ...') + except BaseException: + logger.exception('Got fatal exception!') finally: cleanup() From 65ce948b0b8a6111194d87d229186b089729e0ec Mon Sep 17 00:00:00 2001 From: gcarq Date: Tue, 21 Nov 2017 20:37:29 +0100 Subject: [PATCH 04/13] catch ValueErrors from analyze_ticker (fixes #123) --- freqtrade/analyze.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/freqtrade/analyze.py b/freqtrade/analyze.py index de880c9e6..30de537ae 100644 --- a/freqtrade/analyze.py +++ b/freqtrade/analyze.py @@ -119,7 +119,12 @@ def get_signal(pair: str, signal: SignalType) -> bool: :param pair: pair in format BTC_ANT or BTC-ANT :return: True if pair is good for buying, False otherwise """ - dataframe = analyze_ticker(pair) + try: + dataframe = analyze_ticker(pair) + except ValueError as ex: + logger.warning('Unable to analyze ticker for pair %s: %s', pair, str(ex)) + return False + if dataframe.empty: return False From f3ba3ddd54f608b74bcd6e315aa21f719270503b Mon Sep 17 00:00:00 2001 From: gcarq Date: Tue, 21 Nov 2017 20:41:49 +0100 Subject: [PATCH 05/13] move buy_price and sell_price to plotting script --- freqtrade/analyze.py | 3 --- scripts/plot_dataframe.py | 3 +++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/analyze.py b/freqtrade/analyze.py index 30de537ae..7323357e6 100644 --- a/freqtrade/analyze.py +++ b/freqtrade/analyze.py @@ -107,9 +107,6 @@ def analyze_ticker(pair: str) -> DataFrame: dataframe = populate_indicators(dataframe) dataframe = populate_buy_trend(dataframe) dataframe = populate_sell_trend(dataframe) - # TODO: buy_price and sell_price are only used by the plotter, should probably be moved there - dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close'] - dataframe.loc[dataframe['sell'] == 1, 'sell_price'] = dataframe['close'] return dataframe diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 32d9b3cfa..eeabda007 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -18,6 +18,9 @@ def plot_analyzed_dataframe(pair: str) -> None: exchange._API = exchange.Bittrex({'key': '', 'secret': ''}) dataframe = analyze.analyze_ticker(pair) + dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close'] + dataframe.loc[dataframe['sell'] == 1, 'sell_price'] = dataframe['close'] + # Two subplots sharing x axis fig, (ax1, ax2, ax3) = plt.subplots(3, sharex=True) fig.suptitle(pair, fontsize=14, fontweight='bold') From 02ca2ed5856ef905d93a90d44cf8c358ea5310b4 Mon Sep 17 00:00:00 2001 From: gcarq Date: Tue, 21 Nov 2017 22:33:34 +0100 Subject: [PATCH 06/13] implement trade count lock for backtesting --- freqtrade/analyze.py | 1 + freqtrade/tests/conftest.py | 1 + freqtrade/tests/test_backtesting.py | 18 +++++++++++++++--- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/freqtrade/analyze.py b/freqtrade/analyze.py index 7323357e6..92cd7ead6 100644 --- a/freqtrade/analyze.py +++ b/freqtrade/analyze.py @@ -79,6 +79,7 @@ def populate_buy_trend(dataframe: DataFrame) -> DataFrame: return dataframe + def populate_sell_trend(dataframe: DataFrame) -> DataFrame: """ Based on TA indicators, populates the sell signal for the given dataframe diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 4c126b64d..44c6b9bb8 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -54,6 +54,7 @@ def default_conf(): @pytest.fixture(scope="module") def backtest_conf(): return { + "max_open_trades": 3, "stake_currency": "BTC", "stake_amount": 0.01, "minimal_roi": { diff --git a/freqtrade/tests/test_backtesting.py b/freqtrade/tests/test_backtesting.py index 6b101d60e..ed58c59dc 100644 --- a/freqtrade/tests/test_backtesting.py +++ b/freqtrade/tests/test_backtesting.py @@ -85,22 +85,32 @@ def generate_text_table(data: Dict[str, Dict], results: DataFrame, stake_currenc def backtest(backtest_conf, processed, mocker): trades = [] + trade_count_lock = {} exchange._API = Bittrex({'key': '', 'secret': ''}) mocker.patch.dict('freqtrade.main._CONF', backtest_conf) for pair, pair_data in processed.items(): - pair_data['buy'] = 0 - pair_data['sell'] = 0 + pair_data['buy'], pair_data['sell'] = 0, 0 ticker = populate_sell_trend(populate_buy_trend(pair_data)) # for each buy point for row in ticker[ticker.buy == 1].itertuples(index=True): + # Check if max_open_trades has already been reached for the given date + if not trade_count_lock.get(row.date, 0) < backtest_conf['max_open_trades']: + continue + + # Increase lock + trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1 trade = Trade( open_rate=row.close, open_date=row.date, amount=backtest_conf['stake_amount'], fee=exchange.get_fee() * 2 ) + # calculate win/lose forwards from buy point - for row2 in ticker[row.Index:].itertuples(index=True): + for row2 in ticker[row.Index + 1:].itertuples(index=True): + # Increase trade_count_lock for every iteration + trade_count_lock[row2.date] = trade_count_lock.get(row2.date, 0) + 1 + if min_roi_reached(trade, row2.close, row2.date) or row2.sell == 1: current_profit = trade.calc_profit(row2.close) @@ -140,6 +150,8 @@ def test_backtest(backtest_conf, mocker): config['stake_currency'], config['stake_amount'] )) + print('Using max_open_trades: {} ...'.format(config['max_open_trades'])) + # Print timeframe min_date, max_date = get_timeframe(data) print('Measuring data from {} up to {} ...'.format( From 9136e64d89a6e25c4ab5986451b9f4ce2330601c Mon Sep 17 00:00:00 2001 From: gcarq Date: Wed, 22 Nov 2017 20:51:25 +0100 Subject: [PATCH 07/13] force flush in create_trade and execute_sell (fixes #128) --- freqtrade/main.py | 34 ++++++++++++++------------ freqtrade/tests/test_main.py | 28 +++++++++++----------- freqtrade/tests/test_rpc_telegram.py | 36 +++++++++------------------- 3 files changed, 44 insertions(+), 54 deletions(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index 602ec67b2..acf72ab0c 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -67,11 +67,8 @@ def _process(dynamic_whitelist: Optional[bool] = False) -> bool: if len(trades) < _CONF['max_open_trades']: try: # Create entity and execute trade - trade = create_trade(float(_CONF['stake_amount'])) - if trade: - Trade.session.add(trade) - state_changed = True - else: + state_changed = create_trade(float(_CONF['stake_amount'])) + if not state_changed: logger.info( 'Checked all whitelisted currencies. ' 'Found no suitable entry positions for buying. Will keep looking ...' @@ -126,6 +123,7 @@ def execute_sell(trade: Trade, limit: float) -> None: limit, fmt_exp_profit )) + Trade.session.flush() def min_roi_reached(trade: Trade, current_rate: float, current_time: datetime) -> bool: @@ -172,11 +170,12 @@ def get_target_bid(ticker: Dict[str, float]) -> float: return ticker['ask'] + balance * (ticker['last'] - ticker['ask']) -def create_trade(stake_amount: float) -> Optional[Trade]: +def create_trade(stake_amount: float) -> bool: """ Checks the implemented trading indicator(s) for a randomly picked pair, if one pair triggers the buy_signal a new trade record gets created :param stake_amount: amount of btc to spend + :return: True if a trade object has been created and persisted, False otherwise """ logger.info( 'Checking buy signals to create a new trade with stake_amount: %f ...', @@ -203,7 +202,7 @@ def create_trade(stake_amount: float) -> Optional[Trade]: pair = _pair break else: - return None + return False # Calculate amount and subtract fee fee = exchange.get_fee() @@ -219,14 +218,19 @@ def create_trade(stake_amount: float) -> Optional[Trade]: buy_limit )) # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL - return Trade(pair=pair, - stake_amount=stake_amount, - amount=amount, - fee=fee * 2, - open_rate=buy_limit, - open_date=datetime.utcnow(), - exchange=exchange.get_name().upper(), - open_order_id=order_id) + trade = Trade( + pair=pair, + stake_amount=stake_amount, + amount=amount, + fee=fee * 2, + open_rate=buy_limit, + open_date=datetime.utcnow(), + exchange=exchange.get_name().upper(), + open_order_id=order_id + ) + Trade.session.add(trade) + Trade.session.flush() + return True def init(config: dict, db_url: Optional[str] = None) -> None: diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index 494e50e43..73fe664f0 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -115,9 +115,9 @@ def test_create_trade(default_conf, ticker, limit_buy_order, mocker): whitelist = copy.deepcopy(default_conf['exchange']['pair_whitelist']) init(default_conf, create_engine('sqlite://')) - trade = create_trade(15.0) - Trade.session.add(trade) - Trade.session.flush() + create_trade(15.0) + + trade = Trade.query.first() assert trade is not None assert trade.stake_amount == 15.0 assert trade.is_open @@ -176,13 +176,14 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker): buy=MagicMock(return_value='mocked_limit_buy'), sell=MagicMock(return_value='mocked_limit_sell')) init(default_conf, create_engine('sqlite://')) - trade = create_trade(15.0) - trade.update(limit_buy_order) - Trade.session.add(trade) - Trade.session.flush() - trade = Trade.query.filter(Trade.is_open.is_(True)).first() + create_trade(15.0) + + trade = Trade.query.first() assert trade + trade.update(limit_buy_order) + assert trade.is_open is True + handle_trade(trade) assert trade.open_order_id == 'mocked_limit_sell' @@ -205,15 +206,14 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mo # Create trade and sell it init(default_conf, create_engine('sqlite://')) - trade = create_trade(15.0) - Trade.session.add(trade) - trade.update(limit_buy_order) - trade = Trade.query.filter(Trade.is_open.is_(True)).first() + create_trade(15.0) + + trade = Trade.query.first() assert trade + trade.update(limit_buy_order) trade.update(limit_sell_order) - trade = Trade.query.filter(Trade.is_open.is_(False)).first() - assert trade + assert trade.is_open is False with pytest.raises(ValueError, match=r'.*closed trade.*'): handle_trade(trade) diff --git a/freqtrade/tests/test_rpc_telegram.py b/freqtrade/tests/test_rpc_telegram.py index ebedc5962..d8cb43966 100644 --- a/freqtrade/tests/test_rpc_telegram.py +++ b/freqtrade/tests/test_rpc_telegram.py @@ -101,11 +101,7 @@ def test_status_handle(default_conf, update, ticker, mocker): msg_mock.reset_mock() # Create some test data - trade = create_trade(15.0) - assert trade - Trade.session.add(trade) - Trade.session.flush() - + create_trade(15.0) # Trigger status while we have a fulfilled order for the open trade _status(bot=MagicMock(), update=update) @@ -141,10 +137,7 @@ def test_status_table_handle(default_conf, update, ticker, mocker): msg_mock.reset_mock() # Create some test data - trade = create_trade(15.0) - assert trade - Trade.session.add(trade) - Trade.session.flush() + create_trade(15.0) _status_table(bot=MagicMock(), update=update) @@ -177,8 +170,8 @@ def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell msg_mock.reset_mock() # Create some test data - trade = create_trade(15.0) - assert trade + create_trade(15.0) + trade = Trade.query.first() # Simulate fulfilled LIMIT_BUY order for trade trade.update(limit_buy_order) @@ -193,8 +186,6 @@ def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell trade.close_date = datetime.utcnow() trade.is_open = False - Trade.session.add(trade) - Trade.session.flush() _profit(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 @@ -216,11 +207,10 @@ def test_forcesell_handle(default_conf, update, ticker, mocker): init(default_conf, create_engine('sqlite://')) # Create some test data - trade = create_trade(15.0) - assert trade + create_trade(15.0) - Trade.session.add(trade) - Trade.session.flush() + trade = Trade.query.first() + assert trade update.message.text = '/forcesell 1' _forcesell(bot=MagicMock(), update=update) @@ -245,8 +235,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, mocker): # Create some test data for _ in range(4): - Trade.session.add(create_trade(15.0)) - Trade.session.flush() + create_trade(15.0) rpc_mock.reset_mock() update.message.text = '/forcesell all' @@ -309,7 +298,8 @@ def test_performance_handle( init(default_conf, create_engine('sqlite://')) # Create some test data - trade = create_trade(15.0) + create_trade(15.0) + trade = Trade.query.first() assert trade # Simulate fulfilled LIMIT_BUY order for trade @@ -320,8 +310,6 @@ def test_performance_handle( trade.close_date = datetime.utcnow() trade.is_open = False - Trade.session.add(trade) - Trade.session.flush() _performance(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 @@ -351,9 +339,7 @@ def test_count_handle(default_conf, update, ticker, mocker): update_state(State.RUNNING) # Create some test data - Trade.session.add(create_trade(15.0)) - Trade.session.flush() - + create_trade(15.0) msg_mock.reset_mock() _count(bot=MagicMock(), update=update) From 9a87dcf0a1753616fd53b7271343360ddf175918 Mon Sep 17 00:00:00 2001 From: gcarq Date: Wed, 22 Nov 2017 21:01:44 +0100 Subject: [PATCH 08/13] dont apply fees on trade creation --- freqtrade/main.py | 7 +++---- freqtrade/tests/test_main.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index acf72ab0c..e337a3ed5 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -204,10 +204,9 @@ def create_trade(stake_amount: float) -> bool: else: return False - # Calculate amount and subtract fee - fee = exchange.get_fee() + # Calculate amount buy_limit = get_target_bid(exchange.get_ticker(pair)) - amount = (1 - fee) * stake_amount / buy_limit + amount = stake_amount / buy_limit order_id = exchange.buy(pair, buy_limit, amount) # Create trade entity and return @@ -222,7 +221,7 @@ def create_trade(stake_amount: float) -> bool: pair=pair, stake_amount=stake_amount, amount=amount, - fee=fee * 2, + fee=exchange.get_fee() * 2, open_rate=buy_limit, open_date=datetime.utcnow(), exchange=exchange.get_name().upper(), diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index 73fe664f0..7e03b7512 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -41,7 +41,7 @@ def test_process_trade_creation(default_conf, ticker, health, mocker): assert trade.open_date is not None assert trade.exchange == Exchanges.BITTREX.name assert trade.open_rate == 0.072661 - assert trade.amount == 0.6864067381401302 + assert trade.amount == 0.6881270557795791 def test_process_exchange_failures(default_conf, ticker, health, mocker): From 7727f2cc8ff93e458f8e46378c4147a8c2a2b34c Mon Sep 17 00:00:00 2001 From: gcarq Date: Wed, 22 Nov 2017 21:02:36 +0100 Subject: [PATCH 09/13] implement test --- freqtrade/tests/test_main.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index 7e03b7512..702a5d16c 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -133,6 +133,21 @@ def test_create_trade(default_conf, ticker, limit_buy_order, mocker): assert whitelist == default_conf['exchange']['pair_whitelist'] +def test_create_trade_minimal_amount(default_conf, ticker, mocker): + mocker.patch.dict('freqtrade.main._CONF', default_conf) + mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock()) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) + buy_mock = mocker.patch('freqtrade.main.exchange.buy', MagicMock(return_value='mocked_limit_buy')) + mocker.patch.multiple('freqtrade.main.exchange', + validate_pairs=MagicMock(), + get_ticker=ticker) + init(default_conf, create_engine('sqlite://')) + min_stake_amount = 0.0005 + create_trade(min_stake_amount) + rate, amount = buy_mock.call_args[0][1], buy_mock.call_args[0][2] + assert rate * amount >= min_stake_amount + + def test_create_trade_no_stake_amount(default_conf, ticker, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) From 4a707d74524b5f6dc72005951d893fefede68492 Mon Sep 17 00:00:00 2001 From: gcarq Date: Thu, 23 Nov 2017 00:25:06 +0100 Subject: [PATCH 10/13] add --limit-max-trades --- freqtrade/misc.py | 7 +++++ freqtrade/tests/test_backtesting.py | 47 +++++++++++++++++++---------- freqtrade/tests/test_misc.py | 2 ++ 3 files changed, 40 insertions(+), 16 deletions(-) diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 2bd983137..0140c5c16 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -150,6 +150,12 @@ def build_subcommands(parser: argparse.ArgumentParser) -> None: type=int, metavar='INT', ) + backtest.add_argument( + '--limit-max-trades', + help='uses max_open_trades from config to simulate real world limitations', + action='store_true', + dest='limit_max_trades', + ) def start_backtesting(args) -> None: @@ -165,6 +171,7 @@ def start_backtesting(args) -> None: 'BACKTEST_LIVE': 'true' if args.live else '', 'BACKTEST_CONFIG': args.config, 'BACKTEST_TICKER_INTERVAL': str(args.ticker_interval), + 'BACKTEST_LIMIT_MAX_TRADES': 'true' if args.limit_max_trades else '', }) path = os.path.join(os.path.dirname(__file__), 'tests', 'test_backtesting.py') pytest.main(['-s', path]) diff --git a/freqtrade/tests/test_backtesting.py b/freqtrade/tests/test_backtesting.py index ed58c59dc..2e2c195c4 100644 --- a/freqtrade/tests/test_backtesting.py +++ b/freqtrade/tests/test_backtesting.py @@ -83,33 +83,45 @@ def generate_text_table(data: Dict[str, Dict], results: DataFrame, stake_currenc return tabulate(tabular_data, headers=headers) -def backtest(backtest_conf, processed, mocker): +def backtest(config: Dict, processed, mocker, max_open_trades=0): + """ + Implements backtesting functionality + :param config: config to use + :param processed: a processed dictionary with format {pair, data} + :param mocker: mocker instance + :param max_open_trades: maximum number of concurrent trades (default: 0, disabled) + :return: DataFrame + """ trades = [] trade_count_lock = {} exchange._API = Bittrex({'key': '', 'secret': ''}) - mocker.patch.dict('freqtrade.main._CONF', backtest_conf) + mocker.patch.dict('freqtrade.main._CONF', config) for pair, pair_data in processed.items(): pair_data['buy'], pair_data['sell'] = 0, 0 ticker = populate_sell_trend(populate_buy_trend(pair_data)) # for each buy point for row in ticker[ticker.buy == 1].itertuples(index=True): - # Check if max_open_trades has already been reached for the given date - if not trade_count_lock.get(row.date, 0) < backtest_conf['max_open_trades']: - continue + if max_open_trades > 0: + # Check if max_open_trades has already been reached for the given date + if not trade_count_lock.get(row.date, 0) < max_open_trades: + continue + + if max_open_trades > 0: + # Increase lock + trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1 - # Increase lock - trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1 trade = Trade( open_rate=row.close, open_date=row.date, - amount=backtest_conf['stake_amount'], + amount=config['stake_amount'], fee=exchange.get_fee() * 2 ) # calculate win/lose forwards from buy point for row2 in ticker[row.Index + 1:].itertuples(index=True): - # Increase trade_count_lock for every iteration - trade_count_lock[row2.date] = trade_count_lock.get(row2.date, 0) + 1 + if max_open_trades > 0: + # Increase trade_count_lock for every iteration + trade_count_lock[row2.date] = trade_count_lock.get(row2.date, 0) + 1 if min_roi_reached(trade, row2.close, row2.date) or row2.sell == 1: current_profit = trade.calc_profit(row2.close) @@ -120,6 +132,13 @@ def backtest(backtest_conf, processed, mocker): return DataFrame.from_records(trades, columns=labels) +def get_max_open_trades(config): + if not os.environ.get('BACKTEST_LIMIT_MAX_TRADES'): + return 0 + print('Using max_open_trades: {} ...'.format(config['max_open_trades'])) + return config['max_open_trades'] + + @pytest.mark.skipif(not os.environ.get('BACKTEST'), reason="BACKTEST not set") def test_backtest(backtest_conf, mocker): print('') @@ -150,8 +169,6 @@ def test_backtest(backtest_conf, mocker): config['stake_currency'], config['stake_amount'] )) - print('Using max_open_trades: {} ...'.format(config['max_open_trades'])) - # Print timeframe min_date, max_date = get_timeframe(data) print('Measuring data from {} up to {} ...'.format( @@ -159,8 +176,6 @@ def test_backtest(backtest_conf, mocker): )) # Execute backtest and print results - results = backtest(config, preprocess(data), mocker) - print('====================== BACKTESTING REPORT ======================================\n\n' - 'NOTE: This Report doesn\'t respect the limits of max_open_trades, \n' - ' so the projected values should be taken with a grain of salt.\n') + results = backtest(config, preprocess(data), mocker, get_max_open_trades(config)) + print('====================== BACKTESTING REPORT ======================================\n\n') print(generate_text_table(data, results, config['stake_currency'])) diff --git a/freqtrade/tests/test_misc.py b/freqtrade/tests/test_misc.py index 78507f226..8d373c1d7 100644 --- a/freqtrade/tests/test_misc.py +++ b/freqtrade/tests/test_misc.py @@ -109,6 +109,7 @@ def test_start_backtesting(mocker): live=True, loglevel=20, ticker_interval=1, + limit_max_trades=True, ) start_backtesting(args) assert env_mock == { @@ -116,6 +117,7 @@ def test_start_backtesting(mocker): 'BACKTEST_LIVE': 'true', 'BACKTEST_CONFIG': 'config.json', 'BACKTEST_TICKER_INTERVAL': '1', + 'BACKTEST_LIMIT_MAX_TRADES': 'true', } assert pytest_mock.call_count == 1 From 5fce2c57129c0373fafb649f19a0bb2e70a20762 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Thu, 23 Nov 2017 22:33:41 +0800 Subject: [PATCH 11/13] Better buy and sell strategy: Buy if at the low end of normal range and the price is increasing. Buy into extreme gains regardless of if it's on the low part of the range. Avoid buying when the price is on a long decrease even if it's low. Sell anytime the price is above the top end of normal range and the momentum slows. Sell on an extreme drop. --- freqtrade/analyze.py | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/freqtrade/analyze.py b/freqtrade/analyze.py index e95e27333..a38667b61 100644 --- a/freqtrade/analyze.py +++ b/freqtrade/analyze.py @@ -61,6 +61,10 @@ def populate_indicators(dataframe: DataFrame) -> DataFrame: hilbert = ta.HT_SINE(dataframe) dataframe['htsine'] = hilbert['sine'] dataframe['htleadsine'] = hilbert['leadsine'] + dataframe['plus_dm'] = ta.PLUS_DM(dataframe) + dataframe['plus_di'] = ta.PLUS_DI(dataframe) + dataframe['minus_dm'] = ta.MINUS_DM(dataframe) + dataframe['minus_di'] = ta.MINUS_DI(dataframe) return dataframe @@ -71,10 +75,16 @@ def populate_buy_trend(dataframe: DataFrame) -> DataFrame: :return: DataFrame with buy column """ dataframe.loc[ - (dataframe['tema'] <= dataframe['blower']) & - (dataframe['rsi'] < 37) & - (dataframe['fastd'] < 48) & - (dataframe['adx'] > 31), + ( + (dataframe['rsi'] < 35) & + (dataframe['fastd'] < 35) & + (dataframe['adx'] > 30) & + (dataframe['plus_di'] > 0.5) + ) | + ( + (dataframe['adx'] > 65) & + (dataframe['plus_di'] > 0.5) + ), 'buy'] = 1 return dataframe @@ -86,9 +96,19 @@ def populate_sell_trend(dataframe: DataFrame) -> DataFrame: :return: DataFrame with buy column """ dataframe.loc[ - (crossed_above(dataframe['rsi'], 70)), + ( + ( + (crossed_above(dataframe['rsi'], 70)) | + (crossed_above(dataframe['fastd'], 70)) + ) & + (dataframe['adx'] > 10) & + (dataframe['minus_di'] > 0) + ) | + ( + (dataframe['adx'] > 70) & + (dataframe['minus_di'] > 0.5) + ), 'sell'] = 1 - return dataframe From 84b105c82b5a05351fbc60d9143449885ae1ea6c Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Thu, 23 Nov 2017 18:41:25 +0200 Subject: [PATCH 12/13] fix invalid json in example config --- config.json.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.json.example b/config.json.example index 056b9201e..ade830c43 100644 --- a/config.json.example +++ b/config.json.example @@ -6,7 +6,7 @@ "minimal_roi": { "40": 0.0, "30": 0.01, - "20": 0.02 + "20": 0.02, "0": 0.04 }, "stoploss": -0.40, From 371e6d99c9d580944c9135a35b27810a55bfd02b Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Thu, 23 Nov 2017 18:43:19 +0200 Subject: [PATCH 13/13] set stoploss to -10% --- config.json.example | 2 +- freqtrade/tests/conftest.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config.json.example b/config.json.example index ade830c43..15c58e1d5 100644 --- a/config.json.example +++ b/config.json.example @@ -9,7 +9,7 @@ "20": 0.02, "0": 0.04 }, - "stoploss": -0.40, + "stoploss": -0.10, "bid_strategy": { "ask_last_balance": 0.0 }, diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 44c6b9bb8..e624e96c7 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -23,7 +23,7 @@ def default_conf(): "20": 0.02, "0": 0.04 }, - "stoploss": -0.05, + "stoploss": -0.10, "bid_strategy": { "ask_last_balance": 0.0 }, @@ -63,7 +63,7 @@ def backtest_conf(): "20": 0.02, "0": 0.04 }, - "stoploss": -0.05 + "stoploss": -0.10 }