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]
good-names=logger
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
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
```

View File

@ -1,3 +1,4 @@
__version__ = '0.14.2'
""" FreqTrade bot """
__version__ = '0.14.3'
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
from datetime import timedelta
@ -6,10 +10,15 @@ import talib.abstract as ta
from pandas import DataFrame, to_datetime
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__)
class SignalType(Enum):
""" Enum to distinguish between buy and sell signals """
BUY = "buy"
SELL = "sell"
def parse_ticker_dataframe(ticker: list) -> DataFrame:
"""
@ -57,18 +66,28 @@ def populate_indicators(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
:return: DataFrame with buy column
"""
dataframe.ix[
(dataframe['close'] < dataframe['sma']) &
dataframe.loc[
(dataframe['tema'] <= dataframe['blower']) &
(dataframe['mfi'] < 25) &
(dataframe['fastd'] < 25) &
(dataframe['adx'] > 30),
(dataframe['rsi'] < 37) &
(dataframe['fastd'] < 48) &
(dataframe['adx'] > 31),
'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
@ -87,12 +106,16 @@ def analyze_ticker(pair: str) -> DataFrame:
dataframe = parse_ticker_dataframe(ticker_hist)
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
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
: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):
return False
signal = latest['buy'] == 1
logger.debug('buy_trigger: %s (pair=%s, signal=%s)', latest['date'], pair, signal)
return signal
result = latest[signal.value] == 1
logger.debug('%s_trigger: %s (pair=%s, signal=%s)', signal.value, latest['date'], pair, result)
return result

View File

@ -1,9 +1,12 @@
# pragma pylint: disable=W0603
""" Cryptocurrency Exchanges support """
import enum
import logging
from random import randint
from typing import List, Dict, Any, Optional
import arrow
import requests
from cachetools import cached, TTLCache
from freqtrade.exchange.bittrex import Bittrex
@ -63,7 +66,12 @@ def validate_pairs(pairs: List[str]) -> None:
:param pairs: list of pairs
:return: None
"""
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']
for pair in pairs:
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:
if _CONF['dry_run']:
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] = {
'pair': pair,
'rate': rate,
@ -95,7 +103,7 @@ def buy(pair: str, rate: float, amount: float) -> str:
def sell(pair: str, rate: float, amount: float) -> str:
if _CONF['dry_run']:
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] = {
'pair': pair,
'rate': rate,

View File

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

View File

@ -2,6 +2,7 @@
import copy
import json
import logging
import sys
import time
import traceback
from datetime import datetime
@ -10,13 +11,12 @@ from typing import Dict, Optional, List
import requests
from cachetools import cached, TTLCache
from jsonschema import validate
from freqtrade import __version__, exchange, persistence
from freqtrade.analyze import get_buy_signal
from freqtrade.misc import CONF_SCHEMA, State, get_state, update_state, build_arg_parser, throttle
from freqtrade import __version__, exchange, persistence, rpc
from freqtrade.analyze import get_signal, SignalType
from freqtrade.misc import State, get_state, update_state, parse_args, throttle, \
load_config, FreqtradeException
from freqtrade.persistence import Trade
from freqtrade.rpc import telegram
logger = logging.getLogger('freqtrade')
@ -76,8 +76,8 @@ def _process(dynamic_whitelist: Optional[bool] = False) -> bool:
'Checked all whitelisted currencies. '
'Found no suitable entry positions for buying. Will keep looking ...'
)
except ValueError:
logger.exception('Unable to create trade')
except FreqtradeException as e:
logger.warning('Unable to create trade: %s', e)
for trade in trades:
# 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)
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
state_changed = handle_trade(trade) or state_changed
Trade.session.flush()
except (requests.exceptions.RequestException, json.JSONDecodeError) as error:
msg = 'Got {} in _process(), retrying in 30 seconds...'.format(error.__class__.__name__)
logger.exception(msg)
logger.warning(
'Got %s in _process(), retrying in 30 seconds...',
error
)
time.sleep(30)
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(),
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
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:
"""
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
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.pair.replace('_', '/'),
exchange.get_pair_detail_url(trade.pair),
limit,
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
"""
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.')
return True
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
for duration, threshold in sorted(_CONF['minimal_roi'].items()):
if time_diff > float(duration) and current_profit > threshold:
return True
@ -179,7 +158,7 @@ def handle_trade(trade: Trade) -> bool:
logger.debug('Handling %s ...', trade)
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)
return True
return False
@ -206,7 +185,7 @@ def create_trade(stake_amount: float) -> Optional[Trade]:
whitelist = copy.deepcopy(_CONF['exchange']['pair_whitelist'])
# Check if stake_amount is fulfilled
if exchange.get_balance(_CONF['stake_currency']) < stake_amount:
raise ValueError(
raise FreqtradeException(
'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)
logger.debug('Ignoring %s in pair whitelist', trade.pair)
if not whitelist:
raise ValueError('No pair in whitelist')
raise FreqtradeException('No pair in whitelist')
# Pick pair based on StochRSI buy signals
for _pair in whitelist:
if get_buy_signal(_pair):
if get_signal(_pair, SignalType.BUY):
pair = _pair
break
else:
@ -233,14 +212,12 @@ def create_trade(stake_amount: float) -> Optional[Trade]:
order_id = exchange.buy(pair, buy_limit, amount)
# Create trade entity and return
message = '*{}:* Buying [{}]({}) with limit `{:.8f}`'.format(
rpc.send_msg('*{}:* Buying [{}]({}) with limit `{:.8f}`'.format(
exchange.get_name().upper(),
pair.replace('_', '/'),
exchange.get_pair_detail_url(pair),
buy_limit
)
logger.info(message)
telegram.send_msg(message)
))
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
return Trade(pair=pair,
stake_amount=stake_amount,
@ -260,7 +237,7 @@ def init(config: dict, db_url: Optional[str] = None) -> None:
:return: None
"""
# Initialize all modules
telegram.init(config)
rpc.init(config)
persistence.init(config, db_url)
exchange.init(config)
@ -298,11 +275,11 @@ def cleanup(*args, **kwargs) -> None:
Cleanup the application state und finish all pending tasks
:return: None
"""
telegram.send_msg('*Status:* `Stopping trader...`')
rpc.send_msg('*Status:* `Stopping trader...`')
logger.info('Stopping trader and cleaning up modules...')
update_state(State.STOPPED)
persistence.cleanup()
telegram.cleanup()
rpc.cleanup()
exit(0)
@ -312,7 +289,9 @@ def main():
:return: None
"""
global _CONF
args = build_arg_parser().parse_args()
args = parse_args(sys.argv[1:])
if not args:
exit(0)
# Initialize logger
logging.basicConfig(
@ -327,12 +306,7 @@ def main():
)
# Load and validate configuration
with open(args.config) as file:
_CONF = json.load(file)
if 'internals' not in _CONF:
_CONF['internals'] = {}
logger.info('Validating configuration ...')
validate(_CONF, CONF_SCHEMA)
_CONF = load_config(args.config)
# Initialize all modules and start main loop
if args.dynamic_whitelist:
@ -343,7 +317,7 @@ def main():
new_state = get_state()
# Log state transition
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)
if new_state == State.STOPPED:

View File

@ -1,9 +1,13 @@
import argparse
import enum
import json
import logging
from typing import Any, Callable
import os
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 freqtrade import __version__
@ -11,6 +15,10 @@ from freqtrade import __version__
logger = logging.getLogger(__name__)
class FreqtradeException(BaseException):
pass
class State(enum.Enum):
RUNNING = 0
STOPPED = 1
@ -40,6 +48,27 @@ def get_state() -> 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:
"""
Throttles the given callable that it
@ -57,8 +86,11 @@ def throttle(func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any:
return result
def build_arg_parser() -> argparse.ArgumentParser:
""" Builds and returns an ArgumentParser instance """
def parse_args(args: List[str]):
"""
Parses given arguments and returns an argparse Namespace instance.
Returns None if a sub command has been selected and executed.
"""
parser = argparse.ArgumentParser(
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',
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
@ -146,7 +225,10 @@ CONF_SCHEMA = {
'secret': {'type': 'string'},
'pair_whitelist': {
'type': 'array',
'items': {'type': 'string'},
'items': {
'type': 'string',
'pattern': '^[0-9A-Z]+_[0-9A-Z]+$'
},
'uniqueItems': True
}
},

View File

@ -85,20 +85,27 @@ class Trade(_DECL_BASE):
if not order['closed']:
return
logger.debug('Updating trade (id=%d) ...', self.id)
logger.info('Updating trade (id=%d) ...', self.id)
if order['type'] == 'LIMIT_BUY':
# Update open rate and actual amount
self.open_rate = order['rate']
self.amount = order['amount']
logger.info('LIMIT_BUY has been fulfilled for %s.', self)
elif order['type'] == 'LIMIT_SELL':
# Set close rate and set actual profit
self.close_rate = order['rate']
self.close_profit = self.calc_profit()
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:
raise ValueError('Unknown order type: {}'.format(order['type']))
self.open_order_id = None
Trade.session.flush()
def calc_profit(self, rate: Optional[float] = None) -> float:
"""

View File

@ -1 +1,42 @@
import logging
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
from sqlalchemy import and_, func, text
from telegram import ParseMode, Bot, Update
from telegram.error import NetworkError
from telegram.error import NetworkError, TelegramError
from telegram.ext import CommandHandler, Updater
from freqtrade import exchange, __version__
@ -57,7 +57,7 @@ def init(config: dict) -> None:
_UPDATER.dispatcher.add_handler(handle)
_UPDATER.start_polling(
clean=True,
bootstrap_retries=3,
bootstrap_retries=-1,
timeout=30,
read_latency=60,
)
@ -475,13 +475,17 @@ def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDO
return
bot = bot or _UPDATER.bot
try:
try:
bot.send_message(_CONF['telegram']['chat_id'], msg, parse_mode=parse_mode)
except NetworkError as error:
except NetworkError as network_err:
# Sometimes the telegram server resets the current connection,
# if this is the case we send the message again.
logger.warning(
'Got Telegram NetworkError: %s! Trying one more time.',
error.message
network_err.message
)
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
import json
from datetime import datetime
from unittest.mock import MagicMock
@ -55,6 +54,8 @@ def default_conf():
@pytest.fixture(scope="module")
def backtest_conf():
return {
"stake_currency": "BTC",
"stake_amount": 0.01,
"minimal_roi": {
"40": 0.0,
"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
def update():
_update = Update(0)

View File

@ -1,16 +1,16 @@
# pragma pylint: disable=missing-docstring
from datetime import datetime
# pragma pylint: disable=missing-docstring,W0621
import json
import arrow
import pytest
from pandas import DataFrame
from freqtrade.analyze import parse_ticker_dataframe, populate_buy_trend, populate_indicators, \
get_buy_signal
get_signal, SignalType, populate_sell_trend
@pytest.fixture
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))
@ -20,20 +20,34 @@ def test_dataframe_correct_columns(result):
def test_dataframe_correct_length(result):
assert len(result.index) == 5751
assert len(result.index) == 14382
def test_populates_buy_trend(result):
dataframe = populate_buy_trend(populate_indicators(result))
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):
buydf = DataFrame([{'buy': 1, 'date': datetime.today()}])
buydf = DataFrame([{'buy': 1, 'date': arrow.utcnow()}])
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)
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 os
from typing import Tuple, Dict
import pytest
import arrow
import pytest
from pandas import DataFrame
from tabulate import tabulate
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.main import should_sell
from freqtrade.main import min_roi_reached
from freqtrade.misc import load_config
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):
return 'Made {} buys. Average profit {:.2f}%. Total profit was {:.3f}. Average duration {:.1f} mins.'.format(
len(results.index), results.profit.mean() * 100.0, results.profit.sum(), results.duration.mean() * 5)
def format_results(results: DataFrame):
return ('Made {:6d} buys. Average profit {: 5.2f}%. '
'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):
print('For currency {}:'.format(pair))
print(format_results(results[results.currency == pair]))
def preprocess(backdata) -> Dict[str, DataFrame]:
processed = {}
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 = []
exchange._API = Bittrex({'key': '', 'secret': ''})
mocked_history = mocker.patch('freqtrade.analyze.get_ticker_history')
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 backdata.items():
mocked_history.return_value = pair_data
ticker = analyze_ticker(pair)[['close', 'date', 'buy']].copy()
for pair, pair_data in processed.items():
pair_data['buy'] = 0
pair_data['sell'] = 0
ticker = populate_sell_trend(populate_buy_trend(pair_data))
# for each buy point
for row in ticker[ticker.buy == 1].itertuples(index=True):
trade = Trade(
open_rate=row.close,
open_date=row.date,
amount=1,
amount=backtest_conf['stake_amount'],
fee=exchange.get_fee() * 2
)
# calculate win/lose forwards from buy point
for row2 in ticker[row.Index:].itertuples(index=True):
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)
trades.append((pair, current_profit, row2.Index - row.Index))
break
labels = ['currency', 'profit', 'duration']
results = DataFrame.from_records(trades, columns=labels)
return results
return DataFrame.from_records(trades, columns=labels)
@pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set")
def test_backtest(backtest_conf, backdata, mocker, report=True):
results = backtest(backtest_conf, backdata, mocker)
@pytest.mark.skipif(not os.environ.get('BACKTEST'), reason="BACKTEST not set")
def test_backtest(backtest_conf, mocker):
print('')
exchange._API = Bittrex({'key': '', 'secret': ''})
print('====================== BACKTESTING REPORT ================================')
for pair in backdata:
print_pair_results(pair, results)
print('TOTAL OVER ALL TRADES:')
print(format_results(results))
# Load configuration file based on env variable
conf_path = os.environ.get('BACKTEST_CONFIG')
if conf_path:
print('Using config: {} ...'.format(conf_path))
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
import pytest
@ -33,4 +33,3 @@ def test_validate_pairs_not_compatible(default_conf, mocker):
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
with pytest.raises(RuntimeError, match=r'not compatible'):
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 os
from functools import reduce
@ -9,16 +9,22 @@ import pytest
from hyperopt import fmin, tpe, hp, Trials, STATUS_OK
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 preprocess
from freqtrade.vendor.qtpylib.indicators import crossed_above
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
TARGET_TRADES = 1300
TARGET_TRADES = 1100
TOTAL_TRIES = 4
# pylint: disable=C0103
current_tries = 0
def buy_strategy_generator(params):
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
conditions = []
@ -59,32 +65,36 @@ def buy_strategy_generator(params):
dataframe.loc[
reduce(lambda x, y: x & y, conditions),
'buy'] = 1
dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
return dataframe
return populate_buy_trend
@pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set")
def test_hyperopt(backtest_conf, backdata, mocker):
mocked_buy_trend = mocker.patch('freqtrade.analyze.populate_buy_trend')
def test_hyperopt(backtest_conf, mocker):
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):
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)
total_profit = results.profit.sum() * 1000
trade_count = len(results.index)
trade_loss = 1 - 0.4 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.2)
profit_loss = max(0, 1 - total_profit / 15000) # max profit 15000
trade_loss = 1 - 0.35 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.2)
profit_loss = max(0, 1 - total_profit / 10000) # max profit 10000
# pylint: disable=W0603
global current_tries
current_tries += 1
print('{}/{}: {}'.format(current_tries, TOTAL_TRIES, result))
print('{:5d}/{}: {}'.format(current_tries, TOTAL_TRIES, result))
return {
'loss': trade_loss + profit_loss,
@ -146,3 +156,8 @@ def test_hyperopt(backtest_conf, backdata, mocker):
print('Best parameters {}'.format(best))
newlist = sorted(trials.results, key=itemgetter('loss'))
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
from unittest.mock import MagicMock
@ -7,16 +7,17 @@ import requests
from sqlalchemy import create_engine
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
from freqtrade.misc import get_state, State
from freqtrade.misc import get_state, State, FreqtradeException
from freqtrade.persistence import Trade
def test_process_trade_creation(default_conf, ticker, health, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
@ -25,7 +26,7 @@ def test_process_trade_creation(default_conf, ticker, health, mocker):
init(default_conf, create_engine('sqlite://'))
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
assert len(trades) == 0
assert not trades
result = _process()
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):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
sleep_mock = mocker.patch('time.sleep', side_effect=lambda _: None)
mocker.patch.multiple('freqtrade.main.exchange',
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):
msg_mock = MagicMock()
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=msg_mock)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=msg_mock)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
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):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_signal',
side_effect=lambda *args: False if args[1] == SignalType.SELL else True)
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
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://'))
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
assert len(trades) == 0
assert not trades
result = _process()
assert result is True
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):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
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):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy'),
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'])
def test_create_trade_no_pairs(default_conf, ticker, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
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['exchange']['pair_whitelist'] = []
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):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
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)
assert trade.open_order_id == 'mocked_limit_sell'
assert close_trade_if_fulfilled(trade) is False
# Simulate fulfilled LIMIT_SELL order for trade
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):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
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
init(default_conf, create_engine('sqlite://'))
trade = create_trade(15.0)
trade.update(limit_buy_order)
trade.update(limit_sell_order)
Trade.session.add(trade)
Trade.session.flush()
trade.update(limit_buy_order)
trade = Trade.query.filter(Trade.is_open.is_(True)).first()
assert trade
# Simulate that there is no open order
trade.open_order_id = None
trade.update(limit_sell_order)
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.*'):
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
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():
@ -18,3 +26,124 @@ def test_throttle():
result = throttle(func, -1)
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
from datetime import datetime
from random import randint
from unittest.mock import MagicMock
import pytest
from sqlalchemy import create_engine
from telegram import Update, Message, Chat
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.persistence import Trade
from freqtrade.rpc import telegram
from freqtrade.rpc.telegram import (
_status, _status_table, _profit, _forcesell, _performance, _count, _start, _stop, _balance,
authorized_only, _help, is_enabled, send_msg,
_version)
from freqtrade.rpc.telegram import authorized_only, is_enabled, send_msg, _status, _status_table, \
_profit, _forcesell, _performance, _count, _start, _stop, _balance, _version, _help
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):
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()
mocker.patch.multiple('freqtrade.main.telegram',
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
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
_status(bot=MagicMock(), update=update)
assert msg_mock.call_count == 2
assert '[BTC_ETH]' in msg_mock.call_args_list[-1][0][0]
assert msg_mock.call_count == 1
assert '[BTC_ETH]' in msg_mock.call_args_list[0][0][0]
def test_status_table_handle(default_conf, update, ticker, mocker):
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()
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
mocker.patch.multiple(
'freqtrade.main.telegram',
'freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
@ -155,14 +154,15 @@ def test_status_table_handle(default_conf, update, ticker, mocker):
assert int(fields[0]) == 1
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):
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()
mocker.patch.multiple('freqtrade.main.telegram',
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
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)
_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]
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):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram',
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker)
@ -225,19 +225,19 @@ def test_forcesell_handle(default_conf, update, ticker, mocker):
update.message.text = '/forcesell 1'
_forcesell(bot=MagicMock(), update=update)
assert msg_mock.call_count == 2
assert 'Selling [BTC/ETH]' in msg_mock.call_args_list[-1][0][0]
assert '0.07256061 (profit: ~-0.64%)' in msg_mock.call_args_list[-1][0][0]
assert rpc_mock.call_count == 2
assert 'Selling [BTC/ETH]' in rpc_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):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram',
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker)
@ -247,22 +247,21 @@ def test_forcesell_all_handle(default_conf, update, ticker, mocker):
for _ in range(4):
Trade.session.add(create_trade(15.0))
Trade.session.flush()
msg_mock.reset_mock()
rpc_mock.reset_mock()
update.message.text = '/forcesell all'
_forcesell(bot=MagicMock(), update=update)
assert msg_mock.call_count == 4
for args in msg_mock.call_args_list:
assert rpc_mock.call_count == 4
for args in rpc_mock.call_args_list:
assert '0.07256061 (profit: ~-0.64%)' in args[0][0]
def test_forcesell_handle_invalid(default_conf, update, mocker):
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()
mocker.patch.multiple('freqtrade.main.telegram',
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
@ -297,9 +296,10 @@ def test_forcesell_handle_invalid(default_conf, update, mocker):
def test_performance_handle(
default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker):
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()
mocker.patch.multiple('freqtrade.main.telegram',
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
@ -324,17 +324,17 @@ def test_performance_handle(
Trade.session.flush()
_performance(bot=MagicMock(), update=update)
assert msg_mock.call_count == 2
assert 'Performance' in msg_mock.call_args_list[-1][0][0]
assert '<code>BTC_ETH\t10.05%</code>' in msg_mock.call_args_list[-1][0][0]
assert msg_mock.call_count == 1
assert 'Performance' in msg_mock.call_args_list[0][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):
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()
mocker.patch.multiple(
'freqtrade.main.telegram',
'freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
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):
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()
mocker.patch.multiple('freqtrade.main.telegram',
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
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):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram',
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
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):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram',
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
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):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram',
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
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):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram',
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
@ -473,7 +473,7 @@ def test_balance_handle(default_conf, update, mocker):
}]
mocker.patch.dict('freqtrade.main._CONF', default_conf)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram',
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
@ -489,7 +489,7 @@ def test_balance_handle(default_conf, update, mocker):
def test_help_handle(default_conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram',
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
@ -502,7 +502,7 @@ def test_help_handle(default_conf, update, mocker):
def test_version_handle(default_conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram',
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
@ -514,12 +514,12 @@ def test_version_handle(default_conf, update, mocker):
def test_send_msg(default_conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.main.telegram',
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock())
bot = MagicMock()
send_msg('test', bot)
assert len(bot.method_calls) == 0
assert not bot.method_calls
bot.reset_mock()
default_conf['telegram']['enabled'] = True
@ -529,13 +529,12 @@ def test_send_msg(default_conf, mocker):
def test_send_msg_network_error(default_conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.main.telegram',
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock())
default_conf['telegram']['enabled'] = True
bot = MagicMock()
bot.send_message = MagicMock(side_effect=NetworkError('Oh snap'))
with pytest.raises(NetworkError, match=r'Oh snap'):
send_msg('test', bot)
# Bot should've tried to send it twice

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.exchange import Bittrex
PAIRS = ['BTC-OK', 'BTC-NEO', 'BTC-DASH', 'BTC-ETC', 'BTC-ETH', 'BTC-SNT']
TICKER_INTERVAL = 1 # ticker interval in minutes (currently implemented: 1 and 5)
PAIRS = [
'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__))
# Init Bittrex exchange
@ -16,8 +21,8 @@ exchange._API = Bittrex({'key': '', 'secret': ''})
for pair in PAIRS:
data = exchange.get_ticker_history(pair, TICKER_INTERVAL)
filename = path.join(OUTPUT_DIR, '{}-{}m.json'.format(
pair.lower(),
filename = path.join(OUTPUT_DIR, '{}-{}.json'.format(
pair,
TICKER_INTERVAL,
))
with open(filename, 'w') as fp: