diff --git a/config.json.example b/config.json.example index 056b9201e..15c58e1d5 100644 --- a/config.json.example +++ b/config.json.example @@ -6,10 +6,10 @@ "minimal_roi": { "40": 0.0, "30": 0.01, - "20": 0.02 + "20": 0.02, "0": 0.04 }, - "stoploss": -0.40, + "stoploss": -0.10, "bid_strategy": { "ask_last_balance": 0.0 }, 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/analyze.py b/freqtrade/analyze.py index de880c9e6..ba57d66c5 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 @@ -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,14 +75,21 @@ 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 + def populate_sell_trend(dataframe: DataFrame) -> DataFrame: """ Based on TA indicators, populates the sell signal for the given dataframe @@ -86,9 +97,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 @@ -107,9 +128,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 @@ -119,7 +137,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 diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 98ade43a0..7b5c0c753 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -9,6 +9,7 @@ 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 @@ -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..aa74cba80 100644 --- a/freqtrade/exchange/bittrex.py +++ b/freqtrade/exchange/bittrex.py @@ -4,6 +4,7 @@ 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 import OperationalException from freqtrade.exchange.interface import Exchange 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..e337a3ed5 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') @@ -67,16 +67,13 @@ 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 ...' ) - except FreqtradeException as e: + except DependencyException as e: logger.warning('Unable to create trade: %s', e) for trade in trades: @@ -97,12 +94,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 @@ -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 ...', @@ -185,7 +184,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 +194,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: @@ -203,12 +202,11 @@ 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() + # 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 @@ -219,14 +217,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=exchange.get_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: @@ -248,10 +251,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 +269,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 +282,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 +310,32 @@ 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 KeyboardInterrupt: + logger.info('Got SIGINT, aborting ...') + except BaseException: + logger.exception('Got fatal exception!') + finally: + cleanup() if __name__ == '__main__': diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 2bd983137..409b10231 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 @@ -150,6 +146,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 +167,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/rpc/telegram.py b/freqtrade/rpc/telegram.py index e98f13fc4..2fc2c92be 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/conftest.py b/freqtrade/tests/conftest.py index 4c126b64d..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 }, @@ -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": { @@ -62,7 +63,7 @@ def backtest_conf(): "20": 0.02, "0": 0.04 }, - "stoploss": -0.05 + "stoploss": -0.10 } 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_backtesting.py b/freqtrade/tests/test_backtesting.py index 6b101d60e..2e2c195c4 100644 --- a/freqtrade/tests/test_backtesting.py +++ b/freqtrade/tests/test_backtesting.py @@ -83,24 +83,46 @@ 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'] = 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): + 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 + 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:].itertuples(index=True): + for row2 in ticker[row.Index + 1:].itertuples(index=True): + 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) @@ -110,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('') @@ -147,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_exchange.py b/freqtrade/tests/test_exchange.py index d0083af6a..78bb89bfc 100644 --- a/freqtrade/tests/test_exchange.py +++ b/freqtrade/tests/test_exchange.py @@ -3,6 +3,7 @@ from unittest.mock import MagicMock import pytest +from freqtrade import OperationalException from freqtrade.exchange import validate_pairs @@ -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..702a5d16c 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.exchange import Exchanges +from freqtrade import DependencyException, OperationalException 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, FreqtradeException +from freqtrade.misc import get_state, State from freqtrade.persistence import Trade @@ -40,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): @@ -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): @@ -114,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 @@ -132,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) @@ -141,7 +157,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 +170,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) @@ -175,13 +191,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' @@ -204,15 +221,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_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 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/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) 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 # ============================================= 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')