Merge branch 'release/0.14.3'
This commit is contained in:
commit
e9dbdc9247
@ -1,3 +1,10 @@
|
||||
[MASTER]
|
||||
extension-pkg-whitelist=numpy,talib
|
||||
|
||||
[BASIC]
|
||||
good-names=logger
|
||||
ignore=vendor
|
||||
|
||||
[TYPECHECK]
|
||||
ignored-modules=numpy,talib
|
||||
|
||||
|
37
README.md
37
README.md
@ -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
|
||||
|
||||
```
|
||||
|
@ -1,3 +1,4 @@
|
||||
__version__ = '0.14.2'
|
||||
""" FreqTrade bot """
|
||||
__version__ = '0.14.3'
|
||||
|
||||
from . import main
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
"""
|
||||
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']
|
||||
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,
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
||||
# 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()):
|
||||
# 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:
|
||||
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:
|
||||
|
@ -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
|
||||
}
|
||||
},
|
||||
|
@ -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:
|
||||
"""
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
bot.send_message(_CONF['telegram']['chat_id'], msg, parse_mode=parse_mode)
|
||||
except NetworkError as error:
|
||||
# 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
|
||||
)
|
||||
bot.send_message(_CONF['telegram']['chat_id'], msg, parse_mode=parse_mode)
|
||||
try:
|
||||
bot.send_message(_CONF['telegram']['chat_id'], msg, parse_mode=parse_mode)
|
||||
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.',
|
||||
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)
|
||||
|
@ -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
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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']))
|
||||
|
@ -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'])
|
||||
|
||||
|
@ -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'])
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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')
|
||||
|
58
freqtrade/tests/test_rpc.py
Normal file
58
freqtrade/tests/test_rpc.py
Normal 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
|
@ -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,14 +529,13 @@ 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)
|
||||
send_msg('test', bot)
|
||||
|
||||
# Bot should've tried to send it twice
|
||||
assert len(bot.method_calls) == 2
|
1
freqtrade/tests/testdata/BTC_BCC-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_BCC-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_BCC-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_BCC-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_DASH-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_DASH-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_DASH-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_DASH-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_ETC-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_ETC-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_ETC-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_ETC-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_ETH-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_ETH-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_ETH-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_ETH-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_LSK-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_LSK-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_LSK-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_LSK-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_OK-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_OK-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_OK-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_OK-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_POWR-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_POWR-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_POWR-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_POWR-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_VTC-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_VTC-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_VTC-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_VTC-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_WAVES-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_WAVES-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_WAVES-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_WAVES-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_XLM-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_XLM-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_XLM-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_XLM-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-edg.json
vendored
1
freqtrade/tests/testdata/btc-edg.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-etc.json
vendored
1
freqtrade/tests/testdata/btc-etc.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-eth.json
vendored
1
freqtrade/tests/testdata/btc-eth.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-ltc.json
vendored
1
freqtrade/tests/testdata/btc-ltc.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-mtl.json
vendored
1
freqtrade/tests/testdata/btc-mtl.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-neo.json
vendored
1
freqtrade/tests/testdata/btc-neo.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-omg.json
vendored
1
freqtrade/tests/testdata/btc-omg.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-pay.json
vendored
1
freqtrade/tests/testdata/btc-pay.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-pivx.json
vendored
1
freqtrade/tests/testdata/btc-pivx.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-qtum.json
vendored
1
freqtrade/tests/testdata/btc-qtum.json
vendored
File diff suppressed because one or more lines are too long
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user