Merge branch 'release/0.14.3'

This commit is contained in:
gcarq 2017-11-20 20:01:18 +01:00
commit e9dbdc9247
52 changed files with 784 additions and 267 deletions

View File

@ -1,3 +1,10 @@
[MASTER]
extension-pkg-whitelist=numpy,talib
[BASIC] [BASIC]
good-names=logger good-names=logger
ignore=vendor ignore=vendor
[TYPECHECK]
ignored-modules=numpy,talib

View File

@ -137,6 +137,43 @@ $ docker start freqtrade
You do not need to rebuild the image for configuration You do not need to rebuild the image for configuration
changes, it will suffice to edit `config.json` and restart the container. changes, it will suffice to edit `config.json` and restart the container.
### Usage
```
usage: freqtrade [-h] [-c PATH] [-v] [--version] [--dynamic-whitelist]
{backtesting} ...
Simple High Frequency Trading Bot for crypto currencies
positional arguments:
{backtesting}
backtesting backtesting module
optional arguments:
-h, --help show this help message and exit
-c PATH, --config PATH
specify configuration file (default: config.json)
-v, --verbose be verbose
--version show program's version number and exit
--dynamic-whitelist dynamically generate and update whitelist based on 24h
BaseVolume
```
### Backtesting
Backtesting also uses the config specified via `-c/--config`.
```
usage: freqtrade backtesting [-h] [-l] [-i INT]
optional arguments:
-h, --help show this help message and exit
-l, --live using live data
-i INT, --ticker-interval INT
specify ticker interval in minutes (default: 5)
```
### Execute tests ### Execute tests
``` ```

View File

@ -1,3 +1,4 @@
__version__ = '0.14.2' """ FreqTrade bot """
__version__ = '0.14.3'
from . import main from . import main

View File

@ -1,3 +1,7 @@
"""
Functions to analyze ticker data with indicators and produce buy and sell signals
"""
from enum import Enum
import logging import logging
from datetime import timedelta from datetime import timedelta
@ -6,10 +10,15 @@ import talib.abstract as ta
from pandas import DataFrame, to_datetime from pandas import DataFrame, to_datetime
from freqtrade.exchange import get_ticker_history from freqtrade.exchange import get_ticker_history
from freqtrade.vendor.qtpylib.indicators import awesome_oscillator from freqtrade.vendor.qtpylib.indicators import awesome_oscillator, crossed_above
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class SignalType(Enum):
""" Enum to distinguish between buy and sell signals """
BUY = "buy"
SELL = "sell"
def parse_ticker_dataframe(ticker: list) -> DataFrame: def parse_ticker_dataframe(ticker: list) -> DataFrame:
""" """
@ -57,18 +66,28 @@ def populate_indicators(dataframe: DataFrame) -> DataFrame:
def populate_buy_trend(dataframe: DataFrame) -> DataFrame: def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
""" """
Based on TA indicators, populates the buy trend for the given dataframe Based on TA indicators, populates the buy signal for the given dataframe
:param dataframe: DataFrame :param dataframe: DataFrame
:return: DataFrame with buy column :return: DataFrame with buy column
""" """
dataframe.ix[ dataframe.loc[
(dataframe['close'] < dataframe['sma']) &
(dataframe['tema'] <= dataframe['blower']) & (dataframe['tema'] <= dataframe['blower']) &
(dataframe['mfi'] < 25) & (dataframe['rsi'] < 37) &
(dataframe['fastd'] < 25) & (dataframe['fastd'] < 48) &
(dataframe['adx'] > 30), (dataframe['adx'] > 31),
'buy'] = 1 'buy'] = 1
dataframe.ix[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
return dataframe
def populate_sell_trend(dataframe: DataFrame) -> DataFrame:
"""
Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame
:return: DataFrame with buy column
"""
dataframe.loc[
(crossed_above(dataframe['rsi'], 70)),
'sell'] = 1
return dataframe return dataframe
@ -87,12 +106,16 @@ def analyze_ticker(pair: str) -> DataFrame:
dataframe = parse_ticker_dataframe(ticker_hist) dataframe = parse_ticker_dataframe(ticker_hist)
dataframe = populate_indicators(dataframe) dataframe = populate_indicators(dataframe)
dataframe = populate_buy_trend(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 return dataframe
def get_buy_signal(pair: str) -> bool: def get_signal(pair: str, signal: SignalType) -> bool:
""" """
Calculates a buy signal based several technical analysis indicators Calculates current signal based several technical analysis indicators
:param pair: pair in format BTC_ANT or BTC-ANT :param pair: pair in format BTC_ANT or BTC-ANT
:return: True if pair is good for buying, False otherwise :return: True if pair is good for buying, False otherwise
""" """
@ -107,6 +130,6 @@ def get_buy_signal(pair: str) -> bool:
if signal_date < arrow.now() - timedelta(minutes=10): if signal_date < arrow.now() - timedelta(minutes=10):
return False return False
signal = latest['buy'] == 1 result = latest[signal.value] == 1
logger.debug('buy_trigger: %s (pair=%s, signal=%s)', latest['date'], pair, signal) logger.debug('%s_trigger: %s (pair=%s, signal=%s)', signal.value, latest['date'], pair, result)
return signal return result

View File

@ -1,9 +1,12 @@
# pragma pylint: disable=W0603
""" Cryptocurrency Exchanges support """
import enum import enum
import logging import logging
from random import randint from random import randint
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
import arrow import arrow
import requests
from cachetools import cached, TTLCache from cachetools import cached, TTLCache
from freqtrade.exchange.bittrex import Bittrex from freqtrade.exchange.bittrex import Bittrex
@ -63,7 +66,12 @@ def validate_pairs(pairs: List[str]) -> None:
:param pairs: list of pairs :param pairs: list of pairs
:return: None :return: None
""" """
markets = _API.get_markets() try:
markets = _API.get_markets()
except requests.exceptions.RequestException as e:
logger.warning('Unable to validate pairs (assuming they are correct). Reason: %s', e)
return
stake_cur = _CONF['stake_currency'] stake_cur = _CONF['stake_currency']
for pair in pairs: for pair in pairs:
if not pair.startswith(stake_cur): if not pair.startswith(stake_cur):
@ -77,7 +85,7 @@ def validate_pairs(pairs: List[str]) -> None:
def buy(pair: str, rate: float, amount: float) -> str: def buy(pair: str, rate: float, amount: float) -> str:
if _CONF['dry_run']: if _CONF['dry_run']:
global _DRY_RUN_OPEN_ORDERS global _DRY_RUN_OPEN_ORDERS
order_id = 'dry_run_buy_{}'.format(randint(0, 1e6)) order_id = 'dry_run_buy_{}'.format(randint(0, 10**6))
_DRY_RUN_OPEN_ORDERS[order_id] = { _DRY_RUN_OPEN_ORDERS[order_id] = {
'pair': pair, 'pair': pair,
'rate': rate, 'rate': rate,
@ -95,7 +103,7 @@ def buy(pair: str, rate: float, amount: float) -> str:
def sell(pair: str, rate: float, amount: float) -> str: def sell(pair: str, rate: float, amount: float) -> str:
if _CONF['dry_run']: if _CONF['dry_run']:
global _DRY_RUN_OPEN_ORDERS global _DRY_RUN_OPEN_ORDERS
order_id = 'dry_run_sell_{}'.format(randint(0, 1e6)) order_id = 'dry_run_sell_{}'.format(randint(0, 10**6))
_DRY_RUN_OPEN_ORDERS[order_id] = { _DRY_RUN_OPEN_ORDERS[order_id] = {
'pair': pair, 'pair': pair,
'rate': rate, 'rate': rate,

View File

@ -2,6 +2,7 @@ import logging
from typing import List, Dict from typing import List, Dict
from bittrex.bittrex import Bittrex as _Bittrex, API_V2_0, API_V1_1 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.exchange.interface import Exchange
@ -82,9 +83,13 @@ class Bittrex(Exchange):
raise RuntimeError('{message} params=({pair})'.format( raise RuntimeError('{message} params=({pair})'.format(
message=data['message'], message=data['message'],
pair=pair)) pair=pair))
if not data['result']['Bid'] or not data['result']['Ask'] or not data['result']['Last']:
raise RuntimeError('{message} params=({pair})'.format( if not data.get('result') \
message=data['message'], or not data['result'].get('Bid') \
or not data['result'].get('Ask') \
or not data['result'].get('Last'):
raise ContentDecodingError('{message} params=({pair})'.format(
message='Got invalid response from bittrex',
pair=pair)) pair=pair))
return { return {
'bid': float(data['result']['Bid']), 'bid': float(data['result']['Bid']),
@ -104,13 +109,16 @@ class Bittrex(Exchange):
# These sanity check are necessary because bittrex cannot keep their API stable. # These sanity check are necessary because bittrex cannot keep their API stable.
if not data.get('result'): if not data.get('result'):
return [] raise ContentDecodingError('{message} params=({pair})'.format(
message='Got invalid response from bittrex',
pair=pair))
for prop in ['C', 'V', 'O', 'H', 'L', 'T']: for prop in ['C', 'V', 'O', 'H', 'L', 'T']:
for tick in data['result']: for tick in data['result']:
if prop not in tick.keys(): if prop not in tick.keys():
logger.warning('Required property %s not present in response', prop) raise ContentDecodingError('{message} params=({pair})'.format(
return [] message='Required property {} not present in response'.format(prop),
pair=pair))
if not data['success']: if not data['success']:
raise RuntimeError('{message} params=({pair})'.format( raise RuntimeError('{message} params=({pair})'.format(

View File

@ -2,6 +2,7 @@
import copy import copy
import json import json
import logging import logging
import sys
import time import time
import traceback import traceback
from datetime import datetime from datetime import datetime
@ -10,13 +11,12 @@ from typing import Dict, Optional, List
import requests import requests
from cachetools import cached, TTLCache from cachetools import cached, TTLCache
from jsonschema import validate
from freqtrade import __version__, exchange, persistence from freqtrade import __version__, exchange, persistence, rpc
from freqtrade.analyze import get_buy_signal from freqtrade.analyze import get_signal, SignalType
from freqtrade.misc import CONF_SCHEMA, State, get_state, update_state, build_arg_parser, throttle from freqtrade.misc import State, get_state, update_state, parse_args, throttle, \
load_config, FreqtradeException
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.rpc import telegram
logger = logging.getLogger('freqtrade') logger = logging.getLogger('freqtrade')
@ -76,8 +76,8 @@ def _process(dynamic_whitelist: Optional[bool] = False) -> bool:
'Checked all whitelisted currencies. ' 'Checked all whitelisted currencies. '
'Found no suitable entry positions for buying. Will keep looking ...' 'Found no suitable entry positions for buying. Will keep looking ...'
) )
except ValueError: except FreqtradeException as e:
logger.exception('Unable to create trade') logger.warning('Unable to create trade: %s', e)
for trade in trades: for trade in trades:
# Get order details for actual price per unit # Get order details for actual price per unit
@ -86,17 +86,19 @@ def _process(dynamic_whitelist: Optional[bool] = False) -> bool:
logger.info('Got open order for %s', trade) logger.info('Got open order for %s', trade)
trade.update(exchange.get_order(trade.open_order_id)) trade.update(exchange.get_order(trade.open_order_id))
if not close_trade_if_fulfilled(trade): if trade.is_open and trade.open_order_id is None:
# Check if we can sell our current pair # Check if we can sell our current pair
state_changed = handle_trade(trade) or state_changed state_changed = handle_trade(trade) or state_changed
Trade.session.flush() Trade.session.flush()
except (requests.exceptions.RequestException, json.JSONDecodeError) as error: except (requests.exceptions.RequestException, json.JSONDecodeError) as error:
msg = 'Got {} in _process(), retrying in 30 seconds...'.format(error.__class__.__name__) logger.warning(
logger.exception(msg) 'Got %s in _process(), retrying in 30 seconds...',
error
)
time.sleep(30) time.sleep(30)
except RuntimeError: except RuntimeError:
telegram.send_msg('*Status:* Got RuntimeError:\n```\n{traceback}```{hint}'.format( rpc.send_msg('*Status:* Got RuntimeError:\n```\n{traceback}```{hint}'.format(
traceback=traceback.format_exc(), traceback=traceback.format_exc(),
hint='Issue `/start` if you think it is safe to restart.' hint='Issue `/start` if you think it is safe to restart.'
)) ))
@ -105,27 +107,6 @@ def _process(dynamic_whitelist: Optional[bool] = False) -> bool:
return state_changed return state_changed
def close_trade_if_fulfilled(trade: Trade) -> bool:
"""
Checks if the trade is closable, and if so it is being closed.
:param trade: Trade
:return: True if trade has been closed else False
"""
# If we don't have an open order and the close rate is already set,
# we can close this trade.
if trade.close_profit is not None \
and trade.close_date is not None \
and trade.close_rate is not None \
and trade.open_order_id is None:
trade.is_open = False
logger.info(
'Marking %s as closed as the trade is fulfilled and found no open orders for it.',
trade
)
return True
return False
def execute_sell(trade: Trade, limit: float) -> None: def execute_sell(trade: Trade, limit: float) -> None:
""" """
Executes a limit sell for the given trade and limit Executes a limit sell for the given trade and limit
@ -138,20 +119,18 @@ def execute_sell(trade: Trade, limit: float) -> None:
trade.open_order_id = order_id trade.open_order_id = order_id
fmt_exp_profit = round(trade.calc_profit(limit) * 100, 2) fmt_exp_profit = round(trade.calc_profit(limit) * 100, 2)
message = '*{}:* Selling [{}]({}) with limit `{:.8f} (profit: ~{:.2f}%)`'.format( rpc.send_msg('*{}:* Selling [{}]({}) with limit `{:.8f} (profit: ~{:.2f}%)`'.format(
trade.exchange, trade.exchange,
trade.pair.replace('_', '/'), trade.pair.replace('_', '/'),
exchange.get_pair_detail_url(trade.pair), exchange.get_pair_detail_url(trade.pair),
limit, limit,
fmt_exp_profit fmt_exp_profit
) ))
logger.info(message)
telegram.send_msg(message)
def should_sell(trade: Trade, current_rate: float, current_time: datetime) -> bool: def min_roi_reached(trade: Trade, current_rate: float, current_time: datetime) -> bool:
""" """
Based an earlier trade and current price and configuration, decides whether bot should sell Based an earlier trade and current price and ROI configuration, decides whether bot should sell
:return True if bot should sell at current rate :return True if bot should sell at current rate
""" """
current_profit = trade.calc_profit(current_rate) current_profit = trade.calc_profit(current_rate)
@ -159,9 +138,9 @@ def should_sell(trade: Trade, current_rate: float, current_time: datetime) -> bo
logger.debug('Stop loss hit.') logger.debug('Stop loss hit.')
return True return True
# Check if time matches and current rate is above threshold
time_diff = (current_time - trade.open_date).total_seconds() / 60
for duration, threshold in sorted(_CONF['minimal_roi'].items()): for duration, threshold in sorted(_CONF['minimal_roi'].items()):
# Check if time matches and current rate is above threshold
time_diff = (current_time - trade.open_date).total_seconds() / 60
if time_diff > float(duration) and current_profit > threshold: if time_diff > float(duration) and current_profit > threshold:
return True return True
@ -179,7 +158,7 @@ def handle_trade(trade: Trade) -> bool:
logger.debug('Handling %s ...', trade) logger.debug('Handling %s ...', trade)
current_rate = exchange.get_ticker(trade.pair)['bid'] current_rate = exchange.get_ticker(trade.pair)['bid']
if should_sell(trade, current_rate, datetime.utcnow()): if min_roi_reached(trade, current_rate, datetime.utcnow()) or get_signal(trade.pair, SignalType.SELL):
execute_sell(trade, current_rate) execute_sell(trade, current_rate)
return True return True
return False return False
@ -206,7 +185,7 @@ def create_trade(stake_amount: float) -> Optional[Trade]:
whitelist = copy.deepcopy(_CONF['exchange']['pair_whitelist']) whitelist = copy.deepcopy(_CONF['exchange']['pair_whitelist'])
# Check if stake_amount is fulfilled # Check if stake_amount is fulfilled
if exchange.get_balance(_CONF['stake_currency']) < stake_amount: if exchange.get_balance(_CONF['stake_currency']) < stake_amount:
raise ValueError( raise FreqtradeException(
'stake amount is not fulfilled (currency={})'.format(_CONF['stake_currency']) 'stake amount is not fulfilled (currency={})'.format(_CONF['stake_currency'])
) )
@ -216,11 +195,11 @@ def create_trade(stake_amount: float) -> Optional[Trade]:
whitelist.remove(trade.pair) whitelist.remove(trade.pair)
logger.debug('Ignoring %s in pair whitelist', trade.pair) logger.debug('Ignoring %s in pair whitelist', trade.pair)
if not whitelist: if not whitelist:
raise ValueError('No pair in whitelist') raise FreqtradeException('No pair in whitelist')
# Pick pair based on StochRSI buy signals # Pick pair based on StochRSI buy signals
for _pair in whitelist: for _pair in whitelist:
if get_buy_signal(_pair): if get_signal(_pair, SignalType.BUY):
pair = _pair pair = _pair
break break
else: else:
@ -233,14 +212,12 @@ def create_trade(stake_amount: float) -> Optional[Trade]:
order_id = exchange.buy(pair, buy_limit, amount) order_id = exchange.buy(pair, buy_limit, amount)
# Create trade entity and return # Create trade entity and return
message = '*{}:* Buying [{}]({}) with limit `{:.8f}`'.format( rpc.send_msg('*{}:* Buying [{}]({}) with limit `{:.8f}`'.format(
exchange.get_name().upper(), exchange.get_name().upper(),
pair.replace('_', '/'), pair.replace('_', '/'),
exchange.get_pair_detail_url(pair), exchange.get_pair_detail_url(pair),
buy_limit buy_limit
) ))
logger.info(message)
telegram.send_msg(message)
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
return Trade(pair=pair, return Trade(pair=pair,
stake_amount=stake_amount, stake_amount=stake_amount,
@ -260,7 +237,7 @@ def init(config: dict, db_url: Optional[str] = None) -> None:
:return: None :return: None
""" """
# Initialize all modules # Initialize all modules
telegram.init(config) rpc.init(config)
persistence.init(config, db_url) persistence.init(config, db_url)
exchange.init(config) exchange.init(config)
@ -298,11 +275,11 @@ def cleanup(*args, **kwargs) -> None:
Cleanup the application state und finish all pending tasks Cleanup the application state und finish all pending tasks
:return: None :return: None
""" """
telegram.send_msg('*Status:* `Stopping trader...`') rpc.send_msg('*Status:* `Stopping trader...`')
logger.info('Stopping trader and cleaning up modules...') logger.info('Stopping trader and cleaning up modules...')
update_state(State.STOPPED) update_state(State.STOPPED)
persistence.cleanup() persistence.cleanup()
telegram.cleanup() rpc.cleanup()
exit(0) exit(0)
@ -312,7 +289,9 @@ def main():
:return: None :return: None
""" """
global _CONF global _CONF
args = build_arg_parser().parse_args() args = parse_args(sys.argv[1:])
if not args:
exit(0)
# Initialize logger # Initialize logger
logging.basicConfig( logging.basicConfig(
@ -327,12 +306,7 @@ def main():
) )
# Load and validate configuration # Load and validate configuration
with open(args.config) as file: _CONF = load_config(args.config)
_CONF = json.load(file)
if 'internals' not in _CONF:
_CONF['internals'] = {}
logger.info('Validating configuration ...')
validate(_CONF, CONF_SCHEMA)
# Initialize all modules and start main loop # Initialize all modules and start main loop
if args.dynamic_whitelist: if args.dynamic_whitelist:
@ -343,7 +317,7 @@ def main():
new_state = get_state() new_state = get_state()
# Log state transition # Log state transition
if new_state != old_state: if new_state != old_state:
telegram.send_msg('*Status:* `{}`'.format(new_state.name.lower())) rpc.send_msg('*Status:* `{}`'.format(new_state.name.lower()))
logger.info('Changing state to: %s', new_state.name) logger.info('Changing state to: %s', new_state.name)
if new_state == State.STOPPED: if new_state == State.STOPPED:

View File

@ -1,9 +1,13 @@
import argparse import argparse
import enum import enum
import json
import logging import logging
from typing import Any, Callable import os
import time import time
from typing import Any, Callable, List, Dict
from jsonschema import validate, Draft4Validator
from jsonschema.exceptions import best_match, ValidationError
from wrapt import synchronized from wrapt import synchronized
from freqtrade import __version__ from freqtrade import __version__
@ -11,6 +15,10 @@ from freqtrade import __version__
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class FreqtradeException(BaseException):
pass
class State(enum.Enum): class State(enum.Enum):
RUNNING = 0 RUNNING = 0
STOPPED = 1 STOPPED = 1
@ -40,6 +48,27 @@ def get_state() -> State:
return _STATE return _STATE
def load_config(path: str) -> Dict:
"""
Loads a config file from the given path
:param path: path as str
:return: configuration as dictionary
"""
with open(path) as file:
conf = json.load(file)
if 'internals' not in conf:
conf['internals'] = {}
logger.info('Validating configuration ...')
try:
validate(conf, CONF_SCHEMA)
return conf
except ValidationError:
logger.fatal('Configuration is not valid! See config.json.example')
raise ValidationError(
best_match(Draft4Validator(CONF_SCHEMA).iter_errors(conf)).message
)
def throttle(func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any: def throttle(func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any:
""" """
Throttles the given callable that it Throttles the given callable that it
@ -57,8 +86,11 @@ def throttle(func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any:
return result return result
def build_arg_parser() -> argparse.ArgumentParser: def parse_args(args: List[str]):
""" Builds and returns an ArgumentParser instance """ """
Parses given arguments and returns an argparse Namespace instance.
Returns None if a sub command has been selected and executed.
"""
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description='Simple High Frequency Trading Bot for crypto currencies' description='Simple High Frequency Trading Bot for crypto currencies'
) )
@ -88,7 +120,54 @@ def build_arg_parser() -> argparse.ArgumentParser:
help='dynamically generate and update whitelist based on 24h BaseVolume', help='dynamically generate and update whitelist based on 24h BaseVolume',
action='store_true', action='store_true',
) )
return parser build_subcommands(parser)
parsed_args = parser.parse_args(args)
# No subcommand as been selected
if not hasattr(parsed_args, 'func'):
return parsed_args
parsed_args.func(parsed_args)
return None
def build_subcommands(parser: argparse.ArgumentParser) -> None:
""" Builds and attaches all subcommands """
subparsers = parser.add_subparsers(dest='subparser')
backtest = subparsers.add_parser('backtesting', help='backtesting module')
backtest.set_defaults(func=start_backtesting)
backtest.add_argument(
'-l', '--live',
action='store_true',
dest='live',
help='using live data',
)
backtest.add_argument(
'-i', '--ticker-interval',
help='specify ticker interval in minutes (default: 5)',
dest='ticker_interval',
default=5,
type=int,
metavar='INT',
)
def start_backtesting(args) -> None:
"""
Exports all args as environment variables and starts backtesting via pytest.
:param args: arguments namespace
:return:
"""
import pytest
os.environ.update({
'BACKTEST': 'true',
'BACKTEST_LIVE': 'true' if args.live else '',
'BACKTEST_CONFIG': args.config,
'BACKTEST_TICKER_INTERVAL': str(args.ticker_interval),
})
path = os.path.join(os.path.dirname(__file__), 'tests', 'test_backtesting.py')
pytest.main(['-s', path])
# Required json-schema for user specified config # Required json-schema for user specified config
@ -146,7 +225,10 @@ CONF_SCHEMA = {
'secret': {'type': 'string'}, 'secret': {'type': 'string'},
'pair_whitelist': { 'pair_whitelist': {
'type': 'array', 'type': 'array',
'items': {'type': 'string'}, 'items': {
'type': 'string',
'pattern': '^[0-9A-Z]+_[0-9A-Z]+$'
},
'uniqueItems': True 'uniqueItems': True
} }
}, },

View File

@ -85,20 +85,27 @@ class Trade(_DECL_BASE):
if not order['closed']: if not order['closed']:
return return
logger.debug('Updating trade (id=%d) ...', self.id) logger.info('Updating trade (id=%d) ...', self.id)
if order['type'] == 'LIMIT_BUY': if order['type'] == 'LIMIT_BUY':
# Update open rate and actual amount # Update open rate and actual amount
self.open_rate = order['rate'] self.open_rate = order['rate']
self.amount = order['amount'] self.amount = order['amount']
logger.info('LIMIT_BUY has been fulfilled for %s.', self)
elif order['type'] == 'LIMIT_SELL': elif order['type'] == 'LIMIT_SELL':
# Set close rate and set actual profit # Set close rate and set actual profit
self.close_rate = order['rate'] self.close_rate = order['rate']
self.close_profit = self.calc_profit() self.close_profit = self.calc_profit()
self.close_date = datetime.utcnow() self.close_date = datetime.utcnow()
self.is_open = False
logger.info(
'Marking %s as closed as the trade is fulfilled and found no open orders for it.',
self
)
else: else:
raise ValueError('Unknown order type: {}'.format(order['type'])) raise ValueError('Unknown order type: {}'.format(order['type']))
self.open_order_id = None self.open_order_id = None
Trade.session.flush()
def calc_profit(self, rate: Optional[float] = None) -> float: def calc_profit(self, rate: Optional[float] = None) -> float:
""" """

View File

@ -1 +1,42 @@
import logging
from . import telegram from . import telegram
logger = logging.getLogger(__name__)
REGISTERED_MODULES = []
def init(config: dict) -> None:
"""
Initializes all enabled rpc modules
:param config: config to use
:return: None
"""
if config['telegram'].get('enabled', False):
logger.info('Enabling rpc.telegram ...')
REGISTERED_MODULES.append('telegram')
telegram.init(config)
def cleanup() -> None:
"""
Stops all enabled rpc modules
:return: None
"""
if 'telegram' in REGISTERED_MODULES:
logger.debug('Cleaning up rpc.telegram ...')
telegram.cleanup()
def send_msg(msg: str) -> None:
"""
Send given markdown message to all registered rpc modules
:param msg: message
:return: None
"""
logger.info(msg)
if 'telegram' in REGISTERED_MODULES:
telegram.send_msg(msg)

View File

@ -8,7 +8,7 @@ from tabulate import tabulate
import arrow import arrow
from sqlalchemy import and_, func, text from sqlalchemy import and_, func, text
from telegram import ParseMode, Bot, Update from telegram import ParseMode, Bot, Update
from telegram.error import NetworkError from telegram.error import NetworkError, TelegramError
from telegram.ext import CommandHandler, Updater from telegram.ext import CommandHandler, Updater
from freqtrade import exchange, __version__ from freqtrade import exchange, __version__
@ -57,7 +57,7 @@ def init(config: dict) -> None:
_UPDATER.dispatcher.add_handler(handle) _UPDATER.dispatcher.add_handler(handle)
_UPDATER.start_polling( _UPDATER.start_polling(
clean=True, clean=True,
bootstrap_retries=3, bootstrap_retries=-1,
timeout=30, timeout=30,
read_latency=60, read_latency=60,
) )
@ -475,13 +475,17 @@ def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDO
return return
bot = bot or _UPDATER.bot bot = bot or _UPDATER.bot
try: try:
bot.send_message(_CONF['telegram']['chat_id'], msg, parse_mode=parse_mode) try:
except NetworkError as error: bot.send_message(_CONF['telegram']['chat_id'], msg, parse_mode=parse_mode)
# Sometimes the telegram server resets the current connection, except NetworkError as network_err:
# if this is the case we send the message again. # Sometimes the telegram server resets the current connection,
logger.warning( # if this is the case we send the message again.
'Got Telegram NetworkError: %s! Trying one more time.', logger.warning(
error.message 'Got Telegram NetworkError: %s! Trying one more time.',
) network_err.message
bot.send_message(_CONF['telegram']['chat_id'], msg, parse_mode=parse_mode) )
bot.send_message(_CONF['telegram']['chat_id'], msg, parse_mode=parse_mode)
except TelegramError as telegram_err:
logger.warning('Got TelegramError: %s! Giving up on that message.', telegram_err.message)

View File

@ -0,0 +1,20 @@
# pragma pylint: disable=missing-docstring
import json
import os
def load_backtesting_data(ticker_interval: int = 5):
path = os.path.abspath(os.path.dirname(__file__))
result = {}
pairs = [
'BTC_BCC', 'BTC_ETH', 'BTC_DASH', 'BTC_POWR', 'BTC_ETC',
'BTC_VTC', 'BTC_WAVES', 'BTC_LSK', 'BTC_XLM', 'BTC_OK',
]
for pair in pairs:
with open('{abspath}/testdata/{pair}-{ticker_interval}.json'.format(
abspath=path,
pair=pair,
ticker_interval=ticker_interval,
)) as tickerdata:
result[pair] = json.load(tickerdata)
return result

View File

@ -1,5 +1,4 @@
# pragma pylint: disable=missing-docstring # pragma pylint: disable=missing-docstring
import json
from datetime import datetime from datetime import datetime
from unittest.mock import MagicMock from unittest.mock import MagicMock
@ -55,6 +54,8 @@ def default_conf():
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def backtest_conf(): def backtest_conf():
return { return {
"stake_currency": "BTC",
"stake_amount": 0.01,
"minimal_roi": { "minimal_roi": {
"40": 0.0, "40": 0.0,
"30": 0.01, "30": 0.01,
@ -65,16 +66,6 @@ def backtest_conf():
} }
@pytest.fixture(scope="module")
def backdata():
result = {}
for pair in ['btc-neo', 'btc-eth', 'btc-omg', 'btc-edg', 'btc-pay',
'btc-pivx', 'btc-qtum', 'btc-mtl', 'btc-etc', 'btc-ltc']:
with open('freqtrade/tests/testdata/' + pair + '.json') as data_file:
result[pair] = json.load(data_file)
return result
@pytest.fixture @pytest.fixture
def update(): def update():
_update = Update(0) _update = Update(0)

View File

@ -1,16 +1,16 @@
# pragma pylint: disable=missing-docstring # pragma pylint: disable=missing-docstring,W0621
from datetime import datetime
import json import json
import arrow
import pytest import pytest
from pandas import DataFrame from pandas import DataFrame
from freqtrade.analyze import parse_ticker_dataframe, populate_buy_trend, populate_indicators, \ from freqtrade.analyze import parse_ticker_dataframe, populate_buy_trend, populate_indicators, \
get_buy_signal get_signal, SignalType, populate_sell_trend
@pytest.fixture @pytest.fixture
def result(): def result():
with open('freqtrade/tests/testdata/btc-eth.json') as data_file: with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file:
return parse_ticker_dataframe(json.load(data_file)) return parse_ticker_dataframe(json.load(data_file))
@ -20,20 +20,34 @@ def test_dataframe_correct_columns(result):
def test_dataframe_correct_length(result): def test_dataframe_correct_length(result):
assert len(result.index) == 5751 assert len(result.index) == 14382
def test_populates_buy_trend(result): def test_populates_buy_trend(result):
dataframe = populate_buy_trend(populate_indicators(result)) dataframe = populate_buy_trend(populate_indicators(result))
assert 'buy' in dataframe.columns assert 'buy' in dataframe.columns
assert 'buy_price' in dataframe.columns
def test_populates_sell_trend(result):
dataframe = populate_sell_trend(populate_indicators(result))
assert 'sell' in dataframe.columns
def test_returns_latest_buy_signal(mocker): def test_returns_latest_buy_signal(mocker):
buydf = DataFrame([{'buy': 1, 'date': datetime.today()}]) buydf = DataFrame([{'buy': 1, 'date': arrow.utcnow()}])
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf) mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf)
assert get_buy_signal('BTC-ETH') assert get_signal('BTC-ETH', SignalType.BUY)
buydf = DataFrame([{'buy': 0, 'date': datetime.today()}]) buydf = DataFrame([{'buy': 0, 'date': arrow.utcnow()}])
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf) mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf)
assert not get_buy_signal('BTC-ETH') assert not get_signal('BTC-ETH', SignalType.BUY)
def test_returns_latest_sell_signal(mocker):
selldf = DataFrame([{'sell': 1, 'date': arrow.utcnow()}])
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=selldf)
assert get_signal('BTC-ETH', SignalType.SELL)
selldf = DataFrame([{'sell': 0, 'date': arrow.utcnow()}])
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=selldf)
assert not get_signal('BTC-ETH', SignalType.SELL)

View File

@ -1,65 +1,154 @@
# pragma pylint: disable=missing-docstring # pragma pylint: disable=missing-docstring,W0212
import logging import logging
import os import os
from typing import Tuple, Dict
import pytest
import arrow import arrow
import pytest
from pandas import DataFrame from pandas import DataFrame
from tabulate import tabulate
from freqtrade import exchange from freqtrade import exchange
from freqtrade.analyze import analyze_ticker from freqtrade.analyze import parse_ticker_dataframe, populate_indicators, \
populate_buy_trend, populate_sell_trend
from freqtrade.exchange import Bittrex from freqtrade.exchange import Bittrex
from freqtrade.main import should_sell from freqtrade.main import min_roi_reached
from freqtrade.misc import load_config
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.tests import load_backtesting_data
logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot logger = logging.getLogger(__name__)
def format_results(results): def format_results(results: DataFrame):
return 'Made {} buys. Average profit {:.2f}%. Total profit was {:.3f}. Average duration {:.1f} mins.'.format( return ('Made {:6d} buys. Average profit {: 5.2f}%. '
len(results.index), results.profit.mean() * 100.0, results.profit.sum(), results.duration.mean() * 5) 'Total profit was {: 7.3f}. Average duration {:5.1f} mins.').format(
len(results.index),
results.profit.mean() * 100.0,
results.profit.sum(),
results.duration.mean() * 5,
)
def print_pair_results(pair, results): def preprocess(backdata) -> Dict[str, DataFrame]:
print('For currency {}:'.format(pair)) processed = {}
print(format_results(results[results.currency == pair])) for pair, pair_data in backdata.items():
processed[pair] = populate_indicators(parse_ticker_dataframe(pair_data))
return processed
def backtest(backtest_conf, backdata, mocker): def get_timeframe(data: Dict[str, Dict]) -> Tuple[arrow.Arrow, arrow.Arrow]:
"""
Get the maximum timeframe for the given backtest data
:param data: dictionary with backtesting data
:return: tuple containing min_date, max_date
"""
min_date, max_date = None, None
for values in data.values():
sorted_values = sorted(values, key=lambda d: arrow.get(d['T']))
if not min_date or sorted_values[0]['T'] < min_date:
min_date = sorted_values[0]['T']
if not max_date or sorted_values[-1]['T'] > max_date:
max_date = sorted_values[-1]['T']
return arrow.get(min_date), arrow.get(max_date)
def generate_text_table(data: Dict[str, Dict], results: DataFrame, stake_currency) -> str:
"""
Generates and returns a text table for the given backtest data and the results dataframe
:return: pretty printed table with tabulate as str
"""
tabular_data = []
headers = ['pair', 'buy count', 'avg profit', 'total profit', 'avg duration']
for pair in data:
result = results[results.currency == pair]
tabular_data.append([
pair,
len(result.index),
'{:.2f}%'.format(result.profit.mean() * 100.0),
'{:.08f} {}'.format(result.profit.sum(), stake_currency),
'{:.2f}'.format(result.duration.mean() * 5),
])
# Append Total
tabular_data.append([
'TOTAL',
len(results.index),
'{:.2f}%'.format(results.profit.mean() * 100.0),
'{:.08f} {}'.format(results.profit.sum(), stake_currency),
'{:.2f}'.format(results.duration.mean() * 5),
])
return tabulate(tabular_data, headers=headers)
def backtest(backtest_conf, processed, mocker):
trades = [] trades = []
exchange._API = Bittrex({'key': '', 'secret': ''}) exchange._API = Bittrex({'key': '', 'secret': ''})
mocked_history = mocker.patch('freqtrade.analyze.get_ticker_history')
mocker.patch.dict('freqtrade.main._CONF', backtest_conf) mocker.patch.dict('freqtrade.main._CONF', backtest_conf)
mocker.patch('arrow.utcnow', return_value=arrow.get('2017-08-20T14:50:00')) for pair, pair_data in processed.items():
for pair, pair_data in backdata.items(): pair_data['buy'] = 0
mocked_history.return_value = pair_data pair_data['sell'] = 0
ticker = analyze_ticker(pair)[['close', 'date', 'buy']].copy() ticker = populate_sell_trend(populate_buy_trend(pair_data))
# for each buy point # for each buy point
for row in ticker[ticker.buy == 1].itertuples(index=True): for row in ticker[ticker.buy == 1].itertuples(index=True):
trade = Trade( trade = Trade(
open_rate=row.close, open_rate=row.close,
open_date=row.date, open_date=row.date,
amount=1, amount=backtest_conf['stake_amount'],
fee=exchange.get_fee() * 2 fee=exchange.get_fee() * 2
) )
# calculate win/lose forwards from buy point # calculate win/lose forwards from buy point
for row2 in ticker[row.Index:].itertuples(index=True): for row2 in ticker[row.Index:].itertuples(index=True):
if should_sell(trade, row2.close, row2.date): if min_roi_reached(trade, row2.close, row2.date) or row2.sell == 1:
current_profit = trade.calc_profit(row2.close) current_profit = trade.calc_profit(row2.close)
trades.append((pair, current_profit, row2.Index - row.Index)) trades.append((pair, current_profit, row2.Index - row.Index))
break break
labels = ['currency', 'profit', 'duration'] labels = ['currency', 'profit', 'duration']
results = DataFrame.from_records(trades, columns=labels) return DataFrame.from_records(trades, columns=labels)
return results
@pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set") @pytest.mark.skipif(not os.environ.get('BACKTEST'), reason="BACKTEST not set")
def test_backtest(backtest_conf, backdata, mocker, report=True): def test_backtest(backtest_conf, mocker):
results = backtest(backtest_conf, backdata, mocker) print('')
exchange._API = Bittrex({'key': '', 'secret': ''})
print('====================== BACKTESTING REPORT ================================') # Load configuration file based on env variable
for pair in backdata: conf_path = os.environ.get('BACKTEST_CONFIG')
print_pair_results(pair, results) if conf_path:
print('TOTAL OVER ALL TRADES:') print('Using config: {} ...'.format(conf_path))
print(format_results(results)) config = load_config(conf_path)
else:
config = backtest_conf
# Parse ticker interval
ticker_interval = int(os.environ.get('BACKTEST_TICKER_INTERVAL') or 5)
print('Using ticker_interval: {} ...'.format(ticker_interval))
data = {}
if os.environ.get('BACKTEST_LIVE'):
print('Downloading data for all pairs in whitelist ...')
for pair in config['exchange']['pair_whitelist']:
data[pair] = exchange.get_ticker_history(pair, ticker_interval)
else:
print('Using local backtesting data (ignoring whitelist in given config)...')
data = load_backtesting_data(ticker_interval)
print('Using stake_currency: {} ...\nUsing stake_amount: {} ...'.format(
config['stake_currency'], config['stake_amount']
))
# Print timeframe
min_date, max_date = get_timeframe(data)
print('Measuring data from {} up to {} ...'.format(
min_date.isoformat(), max_date.isoformat()
))
# 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')
print(generate_text_table(data, results, config['stake_currency']))

View File

@ -1,4 +1,4 @@
# pragma pylint: disable=missing-docstring # pragma pylint: disable=missing-docstring,C0103
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pytest import pytest
@ -33,4 +33,3 @@ def test_validate_pairs_not_compatible(default_conf, mocker):
mocker.patch.dict('freqtrade.exchange._CONF', default_conf) mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
with pytest.raises(RuntimeError, match=r'not compatible'): with pytest.raises(RuntimeError, match=r'not compatible'):
validate_pairs(default_conf['exchange']['pair_whitelist']) validate_pairs(default_conf['exchange']['pair_whitelist'])

View File

@ -1,4 +1,4 @@
# pragma pylint: disable=missing-docstring # pragma pylint: disable=missing-docstring,W0212
import logging import logging
import os import os
from functools import reduce from functools import reduce
@ -9,16 +9,22 @@ import pytest
from hyperopt import fmin, tpe, hp, Trials, STATUS_OK from hyperopt import fmin, tpe, hp, Trials, STATUS_OK
from pandas import DataFrame from pandas import DataFrame
from freqtrade import exchange
from freqtrade.exchange import Bittrex
from freqtrade.tests import load_backtesting_data
from freqtrade.tests.test_backtesting import backtest, format_results from freqtrade.tests.test_backtesting import backtest, format_results
from freqtrade.tests.test_backtesting import preprocess
from freqtrade.vendor.qtpylib.indicators import crossed_above from freqtrade.vendor.qtpylib.indicators import crossed_above
logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot
# set TARGET_TRADES to suit your number concurrent trades so its realistic to 20days of data # set TARGET_TRADES to suit your number concurrent trades so its realistic to 20days of data
TARGET_TRADES = 1300 TARGET_TRADES = 1100
TOTAL_TRIES = 4 TOTAL_TRIES = 4
# pylint: disable=C0103
current_tries = 0 current_tries = 0
def buy_strategy_generator(params): def buy_strategy_generator(params):
def populate_buy_trend(dataframe: DataFrame) -> DataFrame: def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
conditions = [] conditions = []
@ -59,32 +65,36 @@ def buy_strategy_generator(params):
dataframe.loc[ dataframe.loc[
reduce(lambda x, y: x & y, conditions), reduce(lambda x, y: x & y, conditions),
'buy'] = 1 'buy'] = 1
dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
return dataframe return dataframe
return populate_buy_trend return populate_buy_trend
@pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set") @pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set")
def test_hyperopt(backtest_conf, backdata, mocker): def test_hyperopt(backtest_conf, mocker):
mocked_buy_trend = mocker.patch('freqtrade.analyze.populate_buy_trend') mocked_buy_trend = mocker.patch('freqtrade.tests.test_backtesting.populate_buy_trend')
backdata = load_backtesting_data()
processed = preprocess(backdata)
exchange._API = Bittrex({'key': '', 'secret': ''})
def optimizer(params): def optimizer(params):
mocked_buy_trend.side_effect = buy_strategy_generator(params) mocked_buy_trend.side_effect = buy_strategy_generator(params)
results = backtest(backtest_conf, backdata, mocker) results = backtest(backtest_conf, processed, mocker)
result = format_results(results) result = format_results(results)
total_profit = results.profit.sum() * 1000 total_profit = results.profit.sum() * 1000
trade_count = len(results.index) trade_count = len(results.index)
trade_loss = 1 - 0.4 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.2) trade_loss = 1 - 0.35 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.2)
profit_loss = max(0, 1 - total_profit / 15000) # max profit 15000 profit_loss = max(0, 1 - total_profit / 10000) # max profit 10000
# pylint: disable=W0603
global current_tries global current_tries
current_tries += 1 current_tries += 1
print('{}/{}: {}'.format(current_tries, TOTAL_TRIES, result)) print('{:5d}/{}: {}'.format(current_tries, TOTAL_TRIES, result))
return { return {
'loss': trade_loss + profit_loss, 'loss': trade_loss + profit_loss,
@ -146,3 +156,8 @@ def test_hyperopt(backtest_conf, backdata, mocker):
print('Best parameters {}'.format(best)) print('Best parameters {}'.format(best))
newlist = sorted(trials.results, key=itemgetter('loss')) newlist = sorted(trials.results, key=itemgetter('loss'))
print('Result: {}'.format(newlist[0]['result'])) print('Result: {}'.format(newlist[0]['result']))
if __name__ == '__main__':
# for profiling with cProfile and line_profiler
pytest.main([__file__, '-s'])

View File

@ -1,4 +1,4 @@
# pragma pylint: disable=missing-docstring # pragma pylint: disable=missing-docstring,C0103
import copy import copy
from unittest.mock import MagicMock from unittest.mock import MagicMock
@ -7,16 +7,17 @@ import requests
from sqlalchemy import create_engine from sqlalchemy import create_engine
from freqtrade.exchange import Exchanges from freqtrade.exchange import Exchanges
from freqtrade.main import create_trade, handle_trade, close_trade_if_fulfilled, init, \ from freqtrade.analyze import SignalType
from freqtrade.main import create_trade, handle_trade, init, \
get_target_bid, _process get_target_bid, _process
from freqtrade.misc import get_state, State from freqtrade.misc import get_state, State, FreqtradeException
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
def test_process_trade_creation(default_conf, ticker, health, mocker): def test_process_trade_creation(default_conf, ticker, health, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.main.exchange', mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
get_ticker=ticker, get_ticker=ticker,
@ -25,7 +26,7 @@ def test_process_trade_creation(default_conf, ticker, health, mocker):
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
trades = Trade.query.filter(Trade.is_open.is_(True)).all() trades = Trade.query.filter(Trade.is_open.is_(True)).all()
assert len(trades) == 0 assert not trades
result = _process() result = _process()
assert result is True assert result is True
@ -44,8 +45,8 @@ def test_process_trade_creation(default_conf, ticker, health, mocker):
def test_process_exchange_failures(default_conf, ticker, health, mocker): def test_process_exchange_failures(default_conf, ticker, health, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
sleep_mock = mocker.patch('time.sleep', side_effect=lambda _: None) sleep_mock = mocker.patch('time.sleep', side_effect=lambda _: None)
mocker.patch.multiple('freqtrade.main.exchange', mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
@ -61,8 +62,8 @@ def test_process_exchange_failures(default_conf, ticker, health, mocker):
def test_process_runtime_error(default_conf, ticker, health, mocker): def test_process_runtime_error(default_conf, ticker, health, mocker):
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=msg_mock) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=msg_mock)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.main.exchange', mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
get_ticker=ticker, get_ticker=ticker,
@ -79,8 +80,9 @@ def test_process_runtime_error(default_conf, ticker, health, mocker):
def test_process_trade_handling(default_conf, ticker, limit_buy_order, health, mocker): def test_process_trade_handling(default_conf, ticker, limit_buy_order, health, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) mocker.patch('freqtrade.main.get_signal',
side_effect=lambda *args: False if args[1] == SignalType.SELL else True)
mocker.patch.multiple('freqtrade.main.exchange', mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
get_ticker=ticker, get_ticker=ticker,
@ -90,7 +92,7 @@ def test_process_trade_handling(default_conf, ticker, limit_buy_order, health, m
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
trades = Trade.query.filter(Trade.is_open.is_(True)).all() trades = Trade.query.filter(Trade.is_open.is_(True)).all()
assert len(trades) == 0 assert not trades
result = _process() result = _process()
assert result is True assert result is True
trades = Trade.query.filter(Trade.is_open.is_(True)).all() trades = Trade.query.filter(Trade.is_open.is_(True)).all()
@ -102,8 +104,8 @@ def test_process_trade_handling(default_conf, ticker, limit_buy_order, health, m
def test_create_trade(default_conf, ticker, limit_buy_order, mocker): def test_create_trade(default_conf, ticker, limit_buy_order, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange', mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
get_ticker=ticker, get_ticker=ticker,
@ -132,27 +134,27 @@ def test_create_trade(default_conf, ticker, limit_buy_order, mocker):
def test_create_trade_no_stake_amount(default_conf, ticker, mocker): def test_create_trade_no_stake_amount(default_conf, ticker, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange', mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
get_ticker=ticker, get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy'), buy=MagicMock(return_value='mocked_limit_buy'),
get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5)) get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5))
with pytest.raises(ValueError, match=r'.*stake amount.*'): with pytest.raises(FreqtradeException, match=r'.*stake amount.*'):
create_trade(default_conf['stake_amount']) create_trade(default_conf['stake_amount'])
def test_create_trade_no_pairs(default_conf, ticker, mocker): def test_create_trade_no_pairs(default_conf, ticker, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange', mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
get_ticker=ticker, get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy')) buy=MagicMock(return_value='mocked_limit_buy'))
with pytest.raises(ValueError, match=r'.*No pair in whitelist.*'): with pytest.raises(FreqtradeException, match=r'.*No pair in whitelist.*'):
conf = copy.deepcopy(default_conf) conf = copy.deepcopy(default_conf)
conf['exchange']['pair_whitelist'] = [] conf['exchange']['pair_whitelist'] = []
mocker.patch.dict('freqtrade.main._CONF', conf) mocker.patch.dict('freqtrade.main._CONF', conf)
@ -161,8 +163,8 @@ def test_create_trade_no_pairs(default_conf, ticker, mocker):
def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker): def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange', mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
get_ticker=MagicMock(return_value={ get_ticker=MagicMock(return_value={
@ -182,7 +184,6 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker):
handle_trade(trade) handle_trade(trade)
assert trade.open_order_id == 'mocked_limit_sell' assert trade.open_order_id == 'mocked_limit_sell'
assert close_trade_if_fulfilled(trade) is False
# Simulate fulfilled LIMIT_SELL order for trade # Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order) trade.update(limit_sell_order)
@ -194,8 +195,8 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker):
def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mocker): def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange', mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
get_ticker=ticker, get_ticker=ticker,
@ -204,20 +205,15 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mo
# Create trade and sell it # Create trade and sell it
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
trade = create_trade(15.0) trade = create_trade(15.0)
trade.update(limit_buy_order)
trade.update(limit_sell_order)
Trade.session.add(trade) Trade.session.add(trade)
Trade.session.flush() trade.update(limit_buy_order)
trade = Trade.query.filter(Trade.is_open.is_(True)).first() trade = Trade.query.filter(Trade.is_open.is_(True)).first()
assert trade assert trade
# Simulate that there is no open order trade.update(limit_sell_order)
trade.open_order_id = None trade = Trade.query.filter(Trade.is_open.is_(False)).first()
assert trade
closed = close_trade_if_fulfilled(trade)
assert closed
assert not trade.is_open
with pytest.raises(ValueError, match=r'.*closed trade.*'): with pytest.raises(ValueError, match=r'.*closed trade.*'):
handle_trade(trade) handle_trade(trade)

View File

@ -1,7 +1,15 @@
# pragma pylint: disable=missing-docstring # pragma pylint: disable=missing-docstring,C0103
import json
import os
import time import time
from argparse import Namespace
from copy import deepcopy
from unittest.mock import MagicMock
from freqtrade.misc import throttle import pytest
from jsonschema import ValidationError
from freqtrade.misc import throttle, parse_args, start_backtesting, load_config
def test_throttle(): def test_throttle():
@ -18,3 +26,124 @@ def test_throttle():
result = throttle(func, -1) result = throttle(func, -1)
assert result == 42 assert result == 42
def test_parse_args_defaults():
args = parse_args([])
assert args is not None
assert args.config == 'config.json'
assert args.dynamic_whitelist is False
assert args.loglevel == 20
def test_parse_args_invalid():
with pytest.raises(SystemExit, match=r'2'):
parse_args(['-c'])
def test_parse_args_config():
args = parse_args(['-c', '/dev/null'])
assert args is not None
assert args.config == '/dev/null'
args = parse_args(['--config', '/dev/null'])
assert args is not None
assert args.config == '/dev/null'
def test_parse_args_verbose():
args = parse_args(['-v'])
assert args is not None
assert args.loglevel == 10
def test_parse_args_dynamic_whitelist():
args = parse_args(['--dynamic-whitelist'])
assert args is not None
assert args.dynamic_whitelist is True
def test_parse_args_backtesting(mocker):
backtesting_mock = mocker.patch('freqtrade.misc.start_backtesting', MagicMock())
args = parse_args(['backtesting'])
assert args is None
assert backtesting_mock.call_count == 1
call_args = backtesting_mock.call_args[0][0]
assert call_args.config == 'config.json'
assert call_args.live is False
assert call_args.loglevel == 20
assert call_args.subparser == 'backtesting'
assert call_args.func is not None
assert call_args.ticker_interval == 5
def test_parse_args_backtesting_invalid():
with pytest.raises(SystemExit, match=r'2'):
parse_args(['--ticker-interval'])
with pytest.raises(SystemExit, match=r'2'):
parse_args(['--ticker-interval', 'abc'])
def test_parse_args_backtesting_custom(mocker):
backtesting_mock = mocker.patch('freqtrade.misc.start_backtesting', MagicMock())
args = parse_args(['-c', 'test_conf.json', 'backtesting', '--live', '--ticker-interval', '1'])
assert args is None
assert backtesting_mock.call_count == 1
call_args = backtesting_mock.call_args[0][0]
assert call_args.config == 'test_conf.json'
assert call_args.live is True
assert call_args.loglevel == 20
assert call_args.subparser == 'backtesting'
assert call_args.func is not None
assert call_args.ticker_interval == 1
def test_start_backtesting(mocker):
pytest_mock = mocker.patch('pytest.main', MagicMock())
env_mock = mocker.patch('os.environ', {})
args = Namespace(
config='config.json',
live=True,
loglevel=20,
ticker_interval=1,
)
start_backtesting(args)
assert env_mock == {
'BACKTEST': 'true',
'BACKTEST_LIVE': 'true',
'BACKTEST_CONFIG': 'config.json',
'BACKTEST_TICKER_INTERVAL': '1',
}
assert pytest_mock.call_count == 1
main_call_args = pytest_mock.call_args[0][0]
assert main_call_args[0] == '-s'
assert main_call_args[1].endswith(os.path.join('freqtrade', 'tests', 'test_backtesting.py'))
def test_load_config(default_conf, mocker):
file_mock = mocker.patch('freqtrade.misc.open', mocker.mock_open(
read_data=json.dumps(default_conf)
))
validated_conf = load_config('somefile')
assert file_mock.call_count == 1
assert validated_conf.items() >= default_conf.items()
def test_load_config_invalid_pair(default_conf, mocker):
conf = deepcopy(default_conf)
conf['exchange']['pair_whitelist'].append('BTC-ETH')
mocker.patch('freqtrade.misc.open', mocker.mock_open(read_data=json.dumps(conf)))
with pytest.raises(ValidationError, match=r'.*does not match.*'):
load_config('somefile')
def test_load_config_missing_attributes(default_conf, mocker):
conf = deepcopy(default_conf)
conf.pop('exchange')
mocker.patch('freqtrade.misc.open', mocker.mock_open(read_data=json.dumps(conf)))
with pytest.raises(ValidationError, match=r'.*\'exchange\' is a required property.*'):
load_config('somefile')

View File

@ -0,0 +1,58 @@
# pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, C0103
from unittest.mock import MagicMock
from copy import deepcopy
from freqtrade.rpc import init, cleanup, send_msg
def test_init_telegram_enabled(default_conf, mocker):
module_list = []
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', module_list)
telegram_mock = mocker.patch('freqtrade.rpc.telegram.init', MagicMock())
init(default_conf)
assert telegram_mock.call_count == 1
assert 'telegram' in module_list
def test_init_telegram_disabled(default_conf, mocker):
module_list = []
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', module_list)
telegram_mock = mocker.patch('freqtrade.rpc.telegram.init', MagicMock())
conf = deepcopy(default_conf)
conf['telegram']['enabled'] = False
init(conf)
assert telegram_mock.call_count == 0
assert 'telegram' not in module_list
def test_cleanup_telegram_enabled(mocker):
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', ['telegram'])
telegram_mock = mocker.patch('freqtrade.rpc.telegram.cleanup', MagicMock())
cleanup()
assert telegram_mock.call_count == 1
def test_cleanup_telegram_disabled(mocker):
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', [])
telegram_mock = mocker.patch('freqtrade.rpc.telegram.cleanup', MagicMock())
cleanup()
assert telegram_mock.call_count == 0
def test_send_msg_telegram_enabled(mocker):
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', ['telegram'])
telegram_mock = mocker.patch('freqtrade.rpc.telegram.send_msg', MagicMock())
send_msg('test')
assert telegram_mock.call_count == 1
def test_send_msg_telegram_disabled(mocker):
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', [])
telegram_mock = mocker.patch('freqtrade.rpc.telegram.send_msg', MagicMock())
send_msg('test')
assert telegram_mock.call_count == 0

View File

@ -1,10 +1,9 @@
# pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors # pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, C0103
import re import re
from datetime import datetime from datetime import datetime
from random import randint from random import randint
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pytest
from sqlalchemy import create_engine from sqlalchemy import create_engine
from telegram import Update, Message, Chat from telegram import Update, Message, Chat
from telegram.error import NetworkError from telegram.error import NetworkError
@ -14,10 +13,8 @@ from freqtrade.main import init, create_trade
from freqtrade.misc import update_state, State, get_state from freqtrade.misc import update_state, State, get_state
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.rpc import telegram from freqtrade.rpc import telegram
from freqtrade.rpc.telegram import ( from freqtrade.rpc.telegram import authorized_only, is_enabled, send_msg, _status, _status_table, \
_status, _status_table, _profit, _forcesell, _performance, _count, _start, _stop, _balance, _profit, _forcesell, _performance, _count, _start, _stop, _balance, _version, _help
authorized_only, _help, is_enabled, send_msg,
_version)
def test_is_enabled(default_conf, mocker): def test_is_enabled(default_conf, mocker):
@ -79,9 +76,10 @@ def test_authorized_only_exception(default_conf, mocker):
def test_status_handle(default_conf, update, ticker, mocker): def test_status_handle(default_conf, update, ticker, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram', mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf, _CONF=default_conf,
init=MagicMock(), init=MagicMock(),
send_msg=msg_mock) send_msg=msg_mock)
@ -111,16 +109,17 @@ def test_status_handle(default_conf, update, ticker, mocker):
# Trigger status while we have a fulfilled order for the open trade # Trigger status while we have a fulfilled order for the open trade
_status(bot=MagicMock(), update=update) _status(bot=MagicMock(), update=update)
assert msg_mock.call_count == 2 assert msg_mock.call_count == 1
assert '[BTC_ETH]' in msg_mock.call_args_list[-1][0][0] assert '[BTC_ETH]' in msg_mock.call_args_list[0][0][0]
def test_status_table_handle(default_conf, update, ticker, mocker): def test_status_table_handle(default_conf, update, ticker, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.main.telegram', 'freqtrade.rpc.telegram',
_CONF=default_conf, _CONF=default_conf,
init=MagicMock(), init=MagicMock(),
send_msg=msg_mock) send_msg=msg_mock)
@ -155,14 +154,15 @@ def test_status_table_handle(default_conf, update, ticker, mocker):
assert int(fields[0]) == 1 assert int(fields[0]) == 1
assert fields[1] == 'BTC_ETH' assert fields[1] == 'BTC_ETH'
assert msg_mock.call_count == 2 assert msg_mock.call_count == 1
def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker): def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram', mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf, _CONF=default_conf,
init=MagicMock(), init=MagicMock(),
send_msg=msg_mock) send_msg=msg_mock)
@ -184,7 +184,7 @@ def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell
trade.update(limit_buy_order) trade.update(limit_buy_order)
_profit(bot=MagicMock(), update=update) _profit(bot=MagicMock(), update=update)
assert msg_mock.call_count == 2 assert msg_mock.call_count == 1
assert 'no closed trade' in msg_mock.call_args_list[-1][0][0] assert 'no closed trade' in msg_mock.call_args_list[-1][0][0]
msg_mock.reset_mock() msg_mock.reset_mock()
@ -204,12 +204,12 @@ def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell
def test_forcesell_handle(default_conf, update, ticker, mocker): def test_forcesell_handle(default_conf, update, ticker, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
msg_mock = MagicMock() rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
mocker.patch.multiple('freqtrade.main.telegram', mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf, _CONF=default_conf,
init=MagicMock(), init=MagicMock(),
send_msg=msg_mock) send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange', mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
get_ticker=ticker) get_ticker=ticker)
@ -225,19 +225,19 @@ def test_forcesell_handle(default_conf, update, ticker, mocker):
update.message.text = '/forcesell 1' update.message.text = '/forcesell 1'
_forcesell(bot=MagicMock(), update=update) _forcesell(bot=MagicMock(), update=update)
assert msg_mock.call_count == 2 assert rpc_mock.call_count == 2
assert 'Selling [BTC/ETH]' in msg_mock.call_args_list[-1][0][0] assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0]
assert '0.07256061 (profit: ~-0.64%)' in msg_mock.call_args_list[-1][0][0] assert '0.07256061 (profit: ~-0.64%)' in rpc_mock.call_args_list[-1][0][0]
def test_forcesell_all_handle(default_conf, update, ticker, mocker): def test_forcesell_all_handle(default_conf, update, ticker, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
msg_mock = MagicMock() rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
mocker.patch.multiple('freqtrade.main.telegram', mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf, _CONF=default_conf,
init=MagicMock(), init=MagicMock(),
send_msg=msg_mock) send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange', mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
get_ticker=ticker) get_ticker=ticker)
@ -247,22 +247,21 @@ def test_forcesell_all_handle(default_conf, update, ticker, mocker):
for _ in range(4): for _ in range(4):
Trade.session.add(create_trade(15.0)) Trade.session.add(create_trade(15.0))
Trade.session.flush() Trade.session.flush()
rpc_mock.reset_mock()
msg_mock.reset_mock()
update.message.text = '/forcesell all' update.message.text = '/forcesell all'
_forcesell(bot=MagicMock(), update=update) _forcesell(bot=MagicMock(), update=update)
assert msg_mock.call_count == 4 assert rpc_mock.call_count == 4
for args in msg_mock.call_args_list: for args in rpc_mock.call_args_list:
assert '0.07256061 (profit: ~-0.64%)' in args[0][0] assert '0.07256061 (profit: ~-0.64%)' in args[0][0]
def test_forcesell_handle_invalid(default_conf, update, mocker): def test_forcesell_handle_invalid(default_conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram', mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf, _CONF=default_conf,
init=MagicMock(), init=MagicMock(),
send_msg=msg_mock) send_msg=msg_mock)
@ -297,9 +296,10 @@ def test_forcesell_handle_invalid(default_conf, update, mocker):
def test_performance_handle( def test_performance_handle(
default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker): default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram', mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf, _CONF=default_conf,
init=MagicMock(), init=MagicMock(),
send_msg=msg_mock) send_msg=msg_mock)
@ -324,17 +324,17 @@ def test_performance_handle(
Trade.session.flush() Trade.session.flush()
_performance(bot=MagicMock(), update=update) _performance(bot=MagicMock(), update=update)
assert msg_mock.call_count == 2 assert msg_mock.call_count == 1
assert 'Performance' in msg_mock.call_args_list[-1][0][0] assert 'Performance' in msg_mock.call_args_list[0][0][0]
assert '<code>BTC_ETH\t10.05%</code>' in msg_mock.call_args_list[-1][0][0] assert '<code>BTC_ETH\t10.05%</code>' in msg_mock.call_args_list[0][0][0]
def test_count_handle(default_conf, update, ticker, mocker): def test_count_handle(default_conf, update, ticker, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.main.telegram', 'freqtrade.rpc.telegram',
_CONF=default_conf, _CONF=default_conf,
init=MagicMock(), init=MagicMock(),
send_msg=msg_mock) send_msg=msg_mock)
@ -365,9 +365,9 @@ def test_count_handle(default_conf, update, ticker, mocker):
def test_performance_handle_invalid(default_conf, update, mocker): def test_performance_handle_invalid(default_conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram', mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf, _CONF=default_conf,
init=MagicMock(), init=MagicMock(),
send_msg=msg_mock) send_msg=msg_mock)
@ -385,7 +385,7 @@ def test_performance_handle_invalid(default_conf, update, mocker):
def test_start_handle(default_conf, update, mocker): def test_start_handle(default_conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram', mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf, _CONF=default_conf,
init=MagicMock(), init=MagicMock(),
send_msg=msg_mock) send_msg=msg_mock)
@ -403,7 +403,7 @@ def test_start_handle(default_conf, update, mocker):
def test_start_handle_already_running(default_conf, update, mocker): def test_start_handle_already_running(default_conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram', mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf, _CONF=default_conf,
init=MagicMock(), init=MagicMock(),
send_msg=msg_mock) send_msg=msg_mock)
@ -422,7 +422,7 @@ def test_start_handle_already_running(default_conf, update, mocker):
def test_stop_handle(default_conf, update, mocker): def test_stop_handle(default_conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram', mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf, _CONF=default_conf,
init=MagicMock(), init=MagicMock(),
send_msg=msg_mock) send_msg=msg_mock)
@ -441,7 +441,7 @@ def test_stop_handle(default_conf, update, mocker):
def test_stop_handle_already_stopped(default_conf, update, mocker): def test_stop_handle_already_stopped(default_conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram', mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf, _CONF=default_conf,
init=MagicMock(), init=MagicMock(),
send_msg=msg_mock) send_msg=msg_mock)
@ -473,7 +473,7 @@ def test_balance_handle(default_conf, update, mocker):
}] }]
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram', mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf, _CONF=default_conf,
init=MagicMock(), init=MagicMock(),
send_msg=msg_mock) send_msg=msg_mock)
@ -489,7 +489,7 @@ def test_balance_handle(default_conf, update, mocker):
def test_help_handle(default_conf, update, mocker): def test_help_handle(default_conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram', mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf, _CONF=default_conf,
init=MagicMock(), init=MagicMock(),
send_msg=msg_mock) send_msg=msg_mock)
@ -502,7 +502,7 @@ def test_help_handle(default_conf, update, mocker):
def test_version_handle(default_conf, update, mocker): def test_version_handle(default_conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram', mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf, _CONF=default_conf,
init=MagicMock(), init=MagicMock(),
send_msg=msg_mock) send_msg=msg_mock)
@ -514,12 +514,12 @@ def test_version_handle(default_conf, update, mocker):
def test_send_msg(default_conf, mocker): def test_send_msg(default_conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.main.telegram', mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf, _CONF=default_conf,
init=MagicMock()) init=MagicMock())
bot = MagicMock() bot = MagicMock()
send_msg('test', bot) send_msg('test', bot)
assert len(bot.method_calls) == 0 assert not bot.method_calls
bot.reset_mock() bot.reset_mock()
default_conf['telegram']['enabled'] = True default_conf['telegram']['enabled'] = True
@ -529,14 +529,13 @@ def test_send_msg(default_conf, mocker):
def test_send_msg_network_error(default_conf, mocker): def test_send_msg_network_error(default_conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.main.telegram', mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf, _CONF=default_conf,
init=MagicMock()) init=MagicMock())
default_conf['telegram']['enabled'] = True default_conf['telegram']['enabled'] = True
bot = MagicMock() bot = MagicMock()
bot.send_message = MagicMock(side_effect=NetworkError('Oh snap')) bot.send_message = MagicMock(side_effect=NetworkError('Oh snap'))
with pytest.raises(NetworkError, match=r'Oh snap'): send_msg('test', bot)
send_msg('test', bot)
# Bot should've tried to send it twice # Bot should've tried to send it twice
assert len(bot.method_calls) == 2 assert len(bot.method_calls) == 2

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -7,8 +7,13 @@ from os import path
from freqtrade import exchange from freqtrade import exchange
from freqtrade.exchange import Bittrex from freqtrade.exchange import Bittrex
PAIRS = ['BTC-OK', 'BTC-NEO', 'BTC-DASH', 'BTC-ETC', 'BTC-ETH', 'BTC-SNT'] PAIRS = [
TICKER_INTERVAL = 1 # ticker interval in minutes (currently implemented: 1 and 5) 'BTC_BCC', 'BTC_ETH', 'BTC_MER', 'BTC_POWR', 'BTC_ETC',
'BTC_OK', 'BTC_NEO', 'BTC_EMC2', 'BTC_DASH', 'BTC_LSK',
'BTC_LTC', 'BTC_XZC', 'BTC_OMG', 'BTC_STRAT', 'BTC_XRP',
'BTC_QTUM', 'BTC_WAVES', 'BTC_VTC', 'BTC_XLM', 'BTC_MCO'
]
TICKER_INTERVAL = 5 # ticker interval in minutes (currently implemented: 1 and 5)
OUTPUT_DIR = path.dirname(path.realpath(__file__)) OUTPUT_DIR = path.dirname(path.realpath(__file__))
# Init Bittrex exchange # Init Bittrex exchange
@ -16,8 +21,8 @@ exchange._API = Bittrex({'key': '', 'secret': ''})
for pair in PAIRS: for pair in PAIRS:
data = exchange.get_ticker_history(pair, TICKER_INTERVAL) data = exchange.get_ticker_history(pair, TICKER_INTERVAL)
filename = path.join(OUTPUT_DIR, '{}-{}m.json'.format( filename = path.join(OUTPUT_DIR, '{}-{}.json'.format(
pair.lower(), pair,
TICKER_INTERVAL, TICKER_INTERVAL,
)) ))
with open(filename, 'w') as fp: with open(filename, 'w') as fp: