Merge pull request #537 from gcarq/feature/objectify
Switch from procedural code to object + Code coverage 99.09%
This commit is contained in:
commit
62a3366fbf
@ -1,4 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
from freqtrade.main import main
|
import sys
|
||||||
main()
|
|
||||||
|
from freqtrade.main import main, set_loggers
|
||||||
|
set_loggers()
|
||||||
|
main(sys.argv[1:])
|
||||||
|
@ -66,7 +66,7 @@ python3 ./freqtrade/main.py --strategy my_awesome_strategy
|
|||||||
```
|
```
|
||||||
|
|
||||||
If the bot does not find your strategy file, it will display in an error
|
If the bot does not find your strategy file, it will display in an error
|
||||||
message the reason (File not found, or errors in your code).
|
message the reason (File not found, or errors in your code).
|
||||||
|
|
||||||
Learn more about strategy file in [optimize your bot](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-optimization.md).
|
Learn more about strategy file in [optimize your bot](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-optimization.md).
|
||||||
|
|
||||||
|
@ -1,122 +1,205 @@
|
|||||||
"""
|
"""
|
||||||
Functions to analyze ticker data with indicators and produce buy and sell signals
|
Functions to analyze ticker data with indicators and produce buy and sell signals
|
||||||
"""
|
"""
|
||||||
import logging
|
from datetime import datetime, timedelta
|
||||||
from datetime import timedelta
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Dict, List
|
from typing import Dict, List, Tuple
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
from pandas import DataFrame, to_datetime
|
from pandas import DataFrame, to_datetime
|
||||||
|
|
||||||
from freqtrade.exchange import get_ticker_history
|
from freqtrade.exchange import get_ticker_history
|
||||||
|
from freqtrade.logger import Logger
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.strategy.strategy import Strategy
|
from freqtrade.strategy.strategy import Strategy
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class SignalType(Enum):
|
class SignalType(Enum):
|
||||||
""" Enum to distinguish between buy and sell signals """
|
"""
|
||||||
|
Enum to distinguish between buy and sell signals
|
||||||
|
"""
|
||||||
BUY = "buy"
|
BUY = "buy"
|
||||||
SELL = "sell"
|
SELL = "sell"
|
||||||
|
|
||||||
|
|
||||||
def parse_ticker_dataframe(ticker: list) -> DataFrame:
|
class Analyze(object):
|
||||||
"""
|
"""
|
||||||
Analyses the trend for the given ticker history
|
Analyze class contains everything the bot need to determine if the situation is good for
|
||||||
:param ticker: See exchange.get_ticker_history
|
buying or selling.
|
||||||
:return: DataFrame
|
|
||||||
"""
|
"""
|
||||||
columns = {'C': 'close', 'V': 'volume', 'O': 'open', 'H': 'high', 'L': 'low', 'T': 'date'}
|
def __init__(self, config: dict) -> None:
|
||||||
frame = DataFrame(ticker) \
|
"""
|
||||||
.rename(columns=columns)
|
Init Analyze
|
||||||
if 'BV' in frame:
|
:param config: Bot configuration (use the one from Configuration())
|
||||||
frame.drop('BV', 1, inplace=True)
|
"""
|
||||||
frame['date'] = to_datetime(frame['date'], utc=True, infer_datetime_format=True)
|
self.logger = Logger(name=__name__, level=config.get('loglevel')).get_logger()
|
||||||
frame.sort_values('date', inplace=True)
|
|
||||||
return frame
|
|
||||||
|
|
||||||
|
self.config = config
|
||||||
|
self.strategy = Strategy(self.config)
|
||||||
|
|
||||||
def populate_indicators(dataframe: DataFrame) -> DataFrame:
|
@staticmethod
|
||||||
"""
|
def parse_ticker_dataframe(ticker: list) -> DataFrame:
|
||||||
Adds several different TA indicators to the given DataFrame
|
"""
|
||||||
|
Analyses the trend for the given ticker history
|
||||||
|
:param ticker: See exchange.get_ticker_history
|
||||||
|
:return: DataFrame
|
||||||
|
"""
|
||||||
|
columns = {'C': 'close', 'V': 'volume', 'O': 'open', 'H': 'high', 'L': 'low', 'T': 'date'}
|
||||||
|
frame = DataFrame(ticker) \
|
||||||
|
.rename(columns=columns)
|
||||||
|
if 'BV' in frame:
|
||||||
|
frame.drop('BV', 1, inplace=True)
|
||||||
|
frame['date'] = to_datetime(frame['date'], utc=True, infer_datetime_format=True)
|
||||||
|
frame.sort_values('date', inplace=True)
|
||||||
|
return frame
|
||||||
|
|
||||||
Performance Note: For the best performance be frugal on the number of indicators
|
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
||||||
you are using. Let uncomment only the indicator you are using in your strategies
|
"""
|
||||||
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
|
Adds several different TA indicators to the given DataFrame
|
||||||
"""
|
|
||||||
strategy = Strategy()
|
|
||||||
return strategy.populate_indicators(dataframe=dataframe)
|
|
||||||
|
|
||||||
|
Performance Note: For the best performance be frugal on the number of indicators
|
||||||
|
you are using. Let uncomment only the indicator you are using in your strategies
|
||||||
|
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
|
||||||
|
"""
|
||||||
|
return self.strategy.populate_indicators(dataframe=dataframe)
|
||||||
|
|
||||||
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Based on TA indicators, populates the buy signal for the given dataframe
|
Based on TA indicators, populates the buy signal for the given dataframe
|
||||||
:param dataframe: DataFrame
|
:param dataframe: DataFrame
|
||||||
:return: DataFrame with buy column
|
:return: DataFrame with buy column
|
||||||
"""
|
"""
|
||||||
strategy = Strategy()
|
return self.strategy.populate_buy_trend(dataframe=dataframe)
|
||||||
return strategy.populate_buy_trend(dataframe=dataframe)
|
|
||||||
|
|
||||||
|
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Based on TA indicators, populates the sell signal for the given dataframe
|
||||||
|
:param dataframe: DataFrame
|
||||||
|
:return: DataFrame with buy column
|
||||||
|
"""
|
||||||
|
return self.strategy.populate_sell_trend(dataframe=dataframe)
|
||||||
|
|
||||||
def populate_sell_trend(dataframe: DataFrame) -> DataFrame:
|
def get_ticker_interval(self) -> int:
|
||||||
"""
|
"""
|
||||||
Based on TA indicators, populates the sell signal for the given dataframe
|
Return ticker interval to use
|
||||||
:param dataframe: DataFrame
|
:return: Ticker interval value to use
|
||||||
:return: DataFrame with buy column
|
"""
|
||||||
"""
|
return self.strategy.ticker_interval
|
||||||
strategy = Strategy()
|
|
||||||
return strategy.populate_sell_trend(dataframe=dataframe)
|
|
||||||
|
|
||||||
|
def analyze_ticker(self, ticker_history: List[Dict]) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Parses the given ticker history and returns a populated DataFrame
|
||||||
|
add several TA indicators and buy signal to it
|
||||||
|
:return DataFrame with ticker data and indicator data
|
||||||
|
"""
|
||||||
|
dataframe = self.parse_ticker_dataframe(ticker_history)
|
||||||
|
dataframe = self.populate_indicators(dataframe)
|
||||||
|
dataframe = self.populate_buy_trend(dataframe)
|
||||||
|
dataframe = self.populate_sell_trend(dataframe)
|
||||||
|
return dataframe
|
||||||
|
|
||||||
def analyze_ticker(ticker_history: List[Dict]) -> DataFrame:
|
def get_signal(self, pair: str, interval: int) -> Tuple[bool, bool]:
|
||||||
"""
|
"""
|
||||||
Parses the given ticker history and returns a populated DataFrame
|
Calculates current signal based several technical analysis indicators
|
||||||
add several TA indicators and buy signal to it
|
:param pair: pair in format BTC_ANT or BTC-ANT
|
||||||
:return DataFrame with ticker data and indicator data
|
:param interval: Interval to use (in min)
|
||||||
"""
|
:return: (Buy, Sell) A bool-tuple indicating buy/sell signal
|
||||||
dataframe = parse_ticker_dataframe(ticker_history)
|
"""
|
||||||
dataframe = populate_indicators(dataframe)
|
ticker_hist = get_ticker_history(pair, interval)
|
||||||
dataframe = populate_buy_trend(dataframe)
|
if not ticker_hist:
|
||||||
dataframe = populate_sell_trend(dataframe)
|
self.logger.warning('Empty ticker history for pair %s', pair)
|
||||||
return dataframe
|
return False, False
|
||||||
|
|
||||||
|
try:
|
||||||
|
dataframe = self.analyze_ticker(ticker_hist)
|
||||||
|
except ValueError as error:
|
||||||
|
self.logger.warning(
|
||||||
|
'Unable to analyze ticker for pair %s: %s',
|
||||||
|
pair,
|
||||||
|
str(error)
|
||||||
|
)
|
||||||
|
return False, False
|
||||||
|
except Exception as error:
|
||||||
|
self.logger.exception(
|
||||||
|
'Unexpected error when analyzing ticker for pair %s: %s',
|
||||||
|
pair,
|
||||||
|
str(error)
|
||||||
|
)
|
||||||
|
return False, False
|
||||||
|
|
||||||
# FIX: Maybe return False, if an error has occured,
|
if dataframe.empty:
|
||||||
# Otherwise we might mask an error as an non-signal-scenario
|
self.logger.warning('Empty dataframe for pair %s', pair)
|
||||||
def get_signal(pair: str, interval: int) -> (bool, bool):
|
return False, False
|
||||||
"""
|
|
||||||
Calculates current signal based several technical analysis indicators
|
|
||||||
:param pair: pair in format BTC_ANT or BTC-ANT
|
|
||||||
:return: (Buy, Sell) A bool-tuple indicating buy/sell signal
|
|
||||||
"""
|
|
||||||
ticker_hist = get_ticker_history(pair, interval)
|
|
||||||
if not ticker_hist:
|
|
||||||
logger.warning('Empty ticker history for pair %s', pair)
|
|
||||||
return (False, False) # return False ?
|
|
||||||
|
|
||||||
try:
|
latest = dataframe.iloc[-1]
|
||||||
dataframe = analyze_ticker(ticker_hist)
|
|
||||||
except ValueError as ex:
|
|
||||||
logger.warning('Unable to analyze ticker for pair %s: %s', pair, str(ex))
|
|
||||||
return (False, False) # return False ?
|
|
||||||
except Exception as ex:
|
|
||||||
logger.exception('Unexpected error when analyzing ticker for pair %s: %s', pair, str(ex))
|
|
||||||
return (False, False) # return False ?
|
|
||||||
|
|
||||||
if dataframe.empty:
|
# Check if dataframe is out of date
|
||||||
logger.warning('Empty dataframe for pair %s', pair)
|
signal_date = arrow.get(latest['date'])
|
||||||
return (False, False) # return False ?
|
if signal_date < arrow.utcnow() - timedelta(minutes=(interval + 5)):
|
||||||
|
self.logger.warning(
|
||||||
|
'Outdated history for pair %s. Last tick is %s minutes old',
|
||||||
|
pair,
|
||||||
|
(arrow.utcnow() - signal_date).seconds // 60
|
||||||
|
)
|
||||||
|
return False, False
|
||||||
|
|
||||||
latest = dataframe.iloc[-1]
|
(buy, sell) = latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1
|
||||||
|
self.logger.debug(
|
||||||
|
'trigger: %s (pair=%s) buy=%s sell=%s',
|
||||||
|
latest['date'],
|
||||||
|
pair,
|
||||||
|
str(buy),
|
||||||
|
str(sell)
|
||||||
|
)
|
||||||
|
return buy, sell
|
||||||
|
|
||||||
# Check if dataframe is out of date
|
def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool, sell: bool) -> bool:
|
||||||
signal_date = arrow.get(latest['date'])
|
"""
|
||||||
if signal_date < arrow.utcnow() - timedelta(minutes=(interval + 5)):
|
This function evaluate if on the condition required to trigger a sell has been reached
|
||||||
logger.warning('Outdated history for pair %s. Last tick is %s minutes old',
|
if the threshold is reached and updates the trade record.
|
||||||
pair, (arrow.utcnow() - signal_date).seconds // 60)
|
:return: True if trade should be sold, False otherwise
|
||||||
return (False, False) # return False ?
|
"""
|
||||||
|
# Check if minimal roi has been reached and no longer in buy conditions (avoiding a fee)
|
||||||
|
if self.min_roi_reached(trade=trade, current_rate=rate, current_time=date):
|
||||||
|
self.logger.debug('Required profit reached. Selling..')
|
||||||
|
return True
|
||||||
|
|
||||||
(buy, sell) = latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1
|
# Experimental: Check if the trade is profitable before selling it (avoid selling at loss)
|
||||||
logger.debug('trigger: %s (pair=%s) buy=%s sell=%s', latest['date'], pair, str(buy), str(sell))
|
if self.config.get('experimental', {}).get('sell_profit_only', False):
|
||||||
return (buy, sell)
|
self.logger.debug('Checking if trade is profitable..')
|
||||||
|
if trade.calc_profit(rate=rate) <= 0:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if sell and not buy and self.config.get('experimental', {}).get('use_sell_signal', False):
|
||||||
|
self.logger.debug('Sell signal received. Selling..')
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def min_roi_reached(self, trade: Trade, current_rate: float, current_time: datetime) -> bool:
|
||||||
|
"""
|
||||||
|
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_percent(current_rate)
|
||||||
|
if self.strategy.stoploss is not None and current_profit < self.strategy.stoploss:
|
||||||
|
self.logger.debug('Stop loss hit.')
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check if time matches and current rate is above threshold
|
||||||
|
time_diff = (current_time.timestamp() - trade.open_date.timestamp()) / 60
|
||||||
|
for duration, threshold in self.strategy.minimal_roi.items():
|
||||||
|
if time_diff <= duration:
|
||||||
|
return False
|
||||||
|
if current_profit > threshold:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def tickerdata_to_dataframe(self, tickerdata: Dict[str, List]) -> Dict[str, DataFrame]:
|
||||||
|
"""
|
||||||
|
Creates a dataframe and populates indicators for given ticker data
|
||||||
|
"""
|
||||||
|
return {pair: self.populate_indicators(self.parse_ticker_dataframe(pair_data))
|
||||||
|
for pair, pair_data in tickerdata.items()}
|
||||||
|
251
freqtrade/arguments.py
Normal file
251
freqtrade/arguments.py
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
"""
|
||||||
|
This module contains the argument manager class
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from typing import List, Tuple, Optional
|
||||||
|
|
||||||
|
from freqtrade import __version__
|
||||||
|
from freqtrade.constants import Constants
|
||||||
|
|
||||||
|
|
||||||
|
class Arguments(object):
|
||||||
|
"""
|
||||||
|
Arguments Class. Manage the arguments received by the cli
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, args: List[str], description: str):
|
||||||
|
self.args = args
|
||||||
|
self.parsed_arg = None
|
||||||
|
self.parser = argparse.ArgumentParser(description=description)
|
||||||
|
|
||||||
|
def _load_args(self) -> None:
|
||||||
|
self.common_args_parser()
|
||||||
|
self._build_subcommands()
|
||||||
|
|
||||||
|
def get_parsed_arg(self) -> argparse.Namespace:
|
||||||
|
"""
|
||||||
|
Return the list of arguments
|
||||||
|
:return: List[str] List of arguments
|
||||||
|
"""
|
||||||
|
if self.parsed_arg is None:
|
||||||
|
self._load_args()
|
||||||
|
self.parsed_arg = self.parse_args()
|
||||||
|
|
||||||
|
return self.parsed_arg
|
||||||
|
|
||||||
|
def parse_args(self) -> argparse.Namespace:
|
||||||
|
"""
|
||||||
|
Parses given arguments and returns an argparse Namespace instance.
|
||||||
|
"""
|
||||||
|
parsed_arg = self.parser.parse_args(self.args)
|
||||||
|
|
||||||
|
return parsed_arg
|
||||||
|
|
||||||
|
def common_args_parser(self) -> None:
|
||||||
|
"""
|
||||||
|
Parses given common arguments and returns them as a parsed object.
|
||||||
|
"""
|
||||||
|
self.parser.add_argument(
|
||||||
|
'-v', '--verbose',
|
||||||
|
help='be verbose',
|
||||||
|
action='store_const',
|
||||||
|
dest='loglevel',
|
||||||
|
const=logging.DEBUG,
|
||||||
|
default=logging.INFO,
|
||||||
|
)
|
||||||
|
self.parser.add_argument(
|
||||||
|
'--version',
|
||||||
|
action='version',
|
||||||
|
version='%(prog)s {}'.format(__version__),
|
||||||
|
)
|
||||||
|
self.parser.add_argument(
|
||||||
|
'-c', '--config',
|
||||||
|
help='specify configuration file (default: %(default)s)',
|
||||||
|
dest='config',
|
||||||
|
default='config.json',
|
||||||
|
type=str,
|
||||||
|
metavar='PATH',
|
||||||
|
)
|
||||||
|
self.parser.add_argument(
|
||||||
|
'-d', '--datadir',
|
||||||
|
help='path to backtest data (default: %(default)s',
|
||||||
|
dest='datadir',
|
||||||
|
default=os.path.join('freqtrade', 'tests', 'testdata'),
|
||||||
|
type=str,
|
||||||
|
metavar='PATH',
|
||||||
|
)
|
||||||
|
self.parser.add_argument(
|
||||||
|
'-s', '--strategy',
|
||||||
|
help='specify strategy file (default: %(default)s)',
|
||||||
|
dest='strategy',
|
||||||
|
default='default_strategy',
|
||||||
|
type=str,
|
||||||
|
metavar='PATH',
|
||||||
|
)
|
||||||
|
self.parser.add_argument(
|
||||||
|
'--dynamic-whitelist',
|
||||||
|
help='dynamically generate and update whitelist \
|
||||||
|
based on 24h BaseVolume (Default 20 currencies)', # noqa
|
||||||
|
dest='dynamic_whitelist',
|
||||||
|
const=Constants.DYNAMIC_WHITELIST,
|
||||||
|
type=int,
|
||||||
|
metavar='INT',
|
||||||
|
nargs='?',
|
||||||
|
)
|
||||||
|
self.parser.add_argument(
|
||||||
|
'--dry-run-db',
|
||||||
|
help='Force dry run to use a local DB "tradesv3.dry_run.sqlite" \
|
||||||
|
instead of memory DB. Work only if dry_run is enabled.',
|
||||||
|
action='store_true',
|
||||||
|
dest='dry_run_db',
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def backtesting_options(parser: argparse.ArgumentParser) -> None:
|
||||||
|
"""
|
||||||
|
Parses given arguments for Backtesting scripts.
|
||||||
|
"""
|
||||||
|
parser.add_argument(
|
||||||
|
'-l', '--live',
|
||||||
|
help='using live data',
|
||||||
|
action='store_true',
|
||||||
|
dest='live',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-r', '--refresh-pairs-cached',
|
||||||
|
help='refresh the pairs files in tests/testdata with the latest data from Bittrex. \
|
||||||
|
Use it if you want to run your backtesting with up-to-date data.',
|
||||||
|
action='store_true',
|
||||||
|
dest='refresh_pairs',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--export',
|
||||||
|
help='export backtest results, argument are: trades\
|
||||||
|
Example --export=trades',
|
||||||
|
type=str,
|
||||||
|
default=None,
|
||||||
|
dest='export',
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def optimizer_shared_options(parser: argparse.ArgumentParser) -> None:
|
||||||
|
parser.add_argument(
|
||||||
|
'-i', '--ticker-interval',
|
||||||
|
help='specify ticker interval in minutes (1, 5, 30, 60, 1440)',
|
||||||
|
dest='ticker_interval',
|
||||||
|
type=int,
|
||||||
|
metavar='INT',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--realistic-simulation',
|
||||||
|
help='uses max_open_trades from config to simulate real world limitations',
|
||||||
|
action='store_true',
|
||||||
|
dest='realistic_simulation',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--timerange',
|
||||||
|
help='specify what timerange of data to use.',
|
||||||
|
default=None,
|
||||||
|
type=str,
|
||||||
|
dest='timerange',
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def hyperopt_options(parser: argparse.ArgumentParser) -> None:
|
||||||
|
"""
|
||||||
|
Parses given arguments for Hyperopt scripts.
|
||||||
|
"""
|
||||||
|
parser.add_argument(
|
||||||
|
'-e', '--epochs',
|
||||||
|
help='specify number of epochs (default: %(default)d)',
|
||||||
|
dest='epochs',
|
||||||
|
default=Constants.HYPEROPT_EPOCH,
|
||||||
|
type=int,
|
||||||
|
metavar='INT',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--use-mongodb',
|
||||||
|
help='parallelize evaluations with mongodb (requires mongod in PATH)',
|
||||||
|
dest='mongodb',
|
||||||
|
action='store_true',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-s', '--spaces',
|
||||||
|
help='Specify which parameters to hyperopt. Space separate list. \
|
||||||
|
Default: %(default)s',
|
||||||
|
choices=['all', 'buy', 'roi', 'stoploss'],
|
||||||
|
default='all',
|
||||||
|
nargs='+',
|
||||||
|
dest='spaces',
|
||||||
|
)
|
||||||
|
|
||||||
|
def _build_subcommands(self) -> None:
|
||||||
|
"""
|
||||||
|
Builds and attaches all subcommands
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
from freqtrade.optimize import backtesting, hyperopt
|
||||||
|
|
||||||
|
subparsers = self.parser.add_subparsers(dest='subparser')
|
||||||
|
|
||||||
|
# Add backtesting subcommand
|
||||||
|
backtesting_cmd = subparsers.add_parser('backtesting', help='backtesting module')
|
||||||
|
backtesting_cmd.set_defaults(func=backtesting.start)
|
||||||
|
self.optimizer_shared_options(backtesting_cmd)
|
||||||
|
self.backtesting_options(backtesting_cmd)
|
||||||
|
|
||||||
|
# Add hyperopt subcommand
|
||||||
|
hyperopt_cmd = subparsers.add_parser('hyperopt', help='hyperopt module')
|
||||||
|
hyperopt_cmd.set_defaults(func=hyperopt.start)
|
||||||
|
self.optimizer_shared_options(hyperopt_cmd)
|
||||||
|
self.hyperopt_options(hyperopt_cmd)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_timerange(text: str) -> Optional[Tuple[List, int, int]]:
|
||||||
|
"""
|
||||||
|
Parse the value of the argument --timerange to determine what is the range desired
|
||||||
|
:param text: value from --timerange
|
||||||
|
:return: Start and End range period
|
||||||
|
"""
|
||||||
|
if text is None:
|
||||||
|
return None
|
||||||
|
syntax = [(r'^-(\d{8})$', (None, 'date')),
|
||||||
|
(r'^(\d{8})-$', ('date', None)),
|
||||||
|
(r'^(\d{8})-(\d{8})$', ('date', 'date')),
|
||||||
|
(r'^(-\d+)$', (None, 'line')),
|
||||||
|
(r'^(\d+)-$', ('line', None)),
|
||||||
|
(r'^(\d+)-(\d+)$', ('index', 'index'))]
|
||||||
|
for rex, stype in syntax:
|
||||||
|
# Apply the regular expression to text
|
||||||
|
match = re.match(rex, text)
|
||||||
|
if match: # Regex has matched
|
||||||
|
rvals = match.groups()
|
||||||
|
index = 0
|
||||||
|
start = None
|
||||||
|
stop = None
|
||||||
|
if stype[0]:
|
||||||
|
start = rvals[index]
|
||||||
|
if stype[0] != 'date':
|
||||||
|
start = int(start)
|
||||||
|
index += 1
|
||||||
|
if stype[1]:
|
||||||
|
stop = rvals[index]
|
||||||
|
if stype[1] != 'date':
|
||||||
|
stop = int(stop)
|
||||||
|
return stype, start, stop
|
||||||
|
raise Exception('Incorrect syntax for timerange "%s"' % text)
|
||||||
|
|
||||||
|
def scripts_options(self) -> None:
|
||||||
|
"""
|
||||||
|
Parses given arguments for plot scripts.
|
||||||
|
"""
|
||||||
|
self.parser.add_argument(
|
||||||
|
'-p', '--pair',
|
||||||
|
help='Show profits for only this pairs. Pairs are comma-separated.',
|
||||||
|
dest='pair',
|
||||||
|
default=None
|
||||||
|
)
|
200
freqtrade/configuration.py
Normal file
200
freqtrade/configuration.py
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
"""
|
||||||
|
This module contains the configuration class
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from argparse import Namespace
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
from jsonschema import Draft4Validator, validate
|
||||||
|
from jsonschema.exceptions import ValidationError, best_match
|
||||||
|
|
||||||
|
from freqtrade.constants import Constants
|
||||||
|
from freqtrade.logger import Logger
|
||||||
|
|
||||||
|
|
||||||
|
class Configuration(object):
|
||||||
|
"""
|
||||||
|
Class to read and init the bot configuration
|
||||||
|
Reuse this class for the bot, backtesting, hyperopt and every script that required configuration
|
||||||
|
"""
|
||||||
|
def __init__(self, args: Namespace) -> None:
|
||||||
|
self.args = args
|
||||||
|
self.logging = Logger(name=__name__)
|
||||||
|
self.logger = self.logging.get_logger()
|
||||||
|
self.config = None
|
||||||
|
|
||||||
|
def load_config(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Extract information for sys.argv and load the bot configuration
|
||||||
|
:return: Configuration dictionary
|
||||||
|
"""
|
||||||
|
self.logger.info('Using config: %s ...', self.args.config)
|
||||||
|
config = self._load_config_file(self.args.config)
|
||||||
|
|
||||||
|
# Add the strategy file to use
|
||||||
|
config.update({'strategy': self.args.strategy})
|
||||||
|
|
||||||
|
# Load Common configuration
|
||||||
|
config = self._load_common_config(config)
|
||||||
|
|
||||||
|
# Load Backtesting
|
||||||
|
config = self._load_backtesting_config(config)
|
||||||
|
|
||||||
|
# Load Hyperopt
|
||||||
|
config = self._load_hyperopt_config(config)
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
def _load_config_file(self, path: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Loads a config file from the given path
|
||||||
|
:param path: path as str
|
||||||
|
:return: configuration as dictionary
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(path) as file:
|
||||||
|
conf = json.load(file)
|
||||||
|
except FileNotFoundError:
|
||||||
|
self.logger.critical(
|
||||||
|
'Config file "%s" not found. Please create your config file',
|
||||||
|
path
|
||||||
|
)
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
if 'internals' not in conf:
|
||||||
|
conf['internals'] = {}
|
||||||
|
self.logger.info('Validating configuration ...')
|
||||||
|
|
||||||
|
return self._validate_config(conf)
|
||||||
|
|
||||||
|
def _load_common_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Extract information for sys.argv and load common configuration
|
||||||
|
:return: configuration as dictionary
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Log level
|
||||||
|
if 'loglevel' in self.args and self.args.loglevel:
|
||||||
|
config.update({'loglevel': self.args.loglevel})
|
||||||
|
self.logging.set_level(self.args.loglevel)
|
||||||
|
self.logger.info('Log level set at %s', config['loglevel'])
|
||||||
|
|
||||||
|
# Add dynamic_whitelist if found
|
||||||
|
if 'dynamic_whitelist' in self.args and self.args.dynamic_whitelist:
|
||||||
|
config.update({'dynamic_whitelist': self.args.dynamic_whitelist})
|
||||||
|
self.logger.info(
|
||||||
|
'Parameter --dynamic-whitelist detected. '
|
||||||
|
'Using dynamically generated whitelist. '
|
||||||
|
'(not applicable with Backtesting and Hyperopt)'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add dry_run_db if found and the bot in dry run
|
||||||
|
if self.args.dry_run_db and config.get('dry_run', False):
|
||||||
|
config.update({'dry_run_db': True})
|
||||||
|
self.logger.info('Parameter --dry-run-db detected ...')
|
||||||
|
|
||||||
|
if config.get('dry_run_db', False):
|
||||||
|
if config.get('dry_run', False):
|
||||||
|
self.logger.info('Dry_run will use the DB file: "tradesv3.dry_run.sqlite"')
|
||||||
|
else:
|
||||||
|
self.logger.info('Dry run is disabled. (--dry_run_db ignored)')
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
def _load_backtesting_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Extract information for sys.argv and load Backtesting configuration
|
||||||
|
:return: configuration as dictionary
|
||||||
|
"""
|
||||||
|
|
||||||
|
# If -i/--ticker-interval is used we override the configuration parameter
|
||||||
|
# (that will override the strategy configuration)
|
||||||
|
if 'ticker_interval' in self.args and self.args.ticker_interval:
|
||||||
|
config.update({'ticker_interval': self.args.ticker_interval})
|
||||||
|
self.logger.info('Parameter -i/--ticker-interval detected ...')
|
||||||
|
self.logger.info('Using ticker_interval: %d ...', config.get('ticker_interval'))
|
||||||
|
|
||||||
|
# If -l/--live is used we add it to the configuration
|
||||||
|
if 'live' in self.args and self.args.live:
|
||||||
|
config.update({'live': True})
|
||||||
|
self.logger.info('Parameter -l/--live detected ...')
|
||||||
|
|
||||||
|
# If --realistic-simulation is used we add it to the configuration
|
||||||
|
if 'realistic_simulation' in self.args and self.args.realistic_simulation:
|
||||||
|
config.update({'realistic_simulation': True})
|
||||||
|
self.logger.info('Parameter --realistic-simulation detected ...')
|
||||||
|
self.logger.info('Using max_open_trades: %s ...', config.get('max_open_trades'))
|
||||||
|
|
||||||
|
# If --timerange is used we add it to the configuration
|
||||||
|
if 'timerange' in self.args and self.args.timerange:
|
||||||
|
config.update({'timerange': self.args.timerange})
|
||||||
|
self.logger.info('Parameter --timerange detected: %s ...', self.args.timerange)
|
||||||
|
|
||||||
|
# If --datadir is used we add it to the configuration
|
||||||
|
if 'datadir' in self.args and self.args.datadir:
|
||||||
|
config.update({'datadir': self.args.datadir})
|
||||||
|
self.logger.info('Parameter --datadir detected: %s ...', self.args.datadir)
|
||||||
|
|
||||||
|
# If -r/--refresh-pairs-cached is used we add it to the configuration
|
||||||
|
if 'refresh_pairs' in self.args and self.args.refresh_pairs:
|
||||||
|
config.update({'refresh_pairs': True})
|
||||||
|
self.logger.info('Parameter -r/--refresh-pairs-cached detected ...')
|
||||||
|
|
||||||
|
# If --export is used we add it to the configuration
|
||||||
|
if 'export' in self.args and self.args.export:
|
||||||
|
config.update({'export': self.args.export})
|
||||||
|
self.logger.info('Parameter --export detected: %s ...', self.args.export)
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
def _load_hyperopt_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Extract information for sys.argv and load Hyperopt configuration
|
||||||
|
:return: configuration as dictionary
|
||||||
|
"""
|
||||||
|
# If --realistic-simulation is used we add it to the configuration
|
||||||
|
if 'epochs' in self.args and self.args.epochs:
|
||||||
|
config.update({'epochs': self.args.epochs})
|
||||||
|
self.logger.info('Parameter --epochs detected ...')
|
||||||
|
self.logger.info('Will run Hyperopt with for %s epochs ...', config.get('epochs'))
|
||||||
|
|
||||||
|
# If --mongodb is used we add it to the configuration
|
||||||
|
if 'mongodb' in self.args and self.args.mongodb:
|
||||||
|
config.update({'mongodb': self.args.mongodb})
|
||||||
|
self.logger.info('Parameter --use-mongodb detected ...')
|
||||||
|
|
||||||
|
# If --spaces is used we add it to the configuration
|
||||||
|
if 'spaces' in self.args and self.args.spaces:
|
||||||
|
config.update({'spaces': self.args.spaces})
|
||||||
|
self.logger.info('Parameter -s/--spaces detected: %s', config.get('spaces'))
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
def _validate_config(self, conf: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Validate the configuration follow the Config Schema
|
||||||
|
:param conf: Config in JSON format
|
||||||
|
:return: Returns the config if valid, otherwise throw an exception
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
validate(conf, Constants.CONF_SCHEMA)
|
||||||
|
return conf
|
||||||
|
except ValidationError as exception:
|
||||||
|
self.logger.fatal(
|
||||||
|
'Invalid configuration. See config.json.example. Reason: %s',
|
||||||
|
exception
|
||||||
|
)
|
||||||
|
raise ValidationError(
|
||||||
|
best_match(Draft4Validator(Constants.CONF_SCHEMA).iter_errors(conf)).message
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_config(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Return the config. Use this method to get the bot config
|
||||||
|
:return: Dict: Bot config
|
||||||
|
"""
|
||||||
|
if self.config is None:
|
||||||
|
self.config = self.load_config()
|
||||||
|
|
||||||
|
return self.config
|
122
freqtrade/constants.py
Normal file
122
freqtrade/constants.py
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
# pragma pylint: disable=too-few-public-methods
|
||||||
|
|
||||||
|
"""
|
||||||
|
List bot constants
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Constants(object):
|
||||||
|
"""
|
||||||
|
Static class that contain all bot constants
|
||||||
|
"""
|
||||||
|
DYNAMIC_WHITELIST = 20 # pairs
|
||||||
|
PROCESS_THROTTLE_SECS = 5 # sec
|
||||||
|
TICKER_INTERVAL = 5 # min
|
||||||
|
HYPEROPT_EPOCH = 100 # epochs
|
||||||
|
RETRY_TIMEOUT = 30 # sec
|
||||||
|
DEFAULT_STRATEGY = 'default_strategy'
|
||||||
|
|
||||||
|
# Required json-schema for user specified config
|
||||||
|
CONF_SCHEMA = {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'max_open_trades': {'type': 'integer', 'minimum': 1},
|
||||||
|
'ticker_interval': {'type': 'integer', 'enum': [1, 5, 30, 60, 1440]},
|
||||||
|
'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT']},
|
||||||
|
'stake_amount': {'type': 'number', 'minimum': 0.0005},
|
||||||
|
'fiat_display_currency': {'type': 'string', 'enum': ['AUD', 'BRL', 'CAD', 'CHF',
|
||||||
|
'CLP', 'CNY', 'CZK', 'DKK',
|
||||||
|
'EUR', 'GBP', 'HKD', 'HUF',
|
||||||
|
'IDR', 'ILS', 'INR', 'JPY',
|
||||||
|
'KRW', 'MXN', 'MYR', 'NOK',
|
||||||
|
'NZD', 'PHP', 'PKR', 'PLN',
|
||||||
|
'RUB', 'SEK', 'SGD', 'THB',
|
||||||
|
'TRY', 'TWD', 'ZAR', 'USD']},
|
||||||
|
'dry_run': {'type': 'boolean'},
|
||||||
|
'minimal_roi': {
|
||||||
|
'type': 'object',
|
||||||
|
'patternProperties': {
|
||||||
|
'^[0-9.]+$': {'type': 'number'}
|
||||||
|
},
|
||||||
|
'minProperties': 1
|
||||||
|
},
|
||||||
|
'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True},
|
||||||
|
'unfilledtimeout': {'type': 'integer', 'minimum': 0},
|
||||||
|
'bid_strategy': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'ask_last_balance': {
|
||||||
|
'type': 'number',
|
||||||
|
'minimum': 0,
|
||||||
|
'maximum': 1,
|
||||||
|
'exclusiveMaximum': False
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'required': ['ask_last_balance']
|
||||||
|
},
|
||||||
|
'exchange': {'$ref': '#/definitions/exchange'},
|
||||||
|
'experimental': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'use_sell_signal': {'type': 'boolean'},
|
||||||
|
'sell_profit_only': {'type': 'boolean'}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'telegram': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'enabled': {'type': 'boolean'},
|
||||||
|
'token': {'type': 'string'},
|
||||||
|
'chat_id': {'type': 'string'},
|
||||||
|
},
|
||||||
|
'required': ['enabled', 'token', 'chat_id']
|
||||||
|
},
|
||||||
|
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
|
||||||
|
'internals': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'process_throttle_secs': {'type': 'number'},
|
||||||
|
'interval': {'type': 'integer'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'definitions': {
|
||||||
|
'exchange': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'name': {'type': 'string'},
|
||||||
|
'key': {'type': 'string'},
|
||||||
|
'secret': {'type': 'string'},
|
||||||
|
'pair_whitelist': {
|
||||||
|
'type': 'array',
|
||||||
|
'items': {
|
||||||
|
'type': 'string',
|
||||||
|
'pattern': '^[0-9A-Z]+_[0-9A-Z]+$'
|
||||||
|
},
|
||||||
|
'uniqueItems': True
|
||||||
|
},
|
||||||
|
'pair_blacklist': {
|
||||||
|
'type': 'array',
|
||||||
|
'items': {
|
||||||
|
'type': 'string',
|
||||||
|
'pattern': '^[0-9A-Z]+_[0-9A-Z]+$'
|
||||||
|
},
|
||||||
|
'uniqueItems': True
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'required': ['name', 'key', 'secret', 'pair_whitelist']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'anyOf': [
|
||||||
|
{'required': ['exchange']}
|
||||||
|
],
|
||||||
|
'required': [
|
||||||
|
'max_open_trades',
|
||||||
|
'stake_currency',
|
||||||
|
'stake_amount',
|
||||||
|
'fiat_display_currency',
|
||||||
|
'dry_run',
|
||||||
|
'bid_strategy',
|
||||||
|
'telegram'
|
||||||
|
]
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
from bittrex.bittrex import Bittrex as _Bittrex
|
|
||||||
from bittrex.bittrex import API_V1_1, API_V2_0
|
from bittrex.bittrex import API_V1_1, API_V2_0
|
||||||
|
from bittrex.bittrex import Bittrex as _Bittrex
|
||||||
from requests.exceptions import ContentDecodingError
|
from requests.exceptions import ContentDecodingError
|
||||||
|
|
||||||
from freqtrade import OperationalException
|
from freqtrade import OperationalException
|
||||||
|
@ -5,12 +5,13 @@ e.g BTC to USD
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from coinmarketcap import Market
|
from coinmarketcap import Market
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class CryptoFiat():
|
class CryptoFiat(object):
|
||||||
"""
|
"""
|
||||||
Object to describe what is the price of Crypto-currency in a FIAT
|
Object to describe what is the price of Crypto-currency in a FIAT
|
||||||
"""
|
"""
|
||||||
|
537
freqtrade/freqtradebot.py
Normal file
537
freqtrade/freqtradebot.py
Normal file
@ -0,0 +1,537 @@
|
|||||||
|
"""
|
||||||
|
Freqtrade is the main module of this bot. It contains the class Freqtrade()
|
||||||
|
"""
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Dict, List, Optional, Any, Callable
|
||||||
|
|
||||||
|
import arrow
|
||||||
|
import requests
|
||||||
|
from cachetools import cached, TTLCache
|
||||||
|
|
||||||
|
from freqtrade import (DependencyException, OperationalException, exchange, persistence)
|
||||||
|
from freqtrade.analyze import Analyze
|
||||||
|
from freqtrade.constants import Constants
|
||||||
|
from freqtrade.fiat_convert import CryptoToFiatConverter
|
||||||
|
from freqtrade.logger import Logger
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
from freqtrade.rpc.rpc_manager import RPCManager
|
||||||
|
from freqtrade.state import State
|
||||||
|
|
||||||
|
|
||||||
|
class FreqtradeBot(object):
|
||||||
|
"""
|
||||||
|
Freqtrade is the main class of the bot.
|
||||||
|
This is from here the bot start its logic.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, config: Dict[str, Any], db_url: Optional[str] = None):
|
||||||
|
"""
|
||||||
|
Init all variables and object the bot need to work
|
||||||
|
:param config: configuration dict, you can use the Configuration.get_config()
|
||||||
|
method to get the config dict.
|
||||||
|
:param db_url: database connector string for sqlalchemy (Optional)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Init the logger
|
||||||
|
self.logger = Logger(name=__name__, level=config.get('loglevel')).get_logger()
|
||||||
|
|
||||||
|
# Init bot states
|
||||||
|
self._state = State.STOPPED
|
||||||
|
|
||||||
|
# Init objects
|
||||||
|
self.config = config
|
||||||
|
self.analyze = None
|
||||||
|
self.fiat_converter = None
|
||||||
|
self.rpc = None
|
||||||
|
self.persistence = None
|
||||||
|
self.exchange = None
|
||||||
|
|
||||||
|
self._init_modules(db_url=db_url)
|
||||||
|
|
||||||
|
def _init_modules(self, db_url: Optional[str] = None) -> None:
|
||||||
|
"""
|
||||||
|
Initializes all modules and updates the config
|
||||||
|
:param db_url: database connector string for sqlalchemy (Optional)
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
# Initialize all modules
|
||||||
|
self.analyze = Analyze(self.config)
|
||||||
|
self.fiat_converter = CryptoToFiatConverter()
|
||||||
|
self.rpc = RPCManager(self)
|
||||||
|
|
||||||
|
persistence.init(self.config, db_url)
|
||||||
|
exchange.init(self.config)
|
||||||
|
|
||||||
|
# Set initial application state
|
||||||
|
initial_state = self.config.get('initial_state')
|
||||||
|
|
||||||
|
if initial_state:
|
||||||
|
self.update_state(State[initial_state.upper()])
|
||||||
|
else:
|
||||||
|
self.update_state(State.STOPPED)
|
||||||
|
|
||||||
|
def clean(self) -> bool:
|
||||||
|
"""
|
||||||
|
Cleanup the application state und finish all pending tasks
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self.rpc.send_msg('*Status:* `Stopping trader...`')
|
||||||
|
self.logger.info('Stopping trader and cleaning up modules...')
|
||||||
|
self.update_state(State.STOPPED)
|
||||||
|
self.rpc.cleanup()
|
||||||
|
persistence.cleanup()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def update_state(self, state: State) -> None:
|
||||||
|
"""
|
||||||
|
Updates the application state
|
||||||
|
:param state: new state
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self._state = state
|
||||||
|
|
||||||
|
def get_state(self) -> State:
|
||||||
|
"""
|
||||||
|
Gets the current application state
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
def worker(self, old_state: None) -> State:
|
||||||
|
"""
|
||||||
|
Trading routine that must be run at each loop
|
||||||
|
:param old_state: the previous service state from the previous call
|
||||||
|
:return: current service state
|
||||||
|
"""
|
||||||
|
new_state = self.get_state()
|
||||||
|
# Log state transition
|
||||||
|
if new_state != old_state:
|
||||||
|
self.rpc.send_msg('*Status:* `{}`'.format(new_state.name.lower()))
|
||||||
|
self.logger.info('Changing state to: %s', new_state.name)
|
||||||
|
|
||||||
|
if new_state == State.STOPPED:
|
||||||
|
time.sleep(1)
|
||||||
|
elif new_state == State.RUNNING:
|
||||||
|
min_secs = self.config.get('internals', {}).get(
|
||||||
|
'process_throttle_secs',
|
||||||
|
Constants.PROCESS_THROTTLE_SECS
|
||||||
|
)
|
||||||
|
|
||||||
|
nb_assets = self.config.get(
|
||||||
|
'dynamic_whitelist',
|
||||||
|
Constants.DYNAMIC_WHITELIST
|
||||||
|
)
|
||||||
|
|
||||||
|
self._throttle(func=self._process,
|
||||||
|
min_secs=min_secs,
|
||||||
|
nb_assets=nb_assets)
|
||||||
|
return new_state
|
||||||
|
|
||||||
|
def _throttle(self, func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any:
|
||||||
|
"""
|
||||||
|
Throttles the given callable that it
|
||||||
|
takes at least `min_secs` to finish execution.
|
||||||
|
:param func: Any callable
|
||||||
|
:param min_secs: minimum execution time in seconds
|
||||||
|
:return: Any
|
||||||
|
"""
|
||||||
|
start = time.time()
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
end = time.time()
|
||||||
|
duration = max(min_secs - (end - start), 0.0)
|
||||||
|
self.logger.debug('Throttling %s for %.2f seconds', func.__name__, duration)
|
||||||
|
time.sleep(duration)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _process(self, nb_assets: Optional[int] = 0) -> bool:
|
||||||
|
"""
|
||||||
|
Queries the persistence layer for open trades and handles them,
|
||||||
|
otherwise a new trade is created.
|
||||||
|
:param: nb_assets: the maximum number of pairs to be traded at the same time
|
||||||
|
:return: True if one or more trades has been created or closed, False otherwise
|
||||||
|
"""
|
||||||
|
state_changed = False
|
||||||
|
try:
|
||||||
|
# Refresh whitelist based on wallet maintenance
|
||||||
|
sanitized_list = self._refresh_whitelist(
|
||||||
|
self._gen_pair_whitelist(
|
||||||
|
self.config['stake_currency']
|
||||||
|
) if nb_assets else self.config['exchange']['pair_whitelist']
|
||||||
|
)
|
||||||
|
|
||||||
|
# Keep only the subsets of pairs wanted (up to nb_assets)
|
||||||
|
final_list = sanitized_list[:nb_assets] if nb_assets else sanitized_list
|
||||||
|
self.config['exchange']['pair_whitelist'] = final_list
|
||||||
|
|
||||||
|
# Query trades from persistence layer
|
||||||
|
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||||
|
|
||||||
|
# First process current opened trades
|
||||||
|
for trade in trades:
|
||||||
|
state_changed |= self.process_maybe_execute_sell(trade)
|
||||||
|
|
||||||
|
# Then looking for buy opportunities
|
||||||
|
if len(trades) < self.config['max_open_trades']:
|
||||||
|
state_changed = self.process_maybe_execute_buy()
|
||||||
|
|
||||||
|
if 'unfilledtimeout' in self.config:
|
||||||
|
# Check and handle any timed out open orders
|
||||||
|
self.check_handle_timedout(self.config['unfilledtimeout'])
|
||||||
|
Trade.session.flush()
|
||||||
|
|
||||||
|
except (requests.exceptions.RequestException, json.JSONDecodeError) as error:
|
||||||
|
self.logger.warning('%s, retrying in 30 seconds...', error)
|
||||||
|
time.sleep(Constants.RETRY_TIMEOUT)
|
||||||
|
except OperationalException:
|
||||||
|
self.rpc.send_msg(
|
||||||
|
'*Status:* OperationalException:\n```\n{traceback}```{hint}'
|
||||||
|
.format(
|
||||||
|
traceback=traceback.format_exc(),
|
||||||
|
hint='Issue `/start` if you think it is safe to restart.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.logger.exception('OperationalException. Stopping trader ...')
|
||||||
|
self.update_state(State.STOPPED)
|
||||||
|
return state_changed
|
||||||
|
|
||||||
|
@cached(TTLCache(maxsize=1, ttl=1800))
|
||||||
|
def _gen_pair_whitelist(self, base_currency: str, key: str = 'BaseVolume') -> List[str]:
|
||||||
|
"""
|
||||||
|
Updates the whitelist with with a dynamically generated list
|
||||||
|
:param base_currency: base currency as str
|
||||||
|
:param key: sort key (defaults to 'BaseVolume')
|
||||||
|
:return: List of pairs
|
||||||
|
"""
|
||||||
|
summaries = sorted(
|
||||||
|
(s for s in exchange.get_market_summaries() if
|
||||||
|
s['MarketName'].startswith(base_currency)),
|
||||||
|
key=lambda s: s.get(key) or 0.0,
|
||||||
|
reverse=True
|
||||||
|
)
|
||||||
|
|
||||||
|
return [s['MarketName'].replace('-', '_') for s in summaries]
|
||||||
|
|
||||||
|
def _refresh_whitelist(self, whitelist: List[str]) -> List[str]:
|
||||||
|
"""
|
||||||
|
Check wallet health and remove pair from whitelist if necessary
|
||||||
|
:param whitelist: the sorted list (based on BaseVolume) of pairs the user might want to
|
||||||
|
trade
|
||||||
|
:return: the list of pairs the user wants to trade without the one unavailable or
|
||||||
|
black_listed
|
||||||
|
"""
|
||||||
|
sanitized_whitelist = whitelist
|
||||||
|
health = exchange.get_wallet_health()
|
||||||
|
known_pairs = set()
|
||||||
|
for status in health:
|
||||||
|
pair = '{}_{}'.format(self.config['stake_currency'], status['Currency'])
|
||||||
|
# pair is not int the generated dynamic market, or in the blacklist ... ignore it
|
||||||
|
if pair not in whitelist or pair in self.config['exchange'].get('pair_blacklist', []):
|
||||||
|
continue
|
||||||
|
# else the pair is valid
|
||||||
|
known_pairs.add(pair)
|
||||||
|
# Market is not active
|
||||||
|
if not status['IsActive']:
|
||||||
|
sanitized_whitelist.remove(pair)
|
||||||
|
self.logger.info(
|
||||||
|
'Ignoring %s from whitelist (reason: %s).',
|
||||||
|
pair, status.get('Notice') or 'wallet is not active'
|
||||||
|
)
|
||||||
|
|
||||||
|
# We need to remove pairs that are unknown
|
||||||
|
final_list = [x for x in sanitized_whitelist if x in known_pairs]
|
||||||
|
return final_list
|
||||||
|
|
||||||
|
def get_target_bid(self, ticker: Dict[str, float]) -> float:
|
||||||
|
"""
|
||||||
|
Calculates bid target between current ask price and last price
|
||||||
|
:param ticker: Ticker to use for getting Ask and Last Price
|
||||||
|
:return: float: Price
|
||||||
|
"""
|
||||||
|
if ticker['ask'] < ticker['last']:
|
||||||
|
return ticker['ask']
|
||||||
|
balance = self.config['bid_strategy']['ask_last_balance']
|
||||||
|
return ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
|
||||||
|
|
||||||
|
def create_trade(self) -> bool:
|
||||||
|
"""
|
||||||
|
Checks the implemented trading indicator(s) for a randomly picked pair,
|
||||||
|
if one pair triggers the buy_signal a new trade record gets created
|
||||||
|
:param stake_amount: amount of btc to spend
|
||||||
|
:param interval: Ticker interval used for Analyze
|
||||||
|
:return: True if a trade object has been created and persisted, False otherwise
|
||||||
|
"""
|
||||||
|
stake_amount = self.config['stake_amount']
|
||||||
|
interval = self.analyze.get_ticker_interval()
|
||||||
|
|
||||||
|
self.logger.info(
|
||||||
|
'Checking buy signals to create a new trade with stake_amount: %f ...',
|
||||||
|
stake_amount
|
||||||
|
)
|
||||||
|
whitelist = copy.deepcopy(self.config['exchange']['pair_whitelist'])
|
||||||
|
# Check if stake_amount is fulfilled
|
||||||
|
if exchange.get_balance(self.config['stake_currency']) < stake_amount:
|
||||||
|
raise DependencyException(
|
||||||
|
'stake amount is not fulfilled (currency={})'.format(self.config['stake_currency'])
|
||||||
|
)
|
||||||
|
|
||||||
|
# Remove currently opened and latest pairs from whitelist
|
||||||
|
for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
|
||||||
|
if trade.pair in whitelist:
|
||||||
|
whitelist.remove(trade.pair)
|
||||||
|
self.logger.debug('Ignoring %s in pair whitelist', trade.pair)
|
||||||
|
|
||||||
|
if not whitelist:
|
||||||
|
raise DependencyException('No currency pairs in whitelist')
|
||||||
|
|
||||||
|
# Pick pair based on StochRSI buy signals
|
||||||
|
for _pair in whitelist:
|
||||||
|
(buy, sell) = self.analyze.get_signal(_pair, interval)
|
||||||
|
if buy and not sell:
|
||||||
|
pair = _pair
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Calculate amount
|
||||||
|
buy_limit = self.get_target_bid(exchange.get_ticker(pair))
|
||||||
|
amount = stake_amount / buy_limit
|
||||||
|
|
||||||
|
order_id = exchange.buy(pair, buy_limit, amount)
|
||||||
|
|
||||||
|
stake_amount_fiat = self.fiat_converter.convert_amount(
|
||||||
|
stake_amount,
|
||||||
|
self.config['stake_currency'],
|
||||||
|
self.config['fiat_display_currency']
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create trade entity and return
|
||||||
|
self.rpc.send_msg(
|
||||||
|
'*{}:* Buying [{}]({}) with limit `{:.8f} ({:.6f} {}, {:.3f} {})` '
|
||||||
|
.format(
|
||||||
|
exchange.get_name().upper(),
|
||||||
|
pair.replace('_', '/'),
|
||||||
|
exchange.get_pair_detail_url(pair),
|
||||||
|
buy_limit,
|
||||||
|
stake_amount,
|
||||||
|
self.config['stake_currency'],
|
||||||
|
stake_amount_fiat,
|
||||||
|
self.config['fiat_display_currency']
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
||||||
|
trade = Trade(
|
||||||
|
pair=pair,
|
||||||
|
stake_amount=stake_amount,
|
||||||
|
amount=amount,
|
||||||
|
fee=exchange.get_fee(),
|
||||||
|
open_rate=buy_limit,
|
||||||
|
open_date=datetime.utcnow(),
|
||||||
|
exchange=exchange.get_name().upper(),
|
||||||
|
open_order_id=order_id
|
||||||
|
)
|
||||||
|
Trade.session.add(trade)
|
||||||
|
Trade.session.flush()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def process_maybe_execute_buy(self) -> bool:
|
||||||
|
"""
|
||||||
|
Tries to execute a buy trade in a safe way
|
||||||
|
:return: True if executed
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Create entity and execute trade
|
||||||
|
if self.create_trade():
|
||||||
|
return True
|
||||||
|
|
||||||
|
self.logger.info('Found no buy signals for whitelisted currencies. Trying again..')
|
||||||
|
return False
|
||||||
|
except DependencyException as exception:
|
||||||
|
self.logger.warning('Unable to create trade: %s', exception)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def process_maybe_execute_sell(self, trade: Trade) -> bool:
|
||||||
|
"""
|
||||||
|
Tries to execute a sell trade
|
||||||
|
:return: True if executed
|
||||||
|
"""
|
||||||
|
# Get order details for actual price per unit
|
||||||
|
if trade.open_order_id:
|
||||||
|
# Update trade with order values
|
||||||
|
self.logger.info('Found open order for %s', trade)
|
||||||
|
trade.update(exchange.get_order(trade.open_order_id))
|
||||||
|
|
||||||
|
if trade.is_open and trade.open_order_id is None:
|
||||||
|
# Check if we can sell our current pair
|
||||||
|
return self.handle_trade(trade)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def handle_trade(self, trade: Trade) -> bool:
|
||||||
|
"""
|
||||||
|
Sells the current pair if the threshold is reached and updates the trade record.
|
||||||
|
:return: True if trade has been sold, False otherwise
|
||||||
|
"""
|
||||||
|
if not trade.is_open:
|
||||||
|
raise ValueError('attempt to handle closed trade: {}'.format(trade))
|
||||||
|
|
||||||
|
self.logger.debug('Handling %s ...', trade)
|
||||||
|
current_rate = exchange.get_ticker(trade.pair)['bid']
|
||||||
|
|
||||||
|
(buy, sell) = (False, False)
|
||||||
|
|
||||||
|
if self.config.get('experimental', {}).get('use_sell_signal'):
|
||||||
|
(buy, sell) = self.analyze.get_signal(trade.pair, self.analyze.get_ticker_interval())
|
||||||
|
|
||||||
|
if self.analyze.should_sell(trade, current_rate, datetime.utcnow(), buy, sell):
|
||||||
|
self.execute_sell(trade, current_rate)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check_handle_timedout(self, timeoutvalue: int) -> None:
|
||||||
|
"""
|
||||||
|
Check if any orders are timed out and cancel if neccessary
|
||||||
|
:param timeoutvalue: Number of minutes until order is considered timed out
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
timeoutthreashold = arrow.utcnow().shift(minutes=-timeoutvalue).datetime
|
||||||
|
|
||||||
|
for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all():
|
||||||
|
try:
|
||||||
|
order = exchange.get_order(trade.open_order_id)
|
||||||
|
except requests.exceptions.RequestException:
|
||||||
|
self.logger.info(
|
||||||
|
'Cannot query order for %s due to %s',
|
||||||
|
trade,
|
||||||
|
traceback.format_exc())
|
||||||
|
continue
|
||||||
|
ordertime = arrow.get(order['opened'])
|
||||||
|
|
||||||
|
# Check if trade is still actually open
|
||||||
|
if int(order['remaining']) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if order['type'] == "LIMIT_BUY" and ordertime < timeoutthreashold:
|
||||||
|
self.handle_timedout_limit_buy(trade, order)
|
||||||
|
elif order['type'] == "LIMIT_SELL" and ordertime < timeoutthreashold:
|
||||||
|
self.handle_timedout_limit_sell(trade, order)
|
||||||
|
|
||||||
|
# FIX: 20180110, why is cancel.order unconditionally here, whereas
|
||||||
|
# it is conditionally called in the
|
||||||
|
# handle_timedout_limit_sell()?
|
||||||
|
def handle_timedout_limit_buy(self, trade: Trade, order: Dict) -> bool:
|
||||||
|
"""Buy timeout - cancel order
|
||||||
|
:return: True if order was fully cancelled
|
||||||
|
"""
|
||||||
|
exchange.cancel_order(trade.open_order_id)
|
||||||
|
if order['remaining'] == order['amount']:
|
||||||
|
# if trade is not partially completed, just delete the trade
|
||||||
|
Trade.session.delete(trade)
|
||||||
|
# FIX? do we really need to flush, caller of
|
||||||
|
# check_handle_timedout will flush afterwards
|
||||||
|
Trade.session.flush()
|
||||||
|
self.logger.info('Buy order timeout for %s.', trade)
|
||||||
|
self.rpc.send_msg('*Timeout:* Unfilled buy order for {} cancelled'.format(
|
||||||
|
trade.pair.replace('_', '/')))
|
||||||
|
return True
|
||||||
|
|
||||||
|
# if trade is partially complete, edit the stake details for the trade
|
||||||
|
# and close the order
|
||||||
|
trade.amount = order['amount'] - order['remaining']
|
||||||
|
trade.stake_amount = trade.amount * trade.open_rate
|
||||||
|
trade.open_order_id = None
|
||||||
|
self.logger.info('Partial buy order timeout for %s.', trade)
|
||||||
|
self.rpc.send_msg('*Timeout:* Remaining buy order for {} cancelled'.format(
|
||||||
|
trade.pair.replace('_', '/')))
|
||||||
|
return False
|
||||||
|
|
||||||
|
# FIX: 20180110, should cancel_order() be cond. or unconditionally called?
|
||||||
|
def handle_timedout_limit_sell(self, trade: Trade, order: Dict) -> bool:
|
||||||
|
"""
|
||||||
|
Sell timeout - cancel order and update trade
|
||||||
|
:return: True if order was fully cancelled
|
||||||
|
"""
|
||||||
|
if order['remaining'] == order['amount']:
|
||||||
|
# if trade is not partially completed, just cancel the trade
|
||||||
|
exchange.cancel_order(trade.open_order_id)
|
||||||
|
trade.close_rate = None
|
||||||
|
trade.close_profit = None
|
||||||
|
trade.close_date = None
|
||||||
|
trade.is_open = True
|
||||||
|
trade.open_order_id = None
|
||||||
|
self.rpc.send_msg('*Timeout:* Unfilled sell order for {} cancelled'.format(
|
||||||
|
trade.pair.replace('_', '/')))
|
||||||
|
self.logger.info('Sell order timeout for %s.', trade)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# TODO: figure out how to handle partially complete sell orders
|
||||||
|
return False
|
||||||
|
|
||||||
|
def execute_sell(self, trade: Trade, limit: float) -> None:
|
||||||
|
"""
|
||||||
|
Executes a limit sell for the given trade and limit
|
||||||
|
:param trade: Trade instance
|
||||||
|
:param limit: limit rate for the sell order
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
# Execute sell and update trade record
|
||||||
|
order_id = exchange.sell(str(trade.pair), limit, trade.amount)
|
||||||
|
trade.open_order_id = order_id
|
||||||
|
|
||||||
|
fmt_exp_profit = round(trade.calc_profit_percent(rate=limit) * 100, 2)
|
||||||
|
profit_trade = trade.calc_profit(rate=limit)
|
||||||
|
current_rate = exchange.get_ticker(trade.pair, False)['bid']
|
||||||
|
profit = trade.calc_profit_percent(current_rate)
|
||||||
|
|
||||||
|
message = "*{exchange}:* Selling\n" \
|
||||||
|
"*Current Pair:* [{pair}]({pair_url})\n" \
|
||||||
|
"*Limit:* `{limit}`\n" \
|
||||||
|
"*Amount:* `{amount}`\n" \
|
||||||
|
"*Open Rate:* `{open_rate:.8f}`\n" \
|
||||||
|
"*Current Rate:* `{current_rate:.8f}`\n" \
|
||||||
|
"*Profit:* `{profit:.2f}%`" \
|
||||||
|
"".format(
|
||||||
|
exchange=trade.exchange,
|
||||||
|
pair=trade.pair,
|
||||||
|
pair_url=exchange.get_pair_detail_url(trade.pair),
|
||||||
|
limit=limit,
|
||||||
|
open_rate=trade.open_rate,
|
||||||
|
current_rate=current_rate,
|
||||||
|
amount=round(trade.amount, 8),
|
||||||
|
profit=round(profit * 100, 2),
|
||||||
|
)
|
||||||
|
|
||||||
|
# For regular case, when the configuration exists
|
||||||
|
if 'stake_currency' in self.config and 'fiat_display_currency' in self.config:
|
||||||
|
fiat_converter = CryptoToFiatConverter()
|
||||||
|
profit_fiat = fiat_converter.convert_amount(
|
||||||
|
profit_trade,
|
||||||
|
self.config['stake_currency'],
|
||||||
|
self.config['fiat_display_currency']
|
||||||
|
)
|
||||||
|
message += '` ({gain}: {profit_percent:.2f}%, {profit_coin:.8f} {coin}`' \
|
||||||
|
'` / {profit_fiat:.3f} {fiat})`' \
|
||||||
|
''.format(
|
||||||
|
gain="profit" if fmt_exp_profit > 0 else "loss",
|
||||||
|
profit_percent=fmt_exp_profit,
|
||||||
|
profit_coin=profit_trade,
|
||||||
|
coin=self.config['stake_currency'],
|
||||||
|
profit_fiat=profit_fiat,
|
||||||
|
fiat=self.config['fiat_display_currency'],
|
||||||
|
)
|
||||||
|
# Because telegram._forcesell does not have the configuration
|
||||||
|
# Ignore the FIAT value and does not show the stake_currency as well
|
||||||
|
else:
|
||||||
|
message += '` ({gain}: {profit_percent:.2f}%, {profit_coin:.8f})`'.format(
|
||||||
|
gain="profit" if fmt_exp_profit > 0 else "loss",
|
||||||
|
profit_percent=fmt_exp_profit,
|
||||||
|
profit_coin=profit_trade
|
||||||
|
)
|
||||||
|
|
||||||
|
# Send the message
|
||||||
|
self.rpc.send_msg(message)
|
||||||
|
Trade.session.flush()
|
@ -1,19 +1,19 @@
|
|||||||
from math import exp, pi, sqrt, cos
|
from math import exp, pi, sqrt, cos
|
||||||
|
|
||||||
import numpy
|
import numpy as np
|
||||||
import talib as ta
|
import talib as ta
|
||||||
from pandas import Series
|
from pandas import Series
|
||||||
|
|
||||||
|
|
||||||
def went_up(series: Series) -> Series:
|
def went_up(series: Series) -> bool:
|
||||||
return series > series.shift(1)
|
return series > series.shift(1)
|
||||||
|
|
||||||
|
|
||||||
def went_down(series: Series) -> Series:
|
def went_down(series: Series) -> bool:
|
||||||
return series < series.shift(1)
|
return series < series.shift(1)
|
||||||
|
|
||||||
|
|
||||||
def ehlers_super_smoother(series: Series, smoothing: float = 6):
|
def ehlers_super_smoother(series: Series, smoothing: float = 6) -> type(Series):
|
||||||
magic = pi * sqrt(2) / smoothing
|
magic = pi * sqrt(2) / smoothing
|
||||||
a1 = exp(-magic)
|
a1 = exp(-magic)
|
||||||
coeff2 = 2 * a1 * cos(magic)
|
coeff2 = 2 * a1 * cos(magic)
|
||||||
@ -29,7 +29,7 @@ def ehlers_super_smoother(series: Series, smoothing: float = 6):
|
|||||||
return filtered
|
return filtered
|
||||||
|
|
||||||
|
|
||||||
def fishers_inverse(series: Series, smoothing: float = 0):
|
def fishers_inverse(series: Series, smoothing: float = 0) -> np.ndarray:
|
||||||
""" Does a smoothed fishers inverse transformation.
|
""" Does a smoothed fishers inverse transformation.
|
||||||
Can be used with any oscillator that goes from 0 to 100 like RSI or MFI """
|
Can be used with any oscillator that goes from 0 to 100 like RSI or MFI """
|
||||||
v1 = 0.1 * (series - 50)
|
v1 = 0.1 * (series - 50)
|
||||||
@ -37,4 +37,4 @@ def fishers_inverse(series: Series, smoothing: float = 0):
|
|||||||
v2 = ta.WMA(v1.values, timeperiod=smoothing)
|
v2 = ta.WMA(v1.values, timeperiod=smoothing)
|
||||||
else:
|
else:
|
||||||
v2 = v1
|
v2 = v1
|
||||||
return (numpy.exp(2 * v2)-1) / (numpy.exp(2 * v2) + 1)
|
return (np.exp(2 * v2)-1) / (np.exp(2 * v2) + 1)
|
||||||
|
83
freqtrade/logger.py
Normal file
83
freqtrade/logger.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
# pragma pylint: disable=too-few-public-methods
|
||||||
|
|
||||||
|
"""
|
||||||
|
This module contains the class for logger and logging messages
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
class Logger(object):
|
||||||
|
"""
|
||||||
|
Logging class
|
||||||
|
"""
|
||||||
|
def __init__(self, name='', level=logging.INFO) -> None:
|
||||||
|
"""
|
||||||
|
Init the logger class
|
||||||
|
:param name: Name of the Logger scope
|
||||||
|
:param level: Logger level that should be used
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self.name = name
|
||||||
|
self.logger = None
|
||||||
|
|
||||||
|
if level is None:
|
||||||
|
level = logging.INFO
|
||||||
|
self.level = level
|
||||||
|
|
||||||
|
self._init_logger()
|
||||||
|
|
||||||
|
def _init_logger(self) -> None:
|
||||||
|
"""
|
||||||
|
Setup the bot logger configuration
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
logging.basicConfig(
|
||||||
|
level=self.level,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.logger = self.get_logger()
|
||||||
|
self.set_level(self.level)
|
||||||
|
|
||||||
|
def get_logger(self) -> logging.RootLogger:
|
||||||
|
"""
|
||||||
|
Return the logger instance to use for sending message
|
||||||
|
:return: the logger instance
|
||||||
|
"""
|
||||||
|
return logging.getLogger(self.name)
|
||||||
|
|
||||||
|
def set_name(self, name: str) -> logging.RootLogger:
|
||||||
|
"""
|
||||||
|
Set the name of the logger
|
||||||
|
:param name: Name of the logger
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self.name = name
|
||||||
|
self.logger = self.get_logger()
|
||||||
|
return self.logger
|
||||||
|
|
||||||
|
def set_level(self, level) -> None:
|
||||||
|
"""
|
||||||
|
Set the level of the logger
|
||||||
|
:param level:
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self.level = level
|
||||||
|
self.logger.setLevel(self.level)
|
||||||
|
|
||||||
|
def set_format(self, log_format: str, propagate: bool = False) -> None:
|
||||||
|
"""
|
||||||
|
Set a new logging format
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
handler = logging.StreamHandler()
|
||||||
|
|
||||||
|
len_handlers = len(self.logger.handlers)
|
||||||
|
if len_handlers:
|
||||||
|
self.logger.removeHandler(handler)
|
||||||
|
|
||||||
|
handler.setFormatter(logging.Formatter(log_format))
|
||||||
|
self.logger.addHandler(handler)
|
||||||
|
|
||||||
|
self.logger.propagate = propagate
|
@ -1,564 +1,74 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import copy
|
"""
|
||||||
import json
|
Main Freqtrade bot script.
|
||||||
|
Read the documentation to know what cli arguments you need.
|
||||||
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
import time
|
from typing import List
|
||||||
import traceback
|
|
||||||
from datetime import datetime
|
|
||||||
from typing import Dict, List, Optional, Any
|
|
||||||
|
|
||||||
import arrow
|
from freqtrade import (__version__)
|
||||||
import requests
|
from freqtrade.arguments import Arguments
|
||||||
from cachetools import cached, TTLCache
|
from freqtrade.configuration import Configuration
|
||||||
|
from freqtrade.freqtradebot import FreqtradeBot
|
||||||
|
from freqtrade.logger import Logger
|
||||||
|
|
||||||
from freqtrade import (DependencyException, OperationalException, __version__,
|
logger = Logger(name='freqtrade').get_logger()
|
||||||
exchange, persistence, rpc)
|
|
||||||
from freqtrade.analyze import get_signal
|
|
||||||
from freqtrade.fiat_convert import CryptoToFiatConverter
|
|
||||||
from freqtrade.misc import (State, get_state, load_config, parse_args,
|
|
||||||
throttle, update_state)
|
|
||||||
from freqtrade.persistence import Trade
|
|
||||||
from freqtrade.strategy.strategy import Strategy
|
|
||||||
|
|
||||||
logger = logging.getLogger('freqtrade')
|
|
||||||
|
|
||||||
_CONF: Dict[str, Any] = {}
|
|
||||||
|
|
||||||
|
|
||||||
def refresh_whitelist(whitelist: List[str]) -> List[str]:
|
def main(sysargv: List[str]) -> None:
|
||||||
"""
|
"""
|
||||||
Check wallet health and remove pair from whitelist if necessary
|
This function will initiate the bot and start the trading loop.
|
||||||
:param whitelist: the sorted list (based on BaseVolume) of pairs the user might want to trade
|
|
||||||
:return: the list of pairs the user wants to trade without the one unavailable or black_listed
|
|
||||||
"""
|
|
||||||
sanitized_whitelist = whitelist
|
|
||||||
health = exchange.get_wallet_health()
|
|
||||||
known_pairs = set()
|
|
||||||
for status in health:
|
|
||||||
pair = '{}_{}'.format(_CONF['stake_currency'], status['Currency'])
|
|
||||||
# pair is not int the generated dynamic market, or in the blacklist ... ignore it
|
|
||||||
if pair not in whitelist or pair in _CONF['exchange'].get('pair_blacklist', []):
|
|
||||||
continue
|
|
||||||
# else the pair is valid
|
|
||||||
known_pairs.add(pair)
|
|
||||||
# Market is not active
|
|
||||||
if not status['IsActive']:
|
|
||||||
sanitized_whitelist.remove(pair)
|
|
||||||
logger.info(
|
|
||||||
'Ignoring %s from whitelist (reason: %s).',
|
|
||||||
pair, status.get('Notice') or 'wallet is not active'
|
|
||||||
)
|
|
||||||
|
|
||||||
# We need to remove pairs that are unknown
|
|
||||||
final_list = [x for x in sanitized_whitelist if x in known_pairs]
|
|
||||||
return final_list
|
|
||||||
|
|
||||||
|
|
||||||
def process_maybe_execute_buy(interval: int) -> bool:
|
|
||||||
"""
|
|
||||||
Tries to execute a buy trade in a safe way
|
|
||||||
:return: True if executed
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Create entity and execute trade
|
|
||||||
if create_trade(float(_CONF['stake_amount']), interval):
|
|
||||||
return True
|
|
||||||
|
|
||||||
logger.info('Found no buy signals for whitelisted currencies. Trying again..')
|
|
||||||
return False
|
|
||||||
except DependencyException as exception:
|
|
||||||
logger.warning('Unable to create trade: %s', exception)
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def process_maybe_execute_sell(trade: Trade, interval: int) -> bool:
|
|
||||||
"""
|
|
||||||
Tries to execute a sell trade
|
|
||||||
:return: True if executed
|
|
||||||
"""
|
|
||||||
# Get order details for actual price per unit
|
|
||||||
if trade.open_order_id:
|
|
||||||
# Update trade with order values
|
|
||||||
logger.info('Found open order for %s', trade)
|
|
||||||
trade.update(exchange.get_order(trade.open_order_id))
|
|
||||||
|
|
||||||
if trade.is_open and trade.open_order_id is None:
|
|
||||||
# Check if we can sell our current pair
|
|
||||||
return handle_trade(trade, interval)
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def _process(interval: int, nb_assets: Optional[int] = 0) -> bool:
|
|
||||||
"""
|
|
||||||
Queries the persistence layer for open trades and handles them,
|
|
||||||
otherwise a new trade is created.
|
|
||||||
:param: nb_assets: the maximum number of pairs to be traded at the same time
|
|
||||||
:return: True if one or more trades has been created or closed, False otherwise
|
|
||||||
"""
|
|
||||||
state_changed = False
|
|
||||||
try:
|
|
||||||
# Refresh whitelist based on wallet maintenance
|
|
||||||
sanitized_list = refresh_whitelist(
|
|
||||||
gen_pair_whitelist(
|
|
||||||
_CONF['stake_currency']
|
|
||||||
) if nb_assets else _CONF['exchange']['pair_whitelist']
|
|
||||||
)
|
|
||||||
|
|
||||||
# Keep only the subsets of pairs wanted (up to nb_assets)
|
|
||||||
final_list = sanitized_list[:nb_assets] if nb_assets else sanitized_list
|
|
||||||
_CONF['exchange']['pair_whitelist'] = final_list
|
|
||||||
|
|
||||||
# Query trades from persistence layer
|
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
|
||||||
|
|
||||||
# First process current opened trades
|
|
||||||
for trade in trades:
|
|
||||||
state_changed |= process_maybe_execute_sell(trade, interval)
|
|
||||||
|
|
||||||
# Then looking for buy opportunities
|
|
||||||
if len(trades) < _CONF['max_open_trades']:
|
|
||||||
state_changed = process_maybe_execute_buy(interval)
|
|
||||||
|
|
||||||
if 'unfilledtimeout' in _CONF:
|
|
||||||
# Check and handle any timed out open orders
|
|
||||||
check_handle_timedout(_CONF['unfilledtimeout'])
|
|
||||||
Trade.session.flush()
|
|
||||||
|
|
||||||
except (requests.exceptions.RequestException, json.JSONDecodeError) as error:
|
|
||||||
logger.warning('%s, retrying in 30 seconds...', error)
|
|
||||||
time.sleep(30)
|
|
||||||
except OperationalException:
|
|
||||||
rpc.send_msg('*Status:* OperationalException:\n```\n{traceback}```{hint}'.format(
|
|
||||||
traceback=traceback.format_exc(),
|
|
||||||
hint='Issue `/start` if you think it is safe to restart.'
|
|
||||||
))
|
|
||||||
logger.exception('OperationalException. Stopping trader ...')
|
|
||||||
update_state(State.STOPPED)
|
|
||||||
return state_changed
|
|
||||||
|
|
||||||
|
|
||||||
# FIX: 20180110, why is cancel.order unconditionally here, whereas
|
|
||||||
# it is conditionally called in the
|
|
||||||
# handle_timedout_limit_sell()?
|
|
||||||
def handle_timedout_limit_buy(trade: Trade, order: Dict) -> bool:
|
|
||||||
"""Buy timeout - cancel order
|
|
||||||
:return: True if order was fully cancelled
|
|
||||||
"""
|
|
||||||
exchange.cancel_order(trade.open_order_id)
|
|
||||||
if order['remaining'] == order['amount']:
|
|
||||||
# if trade is not partially completed, just delete the trade
|
|
||||||
Trade.session.delete(trade)
|
|
||||||
# FIX? do we really need to flush, caller of
|
|
||||||
# check_handle_timedout will flush afterwards
|
|
||||||
Trade.session.flush()
|
|
||||||
logger.info('Buy order timeout for %s.', trade)
|
|
||||||
rpc.send_msg('*Timeout:* Unfilled buy order for {} cancelled'.format(
|
|
||||||
trade.pair.replace('_', '/')))
|
|
||||||
return True
|
|
||||||
|
|
||||||
# if trade is partially complete, edit the stake details for the trade
|
|
||||||
# and close the order
|
|
||||||
trade.amount = order['amount'] - order['remaining']
|
|
||||||
trade.stake_amount = trade.amount * trade.open_rate
|
|
||||||
trade.open_order_id = None
|
|
||||||
logger.info('Partial buy order timeout for %s.', trade)
|
|
||||||
rpc.send_msg('*Timeout:* Remaining buy order for {} cancelled'.format(
|
|
||||||
trade.pair.replace('_', '/')))
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
# FIX: 20180110, should cancel_order() be cond. or unconditionally called?
|
|
||||||
def handle_timedout_limit_sell(trade: Trade, order: Dict) -> bool:
|
|
||||||
"""
|
|
||||||
Sell timeout - cancel order and update trade
|
|
||||||
:return: True if order was fully cancelled
|
|
||||||
"""
|
|
||||||
if order['remaining'] == order['amount']:
|
|
||||||
# if trade is not partially completed, just cancel the trade
|
|
||||||
exchange.cancel_order(trade.open_order_id)
|
|
||||||
trade.close_rate = None
|
|
||||||
trade.close_profit = None
|
|
||||||
trade.close_date = None
|
|
||||||
trade.is_open = True
|
|
||||||
trade.open_order_id = None
|
|
||||||
rpc.send_msg('*Timeout:* Unfilled sell order for {} cancelled'.format(
|
|
||||||
trade.pair.replace('_', '/')))
|
|
||||||
logger.info('Sell order timeout for %s.', trade)
|
|
||||||
return True
|
|
||||||
|
|
||||||
# TODO: figure out how to handle partially complete sell orders
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def check_handle_timedout(timeoutvalue: int) -> None:
|
|
||||||
"""
|
|
||||||
Check if any orders are timed out and cancel if neccessary
|
|
||||||
:param timeoutvalue: Number of minutes until order is considered timed out
|
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
timeoutthreashold = arrow.utcnow().shift(minutes=-timeoutvalue).datetime
|
arguments = Arguments(
|
||||||
|
sysargv,
|
||||||
for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all():
|
'Simple High Frequency Trading Bot for crypto currencies'
|
||||||
try:
|
|
||||||
order = exchange.get_order(trade.open_order_id)
|
|
||||||
except requests.exceptions.RequestException:
|
|
||||||
logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc())
|
|
||||||
continue
|
|
||||||
ordertime = arrow.get(order['opened'])
|
|
||||||
|
|
||||||
# Check if trade is still actually open
|
|
||||||
if int(order['remaining']) == 0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if order['type'] == "LIMIT_BUY" and ordertime < timeoutthreashold:
|
|
||||||
handle_timedout_limit_buy(trade, order)
|
|
||||||
elif order['type'] == "LIMIT_SELL" and ordertime < timeoutthreashold:
|
|
||||||
handle_timedout_limit_sell(trade, order)
|
|
||||||
|
|
||||||
|
|
||||||
def execute_sell(trade: Trade, limit: float) -> None:
|
|
||||||
"""
|
|
||||||
Executes a limit sell for the given trade and limit
|
|
||||||
:param trade: Trade instance
|
|
||||||
:param limit: limit rate for the sell order
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
# Execute sell and update trade record
|
|
||||||
order_id = exchange.sell(str(trade.pair), limit, trade.amount)
|
|
||||||
trade.open_order_id = order_id
|
|
||||||
|
|
||||||
fmt_exp_profit = round(trade.calc_profit_percent(rate=limit) * 100, 2)
|
|
||||||
profit_trade = trade.calc_profit(rate=limit)
|
|
||||||
current_rate = exchange.get_ticker(trade.pair, False)['bid']
|
|
||||||
profit = trade.calc_profit_percent(current_rate)
|
|
||||||
|
|
||||||
message = """*{exchange}:* Selling
|
|
||||||
*Current Pair:* [{pair}]({pair_url})
|
|
||||||
*Limit:* `{limit}`
|
|
||||||
*Amount:* `{amount}`
|
|
||||||
*Open Rate:* `{open_rate:.8f}`
|
|
||||||
*Current Rate:* `{current_rate:.8f}`
|
|
||||||
*Profit:* `{profit:.2f}%`
|
|
||||||
""".format(
|
|
||||||
exchange=trade.exchange,
|
|
||||||
pair=trade.pair,
|
|
||||||
pair_url=exchange.get_pair_detail_url(trade.pair),
|
|
||||||
limit=limit,
|
|
||||||
open_rate=trade.open_rate,
|
|
||||||
current_rate=current_rate,
|
|
||||||
amount=round(trade.amount, 8),
|
|
||||||
profit=round(profit * 100, 2),
|
|
||||||
)
|
)
|
||||||
|
args = arguments.get_parsed_arg()
|
||||||
|
|
||||||
# For regular case, when the configuration exists
|
# A subcommand has been issued.
|
||||||
if 'stake_currency' in _CONF and 'fiat_display_currency' in _CONF:
|
# Means if Backtesting or Hyperopt have been called we exit the bot
|
||||||
fiat_converter = CryptoToFiatConverter()
|
|
||||||
profit_fiat = fiat_converter.convert_amount(
|
|
||||||
profit_trade,
|
|
||||||
_CONF['stake_currency'],
|
|
||||||
_CONF['fiat_display_currency']
|
|
||||||
)
|
|
||||||
message += '` ({gain}: {profit_percent:.2f}%, {profit_coin:.8f} {coin}`' \
|
|
||||||
'` / {profit_fiat:.3f} {fiat})`'.format(
|
|
||||||
gain="profit" if fmt_exp_profit > 0 else "loss",
|
|
||||||
profit_percent=fmt_exp_profit,
|
|
||||||
profit_coin=profit_trade,
|
|
||||||
coin=_CONF['stake_currency'],
|
|
||||||
profit_fiat=profit_fiat,
|
|
||||||
fiat=_CONF['fiat_display_currency'],
|
|
||||||
)
|
|
||||||
# Because telegram._forcesell does not have the configuration
|
|
||||||
# Ignore the FIAT value and does not show the stake_currency as well
|
|
||||||
else:
|
|
||||||
message += '` ({gain}: {profit_percent:.2f}%, {profit_coin:.8f})`'.format(
|
|
||||||
gain="profit" if fmt_exp_profit > 0 else "loss",
|
|
||||||
profit_percent=fmt_exp_profit,
|
|
||||||
profit_coin=profit_trade
|
|
||||||
)
|
|
||||||
|
|
||||||
# Send the message
|
|
||||||
rpc.send_msg(message)
|
|
||||||
Trade.session.flush()
|
|
||||||
|
|
||||||
|
|
||||||
def min_roi_reached(trade: Trade, current_rate: float, current_time: datetime) -> bool:
|
|
||||||
"""
|
|
||||||
Based an earlier trade and current price and ROI configuration, decides whether bot should sell
|
|
||||||
:return True if bot should sell at current rate
|
|
||||||
"""
|
|
||||||
strategy = Strategy()
|
|
||||||
|
|
||||||
current_profit = trade.calc_profit_percent(current_rate)
|
|
||||||
if strategy.stoploss is not None and current_profit < strategy.stoploss:
|
|
||||||
logger.debug('Stop loss hit.')
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Check if time matches and current rate is above threshold
|
|
||||||
time_diff = (current_time.timestamp() - trade.open_date.timestamp()) / 60
|
|
||||||
for duration, threshold in strategy.minimal_roi.items():
|
|
||||||
if time_diff <= duration:
|
|
||||||
return False
|
|
||||||
if current_profit > threshold:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def should_sell(trade: Trade, rate: float, date: datetime, buy: bool, sell: bool) -> bool:
|
|
||||||
"""
|
|
||||||
This function evaluate if on the condition required to trigger a sell has been reached
|
|
||||||
if the threshold is reached and updates the trade record.
|
|
||||||
:return: True if trade should be sold, False otherwise
|
|
||||||
"""
|
|
||||||
# Check if minimal roi has been reached and no longer in buy conditions (avoiding a fee)
|
|
||||||
if min_roi_reached(trade, rate, date):
|
|
||||||
logger.debug('Required profit reached. Selling..')
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Experimental: Check if the trade is profitable before selling it (avoid selling at loss)
|
|
||||||
if _CONF.get('experimental', {}).get('sell_profit_only', False):
|
|
||||||
logger.debug('Checking if trade is profitable..')
|
|
||||||
if trade.calc_profit(rate=rate) <= 0:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if sell and not buy and _CONF.get('experimental', {}).get('use_sell_signal', False):
|
|
||||||
logger.debug('Sell signal received. Selling..')
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def handle_trade(trade: Trade, interval: int) -> bool:
|
|
||||||
"""
|
|
||||||
Sells the current pair if the threshold is reached and updates the trade record.
|
|
||||||
:return: True if trade has been sold, False otherwise
|
|
||||||
"""
|
|
||||||
if not trade.is_open:
|
|
||||||
raise ValueError('attempt to handle closed trade: {}'.format(trade))
|
|
||||||
|
|
||||||
logger.debug('Handling %s ...', trade)
|
|
||||||
current_rate = exchange.get_ticker(trade.pair)['bid']
|
|
||||||
|
|
||||||
(buy, sell) = (False, False)
|
|
||||||
|
|
||||||
if _CONF.get('experimental', {}).get('use_sell_signal'):
|
|
||||||
(buy, sell) = get_signal(trade.pair, interval)
|
|
||||||
|
|
||||||
if should_sell(trade, current_rate, datetime.utcnow(), buy, sell):
|
|
||||||
execute_sell(trade, current_rate)
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def get_target_bid(ticker: Dict[str, float]) -> float:
|
|
||||||
""" Calculates bid target between current ask price and last price """
|
|
||||||
if ticker['ask'] < ticker['last']:
|
|
||||||
return ticker['ask']
|
|
||||||
balance = _CONF['bid_strategy']['ask_last_balance']
|
|
||||||
return ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
|
|
||||||
|
|
||||||
|
|
||||||
def create_trade(stake_amount: float, interval: int) -> bool:
|
|
||||||
"""
|
|
||||||
Checks the implemented trading indicator(s) for a randomly picked pair,
|
|
||||||
if one pair triggers the buy_signal a new trade record gets created
|
|
||||||
:param stake_amount: amount of btc to spend
|
|
||||||
:return: True if a trade object has been created and persisted, False otherwise
|
|
||||||
"""
|
|
||||||
logger.info(
|
|
||||||
'Checking buy signals to create a new trade with stake_amount: %f ...',
|
|
||||||
stake_amount
|
|
||||||
)
|
|
||||||
whitelist = copy.deepcopy(_CONF['exchange']['pair_whitelist'])
|
|
||||||
# Check if stake_amount is fulfilled
|
|
||||||
if exchange.get_balance(_CONF['stake_currency']) < stake_amount:
|
|
||||||
raise DependencyException(
|
|
||||||
'stake amount is not fulfilled (currency={})'.format(_CONF['stake_currency'])
|
|
||||||
)
|
|
||||||
|
|
||||||
# Remove currently opened and latest pairs from whitelist
|
|
||||||
for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
|
|
||||||
if trade.pair in whitelist:
|
|
||||||
whitelist.remove(trade.pair)
|
|
||||||
logger.debug('Ignoring %s in pair whitelist', trade.pair)
|
|
||||||
if not whitelist:
|
|
||||||
raise DependencyException('No currency pairs in whitelist')
|
|
||||||
|
|
||||||
# Pick pair based on StochRSI buy signals
|
|
||||||
for _pair in whitelist:
|
|
||||||
(buy, sell) = get_signal(_pair, interval)
|
|
||||||
if buy and not sell:
|
|
||||||
pair = _pair
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Calculate amount
|
|
||||||
buy_limit = get_target_bid(exchange.get_ticker(pair))
|
|
||||||
amount = stake_amount / buy_limit
|
|
||||||
|
|
||||||
order_id = exchange.buy(pair, buy_limit, amount)
|
|
||||||
|
|
||||||
fiat_converter = CryptoToFiatConverter()
|
|
||||||
stake_amount_fiat = fiat_converter.convert_amount(
|
|
||||||
stake_amount,
|
|
||||||
_CONF['stake_currency'],
|
|
||||||
_CONF['fiat_display_currency']
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create trade entity and return
|
|
||||||
rpc.send_msg('*{}:* Buying [{}]({}) with limit `{:.8f} ({:.6f} {}, {:.3f} {})` '.format(
|
|
||||||
exchange.get_name().upper(),
|
|
||||||
pair.replace('_', '/'),
|
|
||||||
exchange.get_pair_detail_url(pair),
|
|
||||||
buy_limit, stake_amount, _CONF['stake_currency'],
|
|
||||||
stake_amount_fiat, _CONF['fiat_display_currency']
|
|
||||||
))
|
|
||||||
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
|
||||||
trade = Trade(
|
|
||||||
pair=pair,
|
|
||||||
stake_amount=stake_amount,
|
|
||||||
amount=amount,
|
|
||||||
fee=exchange.get_fee(),
|
|
||||||
open_rate=buy_limit,
|
|
||||||
open_date=datetime.utcnow(),
|
|
||||||
exchange=exchange.get_name().upper(),
|
|
||||||
open_order_id=order_id
|
|
||||||
)
|
|
||||||
Trade.session.add(trade)
|
|
||||||
Trade.session.flush()
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def init(config: dict, db_url: Optional[str] = None) -> None:
|
|
||||||
"""
|
|
||||||
Initializes all modules and updates the config
|
|
||||||
:param config: config as dict
|
|
||||||
:param db_url: database connector string for sqlalchemy (Optional)
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
# Initialize all modules
|
|
||||||
rpc.init(config)
|
|
||||||
persistence.init(config, db_url)
|
|
||||||
exchange.init(config)
|
|
||||||
|
|
||||||
strategy = Strategy()
|
|
||||||
strategy.init(config)
|
|
||||||
|
|
||||||
# Set initial application state
|
|
||||||
initial_state = config.get('initial_state')
|
|
||||||
if initial_state:
|
|
||||||
update_state(State[initial_state.upper()])
|
|
||||||
else:
|
|
||||||
update_state(State.STOPPED)
|
|
||||||
|
|
||||||
|
|
||||||
@cached(TTLCache(maxsize=1, ttl=1800))
|
|
||||||
def gen_pair_whitelist(base_currency: str, key: str = 'BaseVolume') -> List[str]:
|
|
||||||
"""
|
|
||||||
Updates the whitelist with with a dynamically generated list
|
|
||||||
:param base_currency: base currency as str
|
|
||||||
:param key: sort key (defaults to 'BaseVolume')
|
|
||||||
:return: List of pairs
|
|
||||||
"""
|
|
||||||
summaries = sorted(
|
|
||||||
(s for s in exchange.get_market_summaries() if s['MarketName'].startswith(base_currency)),
|
|
||||||
key=lambda s: s.get(key) or 0.0,
|
|
||||||
reverse=True
|
|
||||||
)
|
|
||||||
|
|
||||||
return [s['MarketName'].replace('-', '_') for s in summaries]
|
|
||||||
|
|
||||||
|
|
||||||
def cleanup() -> None:
|
|
||||||
"""
|
|
||||||
Cleanup the application state und finish all pending tasks
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
rpc.send_msg('*Status:* `Stopping trader...`')
|
|
||||||
logger.info('Stopping trader and cleaning up modules...')
|
|
||||||
update_state(State.STOPPED)
|
|
||||||
persistence.cleanup()
|
|
||||||
rpc.cleanup()
|
|
||||||
exit(0)
|
|
||||||
|
|
||||||
|
|
||||||
def main(sysargv=sys.argv[1:]) -> int:
|
|
||||||
"""
|
|
||||||
Loads and validates the config and handles the main loop
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
global _CONF
|
|
||||||
args = parse_args(sysargv,
|
|
||||||
'Simple High Frequency Trading Bot for crypto currencies')
|
|
||||||
|
|
||||||
# A subcommand has been issued
|
|
||||||
if hasattr(args, 'func'):
|
if hasattr(args, 'func'):
|
||||||
args.func(args)
|
args.func(args)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
# Initialize logger
|
|
||||||
logging.basicConfig(
|
|
||||||
level=args.loglevel,
|
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
'Starting freqtrade %s (loglevel=%s)',
|
'Starting freqtrade %s (loglevel=%s)',
|
||||||
__version__,
|
__version__,
|
||||||
logging.getLevelName(args.loglevel)
|
logging.getLevelName(args.loglevel)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Load and validate configuration
|
|
||||||
_CONF = load_config(args.config)
|
|
||||||
|
|
||||||
# Add the strategy file to use
|
|
||||||
_CONF.update({'strategy': args.strategy})
|
|
||||||
|
|
||||||
# Initialize all modules and start main loop
|
|
||||||
if args.dynamic_whitelist:
|
|
||||||
logger.info('Using dynamically generated whitelist. (--dynamic-whitelist detected)')
|
|
||||||
|
|
||||||
# If the user ask for Dry run with a local DB instead of memory
|
|
||||||
if args.dry_run_db:
|
|
||||||
if _CONF.get('dry_run', False):
|
|
||||||
_CONF.update({'dry_run_db': True})
|
|
||||||
logger.info(
|
|
||||||
'Dry_run will use the DB file: "tradesv3.dry_run.sqlite". (--dry_run_db detected)'
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.info('Dry run is disabled. (--dry_run_db ignored)')
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
init(_CONF)
|
# Load and validate configuration
|
||||||
old_state = None
|
configuration = Configuration(args)
|
||||||
|
|
||||||
while True:
|
# Init the bot
|
||||||
new_state = get_state()
|
freqtrade = FreqtradeBot(configuration.get_config())
|
||||||
# Log state transition
|
|
||||||
if new_state != old_state:
|
state = None
|
||||||
rpc.send_msg('*Status:* `{}`'.format(new_state.name.lower()))
|
while 1:
|
||||||
logger.info('Changing state to: %s', new_state.name)
|
state = freqtrade.worker(old_state=state)
|
||||||
|
|
||||||
if new_state == State.STOPPED:
|
|
||||||
time.sleep(1)
|
|
||||||
elif new_state == State.RUNNING:
|
|
||||||
throttle(
|
|
||||||
_process,
|
|
||||||
min_secs=_CONF['internals'].get('process_throttle_secs', 10),
|
|
||||||
nb_assets=args.dynamic_whitelist,
|
|
||||||
interval=int(_CONF.get('ticker_interval', 5))
|
|
||||||
)
|
|
||||||
old_state = new_state
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
logger.info('SIGINT received, aborting ...')
|
logger.info('SIGINT received, aborting ...')
|
||||||
except BaseException:
|
except BaseException:
|
||||||
logger.exception('Fatal exception!')
|
logger.exception('Fatal exception!')
|
||||||
finally:
|
finally:
|
||||||
cleanup()
|
freqtrade.clean()
|
||||||
return 0
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
def set_loggers() -> None:
|
||||||
|
"""
|
||||||
|
Set the logger level for Third party libs
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
logging.getLogger('requests.packages.urllib3').setLevel(logging.INFO)
|
||||||
|
logging.getLogger('telegram').setLevel(logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
set_loggers()
|
||||||
main(sys.argv[1:])
|
main(sys.argv[1:])
|
||||||
|
@ -1,30 +1,29 @@
|
|||||||
import argparse
|
"""
|
||||||
import enum
|
Various tool function for Freqtrade and scripts
|
||||||
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import time
|
|
||||||
import os
|
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Callable, Dict, List
|
from typing import Dict
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from jsonschema import Draft4Validator, validate
|
from pandas import DataFrame
|
||||||
from jsonschema.exceptions import ValidationError, best_match
|
|
||||||
from wrapt import synchronized
|
|
||||||
|
|
||||||
from freqtrade import __version__
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class State(enum.Enum):
|
def shorten_date(_date: str) -> str:
|
||||||
RUNNING = 0
|
"""
|
||||||
STOPPED = 1
|
Trim the date so it fits on small screens
|
||||||
|
"""
|
||||||
|
new_date = re.sub('seconds?', 'sec', _date)
|
||||||
# Current application state
|
new_date = re.sub('minutes?', 'min', new_date)
|
||||||
_STATE = State.STOPPED
|
new_date = re.sub('hours?', 'h', new_date)
|
||||||
|
new_date = re.sub('days?', 'd', new_date)
|
||||||
|
new_date = re.sub('^an?', '1', new_date)
|
||||||
|
return new_date
|
||||||
|
|
||||||
|
|
||||||
############################################
|
############################################
|
||||||
@ -32,8 +31,7 @@ _STATE = State.STOPPED
|
|||||||
# Matplotlib doesn't support ::datetime64, #
|
# Matplotlib doesn't support ::datetime64, #
|
||||||
# so we need to convert it into ::datetime #
|
# so we need to convert it into ::datetime #
|
||||||
############################################
|
############################################
|
||||||
|
def datesarray_to_datetimearray(dates: np.ndarray) -> np.ndarray:
|
||||||
def datesarray_to_datetimearray(dates):
|
|
||||||
"""
|
"""
|
||||||
Convert an pandas-array of timestamps into
|
Convert an pandas-array of timestamps into
|
||||||
An numpy-array of datetimes
|
An numpy-array of datetimes
|
||||||
@ -41,13 +39,18 @@ def datesarray_to_datetimearray(dates):
|
|||||||
"""
|
"""
|
||||||
times = []
|
times = []
|
||||||
dates = dates.astype(datetime)
|
dates = dates.astype(datetime)
|
||||||
for i in range(0, dates.size):
|
for index in range(0, dates.size):
|
||||||
date = dates[i].to_pydatetime()
|
date = dates[index].to_pydatetime()
|
||||||
times.append(date)
|
times.append(date)
|
||||||
return np.array(times)
|
return np.array(times)
|
||||||
|
|
||||||
|
|
||||||
def common_datearray(dfs):
|
def common_datearray(dfs: Dict[str, DataFrame]) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
Return dates from Dataframe
|
||||||
|
:param dfs: Dict with format pair: pair_data
|
||||||
|
:return: List of dates
|
||||||
|
"""
|
||||||
alldates = {}
|
alldates = {}
|
||||||
for pair, pair_data in dfs.items():
|
for pair, pair_data in dfs.items():
|
||||||
dates = datesarray_to_datetimearray(pair_data['date'])
|
dates = datesarray_to_datetimearray(pair_data['date'])
|
||||||
@ -61,375 +64,11 @@ def common_datearray(dfs):
|
|||||||
|
|
||||||
|
|
||||||
def file_dump_json(filename, data) -> None:
|
def file_dump_json(filename, data) -> None:
|
||||||
with open(filename, 'w') as fp:
|
|
||||||
json.dump(data, fp)
|
|
||||||
|
|
||||||
|
|
||||||
@synchronized
|
|
||||||
def update_state(state: State) -> None:
|
|
||||||
"""
|
"""
|
||||||
Updates the application state
|
Dump JSON data into a file
|
||||||
:param state: new state
|
:param filename: file to create
|
||||||
:return: None
|
:param data: JSON Data to save
|
||||||
"""
|
|
||||||
global _STATE
|
|
||||||
_STATE = state
|
|
||||||
|
|
||||||
|
|
||||||
@synchronized
|
|
||||||
def get_state() -> State:
|
|
||||||
"""
|
|
||||||
Gets the current application state
|
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return _STATE
|
with open(filename, 'w') as fp:
|
||||||
|
json.dump(data, fp, default=str)
|
||||||
|
|
||||||
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 as exception:
|
|
||||||
logger.fatal('Invalid configuration. See config.json.example. Reason: %s', exception)
|
|
||||||
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
|
|
||||||
takes at least `min_secs` to finish execution.
|
|
||||||
:param func: Any callable
|
|
||||||
:param min_secs: minimum execution time in seconds
|
|
||||||
:return: Any
|
|
||||||
"""
|
|
||||||
start = time.time()
|
|
||||||
result = func(*args, **kwargs)
|
|
||||||
end = time.time()
|
|
||||||
duration = max(min_secs - (end - start), 0.0)
|
|
||||||
logger.debug('Throttling %s for %.2f seconds', func.__name__, duration)
|
|
||||||
time.sleep(duration)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def common_args_parser(description: str):
|
|
||||||
"""
|
|
||||||
Parses given common arguments and returns them as a parsed object.
|
|
||||||
"""
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description=description
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'-v', '--verbose',
|
|
||||||
help='be verbose',
|
|
||||||
action='store_const',
|
|
||||||
dest='loglevel',
|
|
||||||
const=logging.DEBUG,
|
|
||||||
default=logging.INFO,
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'--version',
|
|
||||||
action='version',
|
|
||||||
version='%(prog)s {}'.format(__version__),
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'-c', '--config',
|
|
||||||
help='specify configuration file (default: %(default)s)',
|
|
||||||
dest='config',
|
|
||||||
default='config.json',
|
|
||||||
type=str,
|
|
||||||
metavar='PATH',
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'-d', '--datadir',
|
|
||||||
help='path to backtest data (default: %(default)s',
|
|
||||||
dest='datadir',
|
|
||||||
default=os.path.join('freqtrade', 'tests', 'testdata'),
|
|
||||||
type=str,
|
|
||||||
metavar='PATH',
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'-s', '--strategy',
|
|
||||||
help='specify strategy file (default: %(default)s)',
|
|
||||||
dest='strategy',
|
|
||||||
default='default_strategy',
|
|
||||||
type=str,
|
|
||||||
metavar='PATH',
|
|
||||||
)
|
|
||||||
return parser
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args(args: List[str], description: str):
|
|
||||||
"""
|
|
||||||
Parses given arguments and returns an argparse Namespace instance.
|
|
||||||
Returns None if a sub command has been selected and executed.
|
|
||||||
"""
|
|
||||||
parser = common_args_parser(description)
|
|
||||||
parser.add_argument(
|
|
||||||
'--dry-run-db',
|
|
||||||
help='Force dry run to use a local DB "tradesv3.dry_run.sqlite" \
|
|
||||||
instead of memory DB. Work only if dry_run is enabled.',
|
|
||||||
action='store_true',
|
|
||||||
dest='dry_run_db',
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'--dynamic-whitelist',
|
|
||||||
help='dynamically generate and update whitelist \
|
|
||||||
based on 24h BaseVolume (Default 20 currencies)', # noqa
|
|
||||||
dest='dynamic_whitelist',
|
|
||||||
const=20,
|
|
||||||
type=int,
|
|
||||||
metavar='INT',
|
|
||||||
nargs='?',
|
|
||||||
)
|
|
||||||
|
|
||||||
build_subcommands(parser)
|
|
||||||
return parser.parse_args(args)
|
|
||||||
|
|
||||||
|
|
||||||
def scripts_options(parser: argparse.ArgumentParser) -> None:
|
|
||||||
parser.add_argument(
|
|
||||||
'-p', '--pair',
|
|
||||||
help='Show profits for only this pairs. Pairs are comma-separated.',
|
|
||||||
dest='pair',
|
|
||||||
default=None
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def optimizer_shared_options(parser: argparse.ArgumentParser) -> None:
|
|
||||||
parser.add_argument(
|
|
||||||
'-i', '--ticker-interval',
|
|
||||||
help='specify ticker interval in minutes (1, 5, 30, 60, 1440)',
|
|
||||||
dest='ticker_interval',
|
|
||||||
type=int,
|
|
||||||
metavar='INT',
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'--realistic-simulation',
|
|
||||||
help='uses max_open_trades from config to simulate real world limitations',
|
|
||||||
action='store_true',
|
|
||||||
dest='realistic_simulation',
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'--timerange',
|
|
||||||
help='Specify what timerange of data to use.',
|
|
||||||
default=None,
|
|
||||||
type=str,
|
|
||||||
dest='timerange',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def backtesting_options(parser: argparse.ArgumentParser) -> None:
|
|
||||||
parser.add_argument(
|
|
||||||
'-l', '--live',
|
|
||||||
action='store_true',
|
|
||||||
dest='live',
|
|
||||||
help='using live data',
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'-r', '--refresh-pairs-cached',
|
|
||||||
help='refresh the pairs files in tests/testdata with the latest data from Bittrex. \
|
|
||||||
Use it if you want to run your backtesting with up-to-date data.',
|
|
||||||
action='store_true',
|
|
||||||
dest='refresh_pairs',
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'--export',
|
|
||||||
help='Export backtest results, argument are: trades\
|
|
||||||
Example --export=trades',
|
|
||||||
type=str,
|
|
||||||
default=None,
|
|
||||||
dest='export',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def hyperopt_options(parser: argparse.ArgumentParser) -> None:
|
|
||||||
parser.add_argument(
|
|
||||||
'-e', '--epochs',
|
|
||||||
help='specify number of epochs (default: %(default)d)',
|
|
||||||
dest='epochs',
|
|
||||||
default=100,
|
|
||||||
type=int,
|
|
||||||
metavar='INT',
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'--use-mongodb',
|
|
||||||
help='parallelize evaluations with mongodb (requires mongod in PATH)',
|
|
||||||
dest='mongodb',
|
|
||||||
action='store_true',
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'-s', '--spaces',
|
|
||||||
help='Specify which parameters to hyperopt. Space separate list. \
|
|
||||||
Default: %(default)s',
|
|
||||||
choices=['all', 'buy', 'roi', 'stoploss'],
|
|
||||||
default='all',
|
|
||||||
nargs='+',
|
|
||||||
dest='spaces',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_timerange(text):
|
|
||||||
if text is None:
|
|
||||||
return None
|
|
||||||
syntax = [(r'^-(\d{8})$', (None, 'date')),
|
|
||||||
(r'^(\d{8})-$', ('date', None)),
|
|
||||||
(r'^(\d{8})-(\d{8})$', ('date', 'date')),
|
|
||||||
(r'^(-\d+)$', (None, 'line')),
|
|
||||||
(r'^(\d+)-$', ('line', None)),
|
|
||||||
(r'^(\d+)-(\d+)$', ('index', 'index'))]
|
|
||||||
for rex, stype in syntax:
|
|
||||||
# Apply the regular expression to text
|
|
||||||
match = re.match(rex, text)
|
|
||||||
if match: # Regex has matched
|
|
||||||
rvals = match.groups()
|
|
||||||
index = 0
|
|
||||||
start = None
|
|
||||||
stop = None
|
|
||||||
if stype[0]:
|
|
||||||
start = rvals[index]
|
|
||||||
if stype[0] != 'date':
|
|
||||||
start = int(start)
|
|
||||||
index += 1
|
|
||||||
if stype[1]:
|
|
||||||
stop = rvals[index]
|
|
||||||
if stype[1] != 'date':
|
|
||||||
stop = int(stop)
|
|
||||||
return (stype, start, stop)
|
|
||||||
raise Exception('Incorrect syntax for timerange "%s"' % text)
|
|
||||||
|
|
||||||
|
|
||||||
def build_subcommands(parser: argparse.ArgumentParser) -> None:
|
|
||||||
""" Builds and attaches all subcommands """
|
|
||||||
from freqtrade.optimize import backtesting, hyperopt
|
|
||||||
|
|
||||||
subparsers = parser.add_subparsers(dest='subparser')
|
|
||||||
|
|
||||||
# Add backtesting subcommand
|
|
||||||
backtesting_cmd = subparsers.add_parser('backtesting', help='backtesting module')
|
|
||||||
backtesting_cmd.set_defaults(func=backtesting.start)
|
|
||||||
optimizer_shared_options(backtesting_cmd)
|
|
||||||
backtesting_options(backtesting_cmd)
|
|
||||||
|
|
||||||
# Add hyperopt subcommand
|
|
||||||
hyperopt_cmd = subparsers.add_parser('hyperopt', help='hyperopt module')
|
|
||||||
hyperopt_cmd.set_defaults(func=hyperopt.start)
|
|
||||||
optimizer_shared_options(hyperopt_cmd)
|
|
||||||
hyperopt_options(hyperopt_cmd)
|
|
||||||
|
|
||||||
|
|
||||||
# Required json-schema for user specified config
|
|
||||||
CONF_SCHEMA = {
|
|
||||||
'type': 'object',
|
|
||||||
'properties': {
|
|
||||||
'max_open_trades': {'type': 'integer', 'minimum': 0},
|
|
||||||
'ticker_interval': {'type': 'integer', 'enum': [1, 5, 30, 60, 1440]},
|
|
||||||
'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT']},
|
|
||||||
'stake_amount': {'type': 'number', 'minimum': 0.0005},
|
|
||||||
'fiat_display_currency': {'type': 'string', 'enum': ['AUD', 'BRL', 'CAD', 'CHF',
|
|
||||||
'CLP', 'CNY', 'CZK', 'DKK',
|
|
||||||
'EUR', 'GBP', 'HKD', 'HUF',
|
|
||||||
'IDR', 'ILS', 'INR', 'JPY',
|
|
||||||
'KRW', 'MXN', 'MYR', 'NOK',
|
|
||||||
'NZD', 'PHP', 'PKR', 'PLN',
|
|
||||||
'RUB', 'SEK', 'SGD', 'THB',
|
|
||||||
'TRY', 'TWD', 'ZAR', 'USD']},
|
|
||||||
'dry_run': {'type': 'boolean'},
|
|
||||||
'minimal_roi': {
|
|
||||||
'type': 'object',
|
|
||||||
'patternProperties': {
|
|
||||||
'^[0-9.]+$': {'type': 'number'}
|
|
||||||
},
|
|
||||||
'minProperties': 1
|
|
||||||
},
|
|
||||||
'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True},
|
|
||||||
'unfilledtimeout': {'type': 'integer', 'minimum': 0},
|
|
||||||
'bid_strategy': {
|
|
||||||
'type': 'object',
|
|
||||||
'properties': {
|
|
||||||
'ask_last_balance': {
|
|
||||||
'type': 'number',
|
|
||||||
'minimum': 0,
|
|
||||||
'maximum': 1,
|
|
||||||
'exclusiveMaximum': False
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'required': ['ask_last_balance']
|
|
||||||
},
|
|
||||||
'exchange': {'$ref': '#/definitions/exchange'},
|
|
||||||
'experimental': {
|
|
||||||
'type': 'object',
|
|
||||||
'properties': {
|
|
||||||
'use_sell_signal': {'type': 'boolean'},
|
|
||||||
'sell_profit_only': {'type': 'boolean'}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'telegram': {
|
|
||||||
'type': 'object',
|
|
||||||
'properties': {
|
|
||||||
'enabled': {'type': 'boolean'},
|
|
||||||
'token': {'type': 'string'},
|
|
||||||
'chat_id': {'type': 'string'},
|
|
||||||
},
|
|
||||||
'required': ['enabled', 'token', 'chat_id']
|
|
||||||
},
|
|
||||||
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
|
|
||||||
'internals': {
|
|
||||||
'type': 'object',
|
|
||||||
'properties': {
|
|
||||||
'process_throttle_secs': {'type': 'number'},
|
|
||||||
'interval': {'type': 'integer'}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'definitions': {
|
|
||||||
'exchange': {
|
|
||||||
'type': 'object',
|
|
||||||
'properties': {
|
|
||||||
'name': {'type': 'string'},
|
|
||||||
'key': {'type': 'string'},
|
|
||||||
'secret': {'type': 'string'},
|
|
||||||
'pair_whitelist': {
|
|
||||||
'type': 'array',
|
|
||||||
'items': {
|
|
||||||
'type': 'string',
|
|
||||||
'pattern': '^[0-9A-Z]+_[0-9A-Z]+$'
|
|
||||||
},
|
|
||||||
'uniqueItems': True
|
|
||||||
},
|
|
||||||
'pair_blacklist': {
|
|
||||||
'type': 'array',
|
|
||||||
'items': {
|
|
||||||
'type': 'string',
|
|
||||||
'pattern': '^[0-9A-Z]+_[0-9A-Z]+$'
|
|
||||||
},
|
|
||||||
'uniqueItems': True
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'required': ['name', 'key', 'secret', 'pair_whitelist']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'anyOf': [
|
|
||||||
{'required': ['exchange']}
|
|
||||||
],
|
|
||||||
'required': [
|
|
||||||
'max_open_trades',
|
|
||||||
'stake_currency',
|
|
||||||
'stake_amount',
|
|
||||||
'fiat_display_currency',
|
|
||||||
'dry_run',
|
|
||||||
'bid_strategy',
|
|
||||||
'telegram'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
@ -1,22 +1,20 @@
|
|||||||
# pragma pylint: disable=missing-docstring
|
# pragma pylint: disable=missing-docstring
|
||||||
|
|
||||||
import logging
|
import gzip
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from typing import Optional, List, Dict
|
from typing import Optional, List, Dict, Tuple
|
||||||
from pandas import DataFrame
|
|
||||||
from freqtrade.exchange import get_ticker_history
|
|
||||||
from freqtrade.analyze import populate_indicators, parse_ticker_dataframe
|
|
||||||
|
|
||||||
from freqtrade import misc
|
from freqtrade import misc
|
||||||
|
from freqtrade.exchange import get_ticker_history
|
||||||
|
from freqtrade.logger import Logger
|
||||||
from user_data.hyperopt_conf import hyperopt_optimize_conf
|
from user_data.hyperopt_conf import hyperopt_optimize_conf
|
||||||
import gzip
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = Logger(name=__name__).get_logger()
|
||||||
|
|
||||||
|
|
||||||
def trim_tickerlist(tickerlist, timerange):
|
def trim_tickerlist(tickerlist: List[Dict], timerange: Tuple[Tuple, int, int]) -> List[Dict]:
|
||||||
(stype, start, stop) = timerange
|
stype, start, stop = timerange
|
||||||
if stype == (None, 'line'):
|
if stype == (None, 'line'):
|
||||||
return tickerlist[stop:]
|
return tickerlist[stop:]
|
||||||
elif stype == ('line', None):
|
elif stype == ('line', None):
|
||||||
@ -27,7 +25,10 @@ def trim_tickerlist(tickerlist, timerange):
|
|||||||
return tickerlist
|
return tickerlist
|
||||||
|
|
||||||
|
|
||||||
def load_tickerdata_file(datadir, pair, ticker_interval, timerange=None):
|
def load_tickerdata_file(
|
||||||
|
datadir: str, pair: str,
|
||||||
|
ticker_interval: int,
|
||||||
|
timerange: Optional[Tuple[Tuple, int, int]] = None) -> Optional[List[Dict]]:
|
||||||
"""
|
"""
|
||||||
Load a pair from file,
|
Load a pair from file,
|
||||||
:return dict OR empty if unsuccesful
|
:return dict OR empty if unsuccesful
|
||||||
@ -57,12 +58,12 @@ def load_tickerdata_file(datadir, pair, ticker_interval, timerange=None):
|
|||||||
return pairdata
|
return pairdata
|
||||||
|
|
||||||
|
|
||||||
def load_data(datadir: str, ticker_interval: int, pairs: Optional[List[str]] = None,
|
def load_data(datadir: str, ticker_interval: int,
|
||||||
refresh_pairs: Optional[bool] = False, timerange=None) -> Dict[str, List]:
|
pairs: Optional[List[str]] = None,
|
||||||
|
refresh_pairs: Optional[bool] = False,
|
||||||
|
timerange: Optional[Tuple[Tuple, int, int]] = None) -> Dict[str, List]:
|
||||||
"""
|
"""
|
||||||
Loads ticker history data for the given parameters
|
Loads ticker history data for the given parameters
|
||||||
:param ticker_interval: ticker interval in minutes
|
|
||||||
:param pairs: list of pairs
|
|
||||||
:return: dict
|
:return: dict
|
||||||
"""
|
"""
|
||||||
result = {}
|
result = {}
|
||||||
@ -85,21 +86,13 @@ def load_data(datadir: str, ticker_interval: int, pairs: Optional[List[str]] = N
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def tickerdata_to_dataframe(data):
|
|
||||||
preprocessed = preprocess(data)
|
|
||||||
return preprocessed
|
|
||||||
|
|
||||||
|
|
||||||
def preprocess(tickerdata: Dict[str, List]) -> Dict[str, DataFrame]:
|
|
||||||
"""Creates a dataframe and populates indicators for given ticker data"""
|
|
||||||
return {pair: populate_indicators(parse_ticker_dataframe(pair_data))
|
|
||||||
for pair, pair_data in tickerdata.items()}
|
|
||||||
|
|
||||||
|
|
||||||
def make_testdata_path(datadir: str) -> str:
|
def make_testdata_path(datadir: str) -> str:
|
||||||
"""Return the path where testdata files are stored"""
|
"""Return the path where testdata files are stored"""
|
||||||
return datadir or os.path.abspath(os.path.join(os.path.dirname(__file__),
|
return datadir or os.path.abspath(
|
||||||
'..', 'tests', 'testdata'))
|
os.path.join(
|
||||||
|
os.path.dirname(__file__), '..', 'tests', 'testdata'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def download_pairs(datadir, pairs: List[str], ticker_interval: int) -> bool:
|
def download_pairs(datadir, pairs: List[str], ticker_interval: int) -> bool:
|
||||||
@ -108,19 +101,15 @@ def download_pairs(datadir, pairs: List[str], ticker_interval: int) -> bool:
|
|||||||
try:
|
try:
|
||||||
download_backtesting_testdata(datadir, pair=pair, interval=ticker_interval)
|
download_backtesting_testdata(datadir, pair=pair, interval=ticker_interval)
|
||||||
except BaseException:
|
except BaseException:
|
||||||
logger.info('Failed to download the pair: "{pair}", Interval: {interval} min'.format(
|
logger.info(
|
||||||
pair=pair,
|
'Failed to download the pair: "%s", Interval: %s min',
|
||||||
interval=ticker_interval,
|
pair,
|
||||||
))
|
ticker_interval
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def file_dump_json(filename, data):
|
|
||||||
with open(filename, "wt") as fp:
|
|
||||||
json.dump(data, fp)
|
|
||||||
|
|
||||||
|
|
||||||
# FIX: 20180110, suggest rename interval to tick_interval
|
# FIX: 20180110, suggest rename interval to tick_interval
|
||||||
def download_backtesting_testdata(datadir: str, pair: str, interval: int = 5) -> bool:
|
def download_backtesting_testdata(datadir: str, pair: str, interval: int = 5) -> bool:
|
||||||
"""
|
"""
|
||||||
@ -131,10 +120,11 @@ def download_backtesting_testdata(datadir: str, pair: str, interval: int = 5) ->
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
path = make_testdata_path(datadir)
|
path = make_testdata_path(datadir)
|
||||||
logger.info('Download the pair: "{pair}", Interval: {interval} min'.format(
|
logger.info(
|
||||||
pair=pair,
|
'Download the pair: "%s", Interval: %s min',
|
||||||
interval=interval,
|
pair,
|
||||||
))
|
interval
|
||||||
|
)
|
||||||
|
|
||||||
filepair = pair.replace("-", "_")
|
filepair = pair.replace("-", "_")
|
||||||
filename = os.path.join(path, '{pair}-{interval}.json'.format(
|
filename = os.path.join(path, '{pair}-{interval}.json'.format(
|
||||||
@ -143,10 +133,10 @@ def download_backtesting_testdata(datadir: str, pair: str, interval: int = 5) ->
|
|||||||
))
|
))
|
||||||
|
|
||||||
if os.path.isfile(filename):
|
if os.path.isfile(filename):
|
||||||
with open(filename, "rt") as fp:
|
with open(filename, "rt") as file:
|
||||||
data = json.load(fp)
|
data = json.load(file)
|
||||||
logger.debug("Current Start: {}".format(data[1]['T']))
|
logger.debug("Current Start: %s", data[1]['T'])
|
||||||
logger.debug("Current End: {}".format(data[-1:][0]['T']))
|
logger.debug("Current End: %s", data[-1:][0]['T'])
|
||||||
else:
|
else:
|
||||||
data = []
|
data = []
|
||||||
logger.debug("Current Start: None")
|
logger.debug("Current Start: None")
|
||||||
@ -156,8 +146,8 @@ def download_backtesting_testdata(datadir: str, pair: str, interval: int = 5) ->
|
|||||||
for row in new_data:
|
for row in new_data:
|
||||||
if row not in data:
|
if row not in data:
|
||||||
data.append(row)
|
data.append(row)
|
||||||
logger.debug("New Start: {}".format(data[1]['T']))
|
logger.debug("New Start: %s", data[1]['T'])
|
||||||
logger.debug("New End: {}".format(data[-1:][0]['T']))
|
logger.debug("New End: %s", data[-1:][0]['T'])
|
||||||
data = sorted(data, key=lambda data: data['T'])
|
data = sorted(data, key=lambda data: data['T'])
|
||||||
|
|
||||||
misc.file_dump_json(filename, data)
|
misc.file_dump_json(filename, data)
|
||||||
|
@ -1,231 +1,312 @@
|
|||||||
# pragma pylint: disable=missing-docstring,W0212
|
# pragma pylint: disable=missing-docstring, W0212, too-many-arguments
|
||||||
|
|
||||||
import logging
|
"""
|
||||||
from typing import Dict, Tuple
|
This module contains the backtesting logic
|
||||||
|
"""
|
||||||
|
from argparse import Namespace
|
||||||
|
from typing import Dict, Tuple, Any, List, Optional
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
from pandas import DataFrame, Series
|
from pandas import DataFrame, Series
|
||||||
from tabulate import tabulate
|
from tabulate import tabulate
|
||||||
|
|
||||||
import freqtrade.misc as misc
|
|
||||||
import freqtrade.optimize as optimize
|
import freqtrade.optimize as optimize
|
||||||
from freqtrade import exchange
|
from freqtrade import exchange
|
||||||
from freqtrade.analyze import populate_buy_trend, populate_sell_trend
|
from freqtrade.analyze import Analyze
|
||||||
|
from freqtrade.arguments import Arguments
|
||||||
|
from freqtrade.configuration import Configuration
|
||||||
from freqtrade.exchange import Bittrex
|
from freqtrade.exchange import Bittrex
|
||||||
from freqtrade.main import should_sell
|
from freqtrade.logger import Logger
|
||||||
|
from freqtrade.misc import file_dump_json
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.strategy.strategy import Strategy
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]:
|
class Backtesting(object):
|
||||||
"""
|
"""
|
||||||
Get the maximum timeframe for the given backtest data
|
Backtesting class, this class contains all the logic to run a backtest
|
||||||
:param data: dictionary with preprocessed backtesting data
|
|
||||||
:return: tuple containing min_date, max_date
|
|
||||||
"""
|
|
||||||
all_dates = Series([])
|
|
||||||
for pair_data in data.values():
|
|
||||||
all_dates = all_dates.append(pair_data['date'])
|
|
||||||
all_dates.sort_values(inplace=True)
|
|
||||||
return arrow.get(all_dates.iloc[0]), arrow.get(all_dates.iloc[-1])
|
|
||||||
|
|
||||||
|
To run a backtest:
|
||||||
|
backtesting = Backtesting(config)
|
||||||
|
backtesting.start()
|
||||||
|
"""
|
||||||
|
def __init__(self, config: Dict[str, Any]) -> None:
|
||||||
|
|
||||||
def generate_text_table(
|
# Init the logger
|
||||||
data: Dict[str, Dict], results: DataFrame, stake_currency) -> str:
|
self.logging = Logger(name=__name__, level=config['loglevel'])
|
||||||
"""
|
self.logger = self.logging.get_logger()
|
||||||
Generates and returns a text table for the given backtest data and the results dataframe
|
self.config = config
|
||||||
:return: pretty printed table with tabulate as str
|
self.analyze = None
|
||||||
"""
|
self.ticker_interval = None
|
||||||
floatfmt = ('s', 'd', '.2f', '.8f', '.1f')
|
self.tickerdata_to_dataframe = None
|
||||||
tabular_data = []
|
self.populate_buy_trend = None
|
||||||
headers = ['pair', 'buy count', 'avg profit %',
|
self.populate_sell_trend = None
|
||||||
'total profit ' + stake_currency, 'avg duration', 'profit', 'loss']
|
self._init()
|
||||||
for pair in data:
|
|
||||||
result = results[results.currency == pair]
|
def _init(self) -> None:
|
||||||
|
"""
|
||||||
|
Init objects required for backtesting
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self.analyze = Analyze(self.config)
|
||||||
|
self.ticker_interval = self.analyze.strategy.ticker_interval
|
||||||
|
self.tickerdata_to_dataframe = self.analyze.tickerdata_to_dataframe
|
||||||
|
self.populate_buy_trend = self.analyze.populate_buy_trend
|
||||||
|
self.populate_sell_trend = self.analyze.populate_sell_trend
|
||||||
|
exchange._API = Bittrex({'key': '', 'secret': ''})
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]:
|
||||||
|
"""
|
||||||
|
Get the maximum timeframe for the given backtest data
|
||||||
|
:param data: dictionary with preprocessed backtesting data
|
||||||
|
:return: tuple containing min_date, max_date
|
||||||
|
"""
|
||||||
|
all_dates = Series([])
|
||||||
|
for pair_data in data.values():
|
||||||
|
all_dates = all_dates.append(pair_data['date'])
|
||||||
|
all_dates.sort_values(inplace=True)
|
||||||
|
return arrow.get(all_dates.iloc[0]), arrow.get(all_dates.iloc[-1])
|
||||||
|
|
||||||
|
def _generate_text_table(self, data: Dict[str, Dict], results: DataFrame) -> str:
|
||||||
|
"""
|
||||||
|
Generates and returns a text table for the given backtest data and the results dataframe
|
||||||
|
:return: pretty printed table with tabulate as str
|
||||||
|
"""
|
||||||
|
stake_currency = self.config.get('stake_currency')
|
||||||
|
|
||||||
|
floatfmt = ('s', 'd', '.2f', '.8f', '.1f')
|
||||||
|
tabular_data = []
|
||||||
|
headers = ['pair', 'buy count', 'avg profit %',
|
||||||
|
'total profit ' + stake_currency, 'avg duration', 'profit', 'loss']
|
||||||
|
for pair in data:
|
||||||
|
result = results[results.currency == pair]
|
||||||
|
tabular_data.append([
|
||||||
|
pair,
|
||||||
|
len(result.index),
|
||||||
|
result.profit_percent.mean() * 100.0,
|
||||||
|
result.profit_BTC.sum(),
|
||||||
|
result.duration.mean(),
|
||||||
|
len(result[result.profit_BTC > 0]),
|
||||||
|
len(result[result.profit_BTC < 0])
|
||||||
|
])
|
||||||
|
|
||||||
|
# Append Total
|
||||||
tabular_data.append([
|
tabular_data.append([
|
||||||
pair,
|
'TOTAL',
|
||||||
len(result.index),
|
len(results.index),
|
||||||
result.profit_percent.mean() * 100.0,
|
results.profit_percent.mean() * 100.0,
|
||||||
result.profit_BTC.sum(),
|
results.profit_BTC.sum(),
|
||||||
result.duration.mean(),
|
results.duration.mean(),
|
||||||
len(result[result.profit_BTC > 0]),
|
len(results[results.profit_BTC > 0]),
|
||||||
len(result[result.profit_BTC < 0])
|
len(results[results.profit_BTC < 0])
|
||||||
])
|
])
|
||||||
|
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt)
|
||||||
|
|
||||||
# Append Total
|
def _get_sell_trade_entry(
|
||||||
tabular_data.append([
|
self, pair: str, buy_row: DataFrame,
|
||||||
'TOTAL',
|
partial_ticker: List, trade_count_lock: Dict, args: Dict) -> Optional[Tuple]:
|
||||||
len(results.index),
|
|
||||||
results.profit_percent.mean() * 100.0,
|
|
||||||
results.profit_BTC.sum(),
|
|
||||||
results.duration.mean(),
|
|
||||||
len(results[results.profit_BTC > 0]),
|
|
||||||
len(results[results.profit_BTC < 0])
|
|
||||||
])
|
|
||||||
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt)
|
|
||||||
|
|
||||||
|
stake_amount = args['stake_amount']
|
||||||
|
max_open_trades = args.get('max_open_trades', 0)
|
||||||
|
trade = Trade(
|
||||||
|
open_rate=buy_row.close,
|
||||||
|
open_date=buy_row.date,
|
||||||
|
stake_amount=stake_amount,
|
||||||
|
amount=stake_amount / buy_row.open,
|
||||||
|
fee=exchange.get_fee()
|
||||||
|
)
|
||||||
|
|
||||||
def get_sell_trade_entry(pair, buy_row, partial_ticker, trade_count_lock, args):
|
# calculate win/lose forwards from buy point
|
||||||
stake_amount = args['stake_amount']
|
for sell_row in partial_ticker:
|
||||||
max_open_trades = args.get('max_open_trades', 0)
|
|
||||||
trade = Trade(open_rate=buy_row.close,
|
|
||||||
open_date=buy_row.date,
|
|
||||||
stake_amount=stake_amount,
|
|
||||||
amount=stake_amount / buy_row.open,
|
|
||||||
fee=exchange.get_fee()
|
|
||||||
)
|
|
||||||
|
|
||||||
# calculate win/lose forwards from buy point
|
|
||||||
for sell_row in partial_ticker:
|
|
||||||
if max_open_trades > 0:
|
|
||||||
# Increase trade_count_lock for every iteration
|
|
||||||
trade_count_lock[sell_row.date] = trade_count_lock.get(sell_row.date, 0) + 1
|
|
||||||
|
|
||||||
buy_signal = sell_row.buy
|
|
||||||
if should_sell(trade, sell_row.close, sell_row.date, buy_signal, sell_row.sell):
|
|
||||||
return sell_row, (pair,
|
|
||||||
trade.calc_profit_percent(rate=sell_row.close),
|
|
||||||
trade.calc_profit(rate=sell_row.close),
|
|
||||||
(sell_row.date - buy_row.date).seconds // 60
|
|
||||||
), sell_row.date
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def backtest(args) -> DataFrame:
|
|
||||||
"""
|
|
||||||
Implements backtesting functionality
|
|
||||||
:param args: a dict containing:
|
|
||||||
stake_amount: btc amount to use for each trade
|
|
||||||
processed: a processed dictionary with format {pair, data}
|
|
||||||
max_open_trades: maximum number of concurrent trades (default: 0, disabled)
|
|
||||||
realistic: do we try to simulate realistic trades? (default: True)
|
|
||||||
sell_profit_only: sell if profit only
|
|
||||||
use_sell_signal: act on sell-signal
|
|
||||||
:return: DataFrame
|
|
||||||
"""
|
|
||||||
headers = ['date', 'buy', 'open', 'close', 'sell']
|
|
||||||
processed = args['processed']
|
|
||||||
max_open_trades = args.get('max_open_trades', 0)
|
|
||||||
realistic = args.get('realistic', False)
|
|
||||||
record = args.get('record', None)
|
|
||||||
records = []
|
|
||||||
trades = []
|
|
||||||
trade_count_lock: dict = {}
|
|
||||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
|
||||||
for pair, pair_data in processed.items():
|
|
||||||
pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run
|
|
||||||
|
|
||||||
ticker_data = populate_sell_trend(populate_buy_trend(pair_data))[headers]
|
|
||||||
ticker = [x for x in ticker_data.itertuples()]
|
|
||||||
|
|
||||||
lock_pair_until = None
|
|
||||||
for index, row in enumerate(ticker):
|
|
||||||
if row.buy == 0 or row.sell == 1:
|
|
||||||
continue # skip rows where no buy signal or that would immediately sell off
|
|
||||||
|
|
||||||
if realistic:
|
|
||||||
if lock_pair_until is not None and row.date <= lock_pair_until:
|
|
||||||
continue
|
|
||||||
if max_open_trades > 0:
|
if max_open_trades > 0:
|
||||||
# Check if max_open_trades has already been reached for the given date
|
# Increase trade_count_lock for every iteration
|
||||||
if not trade_count_lock.get(row.date, 0) < max_open_trades:
|
trade_count_lock[sell_row.date] = trade_count_lock.get(sell_row.date, 0) + 1
|
||||||
continue
|
|
||||||
trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1
|
|
||||||
|
|
||||||
ret = get_sell_trade_entry(pair, row, ticker[index+1:], trade_count_lock, args)
|
buy_signal = sell_row.buy
|
||||||
if ret:
|
if self.analyze.should_sell(trade, sell_row.close, sell_row.date, buy_signal,
|
||||||
row2, trade_entry, next_date = ret
|
sell_row.sell):
|
||||||
lock_pair_until = next_date
|
return \
|
||||||
trades.append(trade_entry)
|
sell_row, \
|
||||||
if record:
|
(
|
||||||
# Note, need to be json.dump friendly
|
pair,
|
||||||
# record a tuple of pair, current_profit_percent,
|
trade.calc_profit_percent(rate=sell_row.close),
|
||||||
# entry-date, duration
|
trade.calc_profit(rate=sell_row.close),
|
||||||
records.append((pair, trade_entry[1],
|
(sell_row.date - buy_row.date).seconds // 60
|
||||||
row.date.strftime('%s'),
|
), \
|
||||||
row2.date.strftime('%s'),
|
sell_row.date
|
||||||
row.date, trade_entry[3]))
|
return None
|
||||||
# For now export inside backtest(), maybe change so that backtest()
|
|
||||||
# returns a tuple like: (dataframe, records, logs, etc)
|
def backtest(self, args: Dict) -> DataFrame:
|
||||||
if record and record.find('trades') >= 0:
|
"""
|
||||||
logger.info('Dumping backtest results')
|
Implements backtesting functionality
|
||||||
misc.file_dump_json('backtest-result.json', records)
|
|
||||||
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration']
|
NOTE: This method is used by Hyperopt at each iteration. Please keep it optimized.
|
||||||
return DataFrame.from_records(trades, columns=labels)
|
Of course try to not have ugly code. By some accessor are sometime slower than functions.
|
||||||
|
Avoid, logging on this method
|
||||||
|
|
||||||
|
:param args: a dict containing:
|
||||||
|
stake_amount: btc amount to use for each trade
|
||||||
|
processed: a processed dictionary with format {pair, data}
|
||||||
|
max_open_trades: maximum number of concurrent trades (default: 0, disabled)
|
||||||
|
realistic: do we try to simulate realistic trades? (default: True)
|
||||||
|
sell_profit_only: sell if profit only
|
||||||
|
use_sell_signal: act on sell-signal
|
||||||
|
:return: DataFrame
|
||||||
|
"""
|
||||||
|
headers = ['date', 'buy', 'open', 'close', 'sell']
|
||||||
|
processed = args['processed']
|
||||||
|
max_open_trades = args.get('max_open_trades', 0)
|
||||||
|
realistic = args.get('realistic', False)
|
||||||
|
record = args.get('record', None)
|
||||||
|
records = []
|
||||||
|
trades = []
|
||||||
|
trade_count_lock = {}
|
||||||
|
for pair, pair_data in processed.items():
|
||||||
|
pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run
|
||||||
|
|
||||||
|
ticker_data = self.populate_sell_trend(self.populate_buy_trend(pair_data))[headers]
|
||||||
|
ticker = [x for x in ticker_data.itertuples()]
|
||||||
|
|
||||||
|
lock_pair_until = None
|
||||||
|
for index, row in enumerate(ticker):
|
||||||
|
if row.buy == 0 or row.sell == 1:
|
||||||
|
continue # skip rows where no buy signal or that would immediately sell off
|
||||||
|
|
||||||
|
if realistic:
|
||||||
|
if lock_pair_until is not None and row.date <= lock_pair_until:
|
||||||
|
continue
|
||||||
|
if max_open_trades > 0:
|
||||||
|
# Check if max_open_trades has already been reached for the given date
|
||||||
|
if not trade_count_lock.get(row.date, 0) < max_open_trades:
|
||||||
|
continue
|
||||||
|
|
||||||
|
trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1
|
||||||
|
|
||||||
|
ret = self._get_sell_trade_entry(pair, row, ticker[index + 1:],
|
||||||
|
trade_count_lock, args)
|
||||||
|
|
||||||
|
if ret:
|
||||||
|
row2, trade_entry, next_date = ret
|
||||||
|
lock_pair_until = next_date
|
||||||
|
trades.append(trade_entry)
|
||||||
|
if record:
|
||||||
|
# Note, need to be json.dump friendly
|
||||||
|
# record a tuple of pair, current_profit_percent,
|
||||||
|
# entry-date, duration
|
||||||
|
records.append((pair, trade_entry[1],
|
||||||
|
row.date.strftime('%s'),
|
||||||
|
row2.date.strftime('%s'),
|
||||||
|
row.date, trade_entry[3]))
|
||||||
|
# For now export inside backtest(), maybe change so that backtest()
|
||||||
|
# returns a tuple like: (dataframe, records, logs, etc)
|
||||||
|
if record and record.find('trades') >= 0:
|
||||||
|
self.logger.info('Dumping backtest results')
|
||||||
|
file_dump_json('backtest-result.json', records)
|
||||||
|
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration']
|
||||||
|
return DataFrame.from_records(trades, columns=labels)
|
||||||
|
|
||||||
|
def start(self) -> None:
|
||||||
|
"""
|
||||||
|
Run a backtesting end-to-end
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
data = {}
|
||||||
|
pairs = self.config['exchange']['pair_whitelist']
|
||||||
|
self.logger.info('Using stake_currency: %s ...', self.config['stake_currency'])
|
||||||
|
self.logger.info('Using stake_amount: %s ...', self.config['stake_amount'])
|
||||||
|
|
||||||
|
if self.config.get('live'):
|
||||||
|
self.logger.info('Downloading data for all pairs in whitelist ...')
|
||||||
|
for pair in pairs:
|
||||||
|
data[pair] = exchange.get_ticker_history(pair, self.ticker_interval)
|
||||||
|
else:
|
||||||
|
self.logger.info('Using local backtesting data (using whitelist in given config) ...')
|
||||||
|
|
||||||
|
timerange = Arguments.parse_timerange(self.config.get('timerange'))
|
||||||
|
data = optimize.load_data(
|
||||||
|
self.config['datadir'],
|
||||||
|
pairs=pairs,
|
||||||
|
ticker_interval=self.ticker_interval,
|
||||||
|
refresh_pairs=self.config.get('refresh_pairs', False),
|
||||||
|
timerange=timerange
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ignore max_open_trades in backtesting, except realistic flag was passed
|
||||||
|
if self.config.get('realistic_simulation', False):
|
||||||
|
max_open_trades = self.config['max_open_trades']
|
||||||
|
else:
|
||||||
|
self.logger.info('Ignoring max_open_trades (realistic_simulation not set) ...')
|
||||||
|
max_open_trades = 0
|
||||||
|
|
||||||
|
preprocessed = self.tickerdata_to_dataframe(data)
|
||||||
|
|
||||||
|
# Print timeframe
|
||||||
|
min_date, max_date = self.get_timeframe(preprocessed)
|
||||||
|
self.logger.info(
|
||||||
|
'Measuring data from %s up to %s (%s days)..',
|
||||||
|
min_date.isoformat(),
|
||||||
|
max_date.isoformat(),
|
||||||
|
(max_date - min_date).days
|
||||||
|
)
|
||||||
|
|
||||||
|
# Execute backtest and print results
|
||||||
|
sell_profit_only = self.config.get('experimental', {}).get('sell_profit_only', False)
|
||||||
|
use_sell_signal = self.config.get('experimental', {}).get('use_sell_signal', False)
|
||||||
|
results = self.backtest(
|
||||||
|
{
|
||||||
|
'stake_amount': self.config.get('stake_amount'),
|
||||||
|
'processed': preprocessed,
|
||||||
|
'max_open_trades': max_open_trades,
|
||||||
|
'realistic': self.config.get('realistic_simulation', False),
|
||||||
|
'sell_profit_only': sell_profit_only,
|
||||||
|
'use_sell_signal': use_sell_signal,
|
||||||
|
'record': self.config.get('export')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.logging.set_format('%(message)s')
|
||||||
|
self.logger.info(
|
||||||
|
'\n==================================== '
|
||||||
|
'BACKTESTING REPORT'
|
||||||
|
' ====================================\n'
|
||||||
|
'%s',
|
||||||
|
self._generate_text_table(
|
||||||
|
data,
|
||||||
|
results
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def start(args):
|
def setup_configuration(args: Namespace) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Prepare the configuration for the backtesting
|
||||||
|
:param args: Cli args from Arguments()
|
||||||
|
:return: Configuration
|
||||||
|
"""
|
||||||
|
configuration = Configuration(args)
|
||||||
|
config = configuration.get_config()
|
||||||
|
|
||||||
|
# Ensure we do not use Exchange credentials
|
||||||
|
config['exchange']['key'] = ''
|
||||||
|
config['exchange']['secret'] = ''
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def start(args: Namespace) -> None:
|
||||||
|
"""
|
||||||
|
Start Backtesting script
|
||||||
|
:param args: Cli args from Arguments()
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
|
||||||
# Initialize logger
|
# Initialize logger
|
||||||
logging.basicConfig(
|
logger = Logger(name=__name__).get_logger()
|
||||||
level=args.loglevel,
|
logger.info('Starting freqtrade in Backtesting mode')
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
||||||
)
|
|
||||||
|
|
||||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
# Initialize configuration
|
||||||
|
config = setup_configuration(args)
|
||||||
|
|
||||||
logger.info('Using config: %s ...', args.config)
|
# Initialize backtesting object
|
||||||
config = misc.load_config(args.config)
|
backtesting = Backtesting(config)
|
||||||
|
backtesting.start()
|
||||||
# If -i/--ticker-interval is use we override the configuration parameter
|
|
||||||
# (that will override the strategy configuration)
|
|
||||||
if args.ticker_interval:
|
|
||||||
config.update({'ticker_interval': args.ticker_interval})
|
|
||||||
|
|
||||||
# init the strategy to use
|
|
||||||
config.update({'strategy': args.strategy})
|
|
||||||
strategy = Strategy()
|
|
||||||
strategy.init(config)
|
|
||||||
|
|
||||||
logger.info('Using ticker_interval: %d ...', strategy.ticker_interval)
|
|
||||||
|
|
||||||
data = {}
|
|
||||||
pairs = config['exchange']['pair_whitelist']
|
|
||||||
logger.info('Using stake_currency: %s ...', config['stake_currency'])
|
|
||||||
logger.info('Using stake_amount: %s ...', config['stake_amount'])
|
|
||||||
|
|
||||||
if args.live:
|
|
||||||
logger.info('Downloading data for all pairs in whitelist ...')
|
|
||||||
for pair in pairs:
|
|
||||||
data[pair] = exchange.get_ticker_history(pair, strategy.ticker_interval)
|
|
||||||
else:
|
|
||||||
logger.info('Using local backtesting data (using whitelist in given config) ...')
|
|
||||||
|
|
||||||
timerange = misc.parse_timerange(args.timerange)
|
|
||||||
data = optimize.load_data(args.datadir,
|
|
||||||
pairs=pairs,
|
|
||||||
ticker_interval=strategy.ticker_interval,
|
|
||||||
refresh_pairs=args.refresh_pairs,
|
|
||||||
timerange=timerange)
|
|
||||||
max_open_trades = 0
|
|
||||||
if args.realistic_simulation:
|
|
||||||
logger.info('Using max_open_trades: %s ...', config['max_open_trades'])
|
|
||||||
max_open_trades = config['max_open_trades']
|
|
||||||
|
|
||||||
# Monkey patch config
|
|
||||||
from freqtrade import main
|
|
||||||
main._CONF = config
|
|
||||||
|
|
||||||
preprocessed = optimize.tickerdata_to_dataframe(data)
|
|
||||||
# Print timeframe
|
|
||||||
min_date, max_date = get_timeframe(preprocessed)
|
|
||||||
logger.info('Measuring data from %s up to %s (%s days)..',
|
|
||||||
min_date.isoformat(),
|
|
||||||
max_date.isoformat(),
|
|
||||||
(max_date-min_date).days)
|
|
||||||
# Execute backtest and print results
|
|
||||||
sell_profit_only = config.get('experimental', {}).get('sell_profit_only', False)
|
|
||||||
use_sell_signal = config.get('experimental', {}).get('use_sell_signal', False)
|
|
||||||
results = backtest({'stake_amount': config['stake_amount'],
|
|
||||||
'processed': preprocessed,
|
|
||||||
'max_open_trades': max_open_trades,
|
|
||||||
'realistic': args.realistic_simulation,
|
|
||||||
'sell_profit_only': sell_profit_only,
|
|
||||||
'use_sell_signal': use_sell_signal,
|
|
||||||
'record': args.export
|
|
||||||
})
|
|
||||||
logger.info(
|
|
||||||
'\n==================================== BACKTESTING REPORT ====================================\n%s', # noqa
|
|
||||||
generate_text_table(data, results, config['stake_currency'])
|
|
||||||
)
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,3 +1,7 @@
|
|||||||
|
"""
|
||||||
|
This module contains the class to persist trades into SQLite
|
||||||
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from decimal import Decimal, getcontext
|
from decimal import Decimal, getcontext
|
||||||
@ -72,6 +76,9 @@ def clean_dry_run_db() -> None:
|
|||||||
|
|
||||||
|
|
||||||
class Trade(_DECL_BASE):
|
class Trade(_DECL_BASE):
|
||||||
|
"""
|
||||||
|
Class used to define a trade structure
|
||||||
|
"""
|
||||||
__tablename__ = 'trades'
|
__tablename__ = 'trades'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
@ -200,6 +207,7 @@ class Trade(_DECL_BASE):
|
|||||||
Calculates the profit in percentage (including fee).
|
Calculates the profit in percentage (including fee).
|
||||||
:param rate: rate to compare with (optional).
|
:param rate: rate to compare with (optional).
|
||||||
If rate is not set self.close_rate will be used
|
If rate is not set self.close_rate will be used
|
||||||
|
:param fee: fee to use on the close rate (optional).
|
||||||
:return: profit in percentage as float
|
:return: profit in percentage as float
|
||||||
"""
|
"""
|
||||||
getcontext().prec = 8
|
getcontext().prec = 8
|
||||||
|
@ -1,415 +0,0 @@
|
|||||||
import logging
|
|
||||||
import re
|
|
||||||
import arrow
|
|
||||||
from decimal import Decimal
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from pandas import DataFrame
|
|
||||||
import sqlalchemy as sql
|
|
||||||
# from sqlalchemy import and_, func, text
|
|
||||||
|
|
||||||
from freqtrade.persistence import Trade
|
|
||||||
from freqtrade.misc import State, get_state, update_state
|
|
||||||
from freqtrade import exchange
|
|
||||||
from freqtrade.fiat_convert import CryptoToFiatConverter
|
|
||||||
from . import telegram
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
_FIAT_CONVERT = CryptoToFiatConverter()
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
def shorten_date(_date):
|
|
||||||
"""
|
|
||||||
Trim the date so it fits on small screens
|
|
||||||
"""
|
|
||||||
new_date = re.sub('seconds?', 'sec', _date)
|
|
||||||
new_date = re.sub('minutes?', 'min', new_date)
|
|
||||||
new_date = re.sub('hours?', 'h', new_date)
|
|
||||||
new_date = re.sub('days?', 'd', new_date)
|
|
||||||
new_date = re.sub('^an?', '1', new_date)
|
|
||||||
return new_date
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Below follows the RPC backend
|
|
||||||
# it is prefixed with rpc_
|
|
||||||
# to raise awareness that it is
|
|
||||||
# a remotely exposed function
|
|
||||||
|
|
||||||
|
|
||||||
def rpc_trade_status():
|
|
||||||
# Fetch open trade
|
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
|
||||||
if get_state() != State.RUNNING:
|
|
||||||
return (True, '*Status:* `trader is not running`')
|
|
||||||
elif not trades:
|
|
||||||
return (True, '*Status:* `no active trade`')
|
|
||||||
else:
|
|
||||||
result = []
|
|
||||||
for trade in trades:
|
|
||||||
order = None
|
|
||||||
if trade.open_order_id:
|
|
||||||
order = exchange.get_order(trade.open_order_id)
|
|
||||||
# calculate profit and send message to user
|
|
||||||
current_rate = exchange.get_ticker(trade.pair, False)['bid']
|
|
||||||
current_profit = trade.calc_profit_percent(current_rate)
|
|
||||||
fmt_close_profit = '{:.2f}%'.format(
|
|
||||||
round(trade.close_profit * 100, 2)
|
|
||||||
) if trade.close_profit else None
|
|
||||||
message = """
|
|
||||||
*Trade ID:* `{trade_id}`
|
|
||||||
*Current Pair:* [{pair}]({market_url})
|
|
||||||
*Open Since:* `{date}`
|
|
||||||
*Amount:* `{amount}`
|
|
||||||
*Open Rate:* `{open_rate:.8f}`
|
|
||||||
*Close Rate:* `{close_rate}`
|
|
||||||
*Current Rate:* `{current_rate:.8f}`
|
|
||||||
*Close Profit:* `{close_profit}`
|
|
||||||
*Current Profit:* `{current_profit:.2f}%`
|
|
||||||
*Open Order:* `{open_order}`
|
|
||||||
""".format(
|
|
||||||
trade_id=trade.id,
|
|
||||||
pair=trade.pair,
|
|
||||||
market_url=exchange.get_pair_detail_url(trade.pair),
|
|
||||||
date=arrow.get(trade.open_date).humanize(),
|
|
||||||
open_rate=trade.open_rate,
|
|
||||||
close_rate=trade.close_rate,
|
|
||||||
current_rate=current_rate,
|
|
||||||
amount=round(trade.amount, 8),
|
|
||||||
close_profit=fmt_close_profit,
|
|
||||||
current_profit=round(current_profit * 100, 2),
|
|
||||||
open_order='({} rem={:.8f})'.format(
|
|
||||||
order['type'], order['remaining']
|
|
||||||
) if order else None,
|
|
||||||
)
|
|
||||||
result.append(message)
|
|
||||||
return (False, result)
|
|
||||||
|
|
||||||
|
|
||||||
def rpc_status_table():
|
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
|
||||||
if get_state() != State.RUNNING:
|
|
||||||
return (True, '*Status:* `trader is not running`')
|
|
||||||
elif not trades:
|
|
||||||
return (True, '*Status:* `no active order`')
|
|
||||||
else:
|
|
||||||
trades_list = []
|
|
||||||
for trade in trades:
|
|
||||||
# calculate profit and send message to user
|
|
||||||
current_rate = exchange.get_ticker(trade.pair, False)['bid']
|
|
||||||
trades_list.append([
|
|
||||||
trade.id,
|
|
||||||
trade.pair,
|
|
||||||
shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)),
|
|
||||||
'{:.2f}%'.format(100 * trade.calc_profit_percent(current_rate))
|
|
||||||
])
|
|
||||||
|
|
||||||
columns = ['ID', 'Pair', 'Since', 'Profit']
|
|
||||||
df_statuses = DataFrame.from_records(trades_list, columns=columns)
|
|
||||||
df_statuses = df_statuses.set_index(columns[0])
|
|
||||||
# The style used throughout is to return a tuple
|
|
||||||
# consisting of (error_occured?, result)
|
|
||||||
# Another approach would be to just return the
|
|
||||||
# result, or raise error
|
|
||||||
return (False, df_statuses)
|
|
||||||
|
|
||||||
|
|
||||||
def rpc_daily_profit(timescale, stake_currency, fiat_display_currency):
|
|
||||||
today = datetime.utcnow().date()
|
|
||||||
profit_days = {}
|
|
||||||
|
|
||||||
if not (isinstance(timescale, int) and timescale > 0):
|
|
||||||
return (True, '*Daily [n]:* `must be an integer greater than 0`')
|
|
||||||
|
|
||||||
fiat = _FIAT_CONVERT
|
|
||||||
for day in range(0, timescale):
|
|
||||||
profitday = today - timedelta(days=day)
|
|
||||||
trades = Trade.query \
|
|
||||||
.filter(Trade.is_open.is_(False)) \
|
|
||||||
.filter(Trade.close_date >= profitday)\
|
|
||||||
.filter(Trade.close_date < (profitday + timedelta(days=1)))\
|
|
||||||
.order_by(Trade.close_date)\
|
|
||||||
.all()
|
|
||||||
curdayprofit = sum(trade.calc_profit() for trade in trades)
|
|
||||||
profit_days[profitday] = {
|
|
||||||
'amount': format(curdayprofit, '.8f'),
|
|
||||||
'trades': len(trades)
|
|
||||||
}
|
|
||||||
|
|
||||||
stats = [
|
|
||||||
[
|
|
||||||
key,
|
|
||||||
'{value:.8f} {symbol}'.format(
|
|
||||||
value=float(value['amount']),
|
|
||||||
symbol=stake_currency
|
|
||||||
),
|
|
||||||
'{value:.3f} {symbol}'.format(
|
|
||||||
value=fiat.convert_amount(
|
|
||||||
value['amount'],
|
|
||||||
stake_currency,
|
|
||||||
fiat_display_currency
|
|
||||||
),
|
|
||||||
symbol=fiat_display_currency
|
|
||||||
),
|
|
||||||
'{value} trade{s}'.format(value=value['trades'], s='' if value['trades'] < 2 else 's'),
|
|
||||||
]
|
|
||||||
for key, value in profit_days.items()
|
|
||||||
]
|
|
||||||
return (False, stats)
|
|
||||||
|
|
||||||
|
|
||||||
def rpc_trade_statistics(stake_currency, fiat_display_currency) -> None:
|
|
||||||
"""
|
|
||||||
:return: cumulative profit statistics.
|
|
||||||
"""
|
|
||||||
trades = Trade.query.order_by(Trade.id).all()
|
|
||||||
|
|
||||||
profit_all_coin = []
|
|
||||||
profit_all_percent = []
|
|
||||||
profit_closed_coin = []
|
|
||||||
profit_closed_percent = []
|
|
||||||
durations = []
|
|
||||||
|
|
||||||
for trade in trades:
|
|
||||||
current_rate = None
|
|
||||||
|
|
||||||
if not trade.open_rate:
|
|
||||||
continue
|
|
||||||
if trade.close_date:
|
|
||||||
durations.append((trade.close_date - trade.open_date).total_seconds())
|
|
||||||
|
|
||||||
if not trade.is_open:
|
|
||||||
profit_percent = trade.calc_profit_percent()
|
|
||||||
profit_closed_coin.append(trade.calc_profit())
|
|
||||||
profit_closed_percent.append(profit_percent)
|
|
||||||
else:
|
|
||||||
# Get current rate
|
|
||||||
current_rate = exchange.get_ticker(trade.pair, False)['bid']
|
|
||||||
profit_percent = trade.calc_profit_percent(rate=current_rate)
|
|
||||||
|
|
||||||
profit_all_coin.append(trade.calc_profit(rate=Decimal(trade.close_rate or current_rate)))
|
|
||||||
profit_all_percent.append(profit_percent)
|
|
||||||
|
|
||||||
best_pair = Trade.session.query(Trade.pair,
|
|
||||||
sql.func.sum(Trade.close_profit).label('profit_sum')) \
|
|
||||||
.filter(Trade.is_open.is_(False)) \
|
|
||||||
.group_by(Trade.pair) \
|
|
||||||
.order_by(sql.text('profit_sum DESC')) \
|
|
||||||
.first()
|
|
||||||
|
|
||||||
if not best_pair:
|
|
||||||
return (True, '*Status:* `no closed trade`')
|
|
||||||
|
|
||||||
bp_pair, bp_rate = best_pair
|
|
||||||
|
|
||||||
# FIX: we want to keep fiatconverter in a state/environment,
|
|
||||||
# doing this will utilize its caching functionallity, instead we reinitialize it here
|
|
||||||
fiat = _FIAT_CONVERT
|
|
||||||
# Prepare data to display
|
|
||||||
profit_closed_coin = round(sum(profit_closed_coin), 8)
|
|
||||||
profit_closed_percent = round(sum(profit_closed_percent) * 100, 2)
|
|
||||||
profit_closed_fiat = fiat.convert_amount(
|
|
||||||
profit_closed_coin,
|
|
||||||
stake_currency,
|
|
||||||
fiat_display_currency
|
|
||||||
)
|
|
||||||
profit_all_coin = round(sum(profit_all_coin), 8)
|
|
||||||
profit_all_percent = round(sum(profit_all_percent) * 100, 2)
|
|
||||||
profit_all_fiat = fiat.convert_amount(
|
|
||||||
profit_all_coin,
|
|
||||||
stake_currency,
|
|
||||||
fiat_display_currency
|
|
||||||
)
|
|
||||||
num = float(len(durations) or 1)
|
|
||||||
return (False,
|
|
||||||
{'profit_closed_coin': profit_closed_coin,
|
|
||||||
'profit_closed_percent': profit_closed_percent,
|
|
||||||
'profit_closed_fiat': profit_closed_fiat,
|
|
||||||
'profit_all_coin': profit_all_coin,
|
|
||||||
'profit_all_percent': profit_all_percent,
|
|
||||||
'profit_all_fiat': profit_all_fiat,
|
|
||||||
'trade_count': len(trades),
|
|
||||||
'first_trade_date': arrow.get(trades[0].open_date).humanize(),
|
|
||||||
'latest_trade_date': arrow.get(trades[-1].open_date).humanize(),
|
|
||||||
'avg_duration': str(timedelta(seconds=sum(durations) /
|
|
||||||
num)).split('.')[0],
|
|
||||||
'best_pair': bp_pair,
|
|
||||||
'best_rate': round(bp_rate * 100, 2)
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
def rpc_balance(fiat_display_currency):
|
|
||||||
"""
|
|
||||||
:return: current account balance per crypto
|
|
||||||
"""
|
|
||||||
balances = [
|
|
||||||
c for c in exchange.get_balances()
|
|
||||||
if c['Balance'] or c['Available'] or c['Pending']
|
|
||||||
]
|
|
||||||
if not balances:
|
|
||||||
return (True, '`All balances are zero.`')
|
|
||||||
|
|
||||||
output = []
|
|
||||||
total = 0.0
|
|
||||||
for currency in balances:
|
|
||||||
coin = currency['Currency']
|
|
||||||
if coin == 'BTC':
|
|
||||||
currency["Rate"] = 1.0
|
|
||||||
else:
|
|
||||||
if coin == 'USDT':
|
|
||||||
currency["Rate"] = 1.0 / exchange.get_ticker('USDT_BTC', False)['bid']
|
|
||||||
else:
|
|
||||||
currency["Rate"] = exchange.get_ticker('BTC_' + coin, False)['bid']
|
|
||||||
currency['BTC'] = currency["Rate"] * currency["Balance"]
|
|
||||||
total = total + currency['BTC']
|
|
||||||
output.append({'currency': currency['Currency'],
|
|
||||||
'available': currency['Available'],
|
|
||||||
'balance': currency['Balance'],
|
|
||||||
'pending': currency['Pending'],
|
|
||||||
'est_btc': currency['BTC']
|
|
||||||
})
|
|
||||||
fiat = _FIAT_CONVERT
|
|
||||||
symbol = fiat_display_currency
|
|
||||||
value = fiat.convert_amount(total, 'BTC', symbol)
|
|
||||||
return (False, (output, total, symbol, value))
|
|
||||||
|
|
||||||
|
|
||||||
def rpc_start():
|
|
||||||
"""
|
|
||||||
Handler for start.
|
|
||||||
"""
|
|
||||||
if get_state() == State.RUNNING:
|
|
||||||
return (True, '*Status:* `already running`')
|
|
||||||
else:
|
|
||||||
update_state(State.RUNNING)
|
|
||||||
|
|
||||||
|
|
||||||
def rpc_stop():
|
|
||||||
"""
|
|
||||||
Handler for stop.
|
|
||||||
"""
|
|
||||||
if get_state() == State.RUNNING:
|
|
||||||
update_state(State.STOPPED)
|
|
||||||
return (False, '`Stopping trader ...`')
|
|
||||||
else:
|
|
||||||
return (True, '*Status:* `already stopped`')
|
|
||||||
|
|
||||||
|
|
||||||
# FIX: no test for this!!!!
|
|
||||||
def rpc_forcesell(trade_id) -> None:
|
|
||||||
"""
|
|
||||||
Handler for forcesell <id>.
|
|
||||||
Sells the given trade at current price
|
|
||||||
:return: error or None
|
|
||||||
"""
|
|
||||||
def _exec_forcesell(trade: Trade) -> str:
|
|
||||||
# Check if there is there is an open order
|
|
||||||
if trade.open_order_id:
|
|
||||||
order = exchange.get_order(trade.open_order_id)
|
|
||||||
|
|
||||||
# Cancel open LIMIT_BUY orders and close trade
|
|
||||||
if order and not order['closed'] and order['type'] == 'LIMIT_BUY':
|
|
||||||
exchange.cancel_order(trade.open_order_id)
|
|
||||||
trade.close(order.get('rate') or trade.open_rate)
|
|
||||||
# TODO: sell amount which has been bought already
|
|
||||||
return
|
|
||||||
|
|
||||||
# Ignore trades with an attached LIMIT_SELL order
|
|
||||||
if order and not order['closed'] and order['type'] == 'LIMIT_SELL':
|
|
||||||
return
|
|
||||||
|
|
||||||
# Get current rate and execute sell
|
|
||||||
current_rate = exchange.get_ticker(trade.pair, False)['bid']
|
|
||||||
from freqtrade.main import execute_sell
|
|
||||||
execute_sell(trade, current_rate)
|
|
||||||
# ---- EOF def _exec_forcesell ----
|
|
||||||
|
|
||||||
if get_state() != State.RUNNING:
|
|
||||||
return (True, '`trader is not running`')
|
|
||||||
|
|
||||||
if trade_id == 'all':
|
|
||||||
# Execute sell for all open orders
|
|
||||||
for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
|
|
||||||
_exec_forcesell(trade)
|
|
||||||
return (False, '')
|
|
||||||
|
|
||||||
# Query for trade
|
|
||||||
trade = Trade.query.filter(sql.and_(
|
|
||||||
Trade.id == trade_id,
|
|
||||||
Trade.is_open.is_(True)
|
|
||||||
)).first()
|
|
||||||
if not trade:
|
|
||||||
logger.warning('forcesell: Invalid argument received')
|
|
||||||
return (True, 'Invalid argument.')
|
|
||||||
|
|
||||||
_exec_forcesell(trade)
|
|
||||||
return (False, '')
|
|
||||||
|
|
||||||
|
|
||||||
def rpc_performance() -> None:
|
|
||||||
"""
|
|
||||||
Handler for performance.
|
|
||||||
Shows a performance statistic from finished trades
|
|
||||||
"""
|
|
||||||
if get_state() != State.RUNNING:
|
|
||||||
return (True, '`trader is not running`')
|
|
||||||
|
|
||||||
pair_rates = Trade.session.query(Trade.pair,
|
|
||||||
sql.func.sum(Trade.close_profit).label('profit_sum'),
|
|
||||||
sql.func.count(Trade.pair).label('count')) \
|
|
||||||
.filter(Trade.is_open.is_(False)) \
|
|
||||||
.group_by(Trade.pair) \
|
|
||||||
.order_by(sql.text('profit_sum DESC')) \
|
|
||||||
.all()
|
|
||||||
trades = []
|
|
||||||
for (pair, rate, count) in pair_rates:
|
|
||||||
trades.append({'pair': pair, 'profit': round(rate * 100, 2), 'count': count})
|
|
||||||
|
|
||||||
return (False, trades)
|
|
||||||
|
|
||||||
|
|
||||||
def rpc_count() -> None:
|
|
||||||
"""
|
|
||||||
Returns the number of trades running
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
if get_state() != State.RUNNING:
|
|
||||||
return (True, '`trader is not running`')
|
|
||||||
|
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
|
||||||
return (False, trades)
|
|
385
freqtrade/rpc/rpc.py
Normal file
385
freqtrade/rpc/rpc.py
Normal file
@ -0,0 +1,385 @@
|
|||||||
|
"""
|
||||||
|
This module contains class to define a RPC communications
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from decimal import Decimal
|
||||||
|
from typing import Tuple, Any
|
||||||
|
|
||||||
|
import arrow
|
||||||
|
import sqlalchemy as sql
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade import exchange
|
||||||
|
from freqtrade.logger import Logger
|
||||||
|
from freqtrade.misc import shorten_date
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
from freqtrade.state import State
|
||||||
|
|
||||||
|
|
||||||
|
class RPC(object):
|
||||||
|
"""
|
||||||
|
RPC class can be used to have extra feature, like bot data, and access to DB data
|
||||||
|
"""
|
||||||
|
def __init__(self, freqtrade) -> None:
|
||||||
|
"""
|
||||||
|
Initializes all enabled rpc modules
|
||||||
|
:param freqtrade: Instance of a freqtrade bot
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self.freqtrade = freqtrade
|
||||||
|
self.logger = Logger(
|
||||||
|
name=__name__,
|
||||||
|
level=self.freqtrade.config.get('loglevel')
|
||||||
|
).get_logger()
|
||||||
|
|
||||||
|
def rpc_trade_status(self) -> Tuple[bool, Any]:
|
||||||
|
"""
|
||||||
|
Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is
|
||||||
|
a remotely exposed function
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
# Fetch open trade
|
||||||
|
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||||
|
if self.freqtrade.get_state() != State.RUNNING:
|
||||||
|
return True, '*Status:* `trader is not running`'
|
||||||
|
elif not trades:
|
||||||
|
return True, '*Status:* `no active trade`'
|
||||||
|
else:
|
||||||
|
result = []
|
||||||
|
for trade in trades:
|
||||||
|
order = None
|
||||||
|
if trade.open_order_id:
|
||||||
|
order = exchange.get_order(trade.open_order_id)
|
||||||
|
# calculate profit and send message to user
|
||||||
|
current_rate = exchange.get_ticker(trade.pair, False)['bid']
|
||||||
|
current_profit = trade.calc_profit_percent(current_rate)
|
||||||
|
fmt_close_profit = '{:.2f}%'.format(
|
||||||
|
round(trade.close_profit * 100, 2)
|
||||||
|
) if trade.close_profit else None
|
||||||
|
message = "*Trade ID:* `{trade_id}`\n" \
|
||||||
|
"*Current Pair:* [{pair}]({market_url})\n" \
|
||||||
|
"*Open Since:* `{date}`\n" \
|
||||||
|
"*Amount:* `{amount}`\n" \
|
||||||
|
"*Open Rate:* `{open_rate:.8f}`\n" \
|
||||||
|
"*Close Rate:* `{close_rate}`\n" \
|
||||||
|
"*Current Rate:* `{current_rate:.8f}`\n" \
|
||||||
|
"*Close Profit:* `{close_profit}`\n" \
|
||||||
|
"*Current Profit:* `{current_profit:.2f}%`\n" \
|
||||||
|
"*Open Order:* `{open_order}`"\
|
||||||
|
.format(
|
||||||
|
trade_id=trade.id,
|
||||||
|
pair=trade.pair,
|
||||||
|
market_url=exchange.get_pair_detail_url(trade.pair),
|
||||||
|
date=arrow.get(trade.open_date).humanize(),
|
||||||
|
open_rate=trade.open_rate,
|
||||||
|
close_rate=trade.close_rate,
|
||||||
|
current_rate=current_rate,
|
||||||
|
amount=round(trade.amount, 8),
|
||||||
|
close_profit=fmt_close_profit,
|
||||||
|
current_profit=round(current_profit * 100, 2),
|
||||||
|
open_order='({} rem={:.8f})'.format(
|
||||||
|
order['type'], order['remaining']
|
||||||
|
) if order else None,
|
||||||
|
)
|
||||||
|
result.append(message)
|
||||||
|
return False, result
|
||||||
|
|
||||||
|
def rpc_status_table(self) -> Tuple[bool, Any]:
|
||||||
|
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||||
|
if self.freqtrade.get_state() != State.RUNNING:
|
||||||
|
return True, '*Status:* `trader is not running`'
|
||||||
|
elif not trades:
|
||||||
|
return True, '*Status:* `no active order`'
|
||||||
|
else:
|
||||||
|
trades_list = []
|
||||||
|
for trade in trades:
|
||||||
|
# calculate profit and send message to user
|
||||||
|
current_rate = exchange.get_ticker(trade.pair, False)['bid']
|
||||||
|
trades_list.append([
|
||||||
|
trade.id,
|
||||||
|
trade.pair,
|
||||||
|
shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)),
|
||||||
|
'{:.2f}%'.format(100 * trade.calc_profit_percent(current_rate))
|
||||||
|
])
|
||||||
|
|
||||||
|
columns = ['ID', 'Pair', 'Since', 'Profit']
|
||||||
|
df_statuses = DataFrame.from_records(trades_list, columns=columns)
|
||||||
|
df_statuses = df_statuses.set_index(columns[0])
|
||||||
|
# The style used throughout is to return a tuple
|
||||||
|
# consisting of (error_occured?, result)
|
||||||
|
# Another approach would be to just return the
|
||||||
|
# result, or raise error
|
||||||
|
return False, df_statuses
|
||||||
|
|
||||||
|
def rpc_daily_profit(
|
||||||
|
self, timescale: int,
|
||||||
|
stake_currency: str, fiat_display_currency: str) -> Tuple[bool, Any]:
|
||||||
|
today = datetime.utcnow().date()
|
||||||
|
profit_days = {}
|
||||||
|
|
||||||
|
if not (isinstance(timescale, int) and timescale > 0):
|
||||||
|
return True, '*Daily [n]:* `must be an integer greater than 0`'
|
||||||
|
|
||||||
|
fiat = self.freqtrade.fiat_converter
|
||||||
|
for day in range(0, timescale):
|
||||||
|
profitday = today - timedelta(days=day)
|
||||||
|
trades = Trade.query \
|
||||||
|
.filter(Trade.is_open.is_(False)) \
|
||||||
|
.filter(Trade.close_date >= profitday)\
|
||||||
|
.filter(Trade.close_date < (profitday + timedelta(days=1)))\
|
||||||
|
.order_by(Trade.close_date)\
|
||||||
|
.all()
|
||||||
|
curdayprofit = sum(trade.calc_profit() for trade in trades)
|
||||||
|
profit_days[profitday] = {
|
||||||
|
'amount': format(curdayprofit, '.8f'),
|
||||||
|
'trades': len(trades)
|
||||||
|
}
|
||||||
|
|
||||||
|
stats = [
|
||||||
|
[
|
||||||
|
key,
|
||||||
|
'{value:.8f} {symbol}'.format(
|
||||||
|
value=float(value['amount']),
|
||||||
|
symbol=stake_currency
|
||||||
|
),
|
||||||
|
'{value:.3f} {symbol}'.format(
|
||||||
|
value=fiat.convert_amount(
|
||||||
|
value['amount'],
|
||||||
|
stake_currency,
|
||||||
|
fiat_display_currency
|
||||||
|
),
|
||||||
|
symbol=fiat_display_currency
|
||||||
|
),
|
||||||
|
'{value} trade{s}'.format(
|
||||||
|
value=value['trades'],
|
||||||
|
s='' if value['trades'] < 2 else 's'
|
||||||
|
),
|
||||||
|
]
|
||||||
|
for key, value in profit_days.items()
|
||||||
|
]
|
||||||
|
return False, stats
|
||||||
|
|
||||||
|
def rpc_trade_statistics(
|
||||||
|
self, stake_currency: str, fiat_display_currency: str) -> Tuple[bool, Any]:
|
||||||
|
"""
|
||||||
|
:return: cumulative profit statistics.
|
||||||
|
"""
|
||||||
|
trades = Trade.query.order_by(Trade.id).all()
|
||||||
|
|
||||||
|
profit_all_coin = []
|
||||||
|
profit_all_percent = []
|
||||||
|
profit_closed_coin = []
|
||||||
|
profit_closed_percent = []
|
||||||
|
durations = []
|
||||||
|
|
||||||
|
for trade in trades:
|
||||||
|
current_rate = None
|
||||||
|
|
||||||
|
if not trade.open_rate:
|
||||||
|
continue
|
||||||
|
if trade.close_date:
|
||||||
|
durations.append((trade.close_date - trade.open_date).total_seconds())
|
||||||
|
|
||||||
|
if not trade.is_open:
|
||||||
|
profit_percent = trade.calc_profit_percent()
|
||||||
|
profit_closed_coin.append(trade.calc_profit())
|
||||||
|
profit_closed_percent.append(profit_percent)
|
||||||
|
else:
|
||||||
|
# Get current rate
|
||||||
|
current_rate = exchange.get_ticker(trade.pair, False)['bid']
|
||||||
|
profit_percent = trade.calc_profit_percent(rate=current_rate)
|
||||||
|
|
||||||
|
profit_all_coin.append(
|
||||||
|
trade.calc_profit(rate=Decimal(trade.close_rate or current_rate))
|
||||||
|
)
|
||||||
|
profit_all_percent.append(profit_percent)
|
||||||
|
|
||||||
|
best_pair = Trade.session.query(
|
||||||
|
Trade.pair, sql.func.sum(Trade.close_profit).label('profit_sum')
|
||||||
|
).filter(Trade.is_open.is_(False)) \
|
||||||
|
.group_by(Trade.pair) \
|
||||||
|
.order_by(sql.text('profit_sum DESC')).first()
|
||||||
|
|
||||||
|
if not best_pair:
|
||||||
|
return True, '*Status:* `no closed trade`'
|
||||||
|
|
||||||
|
bp_pair, bp_rate = best_pair
|
||||||
|
|
||||||
|
# FIX: we want to keep fiatconverter in a state/environment,
|
||||||
|
# doing this will utilize its caching functionallity, instead we reinitialize it here
|
||||||
|
fiat = self.freqtrade.fiat_converter
|
||||||
|
# Prepare data to display
|
||||||
|
profit_closed_coin = round(sum(profit_closed_coin), 8)
|
||||||
|
profit_closed_percent = round(sum(profit_closed_percent) * 100, 2)
|
||||||
|
profit_closed_fiat = fiat.convert_amount(
|
||||||
|
profit_closed_coin,
|
||||||
|
stake_currency,
|
||||||
|
fiat_display_currency
|
||||||
|
)
|
||||||
|
profit_all_coin = round(sum(profit_all_coin), 8)
|
||||||
|
profit_all_percent = round(sum(profit_all_percent) * 100, 2)
|
||||||
|
profit_all_fiat = fiat.convert_amount(
|
||||||
|
profit_all_coin,
|
||||||
|
stake_currency,
|
||||||
|
fiat_display_currency
|
||||||
|
)
|
||||||
|
num = float(len(durations) or 1)
|
||||||
|
return (
|
||||||
|
False,
|
||||||
|
{
|
||||||
|
'profit_closed_coin': profit_closed_coin,
|
||||||
|
'profit_closed_percent': profit_closed_percent,
|
||||||
|
'profit_closed_fiat': profit_closed_fiat,
|
||||||
|
'profit_all_coin': profit_all_coin,
|
||||||
|
'profit_all_percent': profit_all_percent,
|
||||||
|
'profit_all_fiat': profit_all_fiat,
|
||||||
|
'trade_count': len(trades),
|
||||||
|
'first_trade_date': arrow.get(trades[0].open_date).humanize(),
|
||||||
|
'latest_trade_date': arrow.get(trades[-1].open_date).humanize(),
|
||||||
|
'avg_duration': str(timedelta(seconds=sum(durations) / num)).split('.')[0],
|
||||||
|
'best_pair': bp_pair,
|
||||||
|
'best_rate': round(bp_rate * 100, 2)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def rpc_balance(self, fiat_display_currency: str) -> Tuple[bool, Any]:
|
||||||
|
"""
|
||||||
|
:return: current account balance per crypto
|
||||||
|
"""
|
||||||
|
balances = [
|
||||||
|
c for c in exchange.get_balances()
|
||||||
|
if c['Balance'] or c['Available'] or c['Pending']
|
||||||
|
]
|
||||||
|
if not balances:
|
||||||
|
return True, '`All balances are zero.`'
|
||||||
|
|
||||||
|
output = []
|
||||||
|
total = 0.0
|
||||||
|
for currency in balances:
|
||||||
|
coin = currency['Currency']
|
||||||
|
if coin == 'BTC':
|
||||||
|
currency["Rate"] = 1.0
|
||||||
|
else:
|
||||||
|
if coin == 'USDT':
|
||||||
|
currency["Rate"] = 1.0 / exchange.get_ticker('USDT_BTC', False)['bid']
|
||||||
|
else:
|
||||||
|
currency["Rate"] = exchange.get_ticker('BTC_' + coin, False)['bid']
|
||||||
|
currency['BTC'] = currency["Rate"] * currency["Balance"]
|
||||||
|
total = total + currency['BTC']
|
||||||
|
output.append(
|
||||||
|
{
|
||||||
|
'currency': currency['Currency'],
|
||||||
|
'available': currency['Available'],
|
||||||
|
'balance': currency['Balance'],
|
||||||
|
'pending': currency['Pending'],
|
||||||
|
'est_btc': currency['BTC']
|
||||||
|
}
|
||||||
|
)
|
||||||
|
fiat = self.freqtrade.fiat_converter
|
||||||
|
symbol = fiat_display_currency
|
||||||
|
value = fiat.convert_amount(total, 'BTC', symbol)
|
||||||
|
return False, (output, total, symbol, value)
|
||||||
|
|
||||||
|
def rpc_start(self) -> (bool, str):
|
||||||
|
"""
|
||||||
|
Handler for start.
|
||||||
|
"""
|
||||||
|
if self.freqtrade.get_state() == State.RUNNING:
|
||||||
|
return True, '*Status:* `already running`'
|
||||||
|
|
||||||
|
self.freqtrade.update_state(State.RUNNING)
|
||||||
|
return False, '`Starting trader ...`'
|
||||||
|
|
||||||
|
def rpc_stop(self) -> (bool, str):
|
||||||
|
"""
|
||||||
|
Handler for stop.
|
||||||
|
"""
|
||||||
|
if self.freqtrade.get_state() == State.RUNNING:
|
||||||
|
self.freqtrade.update_state(State.STOPPED)
|
||||||
|
return False, '`Stopping trader ...`'
|
||||||
|
|
||||||
|
return True, '*Status:* `already stopped`'
|
||||||
|
|
||||||
|
# FIX: no test for this!!!!
|
||||||
|
def rpc_forcesell(self, trade_id) -> Tuple[bool, Any]:
|
||||||
|
"""
|
||||||
|
Handler for forcesell <id>.
|
||||||
|
Sells the given trade at current price
|
||||||
|
:return: error or None
|
||||||
|
"""
|
||||||
|
def _exec_forcesell(trade: Trade) -> None:
|
||||||
|
# Check if there is there is an open order
|
||||||
|
if trade.open_order_id:
|
||||||
|
order = exchange.get_order(trade.open_order_id)
|
||||||
|
|
||||||
|
# Cancel open LIMIT_BUY orders and close trade
|
||||||
|
if order and not order['closed'] and order['type'] == 'LIMIT_BUY':
|
||||||
|
exchange.cancel_order(trade.open_order_id)
|
||||||
|
trade.close(order.get('rate') or trade.open_rate)
|
||||||
|
# TODO: sell amount which has been bought already
|
||||||
|
return
|
||||||
|
|
||||||
|
# Ignore trades with an attached LIMIT_SELL order
|
||||||
|
if order and not order['closed'] and order['type'] == 'LIMIT_SELL':
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get current rate and execute sell
|
||||||
|
current_rate = exchange.get_ticker(trade.pair, False)['bid']
|
||||||
|
self.freqtrade.execute_sell(trade, current_rate)
|
||||||
|
# ---- EOF def _exec_forcesell ----
|
||||||
|
|
||||||
|
if self.freqtrade.get_state() != State.RUNNING:
|
||||||
|
return True, '`trader is not running`'
|
||||||
|
|
||||||
|
if trade_id == 'all':
|
||||||
|
# Execute sell for all open orders
|
||||||
|
for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
|
||||||
|
_exec_forcesell(trade)
|
||||||
|
return False, ''
|
||||||
|
|
||||||
|
# Query for trade
|
||||||
|
trade = Trade.query.filter(
|
||||||
|
sql.and_(
|
||||||
|
Trade.id == trade_id,
|
||||||
|
Trade.is_open.is_(True)
|
||||||
|
)
|
||||||
|
).first()
|
||||||
|
if not trade:
|
||||||
|
self.logger.warning('forcesell: Invalid argument received')
|
||||||
|
return True, 'Invalid argument.'
|
||||||
|
|
||||||
|
_exec_forcesell(trade)
|
||||||
|
return False, ''
|
||||||
|
|
||||||
|
def rpc_performance(self) -> Tuple[bool, Any]:
|
||||||
|
"""
|
||||||
|
Handler for performance.
|
||||||
|
Shows a performance statistic from finished trades
|
||||||
|
"""
|
||||||
|
if self.freqtrade.get_state() != State.RUNNING:
|
||||||
|
return True, '`trader is not running`'
|
||||||
|
|
||||||
|
pair_rates = Trade.session.query(Trade.pair,
|
||||||
|
sql.func.sum(Trade.close_profit).label('profit_sum'),
|
||||||
|
sql.func.count(Trade.pair).label('count')) \
|
||||||
|
.filter(Trade.is_open.is_(False)) \
|
||||||
|
.group_by(Trade.pair) \
|
||||||
|
.order_by(sql.text('profit_sum DESC')) \
|
||||||
|
.all()
|
||||||
|
trades = []
|
||||||
|
for (pair, rate, count) in pair_rates:
|
||||||
|
trades.append({'pair': pair, 'profit': round(rate * 100, 2), 'count': count})
|
||||||
|
|
||||||
|
return False, trades
|
||||||
|
|
||||||
|
def rpc_count(self) -> Tuple[bool, Any]:
|
||||||
|
"""
|
||||||
|
Returns the number of trades running
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
if self.freqtrade.get_state() != State.RUNNING:
|
||||||
|
return True, '`trader is not running`'
|
||||||
|
|
||||||
|
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||||
|
return False, trades
|
59
freqtrade/rpc/rpc_manager.py
Normal file
59
freqtrade/rpc/rpc_manager.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
"""
|
||||||
|
This module contains class to manage RPC communications (Telegram, Slack, ...)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from freqtrade.logger import Logger
|
||||||
|
from freqtrade.rpc.telegram import Telegram
|
||||||
|
|
||||||
|
|
||||||
|
class RPCManager(object):
|
||||||
|
"""
|
||||||
|
Class to manage RPC objects (Telegram, Slack, ...)
|
||||||
|
"""
|
||||||
|
def __init__(self, freqtrade) -> None:
|
||||||
|
"""
|
||||||
|
Initializes all enabled rpc modules
|
||||||
|
:param config: config to use
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self.freqtrade = freqtrade
|
||||||
|
|
||||||
|
# Init the logger
|
||||||
|
self.logger = Logger(
|
||||||
|
name=__name__,
|
||||||
|
level=self.freqtrade.config.get('loglevel')
|
||||||
|
).get_logger()
|
||||||
|
|
||||||
|
self.registered_modules = []
|
||||||
|
self.telegram = None
|
||||||
|
self._init()
|
||||||
|
|
||||||
|
def _init(self) -> None:
|
||||||
|
"""
|
||||||
|
Init RPC modules
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if self.freqtrade.config['telegram'].get('enabled', False):
|
||||||
|
self.logger.info('Enabling rpc.telegram ...')
|
||||||
|
self.registered_modules.append('telegram')
|
||||||
|
self.telegram = Telegram(self.freqtrade)
|
||||||
|
|
||||||
|
def cleanup(self) -> None:
|
||||||
|
"""
|
||||||
|
Stops all enabled rpc modules
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
if 'telegram' in self.registered_modules:
|
||||||
|
self.logger.info('Cleaning up rpc.telegram ...')
|
||||||
|
self.registered_modules.remove('telegram')
|
||||||
|
self.telegram.cleanup()
|
||||||
|
|
||||||
|
def send_msg(self, msg: str) -> None:
|
||||||
|
"""
|
||||||
|
Send given markdown message to all registered rpc modules
|
||||||
|
:param msg: message
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self.logger.info(msg)
|
||||||
|
if 'telegram' in self.registered_modules:
|
||||||
|
self.telegram.send_msg(msg)
|
@ -1,4 +1,9 @@
|
|||||||
import logging
|
# pragma pylint: disable=unused-argument, unused-variable, protected-access, invalid-name
|
||||||
|
|
||||||
|
"""
|
||||||
|
This module manage Telegram communication
|
||||||
|
"""
|
||||||
|
|
||||||
from typing import Any, Callable
|
from typing import Any, Callable
|
||||||
|
|
||||||
from tabulate import tabulate
|
from tabulate import tabulate
|
||||||
@ -6,89 +11,8 @@ from telegram import Bot, ParseMode, ReplyKeyboardMarkup, Update
|
|||||||
from telegram.error import NetworkError, TelegramError
|
from telegram.error import NetworkError, TelegramError
|
||||||
from telegram.ext import CommandHandler, Updater
|
from telegram.ext import CommandHandler, Updater
|
||||||
|
|
||||||
from freqtrade.rpc.__init__ import (rpc_status_table,
|
from freqtrade.__init__ import __version__
|
||||||
rpc_trade_status,
|
from freqtrade.rpc.rpc import RPC
|
||||||
rpc_daily_profit,
|
|
||||||
rpc_trade_statistics,
|
|
||||||
rpc_balance,
|
|
||||||
rpc_start,
|
|
||||||
rpc_stop,
|
|
||||||
rpc_forcesell,
|
|
||||||
rpc_performance,
|
|
||||||
rpc_count,
|
|
||||||
)
|
|
||||||
|
|
||||||
from freqtrade import __version__
|
|
||||||
|
|
||||||
|
|
||||||
# Remove noisy log messages
|
|
||||||
logging.getLogger('requests.packages.urllib3').setLevel(logging.INFO)
|
|
||||||
logging.getLogger('telegram').setLevel(logging.INFO)
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
_UPDATER: Updater = None
|
|
||||||
_CONF = {}
|
|
||||||
|
|
||||||
|
|
||||||
def init(config: dict) -> None:
|
|
||||||
"""
|
|
||||||
Initializes this module with the given config,
|
|
||||||
registers all known command handlers
|
|
||||||
and starts polling for message updates
|
|
||||||
:param config: config to use
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
global _UPDATER
|
|
||||||
|
|
||||||
_CONF.update(config)
|
|
||||||
if not is_enabled():
|
|
||||||
return
|
|
||||||
|
|
||||||
_UPDATER = Updater(token=config['telegram']['token'], workers=0)
|
|
||||||
|
|
||||||
# Register command handler and start telegram message polling
|
|
||||||
handles = [
|
|
||||||
CommandHandler('status', _status),
|
|
||||||
CommandHandler('profit', _profit),
|
|
||||||
CommandHandler('balance', _balance),
|
|
||||||
CommandHandler('start', _start),
|
|
||||||
CommandHandler('stop', _stop),
|
|
||||||
CommandHandler('forcesell', _forcesell),
|
|
||||||
CommandHandler('performance', _performance),
|
|
||||||
CommandHandler('daily', _daily),
|
|
||||||
CommandHandler('count', _count),
|
|
||||||
CommandHandler('help', _help),
|
|
||||||
CommandHandler('version', _version),
|
|
||||||
]
|
|
||||||
for handle in handles:
|
|
||||||
_UPDATER.dispatcher.add_handler(handle)
|
|
||||||
_UPDATER.start_polling(
|
|
||||||
clean=True,
|
|
||||||
bootstrap_retries=-1,
|
|
||||||
timeout=30,
|
|
||||||
read_latency=60,
|
|
||||||
)
|
|
||||||
logger.info(
|
|
||||||
'rpc.telegram is listening for following commands: %s',
|
|
||||||
[h.command for h in handles]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def cleanup() -> None:
|
|
||||||
"""
|
|
||||||
Stops all running telegram threads.
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
if not is_enabled():
|
|
||||||
return
|
|
||||||
_UPDATER.stop()
|
|
||||||
|
|
||||||
|
|
||||||
def is_enabled() -> bool:
|
|
||||||
"""
|
|
||||||
Returns True if the telegram module is activated, False otherwise
|
|
||||||
"""
|
|
||||||
return bool(_CONF['telegram'].get('enabled', False))
|
|
||||||
|
|
||||||
|
|
||||||
def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[..., Any]:
|
def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[..., Any]:
|
||||||
@ -97,340 +21,425 @@ def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[
|
|||||||
:param command_handler: Telegram CommandHandler
|
:param command_handler: Telegram CommandHandler
|
||||||
:return: decorated function
|
:return: decorated function
|
||||||
"""
|
"""
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Decorator logic
|
||||||
|
"""
|
||||||
update = kwargs.get('update') or args[1]
|
update = kwargs.get('update') or args[1]
|
||||||
|
|
||||||
# Reject unauthorized messages
|
# Reject unauthorized messages
|
||||||
chat_id = int(_CONF['telegram']['chat_id'])
|
chat_id = int(self._config['telegram']['chat_id'])
|
||||||
|
|
||||||
if int(update.message.chat_id) != chat_id:
|
if int(update.message.chat_id) != chat_id:
|
||||||
logger.info('Rejected unauthorized message from: %s', update.message.chat_id)
|
self.logger.info(
|
||||||
|
'Rejected unauthorized message from: %s',
|
||||||
|
update.message.chat_id
|
||||||
|
)
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
logger.info('Executing handler: %s for chat_id: %s', command_handler.__name__, chat_id)
|
self.logger.info(
|
||||||
|
'Executing handler: %s for chat_id: %s',
|
||||||
|
command_handler.__name__,
|
||||||
|
chat_id
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
return command_handler(*args, **kwargs)
|
return command_handler(self, *args, **kwargs)
|
||||||
except BaseException:
|
except BaseException:
|
||||||
logger.exception('Exception occurred within Telegram module')
|
self.logger.exception('Exception occurred within Telegram module')
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
@authorized_only
|
class Telegram(RPC):
|
||||||
def _status(bot: Bot, update: Update) -> None:
|
|
||||||
"""
|
"""
|
||||||
Handler for /status.
|
Telegram, this class send messages to Telegram
|
||||||
Returns the current TradeThread status
|
|
||||||
:param bot: telegram bot
|
|
||||||
:param update: message update
|
|
||||||
:return: None
|
|
||||||
"""
|
"""
|
||||||
|
def __init__(self, freqtrade) -> None:
|
||||||
|
"""
|
||||||
|
Init the Telegram call, and init the super class RPC
|
||||||
|
:param freqtrade: Instance of a freqtrade bot
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
super().__init__(freqtrade)
|
||||||
|
|
||||||
# Check if additional parameters are passed
|
self._updater = None
|
||||||
params = update.message.text.replace('/status', '').split(' ') \
|
self._config = freqtrade.config
|
||||||
if update.message.text else []
|
self._init()
|
||||||
if 'table' in params:
|
|
||||||
_status_table(bot, update)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Fetch open trade
|
def _init(self) -> None:
|
||||||
(error, trades) = rpc_trade_status()
|
"""
|
||||||
if error:
|
Initializes this module with the given config,
|
||||||
send_msg(trades, bot=bot)
|
registers all known command handlers
|
||||||
else:
|
and starts polling for message updates
|
||||||
for trademsg in trades:
|
:param config: config to use
|
||||||
send_msg(trademsg, bot=bot)
|
:return: None
|
||||||
|
"""
|
||||||
|
if not self.is_enabled():
|
||||||
|
return
|
||||||
|
|
||||||
|
self._updater = Updater(token=self._config['telegram']['token'], workers=0)
|
||||||
|
|
||||||
@authorized_only
|
# Register command handler and start telegram message polling
|
||||||
def _status_table(bot: Bot, update: Update) -> None:
|
handles = [
|
||||||
"""
|
CommandHandler('status', self._status),
|
||||||
Handler for /status table.
|
CommandHandler('profit', self._profit),
|
||||||
Returns the current TradeThread status in table format
|
CommandHandler('balance', self._balance),
|
||||||
:param bot: telegram bot
|
CommandHandler('start', self._start),
|
||||||
:param update: message update
|
CommandHandler('stop', self._stop),
|
||||||
:return: None
|
CommandHandler('forcesell', self._forcesell),
|
||||||
"""
|
CommandHandler('performance', self._performance),
|
||||||
# Fetch open trade
|
CommandHandler('daily', self._daily),
|
||||||
(err, df_statuses) = rpc_status_table()
|
CommandHandler('count', self._count),
|
||||||
if err:
|
CommandHandler('help', self._help),
|
||||||
send_msg(df_statuses, bot=bot)
|
CommandHandler('version', self._version),
|
||||||
else:
|
]
|
||||||
message = tabulate(df_statuses, headers='keys', tablefmt='simple')
|
for handle in handles:
|
||||||
message = "<pre>{}</pre>".format(message)
|
self._updater.dispatcher.add_handler(handle)
|
||||||
|
self._updater.start_polling(
|
||||||
|
clean=True,
|
||||||
|
bootstrap_retries=-1,
|
||||||
|
timeout=30,
|
||||||
|
read_latency=60,
|
||||||
|
)
|
||||||
|
self.logger.info(
|
||||||
|
'rpc.telegram is listening for following commands: %s',
|
||||||
|
[h.command for h in handles]
|
||||||
|
)
|
||||||
|
|
||||||
send_msg(message, parse_mode=ParseMode.HTML)
|
def cleanup(self) -> None:
|
||||||
|
"""
|
||||||
|
Stops all running telegram threads.
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
if not self.is_enabled():
|
||||||
|
return
|
||||||
|
|
||||||
|
self._updater.stop()
|
||||||
|
|
||||||
@authorized_only
|
def is_enabled(self) -> bool:
|
||||||
def _daily(bot: Bot, update: Update) -> None:
|
"""
|
||||||
"""
|
Returns True if the telegram module is activated, False otherwise
|
||||||
Handler for /daily <n>
|
"""
|
||||||
Returns a daily profit (in BTC) over the last n days.
|
return bool(self._config.get('telegram', {}).get('enabled', False))
|
||||||
:param bot: telegram bot
|
|
||||||
:param update: message update
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
timescale = int(update.message.text.replace('/daily', '').strip())
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
timescale = 7
|
|
||||||
(error, stats) = rpc_daily_profit(timescale,
|
|
||||||
_CONF['stake_currency'],
|
|
||||||
_CONF['fiat_display_currency'])
|
|
||||||
if error:
|
|
||||||
send_msg(stats, bot=bot)
|
|
||||||
else:
|
|
||||||
stats = tabulate(stats,
|
|
||||||
headers=[
|
|
||||||
'Day',
|
|
||||||
'Profit {}'.format(_CONF['stake_currency']),
|
|
||||||
'Profit {}'.format(_CONF['fiat_display_currency'])
|
|
||||||
],
|
|
||||||
tablefmt='simple')
|
|
||||||
message = '<b>Daily Profit over the last {} days</b>:\n<pre>{}</pre>'.format(
|
|
||||||
timescale, stats)
|
|
||||||
send_msg(message, bot=bot, parse_mode=ParseMode.HTML)
|
|
||||||
|
|
||||||
|
@authorized_only
|
||||||
|
def _status(self, bot: Bot, update: Update) -> None:
|
||||||
|
"""
|
||||||
|
Handler for /status.
|
||||||
|
Returns the current TradeThread status
|
||||||
|
:param bot: telegram bot
|
||||||
|
:param update: message update
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
|
||||||
@authorized_only
|
# Check if additional parameters are passed
|
||||||
def _profit(bot: Bot, update: Update) -> None:
|
params = update.message.text.replace('/status', '').split(' ') \
|
||||||
"""
|
if update.message.text else []
|
||||||
Handler for /profit.
|
if 'table' in params:
|
||||||
Returns a cumulative profit statistics.
|
self._status_table(bot, update)
|
||||||
:param bot: telegram bot
|
return
|
||||||
:param update: message update
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
(error, stats) = rpc_trade_statistics(_CONF['stake_currency'],
|
|
||||||
_CONF['fiat_display_currency'])
|
|
||||||
if error:
|
|
||||||
send_msg(stats, bot=bot)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Message to display
|
# Fetch open trade
|
||||||
markdown_msg = """
|
(error, trades) = self.rpc_trade_status()
|
||||||
*ROI:* Close trades
|
if error:
|
||||||
∙ `{profit_closed_coin:.8f} {coin} ({profit_closed_percent:.2f}%)`
|
self.send_msg(trades, bot=bot)
|
||||||
∙ `{profit_closed_fiat:.3f} {fiat}`
|
else:
|
||||||
*ROI:* All trades
|
for trademsg in trades:
|
||||||
∙ `{profit_all_coin:.8f} {coin} ({profit_all_percent:.2f}%)`
|
self.send_msg(trademsg, bot=bot)
|
||||||
∙ `{profit_all_fiat:.3f} {fiat}`
|
|
||||||
|
|
||||||
*Total Trade Count:* `{trade_count}`
|
@authorized_only
|
||||||
*First Trade opened:* `{first_trade_date}`
|
def _status_table(self, bot: Bot, update: Update) -> None:
|
||||||
*Latest Trade opened:* `{latest_trade_date}`
|
"""
|
||||||
*Avg. Duration:* `{avg_duration}`
|
Handler for /status table.
|
||||||
*Best Performing:* `{best_pair}: {best_rate:.2f}%`
|
Returns the current TradeThread status in table format
|
||||||
""".format(
|
:param bot: telegram bot
|
||||||
coin=_CONF['stake_currency'],
|
:param update: message update
|
||||||
fiat=_CONF['fiat_display_currency'],
|
:return: None
|
||||||
profit_closed_coin=stats['profit_closed_coin'],
|
"""
|
||||||
profit_closed_percent=stats['profit_closed_percent'],
|
# Fetch open trade
|
||||||
profit_closed_fiat=stats['profit_closed_fiat'],
|
(err, df_statuses) = self.rpc_status_table()
|
||||||
profit_all_coin=stats['profit_all_coin'],
|
if err:
|
||||||
profit_all_percent=stats['profit_all_percent'],
|
self.send_msg(df_statuses, bot=bot)
|
||||||
profit_all_fiat=stats['profit_all_fiat'],
|
else:
|
||||||
trade_count=stats['trade_count'],
|
message = tabulate(df_statuses, headers='keys', tablefmt='simple')
|
||||||
first_trade_date=stats['first_trade_date'],
|
message = "<pre>{}</pre>".format(message)
|
||||||
latest_trade_date=stats['latest_trade_date'],
|
|
||||||
avg_duration=stats['avg_duration'],
|
|
||||||
best_pair=stats['best_pair'],
|
|
||||||
best_rate=stats['best_rate']
|
|
||||||
)
|
|
||||||
send_msg(markdown_msg, bot=bot)
|
|
||||||
|
|
||||||
|
self.send_msg(message, parse_mode=ParseMode.HTML)
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _balance(bot: Bot, update: Update) -> None:
|
def _daily(self, bot: Bot, update: Update) -> None:
|
||||||
"""
|
"""
|
||||||
Handler for /balance
|
Handler for /daily <n>
|
||||||
"""
|
Returns a daily profit (in BTC) over the last n days.
|
||||||
(error, result) = rpc_balance(_CONF['fiat_display_currency'])
|
:param bot: telegram bot
|
||||||
if error:
|
:param update: message update
|
||||||
send_msg('`All balances are zero.`')
|
:return: None
|
||||||
return
|
"""
|
||||||
|
|
||||||
(currencys, total, symbol, value) = result
|
|
||||||
output = ''
|
|
||||||
for currency in currencys:
|
|
||||||
output += """*Currency*: {currency}
|
|
||||||
*Available*: {available}
|
|
||||||
*Balance*: {balance}
|
|
||||||
*Pending*: {pending}
|
|
||||||
*Est. BTC*: {est_btc: .8f}
|
|
||||||
""".format(**currency)
|
|
||||||
|
|
||||||
output += """*Estimated Value*:
|
|
||||||
*BTC*: {0: .8f}
|
|
||||||
*{1}*: {2: .2f}
|
|
||||||
""".format(total, symbol, value)
|
|
||||||
send_msg(output)
|
|
||||||
|
|
||||||
|
|
||||||
@authorized_only
|
|
||||||
def _start(bot: Bot, update: Update) -> None:
|
|
||||||
"""
|
|
||||||
Handler for /start.
|
|
||||||
Starts TradeThread
|
|
||||||
:param bot: telegram bot
|
|
||||||
:param update: message update
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
(error, msg) = rpc_start()
|
|
||||||
if error:
|
|
||||||
send_msg(msg, bot=bot)
|
|
||||||
|
|
||||||
|
|
||||||
@authorized_only
|
|
||||||
def _stop(bot: Bot, update: Update) -> None:
|
|
||||||
"""
|
|
||||||
Handler for /stop.
|
|
||||||
Stops TradeThread
|
|
||||||
:param bot: telegram bot
|
|
||||||
:param update: message update
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
(error, msg) = rpc_stop()
|
|
||||||
send_msg(msg, bot=bot)
|
|
||||||
|
|
||||||
|
|
||||||
# FIX: no test for this!!!!
|
|
||||||
@authorized_only
|
|
||||||
def _forcesell(bot: Bot, update: Update) -> None:
|
|
||||||
"""
|
|
||||||
Handler for /forcesell <id>.
|
|
||||||
Sells the given trade at current price
|
|
||||||
:param bot: telegram bot
|
|
||||||
:param update: message update
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
|
|
||||||
trade_id = update.message.text.replace('/forcesell', '').strip()
|
|
||||||
(error, message) = rpc_forcesell(trade_id)
|
|
||||||
if error:
|
|
||||||
send_msg(message, bot=bot)
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
@authorized_only
|
|
||||||
def _performance(bot: Bot, update: Update) -> None:
|
|
||||||
"""
|
|
||||||
Handler for /performance.
|
|
||||||
Shows a performance statistic from finished trades
|
|
||||||
:param bot: telegram bot
|
|
||||||
:param update: message update
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
(error, trades) = rpc_performance()
|
|
||||||
if error:
|
|
||||||
send_msg(trades, bot=bot)
|
|
||||||
return
|
|
||||||
|
|
||||||
stats = '\n'.join('{index}.\t<code>{pair}\t{profit:.2f}% ({count})</code>'.format(
|
|
||||||
index=i + 1,
|
|
||||||
pair=trade['pair'],
|
|
||||||
profit=trade['profit'],
|
|
||||||
count=trade['count']
|
|
||||||
) for i, trade in enumerate(trades))
|
|
||||||
message = '<b>Performance:</b>\n{}'.format(stats)
|
|
||||||
send_msg(message, parse_mode=ParseMode.HTML)
|
|
||||||
|
|
||||||
|
|
||||||
@authorized_only
|
|
||||||
def _count(bot: Bot, update: Update) -> None:
|
|
||||||
"""
|
|
||||||
Handler for /count.
|
|
||||||
Returns the number of trades running
|
|
||||||
:param bot: telegram bot
|
|
||||||
:param update: message update
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
(error, trades) = rpc_count()
|
|
||||||
if error:
|
|
||||||
send_msg(trades, bot=bot)
|
|
||||||
return
|
|
||||||
|
|
||||||
message = tabulate({
|
|
||||||
'current': [len(trades)],
|
|
||||||
'max': [_CONF['max_open_trades']]
|
|
||||||
}, headers=['current', 'max'], tablefmt='simple')
|
|
||||||
message = "<pre>{}</pre>".format(message)
|
|
||||||
logger.debug(message)
|
|
||||||
send_msg(message, parse_mode=ParseMode.HTML)
|
|
||||||
|
|
||||||
|
|
||||||
@authorized_only
|
|
||||||
def _help(bot: Bot, update: Update) -> None:
|
|
||||||
"""
|
|
||||||
Handler for /help.
|
|
||||||
Show commands of the bot
|
|
||||||
:param bot: telegram bot
|
|
||||||
:param update: message update
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
message = """
|
|
||||||
*/start:* `Starts the trader`
|
|
||||||
*/stop:* `Stops the trader`
|
|
||||||
*/status [table]:* `Lists all open trades`
|
|
||||||
*table :* `will display trades in a table`
|
|
||||||
*/profit:* `Lists cumulative profit from all finished trades`
|
|
||||||
*/forcesell <trade_id>|all:* `Instantly sells the given trade or all trades, regardless of profit`
|
|
||||||
*/performance:* `Show performance of each finished trade grouped by pair`
|
|
||||||
*/daily <n>:* `Shows profit or loss per day, over the last n days`
|
|
||||||
*/count:* `Show number of trades running compared to allowed number of trades`
|
|
||||||
*/balance:* `Show account balance per currency`
|
|
||||||
*/help:* `This help message`
|
|
||||||
*/version:* `Show version`
|
|
||||||
"""
|
|
||||||
send_msg(message, bot=bot)
|
|
||||||
|
|
||||||
|
|
||||||
@authorized_only
|
|
||||||
def _version(bot: Bot, update: Update) -> None:
|
|
||||||
"""
|
|
||||||
Handler for /version.
|
|
||||||
Show version information
|
|
||||||
:param bot: telegram bot
|
|
||||||
:param update: message update
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
send_msg('*Version:* `{}`'.format(__version__), bot=bot)
|
|
||||||
|
|
||||||
|
|
||||||
def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
|
|
||||||
"""
|
|
||||||
Send given markdown message
|
|
||||||
:param msg: message
|
|
||||||
:param bot: alternative bot
|
|
||||||
:param parse_mode: telegram parse mode
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
if not is_enabled():
|
|
||||||
return
|
|
||||||
|
|
||||||
bot = bot or _UPDATER.bot
|
|
||||||
|
|
||||||
keyboard = [['/daily', '/profit', '/balance'],
|
|
||||||
['/status', '/status table', '/performance'],
|
|
||||||
['/count', '/start', '/stop', '/help']]
|
|
||||||
|
|
||||||
reply_markup = ReplyKeyboardMarkup(keyboard)
|
|
||||||
|
|
||||||
try:
|
|
||||||
try:
|
try:
|
||||||
bot.send_message(
|
timescale = int(update.message.text.replace('/daily', '').strip())
|
||||||
_CONF['telegram']['chat_id'], msg,
|
except (TypeError, ValueError):
|
||||||
parse_mode=parse_mode, reply_markup=reply_markup
|
timescale = 7
|
||||||
|
(error, stats) = self.rpc_daily_profit(
|
||||||
|
timescale,
|
||||||
|
self._config['stake_currency'],
|
||||||
|
self._config['fiat_display_currency']
|
||||||
|
)
|
||||||
|
if error:
|
||||||
|
self.send_msg(stats, bot=bot)
|
||||||
|
else:
|
||||||
|
stats = tabulate(stats,
|
||||||
|
headers=[
|
||||||
|
'Day',
|
||||||
|
'Profit {}'.format(self._config['stake_currency']),
|
||||||
|
'Profit {}'.format(self._config['fiat_display_currency'])
|
||||||
|
],
|
||||||
|
tablefmt='simple')
|
||||||
|
message = '<b>Daily Profit over the last {} days</b>:\n<pre>{}</pre>'\
|
||||||
|
.format(
|
||||||
|
timescale,
|
||||||
|
stats
|
||||||
|
)
|
||||||
|
self.send_msg(message, bot=bot, parse_mode=ParseMode.HTML)
|
||||||
|
|
||||||
|
@authorized_only
|
||||||
|
def _profit(self, bot: Bot, update: Update) -> None:
|
||||||
|
"""
|
||||||
|
Handler for /profit.
|
||||||
|
Returns a cumulative profit statistics.
|
||||||
|
:param bot: telegram bot
|
||||||
|
:param update: message update
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
(error, stats) = self.rpc_trade_statistics(
|
||||||
|
self._config['stake_currency'],
|
||||||
|
self._config['fiat_display_currency']
|
||||||
|
)
|
||||||
|
if error:
|
||||||
|
self.send_msg(stats, bot=bot)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Message to display
|
||||||
|
markdown_msg = "*ROI:* Close trades\n" \
|
||||||
|
"∙ `{profit_closed_coin:.8f} {coin} ({profit_closed_percent:.2f}%)`\n" \
|
||||||
|
"∙ `{profit_closed_fiat:.3f} {fiat}`\n" \
|
||||||
|
"*ROI:* All trades\n" \
|
||||||
|
"∙ `{profit_all_coin:.8f} {coin} ({profit_all_percent:.2f}%)`\n" \
|
||||||
|
"∙ `{profit_all_fiat:.3f} {fiat}`\n" \
|
||||||
|
"*Total Trade Count:* `{trade_count}`\n" \
|
||||||
|
"*First Trade opened:* `{first_trade_date}`\n" \
|
||||||
|
"*Latest Trade opened:* `{latest_trade_date}`\n" \
|
||||||
|
"*Avg. Duration:* `{avg_duration}`\n" \
|
||||||
|
"*Best Performing:* `{best_pair}: {best_rate:.2f}%`"\
|
||||||
|
.format(
|
||||||
|
coin=self._config['stake_currency'],
|
||||||
|
fiat=self._config['fiat_display_currency'],
|
||||||
|
profit_closed_coin=stats['profit_closed_coin'],
|
||||||
|
profit_closed_percent=stats['profit_closed_percent'],
|
||||||
|
profit_closed_fiat=stats['profit_closed_fiat'],
|
||||||
|
profit_all_coin=stats['profit_all_coin'],
|
||||||
|
profit_all_percent=stats['profit_all_percent'],
|
||||||
|
profit_all_fiat=stats['profit_all_fiat'],
|
||||||
|
trade_count=stats['trade_count'],
|
||||||
|
first_trade_date=stats['first_trade_date'],
|
||||||
|
latest_trade_date=stats['latest_trade_date'],
|
||||||
|
avg_duration=stats['avg_duration'],
|
||||||
|
best_pair=stats['best_pair'],
|
||||||
|
best_rate=stats['best_rate']
|
||||||
|
)
|
||||||
|
self.send_msg(markdown_msg, bot=bot)
|
||||||
|
|
||||||
|
@authorized_only
|
||||||
|
def _balance(self, bot: Bot, update: Update) -> None:
|
||||||
|
"""
|
||||||
|
Handler for /balance
|
||||||
|
"""
|
||||||
|
(error, result) = self.rpc_balance(self._config['fiat_display_currency'])
|
||||||
|
if error:
|
||||||
|
self.send_msg('`All balances are zero.`')
|
||||||
|
return
|
||||||
|
|
||||||
|
(currencys, total, symbol, value) = result
|
||||||
|
output = ''
|
||||||
|
for currency in currencys:
|
||||||
|
output += """*Currency*: {currency}
|
||||||
|
*Available*: {available}
|
||||||
|
*Balance*: {balance}
|
||||||
|
*Pending*: {pending}
|
||||||
|
*Est. BTC*: {est_btc: .8f}
|
||||||
|
""".format(**currency)
|
||||||
|
|
||||||
|
output += """*Estimated Value*:
|
||||||
|
*BTC*: {0: .8f}
|
||||||
|
*{1}*: {2: .2f}
|
||||||
|
""".format(total, symbol, value)
|
||||||
|
self.send_msg(output)
|
||||||
|
|
||||||
|
@authorized_only
|
||||||
|
def _start(self, bot: Bot, update: Update) -> None:
|
||||||
|
"""
|
||||||
|
Handler for /start.
|
||||||
|
Starts TradeThread
|
||||||
|
:param bot: telegram bot
|
||||||
|
:param update: message update
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
(error, msg) = self.rpc_start()
|
||||||
|
if error:
|
||||||
|
self.send_msg(msg, bot=bot)
|
||||||
|
|
||||||
|
@authorized_only
|
||||||
|
def _stop(self, bot: Bot, update: Update) -> None:
|
||||||
|
"""
|
||||||
|
Handler for /stop.
|
||||||
|
Stops TradeThread
|
||||||
|
:param bot: telegram bot
|
||||||
|
:param update: message update
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
(error, msg) = self.rpc_stop()
|
||||||
|
self.send_msg(msg, bot=bot)
|
||||||
|
|
||||||
|
@authorized_only
|
||||||
|
def _forcesell(self, bot: Bot, update: Update) -> None:
|
||||||
|
"""
|
||||||
|
Handler for /forcesell <id>.
|
||||||
|
Sells the given trade at current price
|
||||||
|
:param bot: telegram bot
|
||||||
|
:param update: message update
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
|
||||||
|
trade_id = update.message.text.replace('/forcesell', '').strip()
|
||||||
|
(error, message) = self.rpc_forcesell(trade_id)
|
||||||
|
if error:
|
||||||
|
self.send_msg(message, bot=bot)
|
||||||
|
return
|
||||||
|
|
||||||
|
@authorized_only
|
||||||
|
def _performance(self, bot: Bot, update: Update) -> None:
|
||||||
|
"""
|
||||||
|
Handler for /performance.
|
||||||
|
Shows a performance statistic from finished trades
|
||||||
|
:param bot: telegram bot
|
||||||
|
:param update: message update
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
(error, trades) = self.rpc_performance()
|
||||||
|
if error:
|
||||||
|
self.send_msg(trades, bot=bot)
|
||||||
|
return
|
||||||
|
|
||||||
|
stats = '\n'.join('{index}.\t<code>{pair}\t{profit:.2f}% ({count})</code>'.format(
|
||||||
|
index=i + 1,
|
||||||
|
pair=trade['pair'],
|
||||||
|
profit=trade['profit'],
|
||||||
|
count=trade['count']
|
||||||
|
) for i, trade in enumerate(trades))
|
||||||
|
message = '<b>Performance:</b>\n{}'.format(stats)
|
||||||
|
self.send_msg(message, parse_mode=ParseMode.HTML)
|
||||||
|
|
||||||
|
@authorized_only
|
||||||
|
def _count(self, bot: Bot, update: Update) -> None:
|
||||||
|
"""
|
||||||
|
Handler for /count.
|
||||||
|
Returns the number of trades running
|
||||||
|
:param bot: telegram bot
|
||||||
|
:param update: message update
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
(error, trades) = self.rpc_count()
|
||||||
|
if error:
|
||||||
|
self.send_msg(trades, bot=bot)
|
||||||
|
return
|
||||||
|
|
||||||
|
message = tabulate({
|
||||||
|
'current': [len(trades)],
|
||||||
|
'max': [self._config['max_open_trades']]
|
||||||
|
}, headers=['current', 'max'], tablefmt='simple')
|
||||||
|
message = "<pre>{}</pre>".format(message)
|
||||||
|
self.logger.debug(message)
|
||||||
|
self.send_msg(message, parse_mode=ParseMode.HTML)
|
||||||
|
|
||||||
|
@authorized_only
|
||||||
|
def _help(self, bot: Bot, update: Update) -> None:
|
||||||
|
"""
|
||||||
|
Handler for /help.
|
||||||
|
Show commands of the bot
|
||||||
|
:param bot: telegram bot
|
||||||
|
:param update: message update
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
message = "*/start:* `Starts the trader`\n" \
|
||||||
|
"*/stop:* `Stops the trader`\n" \
|
||||||
|
"*/status [table]:* `Lists all open trades`\n" \
|
||||||
|
" *table :* `will display trades in a table`\n" \
|
||||||
|
"*/profit:* `Lists cumulative profit from all finished trades`\n" \
|
||||||
|
"*/forcesell <trade_id>|all:* `Instantly sells the given trade or all trades, " \
|
||||||
|
"regardless of profit`\n" \
|
||||||
|
"*/performance:* `Show performance of each finished trade grouped by pair`\n" \
|
||||||
|
"*/daily <n>:* `Shows profit or loss per day, over the last n days`\n" \
|
||||||
|
"*/count:* `Show number of trades running compared to allowed number of trades`" \
|
||||||
|
"\n" \
|
||||||
|
"*/balance:* `Show account balance per currency`\n" \
|
||||||
|
"*/help:* `This help message`\n" \
|
||||||
|
"*/version:* `Show version`"
|
||||||
|
|
||||||
|
self.send_msg(message, bot=bot)
|
||||||
|
|
||||||
|
@authorized_only
|
||||||
|
def _version(self, bot: Bot, update: Update) -> None:
|
||||||
|
"""
|
||||||
|
Handler for /version.
|
||||||
|
Show version information
|
||||||
|
:param bot: telegram bot
|
||||||
|
:param update: message update
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self.send_msg('*Version:* `{}`'.format(__version__), bot=bot)
|
||||||
|
|
||||||
|
def send_msg(self, msg: str, bot: Bot = None,
|
||||||
|
parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
|
||||||
|
"""
|
||||||
|
Send given markdown message
|
||||||
|
:param msg: message
|
||||||
|
:param bot: alternative bot
|
||||||
|
:param parse_mode: telegram parse mode
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
if not self.is_enabled():
|
||||||
|
return
|
||||||
|
|
||||||
|
bot = bot or self._updater.bot
|
||||||
|
|
||||||
|
keyboard = [['/daily', '/profit', '/balance'],
|
||||||
|
['/status', '/status table', '/performance'],
|
||||||
|
['/count', '/start', '/stop', '/help']]
|
||||||
|
|
||||||
|
reply_markup = ReplyKeyboardMarkup(keyboard)
|
||||||
|
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
bot.send_message(
|
||||||
|
self._config['telegram']['chat_id'],
|
||||||
|
text=msg,
|
||||||
|
parse_mode=parse_mode,
|
||||||
|
reply_markup=reply_markup
|
||||||
|
)
|
||||||
|
except NetworkError as network_err:
|
||||||
|
# Sometimes the telegram server resets the current connection,
|
||||||
|
# if this is the case we send the message again.
|
||||||
|
self.logger.warning(
|
||||||
|
'Telegram NetworkError: %s! Trying one more time.',
|
||||||
|
network_err.message
|
||||||
|
)
|
||||||
|
bot.send_message(
|
||||||
|
self._config['telegram']['chat_id'],
|
||||||
|
text=msg,
|
||||||
|
parse_mode=parse_mode,
|
||||||
|
reply_markup=reply_markup
|
||||||
|
)
|
||||||
|
except TelegramError as telegram_err:
|
||||||
|
self.logger.warning(
|
||||||
|
'TelegramError: %s! Giving up on that message.',
|
||||||
|
telegram_err.message
|
||||||
)
|
)
|
||||||
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(
|
|
||||||
'Telegram NetworkError: %s! Trying one more time.',
|
|
||||||
network_err.message
|
|
||||||
)
|
|
||||||
bot.send_message(
|
|
||||||
_CONF['telegram']['chat_id'], msg,
|
|
||||||
parse_mode=parse_mode, reply_markup=reply_markup
|
|
||||||
)
|
|
||||||
except TelegramError as telegram_err:
|
|
||||||
logger.warning('TelegramError: %s! Giving up on that message.', telegram_err.message)
|
|
||||||
|
14
freqtrade/state.py
Normal file
14
freqtrade/state.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# pragma pylint: disable=too-few-public-methods
|
||||||
|
|
||||||
|
"""
|
||||||
|
Bot state constant
|
||||||
|
"""
|
||||||
|
import enum
|
||||||
|
|
||||||
|
|
||||||
|
class State(enum.Enum):
|
||||||
|
"""
|
||||||
|
Bot running states
|
||||||
|
"""
|
||||||
|
RUNNING = 0
|
||||||
|
STOPPED = 1
|
@ -2,9 +2,10 @@
|
|||||||
|
|
||||||
import talib.abstract as ta
|
import talib.abstract as ta
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||||
from freqtrade.strategy.interface import IStrategy
|
|
||||||
from freqtrade.indicator_helpers import fishers_inverse
|
from freqtrade.indicator_helpers import fishers_inverse
|
||||||
|
from freqtrade.strategy.interface import IStrategy
|
||||||
|
|
||||||
class_name = 'DefaultStrategy'
|
class_name = 'DefaultStrategy'
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ This module defines the interface to apply for strategies
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
|
||||||
|
@ -3,15 +3,16 @@
|
|||||||
"""
|
"""
|
||||||
This module load custom strategies
|
This module load custom strategies
|
||||||
"""
|
"""
|
||||||
|
import importlib
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import logging
|
|
||||||
import importlib
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
from freqtrade.strategy.interface import IStrategy
|
|
||||||
|
|
||||||
|
from freqtrade.constants import Constants
|
||||||
|
from freqtrade.logger import Logger
|
||||||
|
from freqtrade.strategy.interface import IStrategy
|
||||||
|
|
||||||
sys.path.insert(0, r'../../user_data/strategies')
|
sys.path.insert(0, r'../../user_data/strategies')
|
||||||
|
|
||||||
@ -20,32 +21,19 @@ class Strategy(object):
|
|||||||
"""
|
"""
|
||||||
This class contains all the logic to load custom strategy class
|
This class contains all the logic to load custom strategy class
|
||||||
"""
|
"""
|
||||||
__instance = None
|
def __init__(self, config: dict = {}) -> None:
|
||||||
|
|
||||||
DEFAULT_STRATEGY = 'default_strategy'
|
|
||||||
|
|
||||||
def __new__(cls) -> object:
|
|
||||||
"""
|
|
||||||
Used to create the Singleton
|
|
||||||
:return: Strategy object
|
|
||||||
"""
|
|
||||||
if Strategy.__instance is None:
|
|
||||||
Strategy.__instance = object.__new__(cls)
|
|
||||||
return Strategy.__instance
|
|
||||||
|
|
||||||
def init(self, config: dict) -> None:
|
|
||||||
"""
|
"""
|
||||||
Load the custom class from config parameter
|
Load the custom class from config parameter
|
||||||
:param config:
|
:param config:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
self.logger = logging.getLogger(__name__)
|
self.logger = Logger(name=__name__).get_logger()
|
||||||
|
|
||||||
# Verify the strategy is in the configuration, otherwise fallback to the default strategy
|
# Verify the strategy is in the configuration, otherwise fallback to the default strategy
|
||||||
if 'strategy' in config:
|
if 'strategy' in config:
|
||||||
strategy = config['strategy']
|
strategy = config['strategy']
|
||||||
else:
|
else:
|
||||||
strategy = self.DEFAULT_STRATEGY
|
strategy = Constants.DEFAULT_STRATEGY
|
||||||
|
|
||||||
# Load the strategy
|
# Load the strategy
|
||||||
self._load_strategy(strategy)
|
self._load_strategy(strategy)
|
||||||
@ -72,12 +60,12 @@ class Strategy(object):
|
|||||||
# Minimal ROI designed for the strategy
|
# Minimal ROI designed for the strategy
|
||||||
self.minimal_roi = OrderedDict(sorted(
|
self.minimal_roi = OrderedDict(sorted(
|
||||||
{int(key): value for (key, value) in self.custom_strategy.minimal_roi.items()}.items(),
|
{int(key): value for (key, value) in self.custom_strategy.minimal_roi.items()}.items(),
|
||||||
key=lambda tuple: tuple[0])) # sort after converting to number
|
key=lambda t: t[0])) # sort after converting to number
|
||||||
|
|
||||||
# Optimal stoploss designed for the strategy
|
# Optimal stoploss designed for the strategy
|
||||||
self.stoploss = float(self.custom_strategy.stoploss)
|
self.stoploss = float(self.custom_strategy.stoploss)
|
||||||
|
|
||||||
self.ticker_interval = self.custom_strategy.ticker_interval
|
self.ticker_interval = int(self.custom_strategy.ticker_interval)
|
||||||
|
|
||||||
def _load_strategy(self, strategy_name: str) -> None:
|
def _load_strategy(self, strategy_name: str) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -1,17 +1,21 @@
|
|||||||
# pragma pylint: disable=missing-docstring
|
# pragma pylint: disable=missing-docstring
|
||||||
from datetime import datetime
|
|
||||||
from unittest.mock import MagicMock
|
|
||||||
from functools import reduce
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from functools import reduce
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import pytest
|
import pytest
|
||||||
from jsonschema import validate
|
from jsonschema import validate
|
||||||
|
from sqlalchemy import create_engine
|
||||||
from telegram import Chat, Message, Update
|
from telegram import Chat, Message, Update
|
||||||
from freqtrade.analyze import parse_ticker_dataframe
|
|
||||||
from freqtrade.strategy.strategy import Strategy
|
|
||||||
|
|
||||||
from freqtrade.misc import CONF_SCHEMA
|
from freqtrade.analyze import Analyze
|
||||||
|
from freqtrade.constants import Constants
|
||||||
|
from freqtrade.freqtradebot import FreqtradeBot
|
||||||
|
|
||||||
|
logging.getLogger('').setLevel(logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
def log_has(line, logs):
|
def log_has(line, logs):
|
||||||
@ -22,6 +26,26 @@ def log_has(line, logs):
|
|||||||
False)
|
False)
|
||||||
|
|
||||||
|
|
||||||
|
# Functions for recurrent object patching
|
||||||
|
def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
|
||||||
|
"""
|
||||||
|
This function patch _init_modules() to not call dependencies
|
||||||
|
:param mocker: a Mocker object to apply patches
|
||||||
|
:param config: Config to pass to the bot
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
mocker.patch('freqtrade.fiat_convert.Market', {'price_usd': 12345.0})
|
||||||
|
mocker.patch('freqtrade.freqtradebot.Analyze', MagicMock())
|
||||||
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
|
||||||
|
mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock())
|
||||||
|
mocker.patch('freqtrade.freqtradebot.RPCManager._init', MagicMock())
|
||||||
|
mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock())
|
||||||
|
mocker.patch('freqtrade.freqtradebot.Analyze.get_signal', MagicMock())
|
||||||
|
|
||||||
|
return FreqtradeBot(config, create_engine('sqlite://'))
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def default_conf():
|
def default_conf():
|
||||||
""" Returns validated configuration suitable for most tests """
|
""" Returns validated configuration suitable for most tests """
|
||||||
@ -61,9 +85,10 @@ def default_conf():
|
|||||||
"token": "token",
|
"token": "token",
|
||||||
"chat_id": "0"
|
"chat_id": "0"
|
||||||
},
|
},
|
||||||
"initial_state": "running"
|
"initial_state": "running",
|
||||||
|
"loglevel": logging.DEBUG
|
||||||
}
|
}
|
||||||
validate(configuration, CONF_SCHEMA)
|
validate(configuration, Constants.CONF_SCHEMA)
|
||||||
return configuration
|
return configuration
|
||||||
|
|
||||||
|
|
||||||
@ -265,14 +290,7 @@ def ticker_history_without_bv():
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def result():
|
def result():
|
||||||
with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file:
|
with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file:
|
||||||
return parse_ticker_dataframe(json.load(data_file))
|
return Analyze.parse_ticker_dataframe(json.load(data_file))
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def default_strategy():
|
|
||||||
strategy = Strategy()
|
|
||||||
strategy.init({'strategy': 'default_strategy'})
|
|
||||||
return strategy
|
|
||||||
|
|
||||||
|
|
||||||
# FIX:
|
# FIX:
|
||||||
@ -280,3 +298,134 @@ def default_strategy():
|
|||||||
# that inserts a trade of some type and open-status
|
# that inserts a trade of some type and open-status
|
||||||
# return the open-order-id
|
# return the open-order-id
|
||||||
# See tests in rpc/main that could use this
|
# See tests in rpc/main that could use this
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def get_market_summaries_data():
|
||||||
|
"""
|
||||||
|
This fixture is a real result from exchange.get_market_summaries() but reduced to only
|
||||||
|
8 entries. 4 BTC, 4 USTD
|
||||||
|
:return: JSON market summaries
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'Ask': 1.316e-05,
|
||||||
|
'BaseVolume': 5.72599471,
|
||||||
|
'Bid': 1.3e-05,
|
||||||
|
'Created': '2014-04-14T00:00:00',
|
||||||
|
'High': 1.414e-05,
|
||||||
|
'Last': 1.298e-05,
|
||||||
|
'Low': 1.282e-05,
|
||||||
|
'MarketName': 'BTC-XWC',
|
||||||
|
'OpenBuyOrders': 2000,
|
||||||
|
'OpenSellOrders': 1484,
|
||||||
|
'PrevDay': 1.376e-05,
|
||||||
|
'TimeStamp': '2018-02-05T01:32:40.493',
|
||||||
|
'Volume': 424041.21418375
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'Ask': 0.00627051,
|
||||||
|
'BaseVolume': 93.23302388,
|
||||||
|
'Bid': 0.00618192,
|
||||||
|
'Created': '2016-10-20T04:48:30.387',
|
||||||
|
'High': 0.00669897,
|
||||||
|
'Last': 0.00618192,
|
||||||
|
'Low': 0.006,
|
||||||
|
'MarketName': 'BTC-XZC',
|
||||||
|
'OpenBuyOrders': 343,
|
||||||
|
'OpenSellOrders': 2037,
|
||||||
|
'PrevDay': 0.00668229,
|
||||||
|
'TimeStamp': '2018-02-05T01:32:43.383',
|
||||||
|
'Volume': 14863.60730702
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'Ask': 0.01137247,
|
||||||
|
'BaseVolume': 383.55922657,
|
||||||
|
'Bid': 0.01136006,
|
||||||
|
'Created': '2016-11-15T20:29:59.73',
|
||||||
|
'High': 0.012,
|
||||||
|
'Last': 0.01137247,
|
||||||
|
'Low': 0.01119883,
|
||||||
|
'MarketName': 'BTC-ZCL',
|
||||||
|
'OpenBuyOrders': 1332,
|
||||||
|
'OpenSellOrders': 5317,
|
||||||
|
'PrevDay': 0.01179603,
|
||||||
|
'TimeStamp': '2018-02-05T01:32:42.773',
|
||||||
|
'Volume': 33308.07358285
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'Ask': 0.04155821,
|
||||||
|
'BaseVolume': 274.75369074,
|
||||||
|
'Bid': 0.04130002,
|
||||||
|
'Created': '2016-10-28T17:13:10.833',
|
||||||
|
'High': 0.04354429,
|
||||||
|
'Last': 0.041585,
|
||||||
|
'Low': 0.0413,
|
||||||
|
'MarketName': 'BTC-ZEC',
|
||||||
|
'OpenBuyOrders': 863,
|
||||||
|
'OpenSellOrders': 5579,
|
||||||
|
'PrevDay': 0.0429,
|
||||||
|
'TimeStamp': '2018-02-05T01:32:43.21',
|
||||||
|
'Volume': 6479.84033259
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'Ask': 210.99999999,
|
||||||
|
'BaseVolume': 615132.70989532,
|
||||||
|
'Bid': 210.05503736,
|
||||||
|
'Created': '2017-07-21T01:08:49.397',
|
||||||
|
'High': 257.396,
|
||||||
|
'Last': 211.0,
|
||||||
|
'Low': 209.05333589,
|
||||||
|
'MarketName': 'USDT-XMR',
|
||||||
|
'OpenBuyOrders': 180,
|
||||||
|
'OpenSellOrders': 1203,
|
||||||
|
'PrevDay': 247.93528899,
|
||||||
|
'TimeStamp': '2018-02-05T01:32:43.117',
|
||||||
|
'Volume': 2688.17410793
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'Ask': 0.79589979,
|
||||||
|
'BaseVolume': 9349557.01853031,
|
||||||
|
'Bid': 0.789226,
|
||||||
|
'Created': '2017-07-14T17:10:10.737',
|
||||||
|
'High': 0.977,
|
||||||
|
'Last': 0.79589979,
|
||||||
|
'Low': 0.781,
|
||||||
|
'MarketName': 'USDT-XRP',
|
||||||
|
'OpenBuyOrders': 1075,
|
||||||
|
'OpenSellOrders': 6508,
|
||||||
|
'PrevDay': 0.93300218,
|
||||||
|
'TimeStamp': '2018-02-05T01:32:42.383',
|
||||||
|
'Volume': 10801663.00788851
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'Ask': 0.05154982,
|
||||||
|
'BaseVolume': 2311087.71232136,
|
||||||
|
'Bid': 0.05040107,
|
||||||
|
'Created': '2017-12-29T19:29:18.357',
|
||||||
|
'High': 0.06668561,
|
||||||
|
'Last': 0.0508,
|
||||||
|
'Low': 0.05006731,
|
||||||
|
'MarketName': 'USDT-XVG',
|
||||||
|
'OpenBuyOrders': 655,
|
||||||
|
'OpenSellOrders': 5544,
|
||||||
|
'PrevDay': 0.0627,
|
||||||
|
'TimeStamp': '2018-02-05T01:32:41.507',
|
||||||
|
'Volume': 40031424.2152716
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'Ask': 332.65500022,
|
||||||
|
'BaseVolume': 562911.87455665,
|
||||||
|
'Bid': 330.00000001,
|
||||||
|
'Created': '2017-07-14T17:10:10.673',
|
||||||
|
'High': 401.59999999,
|
||||||
|
'Last': 332.65500019,
|
||||||
|
'Low': 330.0,
|
||||||
|
'MarketName': 'USDT-ZEC',
|
||||||
|
'OpenBuyOrders': 161,
|
||||||
|
'OpenSellOrders': 1731,
|
||||||
|
'PrevDay': 391.42,
|
||||||
|
'TimeStamp': '2018-02-05T01:32:42.947',
|
||||||
|
'Volume': 1571.09647946
|
||||||
|
}
|
||||||
|
]
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
# pragma pylint: disable=missing-docstring, C0103, bad-continuation, global-statement
|
# pragma pylint: disable=missing-docstring, C0103, bad-continuation, global-statement
|
||||||
# pragma pylint: disable=protected-access
|
# pragma pylint: disable=protected-access
|
||||||
from unittest.mock import MagicMock
|
|
||||||
from random import randint
|
|
||||||
import logging
|
import logging
|
||||||
from requests.exceptions import RequestException
|
from random import randint
|
||||||
import pytest
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from requests.exceptions import RequestException
|
||||||
|
|
||||||
|
import freqtrade.exchange as exchange
|
||||||
from freqtrade import OperationalException
|
from freqtrade import OperationalException
|
||||||
from freqtrade.exchange import init, validate_pairs, buy, sell, get_balance, get_balances, \
|
from freqtrade.exchange import init, validate_pairs, buy, sell, get_balance, get_balances, \
|
||||||
get_ticker, get_ticker_history, cancel_order, get_name, get_fee
|
get_ticker, get_ticker_history, cancel_order, get_name, get_fee
|
||||||
import freqtrade.exchange as exchange
|
|
||||||
from freqtrade.tests.conftest import log_has
|
from freqtrade.tests.conftest import log_has
|
||||||
|
|
||||||
API_INIT = False
|
API_INIT = False
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
# pragma pylint: disable=missing-docstring, C0103, protected-access, unused-argument
|
# pragma pylint: disable=missing-docstring, C0103, protected-access, unused-argument
|
||||||
|
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from requests.exceptions import ContentDecodingError
|
from requests.exceptions import ContentDecodingError
|
||||||
from freqtrade.exchange.bittrex import Bittrex
|
|
||||||
import freqtrade.exchange.bittrex as btx
|
import freqtrade.exchange.bittrex as btx
|
||||||
|
from freqtrade.exchange.bittrex import Bittrex
|
||||||
|
|
||||||
|
|
||||||
# Eat this flake8
|
# Eat this flake8
|
||||||
|
@ -1,16 +1,28 @@
|
|||||||
# pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103
|
# pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, unused-argument
|
||||||
import random
|
|
||||||
import logging
|
import json
|
||||||
import math
|
import math
|
||||||
|
import random
|
||||||
|
from copy import deepcopy
|
||||||
|
from typing import List
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
import pandas as pd
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from freqtrade import exchange, optimize
|
import pandas as pd
|
||||||
from freqtrade.exchange import Bittrex
|
from arrow import Arrow
|
||||||
from freqtrade.optimize import preprocess
|
|
||||||
from freqtrade.optimize.backtesting import backtest, generate_text_table, get_timeframe
|
from freqtrade import optimize
|
||||||
import freqtrade.optimize.backtesting as backtesting
|
from freqtrade.analyze import Analyze
|
||||||
from freqtrade.tests.conftest import log_has
|
from freqtrade.arguments import Arguments
|
||||||
|
from freqtrade.optimize.backtesting import Backtesting, start, setup_configuration
|
||||||
|
from freqtrade.tests.conftest import default_conf, log_has
|
||||||
|
|
||||||
|
# Avoid to reinit the same object again and again
|
||||||
|
_BACKTESTING = Backtesting(default_conf())
|
||||||
|
|
||||||
|
|
||||||
|
def get_args(args) -> List[str]:
|
||||||
|
return Arguments(args, '').get_parsed_arg()
|
||||||
|
|
||||||
|
|
||||||
def trim_dictlist(dict_list, num):
|
def trim_dictlist(dict_list, num):
|
||||||
@ -20,124 +32,6 @@ def trim_dictlist(dict_list, num):
|
|||||||
return new
|
return new
|
||||||
|
|
||||||
|
|
||||||
# use for mock freqtrade.exchange.get_ticker_history'
|
|
||||||
def _load_pair_as_ticks(pair, tickfreq):
|
|
||||||
ticks = optimize.load_data(None, ticker_interval=8, pairs=[pair])
|
|
||||||
ticks = trim_dictlist(ticks, -200)
|
|
||||||
return ticks[pair]
|
|
||||||
|
|
||||||
|
|
||||||
# FIX: fixturize this?
|
|
||||||
def _make_backtest_conf(conf=None,
|
|
||||||
pair='BTC_UNITEST',
|
|
||||||
record=None):
|
|
||||||
data = optimize.load_data(None, ticker_interval=8, pairs=[pair])
|
|
||||||
data = trim_dictlist(data, -200)
|
|
||||||
return {'stake_amount': conf['stake_amount'],
|
|
||||||
'processed': optimize.preprocess(data),
|
|
||||||
'max_open_trades': 10,
|
|
||||||
'realistic': True,
|
|
||||||
'record': record}
|
|
||||||
|
|
||||||
|
|
||||||
def _trend(signals, buy_value, sell_value):
|
|
||||||
n = len(signals['low'])
|
|
||||||
buy = np.zeros(n)
|
|
||||||
sell = np.zeros(n)
|
|
||||||
for i in range(0, len(signals['buy'])):
|
|
||||||
if random.random() > 0.5: # Both buy and sell signals at same timeframe
|
|
||||||
buy[i] = buy_value
|
|
||||||
sell[i] = sell_value
|
|
||||||
signals['buy'] = buy
|
|
||||||
signals['sell'] = sell
|
|
||||||
return signals
|
|
||||||
|
|
||||||
|
|
||||||
def _trend_alternate(dataframe=None):
|
|
||||||
signals = dataframe
|
|
||||||
low = signals['low']
|
|
||||||
n = len(low)
|
|
||||||
buy = np.zeros(n)
|
|
||||||
sell = np.zeros(n)
|
|
||||||
for i in range(0, len(buy)):
|
|
||||||
if i % 2 == 0:
|
|
||||||
buy[i] = 1
|
|
||||||
else:
|
|
||||||
sell[i] = 1
|
|
||||||
signals['buy'] = buy
|
|
||||||
signals['sell'] = sell
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
|
|
||||||
def _run_backtest_1(strategy, fun, backtest_conf):
|
|
||||||
# strategy is a global (hidden as a singleton), so we
|
|
||||||
# emulate strategy being pure, by override/restore here
|
|
||||||
# if we dont do this, the override in strategy will carry over
|
|
||||||
# to other tests
|
|
||||||
old_buy = strategy.populate_buy_trend
|
|
||||||
old_sell = strategy.populate_sell_trend
|
|
||||||
strategy.populate_buy_trend = fun # Override
|
|
||||||
strategy.populate_sell_trend = fun # Override
|
|
||||||
results = backtest(backtest_conf)
|
|
||||||
strategy.populate_buy_trend = old_buy # restore override
|
|
||||||
strategy.populate_sell_trend = old_sell # restore override
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
def test_generate_text_table():
|
|
||||||
results = pd.DataFrame(
|
|
||||||
{
|
|
||||||
'currency': ['BTC_ETH', 'BTC_ETH'],
|
|
||||||
'profit_percent': [0.1, 0.2],
|
|
||||||
'profit_BTC': [0.2, 0.4],
|
|
||||||
'duration': [10, 30],
|
|
||||||
'profit': [2, 0],
|
|
||||||
'loss': [0, 0]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
print(generate_text_table({'BTC_ETH': {}}, results, 'BTC'))
|
|
||||||
assert generate_text_table({'BTC_ETH': {}}, results, 'BTC') == (
|
|
||||||
'pair buy count avg profit % total profit BTC avg duration profit loss\n' # noqa
|
|
||||||
'------- ----------- -------------- ------------------ -------------- -------- ------\n' # noqa
|
|
||||||
'BTC_ETH 2 15.00 0.60000000 20.0 2 0\n' # noqa
|
|
||||||
'TOTAL 2 15.00 0.60000000 20.0 2 0') # noqa
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_timeframe(default_strategy):
|
|
||||||
data = preprocess(optimize.load_data(
|
|
||||||
None, ticker_interval=1, pairs=['BTC_UNITEST']))
|
|
||||||
min_date, max_date = get_timeframe(data)
|
|
||||||
assert min_date.isoformat() == '2017-11-04T23:02:00+00:00'
|
|
||||||
assert max_date.isoformat() == '2017-11-14T22:59:00+00:00'
|
|
||||||
|
|
||||||
|
|
||||||
def test_backtest(default_strategy, default_conf, mocker):
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
|
||||||
|
|
||||||
data = optimize.load_data(None, ticker_interval=5, pairs=['BTC_ETH'])
|
|
||||||
data = trim_dictlist(data, -200)
|
|
||||||
results = backtest({'stake_amount': default_conf['stake_amount'],
|
|
||||||
'processed': optimize.preprocess(data),
|
|
||||||
'max_open_trades': 10,
|
|
||||||
'realistic': True})
|
|
||||||
assert not results.empty
|
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_1min_ticker_interval(default_strategy, default_conf, mocker):
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
|
||||||
|
|
||||||
# Run a backtesting for an exiting 5min ticker_interval
|
|
||||||
data = optimize.load_data(None, ticker_interval=1, pairs=['BTC_UNITEST'])
|
|
||||||
data = trim_dictlist(data, -200)
|
|
||||||
results = backtest({'stake_amount': default_conf['stake_amount'],
|
|
||||||
'processed': optimize.preprocess(data),
|
|
||||||
'max_open_trades': 1,
|
|
||||||
'realistic': True})
|
|
||||||
assert not results.empty
|
|
||||||
|
|
||||||
|
|
||||||
def load_data_test(what):
|
def load_data_test(what):
|
||||||
timerange = ((None, 'line'), None, -100)
|
timerange = ((None, 'line'), None, -100)
|
||||||
data = optimize.load_data(None, ticker_interval=1, pairs=['BTC_UNITEST'], timerange=timerange)
|
data = optimize.load_data(None, ticker_interval=1, pairs=['BTC_UNITEST'], timerange=timerange)
|
||||||
@ -181,34 +75,429 @@ def load_data_test(what):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def simple_backtest(config, contour, num_results):
|
def simple_backtest(config, contour, num_results) -> None:
|
||||||
|
backtesting = _BACKTESTING
|
||||||
|
|
||||||
data = load_data_test(contour)
|
data = load_data_test(contour)
|
||||||
processed = optimize.preprocess(data)
|
processed = backtesting.tickerdata_to_dataframe(data)
|
||||||
assert isinstance(processed, dict)
|
assert isinstance(processed, dict)
|
||||||
results = backtest({'stake_amount': config['stake_amount'],
|
results = backtesting.backtest(
|
||||||
'processed': processed,
|
{
|
||||||
'max_open_trades': 1,
|
'stake_amount': config['stake_amount'],
|
||||||
'realistic': True})
|
'processed': processed,
|
||||||
|
'max_open_trades': 1,
|
||||||
|
'realistic': True
|
||||||
|
}
|
||||||
|
)
|
||||||
# results :: <class 'pandas.core.frame.DataFrame'>
|
# results :: <class 'pandas.core.frame.DataFrame'>
|
||||||
assert len(results) == num_results
|
assert len(results) == num_results
|
||||||
|
|
||||||
|
|
||||||
|
def mocked_load_data(datadir, pairs=[], ticker_interval=0, refresh_pairs=False, timerange=None):
|
||||||
|
tickerdata = optimize.load_tickerdata_file(datadir, 'BTC_UNITEST', 1, timerange=timerange)
|
||||||
|
pairdata = {'BTC_UNITEST': tickerdata}
|
||||||
|
return pairdata
|
||||||
|
|
||||||
|
|
||||||
|
# use for mock freqtrade.exchange.get_ticker_history'
|
||||||
|
def _load_pair_as_ticks(pair, tickfreq):
|
||||||
|
ticks = optimize.load_data(None, ticker_interval=tickfreq, pairs=[pair])
|
||||||
|
ticks = trim_dictlist(ticks, -200)
|
||||||
|
return ticks[pair]
|
||||||
|
|
||||||
|
|
||||||
|
# FIX: fixturize this?
|
||||||
|
def _make_backtest_conf(conf=None, pair='BTC_UNITEST', record=None):
|
||||||
|
data = optimize.load_data(None, ticker_interval=8, pairs=[pair])
|
||||||
|
data = trim_dictlist(data, -200)
|
||||||
|
return {
|
||||||
|
'stake_amount': conf['stake_amount'],
|
||||||
|
'processed': _BACKTESTING.tickerdata_to_dataframe(data),
|
||||||
|
'max_open_trades': 10,
|
||||||
|
'realistic': True,
|
||||||
|
'record': record
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _trend(signals, buy_value, sell_value):
|
||||||
|
n = len(signals['low'])
|
||||||
|
buy = np.zeros(n)
|
||||||
|
sell = np.zeros(n)
|
||||||
|
for i in range(0, len(signals['buy'])):
|
||||||
|
if random.random() > 0.5: # Both buy and sell signals at same timeframe
|
||||||
|
buy[i] = buy_value
|
||||||
|
sell[i] = sell_value
|
||||||
|
signals['buy'] = buy
|
||||||
|
signals['sell'] = sell
|
||||||
|
return signals
|
||||||
|
|
||||||
|
|
||||||
|
def _trend_alternate(dataframe=None):
|
||||||
|
signals = dataframe
|
||||||
|
low = signals['low']
|
||||||
|
n = len(low)
|
||||||
|
buy = np.zeros(n)
|
||||||
|
sell = np.zeros(n)
|
||||||
|
for i in range(0, len(buy)):
|
||||||
|
if i % 2 == 0:
|
||||||
|
buy[i] = 1
|
||||||
|
else:
|
||||||
|
sell[i] = 1
|
||||||
|
signals['buy'] = buy
|
||||||
|
signals['sell'] = sell
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
|
||||||
|
def _run_backtest_1(fun, backtest_conf):
|
||||||
|
# strategy is a global (hidden as a singleton), so we
|
||||||
|
# emulate strategy being pure, by override/restore here
|
||||||
|
# if we dont do this, the override in strategy will carry over
|
||||||
|
# to other tests
|
||||||
|
old_buy = _BACKTESTING.populate_buy_trend
|
||||||
|
old_sell = _BACKTESTING.populate_sell_trend
|
||||||
|
_BACKTESTING.populate_buy_trend = fun # Override
|
||||||
|
_BACKTESTING.populate_sell_trend = fun # Override
|
||||||
|
results = _BACKTESTING.backtest(backtest_conf)
|
||||||
|
_BACKTESTING.populate_buy_trend = old_buy # restore override
|
||||||
|
_BACKTESTING.populate_sell_trend = old_sell # restore override
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
# Unit tests
|
||||||
|
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test setup_configuration() function
|
||||||
|
"""
|
||||||
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(default_conf)
|
||||||
|
))
|
||||||
|
|
||||||
|
args = [
|
||||||
|
'--config', 'config.json',
|
||||||
|
'--strategy', 'default_strategy',
|
||||||
|
'backtesting'
|
||||||
|
]
|
||||||
|
|
||||||
|
config = setup_configuration(get_args(args))
|
||||||
|
assert 'max_open_trades' in config
|
||||||
|
assert 'stake_currency' in config
|
||||||
|
assert 'stake_amount' in config
|
||||||
|
assert 'exchange' in config
|
||||||
|
assert 'pair_whitelist' in config['exchange']
|
||||||
|
assert 'datadir' in config
|
||||||
|
assert log_has(
|
||||||
|
'Parameter --datadir detected: {} ...'.format(config['datadir']),
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
assert 'ticker_interval' in config
|
||||||
|
assert not log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
assert 'live' not in config
|
||||||
|
assert not log_has('Parameter -l/--live detected ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
assert 'realistic_simulation' not in config
|
||||||
|
assert not log_has('Parameter --realistic-simulation detected ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
assert 'refresh_pairs' not in config
|
||||||
|
assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
assert 'timerange' not in config
|
||||||
|
assert 'export' not in config
|
||||||
|
|
||||||
|
|
||||||
|
def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test setup_configuration() function
|
||||||
|
"""
|
||||||
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(default_conf)
|
||||||
|
))
|
||||||
|
|
||||||
|
args = [
|
||||||
|
'--config', 'config.json',
|
||||||
|
'--strategy', 'default_strategy',
|
||||||
|
'--datadir', '/foo/bar',
|
||||||
|
'backtesting',
|
||||||
|
'--ticker-interval', '1',
|
||||||
|
'--live',
|
||||||
|
'--realistic-simulation',
|
||||||
|
'--refresh-pairs-cached',
|
||||||
|
'--timerange', ':100',
|
||||||
|
'--export', '/bar/foo'
|
||||||
|
]
|
||||||
|
|
||||||
|
config = setup_configuration(get_args(args))
|
||||||
|
assert 'max_open_trades' in config
|
||||||
|
assert 'stake_currency' in config
|
||||||
|
assert 'stake_amount' in config
|
||||||
|
assert 'exchange' in config
|
||||||
|
assert 'pair_whitelist' in config['exchange']
|
||||||
|
assert 'datadir' in config
|
||||||
|
assert log_has(
|
||||||
|
'Parameter --datadir detected: {} ...'.format(config['datadir']),
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
assert 'ticker_interval' in config
|
||||||
|
assert log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples)
|
||||||
|
assert log_has(
|
||||||
|
'Using ticker_interval: 1 ...',
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
|
||||||
|
assert 'live' in config
|
||||||
|
assert log_has('Parameter -l/--live detected ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
assert 'realistic_simulation'in config
|
||||||
|
assert log_has('Parameter --realistic-simulation detected ...', caplog.record_tuples)
|
||||||
|
assert log_has('Using max_open_trades: 1 ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
assert 'refresh_pairs'in config
|
||||||
|
assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
|
||||||
|
assert 'timerange' in config
|
||||||
|
assert log_has(
|
||||||
|
'Parameter --timerange detected: {} ...'.format(config['timerange']),
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
|
||||||
|
assert 'export' in config
|
||||||
|
assert log_has(
|
||||||
|
'Parameter --export detected: {} ...'.format(config['export']),
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_start(mocker, default_conf, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test start() function
|
||||||
|
"""
|
||||||
|
start_mock = MagicMock()
|
||||||
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.start', start_mock)
|
||||||
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(default_conf)
|
||||||
|
))
|
||||||
|
args = [
|
||||||
|
'--config', 'config.json',
|
||||||
|
'--strategy', 'default_strategy',
|
||||||
|
'backtesting'
|
||||||
|
]
|
||||||
|
args = get_args(args)
|
||||||
|
start(args)
|
||||||
|
assert log_has(
|
||||||
|
'Starting freqtrade in Backtesting mode',
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
assert start_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_backtesting__init__(mocker, default_conf) -> None:
|
||||||
|
"""
|
||||||
|
Test Backtesting.__init__() method
|
||||||
|
"""
|
||||||
|
init_mock = MagicMock()
|
||||||
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting._init', init_mock)
|
||||||
|
|
||||||
|
backtesting = Backtesting(default_conf)
|
||||||
|
assert backtesting.config == default_conf
|
||||||
|
assert backtesting.analyze is None
|
||||||
|
assert backtesting.ticker_interval is None
|
||||||
|
assert backtesting.tickerdata_to_dataframe is None
|
||||||
|
assert backtesting.populate_buy_trend is None
|
||||||
|
assert backtesting.populate_sell_trend is None
|
||||||
|
assert init_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_backtesting_init(default_conf) -> None:
|
||||||
|
"""
|
||||||
|
Test Backtesting._init() method
|
||||||
|
"""
|
||||||
|
backtesting = Backtesting(default_conf)
|
||||||
|
assert backtesting.config == default_conf
|
||||||
|
assert isinstance(backtesting.analyze, Analyze)
|
||||||
|
assert backtesting.ticker_interval == 5
|
||||||
|
assert callable(backtesting.tickerdata_to_dataframe)
|
||||||
|
assert callable(backtesting.populate_buy_trend)
|
||||||
|
assert callable(backtesting.populate_sell_trend)
|
||||||
|
|
||||||
|
|
||||||
|
def test_tickerdata_to_dataframe(default_conf) -> None:
|
||||||
|
"""
|
||||||
|
Test Backtesting.tickerdata_to_dataframe() method
|
||||||
|
"""
|
||||||
|
|
||||||
|
timerange = ((None, 'line'), None, -100)
|
||||||
|
tick = optimize.load_tickerdata_file(None, 'BTC_UNITEST', 1, timerange=timerange)
|
||||||
|
tickerlist = {'BTC_UNITEST': tick}
|
||||||
|
|
||||||
|
backtesting = _BACKTESTING
|
||||||
|
data = backtesting.tickerdata_to_dataframe(tickerlist)
|
||||||
|
assert len(data['BTC_UNITEST']) == 100
|
||||||
|
|
||||||
|
# Load Analyze to compare the result between Backtesting function and Analyze are the same
|
||||||
|
analyze = Analyze(default_conf)
|
||||||
|
data2 = analyze.tickerdata_to_dataframe(tickerlist)
|
||||||
|
assert data['BTC_UNITEST'].equals(data2['BTC_UNITEST'])
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_timeframe() -> None:
|
||||||
|
"""
|
||||||
|
Test Backtesting.get_timeframe() method
|
||||||
|
"""
|
||||||
|
backtesting = _BACKTESTING
|
||||||
|
|
||||||
|
data = backtesting.tickerdata_to_dataframe(
|
||||||
|
optimize.load_data(
|
||||||
|
None,
|
||||||
|
ticker_interval=1,
|
||||||
|
pairs=['BTC_UNITEST']
|
||||||
|
)
|
||||||
|
)
|
||||||
|
min_date, max_date = backtesting.get_timeframe(data)
|
||||||
|
assert min_date.isoformat() == '2017-11-04T23:02:00+00:00'
|
||||||
|
assert max_date.isoformat() == '2017-11-14T22:59:00+00:00'
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_text_table():
|
||||||
|
"""
|
||||||
|
Test Backtesting.generate_text_table() method
|
||||||
|
"""
|
||||||
|
backtesting = _BACKTESTING
|
||||||
|
|
||||||
|
results = pd.DataFrame(
|
||||||
|
{
|
||||||
|
'currency': ['BTC_ETH', 'BTC_ETH'],
|
||||||
|
'profit_percent': [0.1, 0.2],
|
||||||
|
'profit_BTC': [0.2, 0.4],
|
||||||
|
'duration': [10, 30],
|
||||||
|
'profit': [2, 0],
|
||||||
|
'loss': [0, 0]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
result_str = (
|
||||||
|
'pair buy count avg profit % '
|
||||||
|
'total profit BTC avg duration profit loss\n'
|
||||||
|
'------- ----------- -------------- '
|
||||||
|
'------------------ -------------- -------- ------\n'
|
||||||
|
'BTC_ETH 2 15.00 '
|
||||||
|
'0.60000000 20.0 2 0\n'
|
||||||
|
'TOTAL 2 15.00 '
|
||||||
|
'0.60000000 20.0 2 0'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert backtesting._generate_text_table(data={'BTC_ETH': {}}, results=results) == result_str
|
||||||
|
|
||||||
|
|
||||||
|
def test_backtesting_start(default_conf, mocker, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test Backtesting.start() method
|
||||||
|
"""
|
||||||
|
def get_timeframe(input1, input2):
|
||||||
|
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.freqtradebot.Analyze', MagicMock())
|
||||||
|
mocker.patch('freqtrade.optimize.load_data', mocked_load_data)
|
||||||
|
mocker.patch('freqtrade.exchange.get_ticker_history')
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.optimize.backtesting.Backtesting',
|
||||||
|
backtest=MagicMock(),
|
||||||
|
_generate_text_table=MagicMock(return_value='1'),
|
||||||
|
get_timeframe=get_timeframe,
|
||||||
|
)
|
||||||
|
|
||||||
|
conf = deepcopy(default_conf)
|
||||||
|
conf['exchange']['pair_whitelist'] = ['BTC_UNITEST']
|
||||||
|
conf['ticker_interval'] = 1
|
||||||
|
conf['live'] = False
|
||||||
|
conf['datadir'] = None
|
||||||
|
conf['export'] = None
|
||||||
|
conf['timerange'] = '-100'
|
||||||
|
|
||||||
|
backtesting = Backtesting(conf)
|
||||||
|
backtesting.start()
|
||||||
|
# check the logs, that will contain the backtest result
|
||||||
|
exists = [
|
||||||
|
'Using local backtesting data (using whitelist in given config) ...',
|
||||||
|
'Using stake_currency: BTC ...',
|
||||||
|
'Using stake_amount: 0.001 ...',
|
||||||
|
'Measuring data from 2017-11-14T21:17:00+00:00 '
|
||||||
|
'up to 2017-11-14T22:59:00+00:00 (0 days)..'
|
||||||
|
]
|
||||||
|
for line in exists:
|
||||||
|
assert log_has(line, caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_backtest(default_conf) -> None:
|
||||||
|
"""
|
||||||
|
Test Backtesting.backtest() method
|
||||||
|
"""
|
||||||
|
backtesting = _BACKTESTING
|
||||||
|
|
||||||
|
data = optimize.load_data(None, ticker_interval=5, pairs=['BTC_ETH'])
|
||||||
|
data = trim_dictlist(data, -200)
|
||||||
|
results = backtesting.backtest(
|
||||||
|
{
|
||||||
|
'stake_amount': default_conf['stake_amount'],
|
||||||
|
'processed': backtesting.tickerdata_to_dataframe(data),
|
||||||
|
'max_open_trades': 10,
|
||||||
|
'realistic': True
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert not results.empty
|
||||||
|
|
||||||
|
|
||||||
|
def test_backtest_1min_ticker_interval(default_conf) -> None:
|
||||||
|
"""
|
||||||
|
Test Backtesting.backtest() method with 1 min ticker
|
||||||
|
"""
|
||||||
|
backtesting = _BACKTESTING
|
||||||
|
|
||||||
|
# Run a backtesting for an exiting 5min ticker_interval
|
||||||
|
data = optimize.load_data(None, ticker_interval=1, pairs=['BTC_UNITEST'])
|
||||||
|
data = trim_dictlist(data, -200)
|
||||||
|
results = backtesting.backtest(
|
||||||
|
{
|
||||||
|
'stake_amount': default_conf['stake_amount'],
|
||||||
|
'processed': backtesting.tickerdata_to_dataframe(data),
|
||||||
|
'max_open_trades': 1,
|
||||||
|
'realistic': True
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert not results.empty
|
||||||
|
|
||||||
|
|
||||||
|
def test_processed() -> None:
|
||||||
|
"""
|
||||||
|
Test Backtesting.backtest() method with offline data
|
||||||
|
"""
|
||||||
|
backtesting = _BACKTESTING
|
||||||
|
|
||||||
|
dict_of_tickerrows = load_data_test('raise')
|
||||||
|
dataframes = backtesting.tickerdata_to_dataframe(dict_of_tickerrows)
|
||||||
|
dataframe = dataframes['BTC_UNITEST']
|
||||||
|
cols = dataframe.columns
|
||||||
|
# assert the dataframe got some of the indicator columns
|
||||||
|
for col in ['close', 'high', 'low', 'open', 'date',
|
||||||
|
'ema50', 'ao', 'macd', 'plus_dm']:
|
||||||
|
assert col in cols
|
||||||
|
|
||||||
|
|
||||||
|
def test_backtest_pricecontours(default_conf) -> None:
|
||||||
|
tests = [['raise', 17], ['lower', 0], ['sine', 17]]
|
||||||
|
for [contour, numres] in tests:
|
||||||
|
simple_backtest(default_conf, contour, numres)
|
||||||
|
|
||||||
|
|
||||||
# Test backtest using offline data (testdata directory)
|
# Test backtest using offline data (testdata directory)
|
||||||
|
def test_backtest_ticks(default_conf):
|
||||||
|
|
||||||
def test_backtest_ticks(default_conf, mocker, default_strategy):
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
ticks = [1, 5]
|
ticks = [1, 5]
|
||||||
fun = default_strategy.populate_buy_trend
|
fun = _BACKTESTING.populate_buy_trend
|
||||||
for tick in ticks:
|
for tick in ticks:
|
||||||
backtest_conf = _make_backtest_conf(conf=default_conf)
|
backtest_conf = _make_backtest_conf(conf=default_conf)
|
||||||
results = _run_backtest_1(default_strategy, fun, backtest_conf)
|
results = _run_backtest_1(fun, backtest_conf)
|
||||||
assert not results.empty
|
assert not results.empty
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_clash_buy_sell(default_conf, mocker, default_strategy):
|
def test_backtest_clash_buy_sell(default_conf):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
|
|
||||||
# Override the default buy trend function in our default_strategy
|
# Override the default buy trend function in our default_strategy
|
||||||
def fun(dataframe=None):
|
def fun(dataframe=None):
|
||||||
buy_value = 1
|
buy_value = 1
|
||||||
@ -216,13 +505,11 @@ def test_backtest_clash_buy_sell(default_conf, mocker, default_strategy):
|
|||||||
return _trend(dataframe, buy_value, sell_value)
|
return _trend(dataframe, buy_value, sell_value)
|
||||||
|
|
||||||
backtest_conf = _make_backtest_conf(conf=default_conf)
|
backtest_conf = _make_backtest_conf(conf=default_conf)
|
||||||
results = _run_backtest_1(default_strategy, fun, backtest_conf)
|
results = _run_backtest_1(fun, backtest_conf)
|
||||||
assert results.empty
|
assert results.empty
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_only_sell(default_conf, mocker, default_strategy):
|
def test_backtest_only_sell(default_conf):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
|
|
||||||
# Override the default buy trend function in our default_strategy
|
# Override the default buy trend function in our default_strategy
|
||||||
def fun(dataframe=None):
|
def fun(dataframe=None):
|
||||||
buy_value = 0
|
buy_value = 0
|
||||||
@ -230,31 +517,29 @@ def test_backtest_only_sell(default_conf, mocker, default_strategy):
|
|||||||
return _trend(dataframe, buy_value, sell_value)
|
return _trend(dataframe, buy_value, sell_value)
|
||||||
|
|
||||||
backtest_conf = _make_backtest_conf(conf=default_conf)
|
backtest_conf = _make_backtest_conf(conf=default_conf)
|
||||||
results = _run_backtest_1(default_strategy, fun, backtest_conf)
|
results = _run_backtest_1(fun, backtest_conf)
|
||||||
assert results.empty
|
assert results.empty
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_alternate_buy_sell(default_conf, mocker, default_strategy):
|
def test_backtest_alternate_buy_sell(default_conf):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
backtest_conf = _make_backtest_conf(conf=default_conf, pair='BTC_UNITEST')
|
backtest_conf = _make_backtest_conf(conf=default_conf, pair='BTC_UNITEST')
|
||||||
results = _run_backtest_1(default_strategy, _trend_alternate,
|
results = _run_backtest_1(_trend_alternate, backtest_conf)
|
||||||
backtest_conf)
|
|
||||||
assert len(results) == 3
|
assert len(results) == 3
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_record(default_conf, mocker, default_strategy):
|
def test_backtest_record(default_conf, mocker):
|
||||||
names = []
|
names = []
|
||||||
records = []
|
records = []
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch(
|
||||||
mocker.patch('freqtrade.misc.file_dump_json',
|
'freqtrade.optimize.backtesting.file_dump_json',
|
||||||
new=lambda n, r: (names.append(n), records.append(r)))
|
new=lambda n, r: (names.append(n), records.append(r))
|
||||||
|
)
|
||||||
backtest_conf = _make_backtest_conf(
|
backtest_conf = _make_backtest_conf(
|
||||||
conf=default_conf,
|
conf=default_conf,
|
||||||
pair='BTC_UNITEST',
|
pair='BTC_UNITEST',
|
||||||
record="trades"
|
record="trades"
|
||||||
)
|
)
|
||||||
results = _run_backtest_1(default_strategy, _trend_alternate,
|
results = _run_backtest_1(_trend_alternate, backtest_conf)
|
||||||
backtest_conf)
|
|
||||||
assert len(results) == 3
|
assert len(results) == 3
|
||||||
# Assert file_dump_json was only called once
|
# Assert file_dump_json was only called once
|
||||||
assert names == ['backtest-result.json']
|
assert names == ['backtest-result.json']
|
||||||
@ -277,74 +562,48 @@ def test_backtest_record(default_conf, mocker, default_strategy):
|
|||||||
assert dur > 0
|
assert dur > 0
|
||||||
|
|
||||||
|
|
||||||
def test_processed(default_conf, mocker, default_strategy):
|
def test_backtest_start_live(default_conf, mocker, caplog):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
dict_of_tickerrows = load_data_test('raise')
|
|
||||||
dataframes = optimize.preprocess(dict_of_tickerrows)
|
|
||||||
dataframe = dataframes['BTC_UNITEST']
|
|
||||||
cols = dataframe.columns
|
|
||||||
# assert the dataframe got some of the indicator columns
|
|
||||||
for col in ['close', 'high', 'low', 'open', 'date',
|
|
||||||
'ema50', 'ao', 'macd', 'plus_dm']:
|
|
||||||
assert col in cols
|
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_pricecontours(default_conf, mocker, default_strategy):
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
tests = [['raise', 17], ['lower', 0], ['sine', 17]]
|
|
||||||
for [contour, numres] in tests:
|
|
||||||
simple_backtest(default_conf, contour, numres)
|
|
||||||
|
|
||||||
|
|
||||||
def mocked_load_data(datadir, pairs=[], ticker_interval=0, refresh_pairs=False, timerange=None):
|
|
||||||
tickerdata = optimize.load_tickerdata_file(datadir, 'BTC_UNITEST', 1, timerange=timerange)
|
|
||||||
pairdata = {'BTC_UNITEST': tickerdata}
|
|
||||||
return pairdata
|
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_start(default_conf, mocker, caplog):
|
|
||||||
caplog.set_level(logging.INFO)
|
|
||||||
default_conf['exchange']['pair_whitelist'] = ['BTC_UNITEST']
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
mocker.patch('freqtrade.misc.load_config', new=lambda s: default_conf)
|
|
||||||
mocker.patch.multiple('freqtrade.optimize',
|
|
||||||
load_data=mocked_load_data)
|
|
||||||
args = MagicMock()
|
|
||||||
args.ticker_interval = 1
|
|
||||||
args.level = 10
|
|
||||||
args.live = False
|
|
||||||
args.datadir = None
|
|
||||||
args.export = None
|
|
||||||
args.timerange = '-100' # needed due to MagicMock malleability
|
|
||||||
backtesting.start(args)
|
|
||||||
# check the logs, that will contain the backtest result
|
|
||||||
exists = ['Using max_open_trades: 1 ...',
|
|
||||||
'Using stake_amount: 0.001 ...',
|
|
||||||
'Measuring data from 2017-11-14T21:17:00+00:00 '
|
|
||||||
'up to 2017-11-14T22:59:00+00:00 (0 days)..']
|
|
||||||
for line in exists:
|
|
||||||
assert log_has(line, caplog.record_tuples)
|
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_start_live(default_strategy, default_conf, mocker, caplog):
|
|
||||||
caplog.set_level(logging.INFO)
|
|
||||||
default_conf['exchange']['pair_whitelist'] = ['BTC_UNITEST']
|
default_conf['exchange']['pair_whitelist'] = ['BTC_UNITEST']
|
||||||
mocker.patch('freqtrade.exchange.get_ticker_history',
|
mocker.patch('freqtrade.exchange.get_ticker_history',
|
||||||
new=lambda n, i: _load_pair_as_ticks(n, i))
|
new=lambda n, i: _load_pair_as_ticks(n, i))
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock())
|
||||||
mocker.patch('freqtrade.misc.load_config', new=lambda s: default_conf)
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', MagicMock())
|
||||||
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(default_conf)
|
||||||
|
))
|
||||||
|
|
||||||
args = MagicMock()
|
args = MagicMock()
|
||||||
args.ticker_interval = 1
|
args.ticker_interval = 1
|
||||||
args.level = 10
|
args.level = 10
|
||||||
args.live = True
|
args.live = True
|
||||||
args.datadir = None
|
args.datadir = None
|
||||||
args.export = None
|
args.export = None
|
||||||
|
args.strategy = 'default_strategy'
|
||||||
args.timerange = '-100' # needed due to MagicMock malleability
|
args.timerange = '-100' # needed due to MagicMock malleability
|
||||||
backtesting.start(args)
|
|
||||||
|
args = [
|
||||||
|
'--config', 'config.json',
|
||||||
|
'--strategy', 'default_strategy',
|
||||||
|
'backtesting',
|
||||||
|
'--ticker-interval', '1',
|
||||||
|
'--live',
|
||||||
|
'--timerange', '-100'
|
||||||
|
]
|
||||||
|
args = get_args(args)
|
||||||
|
start(args)
|
||||||
# check the logs, that will contain the backtest result
|
# check the logs, that will contain the backtest result
|
||||||
exists = ['Using max_open_trades: 1 ...',
|
exists = [
|
||||||
'Using stake_amount: 0.001 ...',
|
'Parameter -i/--ticker-interval detected ...',
|
||||||
'Measuring data from 2017-11-14T19:32:00+00:00 '
|
'Using ticker_interval: 1 ...',
|
||||||
'up to 2017-11-14T22:59:00+00:00 (0 days)..']
|
'Parameter -l/--live detected ...',
|
||||||
|
'Using max_open_trades: 1 ...',
|
||||||
|
'Parameter --timerange detected: -100 ..',
|
||||||
|
'Parameter --datadir detected: freqtrade/tests/testdata ...',
|
||||||
|
'Using stake_currency: BTC ...',
|
||||||
|
'Using stake_amount: 0.001 ...',
|
||||||
|
'Downloading data for all pairs in whitelist ...',
|
||||||
|
'Measuring data from 2017-11-14T19:32:00+00:00 up to 2017-11-14T22:59:00+00:00 (0 days)..'
|
||||||
|
]
|
||||||
|
|
||||||
for line in exists:
|
for line in exists:
|
||||||
assert log_has(line, caplog.record_tuples)
|
log_has(line, caplog.record_tuples)
|
||||||
|
@ -1,125 +1,141 @@
|
|||||||
# pragma pylint: disable=missing-docstring,W0212,C0103
|
# pragma pylint: disable=missing-docstring,W0212,C0103
|
||||||
import logging
|
import json
|
||||||
|
import os
|
||||||
|
from copy import deepcopy
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from freqtrade.optimize.hyperopt import calculate_loss, TARGET_TRADES, EXPECTED_MAX_PROFIT, start, \
|
from freqtrade.optimize.__init__ import load_tickerdata_file
|
||||||
log_results, save_trials, read_trials, generate_roi_table, has_space
|
from freqtrade.optimize.hyperopt import Hyperopt, start
|
||||||
from freqtrade.strategy.strategy import Strategy
|
from freqtrade.strategy.strategy import Strategy
|
||||||
import freqtrade.optimize.hyperopt as hyperopt
|
from freqtrade.tests.conftest import default_conf, log_has
|
||||||
|
from freqtrade.tests.optimize.test_backtesting import get_args
|
||||||
|
|
||||||
|
|
||||||
def test_loss_calculation_prefer_correct_trade_count():
|
# Avoid to reinit the same object again and again
|
||||||
correct = calculate_loss(1, TARGET_TRADES, 20)
|
_HYPEROPT = Hyperopt(default_conf())
|
||||||
over = calculate_loss(1, TARGET_TRADES + 100, 20)
|
|
||||||
under = calculate_loss(1, TARGET_TRADES - 100, 20)
|
|
||||||
assert over > correct
|
|
||||||
assert under > correct
|
|
||||||
|
|
||||||
|
|
||||||
def test_loss_calculation_prefer_shorter_trades():
|
# Functions for recurrent object patching
|
||||||
shorter = calculate_loss(1, 100, 20)
|
def create_trials(mocker) -> None:
|
||||||
longer = calculate_loss(1, 100, 30)
|
|
||||||
assert shorter < longer
|
|
||||||
|
|
||||||
|
|
||||||
def test_loss_calculation_has_limited_profit():
|
|
||||||
correct = calculate_loss(EXPECTED_MAX_PROFIT, TARGET_TRADES, 20)
|
|
||||||
over = calculate_loss(EXPECTED_MAX_PROFIT * 2, TARGET_TRADES, 20)
|
|
||||||
under = calculate_loss(EXPECTED_MAX_PROFIT / 2, TARGET_TRADES, 20)
|
|
||||||
assert over == correct
|
|
||||||
assert under > correct
|
|
||||||
|
|
||||||
|
|
||||||
def create_trials(mocker):
|
|
||||||
"""
|
"""
|
||||||
When creating trials, mock the hyperopt Trials so that *by default*
|
When creating trials, mock the hyperopt Trials so that *by default*
|
||||||
- we don't create any pickle'd files in the filesystem
|
- we don't create any pickle'd files in the filesystem
|
||||||
- we might have a pickle'd file so make sure that we return
|
- we might have a pickle'd file so make sure that we return
|
||||||
false when looking for it
|
false when looking for it
|
||||||
"""
|
"""
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.TRIALS_FILE',
|
_HYPEROPT.trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle')
|
||||||
return_value='freqtrade/tests/optimize/ut_trials.pickle')
|
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.os.path.exists',
|
mocker.patch('freqtrade.optimize.hyperopt.os.path.exists', return_value=False)
|
||||||
return_value=False)
|
mocker.patch('freqtrade.optimize.hyperopt.os.path.getsize', return_value=1)
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.save_trials',
|
mocker.patch('freqtrade.optimize.hyperopt.os.remove', return_value=True)
|
||||||
return_value=None)
|
mocker.patch('freqtrade.optimize.hyperopt.pickle.dump', return_value=None)
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.read_trials',
|
|
||||||
return_value=None)
|
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.os.remove',
|
|
||||||
return_value=True)
|
|
||||||
return mocker.Mock(
|
return mocker.Mock(
|
||||||
results=[{
|
results=[
|
||||||
'loss': 1,
|
{
|
||||||
'result': 'foo',
|
'loss': 1,
|
||||||
'status': 'ok'
|
'result': 'foo',
|
||||||
}],
|
'status': 'ok'
|
||||||
|
}
|
||||||
|
],
|
||||||
best_trial={'misc': {'vals': {'adx': 999}}}
|
best_trial={'misc': {'vals': {'adx': 999}}}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_start_calls_fmin(mocker):
|
# Unit tests
|
||||||
trials = create_trials(mocker)
|
def test_start(mocker, default_conf, caplog) -> None:
|
||||||
mocker.patch('freqtrade.optimize.tickerdata_to_dataframe')
|
"""
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.TRIALS', return_value=trials)
|
Test start() function
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.sorted',
|
"""
|
||||||
return_value=trials.results)
|
start_mock = MagicMock()
|
||||||
mocker.patch('freqtrade.optimize.preprocess')
|
mocker.patch('freqtrade.logger.Logger.set_format', MagicMock())
|
||||||
mocker.patch('freqtrade.optimize.load_data')
|
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock)
|
||||||
mock_fmin = mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(default_conf)
|
||||||
args = mocker.Mock(epochs=1, config='config.json.example', mongodb=False,
|
))
|
||||||
timerange=None, spaces='all')
|
args = [
|
||||||
Strategy().init({'strategy': 'default_strategy'})
|
'--config', 'config.json',
|
||||||
|
'--strategy', 'default_strategy',
|
||||||
|
'hyperopt',
|
||||||
|
'--epochs', '5'
|
||||||
|
]
|
||||||
|
args = get_args(args)
|
||||||
|
Strategy({'strategy': 'default_strategy'})
|
||||||
start(args)
|
start(args)
|
||||||
|
|
||||||
mock_fmin.assert_called_once()
|
import pprint
|
||||||
|
pprint.pprint(caplog.record_tuples)
|
||||||
|
|
||||||
|
assert log_has(
|
||||||
|
'Starting freqtrade in Hyperopt mode',
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
assert start_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_start_uses_mongotrials(mocker):
|
def test_loss_calculation_prefer_correct_trade_count() -> None:
|
||||||
mock_mongotrials = mocker.patch('freqtrade.optimize.hyperopt.MongoTrials',
|
"""
|
||||||
return_value=create_trials(mocker))
|
Test Hyperopt.calculate_loss()
|
||||||
mocker.patch('freqtrade.optimize.tickerdata_to_dataframe')
|
"""
|
||||||
mocker.patch('freqtrade.optimize.load_data')
|
hyperopt = _HYPEROPT
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
|
Strategy({'strategy': 'default_strategy'})
|
||||||
|
|
||||||
args = mocker.Mock(epochs=1, config='config.json.example', mongodb=True,
|
correct = hyperopt.calculate_loss(1, hyperopt.target_trades, 20)
|
||||||
timerange=None, spaces='all')
|
over = hyperopt.calculate_loss(1, hyperopt.target_trades + 100, 20)
|
||||||
Strategy().init({'strategy': 'default_strategy'})
|
under = hyperopt.calculate_loss(1, hyperopt.target_trades - 100, 20)
|
||||||
start(args)
|
assert over > correct
|
||||||
|
assert under > correct
|
||||||
mock_mongotrials.assert_called_once()
|
|
||||||
|
|
||||||
|
|
||||||
def test_log_results_if_loss_improves(mocker):
|
def test_loss_calculation_prefer_shorter_trades() -> None:
|
||||||
logger = mocker.patch('freqtrade.optimize.hyperopt.logger.info')
|
"""
|
||||||
global CURRENT_BEST_LOSS
|
Test Hyperopt.calculate_loss()
|
||||||
CURRENT_BEST_LOSS = 2
|
"""
|
||||||
log_results({
|
hyperopt = _HYPEROPT
|
||||||
'loss': 1,
|
|
||||||
'current_tries': 1,
|
|
||||||
'total_tries': 2,
|
|
||||||
'result': 'foo'
|
|
||||||
})
|
|
||||||
|
|
||||||
logger.assert_called_once()
|
shorter = hyperopt.calculate_loss(1, 100, 20)
|
||||||
|
longer = hyperopt.calculate_loss(1, 100, 30)
|
||||||
|
assert shorter < longer
|
||||||
|
|
||||||
|
|
||||||
def test_no_log_if_loss_does_not_improve(mocker):
|
def test_loss_calculation_has_limited_profit() -> None:
|
||||||
logger = mocker.patch('freqtrade.optimize.hyperopt.logger.info')
|
hyperopt = _HYPEROPT
|
||||||
global CURRENT_BEST_LOSS
|
|
||||||
CURRENT_BEST_LOSS = 2
|
|
||||||
log_results({
|
|
||||||
'loss': 3,
|
|
||||||
})
|
|
||||||
|
|
||||||
assert not logger.called
|
correct = hyperopt.calculate_loss(hyperopt.expected_max_profit, hyperopt.target_trades, 20)
|
||||||
|
over = hyperopt.calculate_loss(hyperopt.expected_max_profit * 2, hyperopt.target_trades, 20)
|
||||||
|
under = hyperopt.calculate_loss(hyperopt.expected_max_profit / 2, hyperopt.target_trades, 20)
|
||||||
|
assert over == correct
|
||||||
|
assert under > correct
|
||||||
|
|
||||||
|
|
||||||
def test_fmin_best_results(mocker, caplog):
|
def test_log_results_if_loss_improves(caplog) -> None:
|
||||||
caplog.set_level(logging.INFO)
|
hyperopt = _HYPEROPT
|
||||||
|
hyperopt.current_best_loss = 2
|
||||||
|
hyperopt.log_results(
|
||||||
|
{
|
||||||
|
'loss': 1,
|
||||||
|
'current_tries': 1,
|
||||||
|
'total_tries': 2,
|
||||||
|
'result': 'foo'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert log_has(' 1/2: foo. Loss 1.00000', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_log_if_loss_does_not_improve(caplog) -> None:
|
||||||
|
hyperopt = _HYPEROPT
|
||||||
|
hyperopt.current_best_loss = 2
|
||||||
|
hyperopt.log_results(
|
||||||
|
{
|
||||||
|
'loss': 3,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert caplog.record_tuples == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_fmin_best_results(mocker, default_conf, caplog) -> None:
|
||||||
fmin_result = {
|
fmin_result = {
|
||||||
"macd_below_zero": 0,
|
"macd_below_zero": 0,
|
||||||
"adx": 1,
|
"adx": 1,
|
||||||
@ -144,41 +160,67 @@ def test_fmin_best_results(mocker, caplog):
|
|||||||
"roi_p3": 3,
|
"roi_p3": 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.MongoTrials', return_value=create_trials(mocker))
|
conf = deepcopy(default_conf)
|
||||||
mocker.patch('freqtrade.optimize.tickerdata_to_dataframe')
|
conf.update({'config': 'config.json.example'})
|
||||||
mocker.patch('freqtrade.optimize.load_data')
|
conf.update({'epochs': 1})
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value=fmin_result)
|
conf.update({'timerange': None})
|
||||||
|
conf.update({'spaces': 'all'})
|
||||||
|
|
||||||
args = mocker.Mock(epochs=1, config='config.json.example',
|
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
||||||
timerange=None, spaces='all')
|
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value=fmin_result)
|
||||||
Strategy().init({'strategy': 'default_strategy'})
|
mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf', return_value=conf)
|
||||||
start(args)
|
mocker.patch('freqtrade.logger.Logger.set_format', MagicMock())
|
||||||
|
|
||||||
|
Strategy({'strategy': 'default_strategy'})
|
||||||
|
hyperopt = Hyperopt(conf)
|
||||||
|
hyperopt.trials = create_trials(mocker)
|
||||||
|
hyperopt.tickerdata_to_dataframe = MagicMock()
|
||||||
|
hyperopt.start()
|
||||||
|
|
||||||
exists = [
|
exists = [
|
||||||
'Best parameters',
|
'Best parameters:',
|
||||||
'"adx": {\n "enabled": true,\n "value": 15.0\n },',
|
'"adx": {\n "enabled": true,\n "value": 15.0\n },',
|
||||||
|
'"fastd": {\n "enabled": true,\n "value": 40.0\n },',
|
||||||
'"green_candle": {\n "enabled": true\n },',
|
'"green_candle": {\n "enabled": true\n },',
|
||||||
|
'"macd_below_zero": {\n "enabled": false\n },',
|
||||||
'"mfi": {\n "enabled": false\n },',
|
'"mfi": {\n "enabled": false\n },',
|
||||||
|
'"over_sar": {\n "enabled": false\n },',
|
||||||
|
'"roi_p1": 1.0,',
|
||||||
|
'"roi_p2": 2.0,',
|
||||||
|
'"roi_p3": 3.0,',
|
||||||
|
'"roi_t1": 1.0,',
|
||||||
|
'"roi_t2": 2.0,',
|
||||||
|
'"roi_t3": 3.0,',
|
||||||
|
'"rsi": {\n "enabled": true,\n "value": 37.0\n },',
|
||||||
|
'"stoploss": -0.1,',
|
||||||
'"trigger": {\n "type": "faststoch10"\n },',
|
'"trigger": {\n "type": "faststoch10"\n },',
|
||||||
'"stoploss": -0.1',
|
'"uptrend_long_ema": {\n "enabled": true\n },',
|
||||||
|
'"uptrend_short_ema": {\n "enabled": false\n },',
|
||||||
|
'"uptrend_sma": {\n "enabled": false\n }',
|
||||||
|
'ROI table:\n{0: 6.0, 3.0: 3.0, 5.0: 1.0, 6.0: 0}',
|
||||||
|
'Best Result:\nfoo'
|
||||||
]
|
]
|
||||||
|
|
||||||
for line in exists:
|
for line in exists:
|
||||||
assert line in caplog.text
|
assert line in caplog.text
|
||||||
|
|
||||||
|
|
||||||
def test_fmin_throw_value_error(mocker, caplog):
|
def test_fmin_throw_value_error(mocker, default_conf, caplog) -> None:
|
||||||
caplog.set_level(logging.INFO)
|
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
||||||
Strategy().init({'strategy': 'default_strategy'})
|
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.MongoTrials', return_value=create_trials(mocker))
|
|
||||||
mocker.patch('freqtrade.optimize.tickerdata_to_dataframe')
|
|
||||||
mocker.patch('freqtrade.optimize.load_data')
|
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.fmin', side_effect=ValueError())
|
mocker.patch('freqtrade.optimize.hyperopt.fmin', side_effect=ValueError())
|
||||||
|
|
||||||
args = mocker.Mock(epochs=1, config='config.json.example',
|
conf = deepcopy(default_conf)
|
||||||
timerange=None, spaces='all')
|
conf.update({'config': 'config.json.example'})
|
||||||
Strategy().init({'strategy': 'default_strategy'})
|
conf.update({'epochs': 1})
|
||||||
start(args)
|
conf.update({'timerange': None})
|
||||||
|
conf.update({'spaces': 'all'})
|
||||||
|
mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf', return_value=conf)
|
||||||
|
mocker.patch('freqtrade.logger.Logger.set_format', MagicMock())
|
||||||
|
Strategy({'strategy': 'default_strategy'})
|
||||||
|
hyperopt = Hyperopt(conf)
|
||||||
|
hyperopt.trials = create_trials(mocker)
|
||||||
|
hyperopt.tickerdata_to_dataframe = MagicMock()
|
||||||
|
|
||||||
|
hyperopt.start()
|
||||||
|
|
||||||
exists = [
|
exists = [
|
||||||
'Best Result:',
|
'Best Result:',
|
||||||
@ -190,69 +232,82 @@ def test_fmin_throw_value_error(mocker, caplog):
|
|||||||
assert line in caplog.text
|
assert line in caplog.text
|
||||||
|
|
||||||
|
|
||||||
def test_resuming_previous_hyperopt_results_succeeds(mocker):
|
def test_resuming_previous_hyperopt_results_succeeds(mocker, default_conf) -> None:
|
||||||
import freqtrade.optimize.hyperopt as hyperopt
|
|
||||||
trials = create_trials(mocker)
|
trials = create_trials(mocker)
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.TRIALS',
|
|
||||||
return_value=trials)
|
conf = deepcopy(default_conf)
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.os.path.exists',
|
conf.update({'config': 'config.json.example'})
|
||||||
return_value=True)
|
conf.update({'epochs': 1})
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.len',
|
conf.update({'mongodb': False})
|
||||||
return_value=len(trials.results))
|
conf.update({'timerange': None})
|
||||||
mock_read = mocker.patch('freqtrade.optimize.hyperopt.read_trials',
|
conf.update({'spaces': 'all'})
|
||||||
return_value=trials)
|
|
||||||
mock_save = mocker.patch('freqtrade.optimize.hyperopt.save_trials',
|
mocker.patch('freqtrade.optimize.hyperopt.os.path.exists', return_value=True)
|
||||||
return_value=None)
|
mocker.patch('freqtrade.optimize.hyperopt.len', return_value=len(trials.results))
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.sorted',
|
mock_read = mocker.patch(
|
||||||
return_value=trials.results)
|
'freqtrade.optimize.hyperopt.Hyperopt.read_trials',
|
||||||
mocker.patch('freqtrade.optimize.preprocess')
|
return_value=trials
|
||||||
mocker.patch('freqtrade.optimize.load_data')
|
)
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.fmin',
|
mock_save = mocker.patch(
|
||||||
return_value={})
|
'freqtrade.optimize.hyperopt.Hyperopt.save_trials',
|
||||||
args = mocker.Mock(epochs=1,
|
return_value=None
|
||||||
config='config.json.example',
|
)
|
||||||
mongodb=False,
|
mocker.patch('freqtrade.optimize.hyperopt.sorted', return_value=trials.results)
|
||||||
timerange=None,
|
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
||||||
spaces='all')
|
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
|
||||||
Strategy().init({'strategy': 'default_strategy'})
|
mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf', return_value=conf)
|
||||||
start(args)
|
mocker.patch('freqtrade.logger.Logger.set_format', MagicMock())
|
||||||
|
|
||||||
|
Strategy({'strategy': 'default_strategy'})
|
||||||
|
hyperopt = Hyperopt(conf)
|
||||||
|
hyperopt.trials = trials
|
||||||
|
hyperopt.tickerdata_to_dataframe = MagicMock()
|
||||||
|
|
||||||
|
hyperopt.start()
|
||||||
|
|
||||||
mock_read.assert_called_once()
|
mock_read.assert_called_once()
|
||||||
mock_save.assert_called_once()
|
mock_save.assert_called_once()
|
||||||
|
|
||||||
current_tries = hyperopt._CURRENT_TRIES
|
current_tries = hyperopt.current_tries
|
||||||
total_tries = hyperopt.TOTAL_TRIES
|
total_tries = hyperopt.total_tries
|
||||||
|
|
||||||
assert current_tries == len(trials.results)
|
assert current_tries == len(trials.results)
|
||||||
assert total_tries == (current_tries + len(trials.results))
|
assert total_tries == (current_tries + len(trials.results))
|
||||||
|
|
||||||
|
|
||||||
def test_save_trials_saves_trials(mocker):
|
def test_save_trials_saves_trials(mocker, caplog) -> None:
|
||||||
|
create_trials(mocker)
|
||||||
|
mock_dump = mocker.patch('freqtrade.optimize.hyperopt.pickle.dump', return_value=None)
|
||||||
|
|
||||||
|
hyperopt = _HYPEROPT
|
||||||
|
mocker.patch('freqtrade.optimize.hyperopt.open', return_value=hyperopt.trials_file)
|
||||||
|
|
||||||
|
hyperopt.save_trials()
|
||||||
|
|
||||||
|
assert log_has(
|
||||||
|
'Saving Trials to \'freqtrade/tests/optimize/ut_trials.pickle\'',
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
mock_dump.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_trials_returns_trials_file(mocker, caplog) -> None:
|
||||||
trials = create_trials(mocker)
|
trials = create_trials(mocker)
|
||||||
mock_dump = mocker.patch('freqtrade.optimize.hyperopt.pickle.dump',
|
mock_load = mocker.patch('freqtrade.optimize.hyperopt.pickle.load', return_value=trials)
|
||||||
return_value=None)
|
mock_open = mocker.patch('freqtrade.optimize.hyperopt.open', return_value=mock_load)
|
||||||
trials_path = mocker.patch('freqtrade.optimize.hyperopt.TRIALS_FILE',
|
|
||||||
return_value='ut_trials.pickle')
|
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.open',
|
|
||||||
return_value=trials_path)
|
|
||||||
save_trials(trials, trials_path)
|
|
||||||
|
|
||||||
mock_dump.assert_called_once_with(trials, trials_path)
|
hyperopt = _HYPEROPT
|
||||||
|
hyperopt_trial = hyperopt.read_trials()
|
||||||
|
assert log_has(
|
||||||
def test_read_trials_returns_trials_file(mocker):
|
'Reading Trials from \'freqtrade/tests/optimize/ut_trials.pickle\'',
|
||||||
trials = create_trials(mocker)
|
caplog.record_tuples
|
||||||
mock_load = mocker.patch('freqtrade.optimize.hyperopt.pickle.load',
|
)
|
||||||
return_value=trials)
|
assert hyperopt_trial == trials
|
||||||
mock_open = mocker.patch('freqtrade.optimize.hyperopt.open',
|
|
||||||
return_value=mock_load)
|
|
||||||
|
|
||||||
assert read_trials() == trials
|
|
||||||
mock_open.assert_called_once()
|
mock_open.assert_called_once()
|
||||||
mock_load.assert_called_once()
|
mock_load.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
def test_roi_table_generation():
|
def test_roi_table_generation() -> None:
|
||||||
params = {
|
params = {
|
||||||
'roi_t1': 5,
|
'roi_t1': 5,
|
||||||
'roi_t2': 10,
|
'roi_t2': 10,
|
||||||
@ -261,7 +316,54 @@ def test_roi_table_generation():
|
|||||||
'roi_p2': 2,
|
'roi_p2': 2,
|
||||||
'roi_p3': 3,
|
'roi_p3': 3,
|
||||||
}
|
}
|
||||||
assert generate_roi_table(params) == {0: 6, 15: 3, 25: 1, 30: 0}
|
|
||||||
|
hyperopt = _HYPEROPT
|
||||||
|
assert hyperopt.generate_roi_table(params) == {0: 6, 15: 3, 25: 1, 30: 0}
|
||||||
|
|
||||||
|
|
||||||
|
def test_start_calls_fmin(mocker, default_conf) -> None:
|
||||||
|
trials = create_trials(mocker)
|
||||||
|
mocker.patch('freqtrade.optimize.hyperopt.sorted', return_value=trials.results)
|
||||||
|
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
||||||
|
mock_fmin = mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
|
||||||
|
|
||||||
|
conf = deepcopy(default_conf)
|
||||||
|
conf.update({'config': 'config.json.example'})
|
||||||
|
conf.update({'epochs': 1})
|
||||||
|
conf.update({'mongodb': False})
|
||||||
|
conf.update({'timerange': None})
|
||||||
|
conf.update({'spaces': 'all'})
|
||||||
|
|
||||||
|
hyperopt = Hyperopt(conf)
|
||||||
|
hyperopt.trials = trials
|
||||||
|
hyperopt.tickerdata_to_dataframe = MagicMock()
|
||||||
|
|
||||||
|
hyperopt.start()
|
||||||
|
mock_fmin.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_start_uses_mongotrials(mocker, default_conf) -> None:
|
||||||
|
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
||||||
|
mock_fmin = mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
|
||||||
|
mock_mongotrials = mocker.patch(
|
||||||
|
'freqtrade.optimize.hyperopt.MongoTrials',
|
||||||
|
return_value=create_trials(mocker)
|
||||||
|
)
|
||||||
|
|
||||||
|
conf = deepcopy(default_conf)
|
||||||
|
conf.update({'config': 'config.json.example'})
|
||||||
|
conf.update({'epochs': 1})
|
||||||
|
conf.update({'mongodb': True})
|
||||||
|
conf.update({'timerange': None})
|
||||||
|
conf.update({'spaces': 'all'})
|
||||||
|
mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf', return_value=conf)
|
||||||
|
|
||||||
|
hyperopt = Hyperopt(conf)
|
||||||
|
hyperopt.tickerdata_to_dataframe = MagicMock()
|
||||||
|
|
||||||
|
hyperopt.start()
|
||||||
|
mock_mongotrials.assert_called_once()
|
||||||
|
mock_fmin.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
# test log_trials_result
|
# test log_trials_result
|
||||||
@ -269,26 +371,167 @@ def test_roi_table_generation():
|
|||||||
# test optimizer if 'ro_t1' in params
|
# test optimizer if 'ro_t1' in params
|
||||||
|
|
||||||
def test_format_results():
|
def test_format_results():
|
||||||
trades = [('BTC_ETH', 2, 2, 123),
|
"""
|
||||||
('BTC_LTC', 1, 1, 123),
|
Test Hyperopt.format_results()
|
||||||
('BTC_XRP', -1, -2, -246)]
|
"""
|
||||||
|
trades = [
|
||||||
|
('BTC_ETH', 2, 2, 123),
|
||||||
|
('BTC_LTC', 1, 1, 123),
|
||||||
|
('BTC_XRP', -1, -2, -246)
|
||||||
|
]
|
||||||
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration']
|
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration']
|
||||||
df = pd.DataFrame.from_records(trades, columns=labels)
|
df = pd.DataFrame.from_records(trades, columns=labels)
|
||||||
x = hyperopt.format_results(df)
|
x = Hyperopt.format_results(df)
|
||||||
assert x.find(' 66.67%')
|
assert x.find(' 66.67%')
|
||||||
|
|
||||||
|
|
||||||
def test_signal_handler(mocker):
|
def test_signal_handler(mocker):
|
||||||
|
"""
|
||||||
|
Test Hyperopt.signal_handler()
|
||||||
|
"""
|
||||||
m = MagicMock()
|
m = MagicMock()
|
||||||
mocker.patch('sys.exit', m)
|
mocker.patch('sys.exit', m)
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.save_trials', m)
|
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.save_trials', m)
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.log_trials_result', m)
|
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.log_trials_result', m)
|
||||||
|
|
||||||
|
hyperopt = _HYPEROPT
|
||||||
hyperopt.signal_handler(9, None)
|
hyperopt.signal_handler(9, None)
|
||||||
assert m.call_count == 3
|
assert m.call_count == 3
|
||||||
|
|
||||||
|
|
||||||
def test_has_space():
|
def test_has_space():
|
||||||
assert has_space(['buy', 'roi'], 'roi')
|
"""
|
||||||
assert has_space(['buy', 'roi'], 'buy')
|
Test Hyperopt.has_space() method
|
||||||
assert not has_space(['buy', 'roi'], 'stoploss')
|
"""
|
||||||
assert has_space(['all'], 'buy')
|
_HYPEROPT.config.update({'spaces': ['buy', 'roi']})
|
||||||
|
assert _HYPEROPT.has_space('roi')
|
||||||
|
assert _HYPEROPT.has_space('buy')
|
||||||
|
assert not _HYPEROPT.has_space('stoploss')
|
||||||
|
|
||||||
|
_HYPEROPT.config.update({'spaces': ['all']})
|
||||||
|
assert _HYPEROPT.has_space('buy')
|
||||||
|
|
||||||
|
|
||||||
|
def test_populate_indicators() -> None:
|
||||||
|
"""
|
||||||
|
Test Hyperopt.populate_indicators()
|
||||||
|
"""
|
||||||
|
tick = load_tickerdata_file(None, 'BTC_UNITEST', 1)
|
||||||
|
tickerlist = {'BTC_UNITEST': tick}
|
||||||
|
dataframes = _HYPEROPT.tickerdata_to_dataframe(tickerlist)
|
||||||
|
dataframe = _HYPEROPT.populate_indicators(dataframes['BTC_UNITEST'])
|
||||||
|
|
||||||
|
# Check if some indicators are generated. We will not test all of them
|
||||||
|
assert 'adx' in dataframe
|
||||||
|
assert 'ao' in dataframe
|
||||||
|
assert 'cci' in dataframe
|
||||||
|
|
||||||
|
|
||||||
|
def test_buy_strategy_generator() -> None:
|
||||||
|
"""
|
||||||
|
Test Hyperopt.buy_strategy_generator()
|
||||||
|
"""
|
||||||
|
tick = load_tickerdata_file(None, 'BTC_UNITEST', 1)
|
||||||
|
tickerlist = {'BTC_UNITEST': tick}
|
||||||
|
dataframes = _HYPEROPT.tickerdata_to_dataframe(tickerlist)
|
||||||
|
dataframe = _HYPEROPT.populate_indicators(dataframes['BTC_UNITEST'])
|
||||||
|
|
||||||
|
populate_buy_trend = _HYPEROPT.buy_strategy_generator(
|
||||||
|
{
|
||||||
|
'uptrend_long_ema': {
|
||||||
|
'enabled': True
|
||||||
|
},
|
||||||
|
'macd_below_zero': {
|
||||||
|
'enabled': True
|
||||||
|
},
|
||||||
|
'uptrend_short_ema': {
|
||||||
|
'enabled': True
|
||||||
|
},
|
||||||
|
'mfi': {
|
||||||
|
'enabled': True,
|
||||||
|
'value': 20
|
||||||
|
},
|
||||||
|
'fastd': {
|
||||||
|
'enabled': True,
|
||||||
|
'value': 20
|
||||||
|
},
|
||||||
|
'adx': {
|
||||||
|
'enabled': True,
|
||||||
|
'value': 20
|
||||||
|
},
|
||||||
|
'rsi': {
|
||||||
|
'enabled': True,
|
||||||
|
'value': 20
|
||||||
|
},
|
||||||
|
'over_sar': {
|
||||||
|
'enabled': True,
|
||||||
|
},
|
||||||
|
'green_candle': {
|
||||||
|
'enabled': True,
|
||||||
|
},
|
||||||
|
'uptrend_sma': {
|
||||||
|
'enabled': True,
|
||||||
|
},
|
||||||
|
|
||||||
|
'trigger': {
|
||||||
|
'type': 'lower_bb'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
result = populate_buy_trend(dataframe)
|
||||||
|
# Check if some indicators are generated. We will not test all of them
|
||||||
|
assert 'buy' in result
|
||||||
|
assert 1 in result['buy']
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_optimizer(mocker, default_conf) -> None:
|
||||||
|
"""
|
||||||
|
Test Hyperopt.generate_optimizer() function
|
||||||
|
"""
|
||||||
|
conf = deepcopy(default_conf)
|
||||||
|
conf.update({'config': 'config.json.example'})
|
||||||
|
conf.update({'timerange': None})
|
||||||
|
conf.update({'spaces': 'all'})
|
||||||
|
|
||||||
|
trades = [
|
||||||
|
('BTC_POWR', 0.023117, 0.000233, 100)
|
||||||
|
]
|
||||||
|
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration']
|
||||||
|
backtest_result = pd.DataFrame.from_records(trades, columns=labels)
|
||||||
|
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.optimize.hyperopt.Hyperopt.backtest',
|
||||||
|
MagicMock(return_value=backtest_result)
|
||||||
|
)
|
||||||
|
|
||||||
|
optimizer_param = {
|
||||||
|
'adx': {'enabled': False},
|
||||||
|
'fastd': {'enabled': True, 'value': 35.0},
|
||||||
|
'green_candle': {'enabled': True},
|
||||||
|
'macd_below_zero': {'enabled': True},
|
||||||
|
'mfi': {'enabled': False},
|
||||||
|
'over_sar': {'enabled': False},
|
||||||
|
'roi_p1': 0.01,
|
||||||
|
'roi_p2': 0.01,
|
||||||
|
'roi_p3': 0.1,
|
||||||
|
'roi_t1': 60.0,
|
||||||
|
'roi_t2': 30.0,
|
||||||
|
'roi_t3': 20.0,
|
||||||
|
'rsi': {'enabled': False},
|
||||||
|
'stoploss': -0.4,
|
||||||
|
'trigger': {'type': 'macd_cross_signal'},
|
||||||
|
'uptrend_long_ema': {'enabled': False},
|
||||||
|
'uptrend_short_ema': {'enabled': True},
|
||||||
|
'uptrend_sma': {'enabled': True}
|
||||||
|
}
|
||||||
|
|
||||||
|
response_expected = {
|
||||||
|
'loss': 1.9840569076926293,
|
||||||
|
'result': ' 1 trades. Avg profit 2.31%. Total profit 0.00023300 BTC '
|
||||||
|
'(0.0231Σ%). Avg duration 100.0 mins.',
|
||||||
|
'status': 'ok'
|
||||||
|
}
|
||||||
|
|
||||||
|
hyperopt = Hyperopt(conf)
|
||||||
|
generate_optimizer_value = hyperopt.generate_optimizer(optimizer_param)
|
||||||
|
assert generate_optimizer_value == response_expected
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
# pragma pylint: disable=missing-docstring, protected-access, C0103
|
# pragma pylint: disable=missing-docstring, protected-access, C0103
|
||||||
|
|
||||||
import os
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
from shutil import copyfile
|
from shutil import copyfile
|
||||||
from freqtrade import exchange, optimize
|
|
||||||
from freqtrade.exchange import Bittrex
|
from freqtrade import optimize
|
||||||
from freqtrade.optimize.__init__ import make_testdata_path, download_pairs,\
|
from freqtrade.misc import file_dump_json
|
||||||
download_backtesting_testdata, load_tickerdata_file, trim_tickerlist, file_dump_json
|
from freqtrade.optimize.__init__ import make_testdata_path, download_pairs, \
|
||||||
|
download_backtesting_testdata, load_tickerdata_file, trim_tickerlist
|
||||||
from freqtrade.tests.conftest import log_has
|
from freqtrade.tests.conftest import log_has
|
||||||
from freqtrade.strategy.strategy import Strategy
|
|
||||||
|
|
||||||
# Change this if modifying BTC_UNITEST testdatafile
|
# Change this if modifying BTC_UNITEST testdatafile
|
||||||
_BTC_UNITTEST_LENGTH = 13681
|
_BTC_UNITTEST_LENGTH = 13681
|
||||||
@ -47,12 +46,11 @@ def _clean_test_file(file: str) -> None:
|
|||||||
os.rename(file_swp, file)
|
os.rename(file_swp, file)
|
||||||
|
|
||||||
|
|
||||||
def test_load_data_30min_ticker(default_conf, ticker_history, mocker, caplog):
|
def test_load_data_30min_ticker(ticker_history, mocker, caplog) -> None:
|
||||||
caplog.set_level(logging.INFO)
|
"""
|
||||||
|
Test load_data() with 30 min ticker
|
||||||
|
"""
|
||||||
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
|
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
|
|
||||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
|
||||||
|
|
||||||
file = 'freqtrade/tests/testdata/BTC_UNITTEST-30.json'
|
file = 'freqtrade/tests/testdata/BTC_UNITTEST-30.json'
|
||||||
_backup_file(file, copy_file=True)
|
_backup_file(file, copy_file=True)
|
||||||
@ -62,12 +60,11 @@ def test_load_data_30min_ticker(default_conf, ticker_history, mocker, caplog):
|
|||||||
_clean_test_file(file)
|
_clean_test_file(file)
|
||||||
|
|
||||||
|
|
||||||
def test_load_data_5min_ticker(default_conf, ticker_history, mocker, caplog):
|
def test_load_data_5min_ticker(ticker_history, mocker, caplog) -> None:
|
||||||
caplog.set_level(logging.INFO)
|
"""
|
||||||
|
Test load_data() with 5 min ticker
|
||||||
|
"""
|
||||||
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
|
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
|
|
||||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
|
||||||
|
|
||||||
file = 'freqtrade/tests/testdata/BTC_ETH-5.json'
|
file = 'freqtrade/tests/testdata/BTC_ETH-5.json'
|
||||||
_backup_file(file, copy_file=True)
|
_backup_file(file, copy_file=True)
|
||||||
@ -77,12 +74,11 @@ def test_load_data_5min_ticker(default_conf, ticker_history, mocker, caplog):
|
|||||||
_clean_test_file(file)
|
_clean_test_file(file)
|
||||||
|
|
||||||
|
|
||||||
def test_load_data_1min_ticker(default_conf, ticker_history, mocker, caplog):
|
def test_load_data_1min_ticker(ticker_history, mocker, caplog) -> None:
|
||||||
caplog.set_level(logging.INFO)
|
"""
|
||||||
|
Test load_data() with 1 min ticker
|
||||||
|
"""
|
||||||
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
|
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
|
|
||||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
|
||||||
|
|
||||||
file = 'freqtrade/tests/testdata/BTC_ETH-1.json'
|
file = 'freqtrade/tests/testdata/BTC_ETH-1.json'
|
||||||
_backup_file(file, copy_file=True)
|
_backup_file(file, copy_file=True)
|
||||||
@ -92,12 +88,11 @@ def test_load_data_1min_ticker(default_conf, ticker_history, mocker, caplog):
|
|||||||
_clean_test_file(file)
|
_clean_test_file(file)
|
||||||
|
|
||||||
|
|
||||||
def test_load_data_with_new_pair_1min(default_conf, ticker_history, mocker, caplog):
|
def test_load_data_with_new_pair_1min(ticker_history, mocker, caplog) -> None:
|
||||||
caplog.set_level(logging.INFO)
|
"""
|
||||||
|
Test load_data() with 1 min ticker
|
||||||
|
"""
|
||||||
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
|
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
|
|
||||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
|
||||||
|
|
||||||
file = 'freqtrade/tests/testdata/BTC_MEME-1.json'
|
file = 'freqtrade/tests/testdata/BTC_MEME-1.json'
|
||||||
_backup_file(file)
|
_backup_file(file)
|
||||||
@ -107,14 +102,12 @@ def test_load_data_with_new_pair_1min(default_conf, ticker_history, mocker, capl
|
|||||||
_clean_test_file(file)
|
_clean_test_file(file)
|
||||||
|
|
||||||
|
|
||||||
def test_testdata_path():
|
def test_testdata_path() -> None:
|
||||||
assert os.path.join('freqtrade', 'tests', 'testdata') in make_testdata_path(None)
|
assert os.path.join('freqtrade', 'tests', 'testdata') in make_testdata_path(None)
|
||||||
|
|
||||||
|
|
||||||
def test_download_pairs(default_conf, ticker_history, mocker):
|
def test_download_pairs(ticker_history, mocker) -> None:
|
||||||
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history)
|
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history)
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
|
||||||
|
|
||||||
file1_1 = 'freqtrade/tests/testdata/BTC_MEME-1.json'
|
file1_1 = 'freqtrade/tests/testdata/BTC_MEME-1.json'
|
||||||
file1_5 = 'freqtrade/tests/testdata/BTC_MEME-5.json'
|
file1_5 = 'freqtrade/tests/testdata/BTC_MEME-5.json'
|
||||||
@ -151,13 +144,10 @@ def test_download_pairs(default_conf, ticker_history, mocker):
|
|||||||
_clean_test_file(file2_5)
|
_clean_test_file(file2_5)
|
||||||
|
|
||||||
|
|
||||||
def test_download_pairs_exception(default_conf, ticker_history, mocker, caplog):
|
def test_download_pairs_exception(ticker_history, mocker, caplog) -> None:
|
||||||
caplog.set_level(logging.INFO)
|
|
||||||
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history)
|
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history)
|
||||||
mocker.patch('freqtrade.optimize.__init__.download_backtesting_testdata',
|
mocker.patch('freqtrade.optimize.__init__.download_backtesting_testdata',
|
||||||
side_effect=BaseException('File Error'))
|
side_effect=BaseException('File Error'))
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
|
||||||
|
|
||||||
file1_1 = 'freqtrade/tests/testdata/BTC_MEME-1.json'
|
file1_1 = 'freqtrade/tests/testdata/BTC_MEME-1.json'
|
||||||
file1_5 = 'freqtrade/tests/testdata/BTC_MEME-5.json'
|
file1_5 = 'freqtrade/tests/testdata/BTC_MEME-5.json'
|
||||||
@ -171,10 +161,8 @@ def test_download_pairs_exception(default_conf, ticker_history, mocker, caplog):
|
|||||||
assert log_has('Failed to download the pair: "BTC-MEME", Interval: 1 min', caplog.record_tuples)
|
assert log_has('Failed to download the pair: "BTC-MEME", Interval: 1 min', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
def test_download_backtesting_testdata(default_conf, ticker_history, mocker):
|
def test_download_backtesting_testdata(ticker_history, mocker) -> None:
|
||||||
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history)
|
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history)
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
|
||||||
|
|
||||||
# Download a 1 min ticker file
|
# Download a 1 min ticker file
|
||||||
file1 = 'freqtrade/tests/testdata/BTC_XEL-1.json'
|
file1 = 'freqtrade/tests/testdata/BTC_XEL-1.json'
|
||||||
@ -192,7 +180,7 @@ def test_download_backtesting_testdata(default_conf, ticker_history, mocker):
|
|||||||
_clean_test_file(file2)
|
_clean_test_file(file2)
|
||||||
|
|
||||||
|
|
||||||
def test_download_backtesting_testdata2(mocker):
|
def test_download_backtesting_testdata2(mocker) -> None:
|
||||||
tick = [{'T': 'bar'}, {'T': 'foo'}]
|
tick = [{'T': 'bar'}, {'T': 'foo'}]
|
||||||
mocker.patch('freqtrade.misc.file_dump_json', return_value=None)
|
mocker.patch('freqtrade.misc.file_dump_json', return_value=None)
|
||||||
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=tick)
|
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=tick)
|
||||||
@ -200,7 +188,7 @@ def test_download_backtesting_testdata2(mocker):
|
|||||||
assert download_backtesting_testdata(None, pair="BTC-UNITEST", interval=3)
|
assert download_backtesting_testdata(None, pair="BTC-UNITEST", interval=3)
|
||||||
|
|
||||||
|
|
||||||
def test_load_tickerdata_file():
|
def test_load_tickerdata_file() -> None:
|
||||||
# 7 does not exist in either format.
|
# 7 does not exist in either format.
|
||||||
assert not load_tickerdata_file(None, 'BTC_UNITEST', 7)
|
assert not load_tickerdata_file(None, 'BTC_UNITEST', 7)
|
||||||
# 1 exists only as a .json
|
# 1 exists only as a .json
|
||||||
@ -211,23 +199,18 @@ def test_load_tickerdata_file():
|
|||||||
assert _BTC_UNITTEST_LENGTH == len(tickerdata)
|
assert _BTC_UNITTEST_LENGTH == len(tickerdata)
|
||||||
|
|
||||||
|
|
||||||
def test_init(default_conf, mocker):
|
def test_init(default_conf, mocker) -> None:
|
||||||
conf = {'exchange': {'pair_whitelist': []}}
|
conf = {'exchange': {'pair_whitelist': []}}
|
||||||
mocker.patch('freqtrade.optimize.hyperopt_optimize_conf', return_value=conf)
|
mocker.patch('freqtrade.optimize.hyperopt_optimize_conf', return_value=conf)
|
||||||
assert {} == optimize.load_data('', pairs=[], refresh_pairs=True,
|
assert {} == optimize.load_data(
|
||||||
ticker_interval=int(default_conf['ticker_interval']))
|
'',
|
||||||
|
pairs=[],
|
||||||
|
refresh_pairs=True,
|
||||||
|
ticker_interval=int(default_conf['ticker_interval'])
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_tickerdata_to_dataframe():
|
def test_trim_tickerlist() -> None:
|
||||||
Strategy().init({'strategy': 'default_strategy'})
|
|
||||||
timerange = ((None, 'line'), None, -100)
|
|
||||||
tick = load_tickerdata_file(None, 'BTC_UNITEST', 1, timerange=timerange)
|
|
||||||
tickerlist = {'BTC_UNITEST': tick}
|
|
||||||
data = optimize.tickerdata_to_dataframe(tickerlist)
|
|
||||||
assert len(data['BTC_UNITEST']) == 100
|
|
||||||
|
|
||||||
|
|
||||||
def test_trim_tickerlist():
|
|
||||||
with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file:
|
with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file:
|
||||||
ticker_list = json.load(data_file)
|
ticker_list = json.load(data_file)
|
||||||
ticker_list_len = len(ticker_list)
|
ticker_list_len = len(ticker_list)
|
||||||
@ -272,7 +255,11 @@ def test_trim_tickerlist():
|
|||||||
assert ticker_list_len == ticker_len
|
assert ticker_list_len == ticker_len
|
||||||
|
|
||||||
|
|
||||||
def test_file_dump_json():
|
def test_file_dump_json() -> None:
|
||||||
|
"""
|
||||||
|
Test file_dump_json()
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
file = 'freqtrade/tests/testdata/test_{id}.json'.format(id=str(uuid.uuid4()))
|
file = 'freqtrade/tests/testdata/test_{id}.json'.format(id=str(uuid.uuid4()))
|
||||||
data = {'bar': 'foo'}
|
data = {'bar': 'foo'}
|
||||||
|
|
||||||
|
@ -1,203 +1,133 @@
|
|||||||
# pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, C0103
|
# pragma pylint: disable=invalid-sequence-index, invalid-name, too-many-arguments
|
||||||
|
|
||||||
|
"""
|
||||||
|
Unit test file for rpc/rpc.py
|
||||||
|
"""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from copy import deepcopy
|
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
|
|
||||||
from freqtrade.rpc import init, cleanup, send_msg
|
from freqtrade.freqtradebot import FreqtradeBot
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
import freqtrade.main as main
|
from freqtrade.rpc.rpc import RPC
|
||||||
import freqtrade.misc as misc
|
from freqtrade.state import State
|
||||||
import freqtrade.rpc as rpc
|
from freqtrade.tests.test_freqtradebot import patch_get_signal, patch_coinmarketcap
|
||||||
|
|
||||||
|
|
||||||
def prec_satoshi(a, b):
|
# Functions for recurrent object patching
|
||||||
|
def prec_satoshi(a, b) -> float:
|
||||||
"""
|
"""
|
||||||
:return: True if A and B differs less than one satoshi.
|
:return: True if A and B differs less than one satoshi.
|
||||||
"""
|
"""
|
||||||
return abs(a - b) < 0.00000001
|
return abs(a - b) < 0.00000001
|
||||||
|
|
||||||
|
|
||||||
def test_init_telegram_enabled(default_conf, mocker):
|
# Unit tests
|
||||||
module_list = []
|
def test_rpc_trade_status(default_conf, ticker, mocker) -> None:
|
||||||
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', module_list)
|
"""
|
||||||
telegram_mock = mocker.patch('freqtrade.rpc.telegram.init', MagicMock())
|
Test rpc_trade_status() method
|
||||||
|
"""
|
||||||
|
patch_get_signal(mocker, (True, False))
|
||||||
|
patch_coinmarketcap(mocker)
|
||||||
|
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.freqtradebot.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=ticker
|
||||||
|
)
|
||||||
|
|
||||||
init(default_conf)
|
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||||
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
assert telegram_mock.call_count == 1
|
freqtradebot.update_state(State.STOPPED)
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_forcesell(default_conf, update, ticker, mocker):
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
|
||||||
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
|
||||||
mocker.patch.multiple('freqtrade.rpc.telegram',
|
|
||||||
_CONF=default_conf,
|
|
||||||
init=MagicMock())
|
|
||||||
cancel_order_mock = MagicMock()
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
|
||||||
cancel_order=cancel_order_mock,
|
|
||||||
get_order=MagicMock(return_value={
|
|
||||||
'closed': True,
|
|
||||||
'type': 'LIMIT_BUY',
|
|
||||||
}))
|
|
||||||
main.init(default_conf, create_engine('sqlite://'))
|
|
||||||
|
|
||||||
misc.update_state(misc.State.STOPPED)
|
|
||||||
(error, res) = rpc.rpc_forcesell(None)
|
|
||||||
assert error
|
|
||||||
assert res == '`trader is not running`'
|
|
||||||
misc.update_state(misc.State.RUNNING)
|
|
||||||
(error, res) = rpc.rpc_forcesell(None)
|
|
||||||
assert error
|
|
||||||
assert res == 'Invalid argument.'
|
|
||||||
|
|
||||||
(error, res) = rpc.rpc_forcesell('all')
|
|
||||||
assert not error
|
|
||||||
assert res == ''
|
|
||||||
|
|
||||||
main.create_trade(0.001, 5)
|
|
||||||
(error, res) = rpc.rpc_forcesell('all')
|
|
||||||
assert not error
|
|
||||||
assert res == ''
|
|
||||||
|
|
||||||
(error, res) = rpc.rpc_forcesell('1')
|
|
||||||
assert not error
|
|
||||||
assert res == ''
|
|
||||||
|
|
||||||
misc.update_state(misc.State.STOPPED)
|
|
||||||
|
|
||||||
(error, res) = rpc.rpc_forcesell(None)
|
|
||||||
assert error
|
|
||||||
assert res == '`trader is not running`'
|
|
||||||
|
|
||||||
(error, res) = rpc.rpc_forcesell('all')
|
|
||||||
assert error
|
|
||||||
assert res == '`trader is not running`'
|
|
||||||
|
|
||||||
misc.update_state(misc.State.RUNNING)
|
|
||||||
|
|
||||||
assert cancel_order_mock.call_count == 0
|
|
||||||
# make an limit-buy open trade
|
|
||||||
mocker.patch.multiple('freqtrade.exchange',
|
|
||||||
get_order=MagicMock(return_value={
|
|
||||||
'closed': None,
|
|
||||||
'type': 'LIMIT_BUY'
|
|
||||||
}))
|
|
||||||
# check that the trade is called, which is done
|
|
||||||
# by ensuring exchange.cancel_order is called
|
|
||||||
(error, res) = rpc.rpc_forcesell('1')
|
|
||||||
assert not error
|
|
||||||
assert res == ''
|
|
||||||
assert cancel_order_mock.call_count == 1
|
|
||||||
|
|
||||||
main.create_trade(0.001, 5)
|
|
||||||
# make an limit-sell open trade
|
|
||||||
mocker.patch.multiple('freqtrade.exchange',
|
|
||||||
get_order=MagicMock(return_value={
|
|
||||||
'closed': None,
|
|
||||||
'type': 'LIMIT_SELL'
|
|
||||||
}))
|
|
||||||
(error, res) = rpc.rpc_forcesell('2')
|
|
||||||
assert not error
|
|
||||||
assert res == ''
|
|
||||||
# status quo, no exchange calls
|
|
||||||
assert cancel_order_mock.call_count == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_trade_status(default_conf, update, ticker, mocker):
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
|
||||||
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
|
||||||
mocker.patch.multiple('freqtrade.rpc.telegram',
|
|
||||||
_CONF=default_conf,
|
|
||||||
init=MagicMock())
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker)
|
|
||||||
main.init(default_conf, create_engine('sqlite://'))
|
|
||||||
|
|
||||||
misc.update_state(misc.State.STOPPED)
|
|
||||||
(error, result) = rpc.rpc_trade_status()
|
(error, result) = rpc.rpc_trade_status()
|
||||||
assert error
|
assert error
|
||||||
assert result.find('trader is not running') >= 0
|
assert 'trader is not running' in result
|
||||||
|
|
||||||
misc.update_state(misc.State.RUNNING)
|
freqtradebot.update_state(State.RUNNING)
|
||||||
(error, result) = rpc.rpc_trade_status()
|
(error, result) = rpc.rpc_trade_status()
|
||||||
assert error
|
assert error
|
||||||
assert result.find('no active trade') >= 0
|
assert 'no active trade' in result
|
||||||
|
|
||||||
main.create_trade(0.001, 5)
|
freqtradebot.create_trade()
|
||||||
(error, result) = rpc.rpc_trade_status()
|
(error, result) = rpc.rpc_trade_status()
|
||||||
assert not error
|
assert not error
|
||||||
trade = result[0]
|
trade = result[0]
|
||||||
|
|
||||||
|
result_message = [
|
||||||
|
'*Trade ID:* `1`\n'
|
||||||
|
'*Current Pair:* '
|
||||||
|
'[BTC_ETH](https://www.bittrex.com/Market/Index?MarketName=BTC-ETH)\n'
|
||||||
|
'*Open Since:* `just now`\n'
|
||||||
|
'*Amount:* `90.99181074`\n'
|
||||||
|
'*Open Rate:* `0.00001099`\n'
|
||||||
|
'*Close Rate:* `None`\n'
|
||||||
|
'*Current Rate:* `0.00001098`\n'
|
||||||
|
'*Close Profit:* `None`\n'
|
||||||
|
'*Current Profit:* `-0.59%`\n'
|
||||||
|
'*Open Order:* `(LIMIT_BUY rem=0.00000000)`'
|
||||||
|
]
|
||||||
|
assert result == result_message
|
||||||
assert trade.find('[BTC_ETH]') >= 0
|
assert trade.find('[BTC_ETH]') >= 0
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_daily_profit(default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker):
|
def test_rpc_status_table(default_conf, ticker, mocker) -> None:
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
"""
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
Test rpc_status_table() method
|
||||||
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
"""
|
||||||
mocker.patch.multiple('freqtrade.rpc.telegram',
|
patch_get_signal(mocker, (True, False))
|
||||||
_CONF=default_conf,
|
patch_coinmarketcap(mocker)
|
||||||
init=MagicMock())
|
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple(
|
||||||
validate_pairs=MagicMock(),
|
'freqtrade.freqtradebot.exchange',
|
||||||
get_ticker=ticker)
|
validate_pairs=MagicMock(),
|
||||||
mocker.patch.multiple('freqtrade.fiat_convert.Market',
|
get_ticker=ticker
|
||||||
ticker=MagicMock(return_value={'price_usd': 15000.0}))
|
)
|
||||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
|
||||||
main.init(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||||
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
|
freqtradebot.update_state(State.STOPPED)
|
||||||
|
(error, result) = rpc.rpc_status_table()
|
||||||
|
assert error
|
||||||
|
assert '*Status:* `trader is not running`' in result
|
||||||
|
|
||||||
|
freqtradebot.update_state(State.RUNNING)
|
||||||
|
(error, result) = rpc.rpc_status_table()
|
||||||
|
assert error
|
||||||
|
assert '*Status:* `no active order`' in result
|
||||||
|
|
||||||
|
freqtradebot.create_trade()
|
||||||
|
(error, result) = rpc.rpc_status_table()
|
||||||
|
assert 'just now' in result['Since'].all()
|
||||||
|
assert 'BTC_ETH' in result['Pair'].all()
|
||||||
|
assert '-0.59%' in result['Profit'].all()
|
||||||
|
|
||||||
|
|
||||||
|
def test_rpc_daily_profit(default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker)\
|
||||||
|
-> None:
|
||||||
|
"""
|
||||||
|
Test rpc_daily_profit() method
|
||||||
|
"""
|
||||||
|
patch_get_signal(mocker, (True, False))
|
||||||
|
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
||||||
|
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.freqtradebot.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=ticker
|
||||||
|
)
|
||||||
|
|
||||||
|
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||||
stake_currency = default_conf['stake_currency']
|
stake_currency = default_conf['stake_currency']
|
||||||
fiat_display_currency = default_conf['fiat_display_currency']
|
fiat_display_currency = default_conf['fiat_display_currency']
|
||||||
|
|
||||||
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
main.create_trade(0.001, 5)
|
freqtradebot.create_trade()
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
assert trade
|
assert trade
|
||||||
|
|
||||||
@ -209,8 +139,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, limit_buy_order, limit_s
|
|||||||
|
|
||||||
# Try valid data
|
# Try valid data
|
||||||
update.message.text = '/daily 2'
|
update.message.text = '/daily 2'
|
||||||
(error, days) = rpc.rpc_daily_profit(7, stake_currency,
|
(error, days) = rpc.rpc_daily_profit(7, stake_currency, fiat_display_currency)
|
||||||
fiat_display_currency)
|
|
||||||
assert not error
|
assert not error
|
||||||
assert len(days) == 7
|
assert len(days) == 7
|
||||||
for day in days:
|
for day in days:
|
||||||
@ -224,50 +153,56 @@ def test_rpc_daily_profit(default_conf, update, ticker, limit_buy_order, limit_s
|
|||||||
assert str(days[0][0]) == str(datetime.utcnow().date())
|
assert str(days[0][0]) == str(datetime.utcnow().date())
|
||||||
|
|
||||||
# Try invalid data
|
# Try invalid data
|
||||||
(error, days) = rpc.rpc_daily_profit(0, stake_currency,
|
(error, days) = rpc.rpc_daily_profit(0, stake_currency, fiat_display_currency)
|
||||||
fiat_display_currency)
|
|
||||||
assert error
|
assert error
|
||||||
assert days.find('must be an integer greater than 0') >= 0
|
assert days.find('must be an integer greater than 0') >= 0
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_trade_statistics(
|
def test_rpc_trade_statistics(
|
||||||
default_conf, update, ticker, ticker_sell_up, limit_buy_order, limit_sell_order, mocker):
|
default_conf, ticker, ticker_sell_up, limit_buy_order, limit_sell_order, mocker) -> None:
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
"""
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
Test rpc_trade_statistics() method
|
||||||
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
"""
|
||||||
mocker.patch.multiple('freqtrade.rpc.telegram',
|
patch_get_signal(mocker, (True, False))
|
||||||
_CONF=default_conf,
|
mocker.patch.multiple(
|
||||||
init=MagicMock())
|
'freqtrade.fiat_convert.Market',
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||||
validate_pairs=MagicMock(),
|
)
|
||||||
get_ticker=ticker)
|
|
||||||
mocker.patch.multiple('freqtrade.fiat_convert.Market',
|
|
||||||
ticker=MagicMock(return_value={'price_usd': 15000.0}))
|
|
||||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||||
main.init(default_conf, create_engine('sqlite://'))
|
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.freqtradebot.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=ticker
|
||||||
|
)
|
||||||
|
|
||||||
|
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||||
stake_currency = default_conf['stake_currency']
|
stake_currency = default_conf['stake_currency']
|
||||||
fiat_display_currency = default_conf['fiat_display_currency']
|
fiat_display_currency = default_conf['fiat_display_currency']
|
||||||
|
|
||||||
(error, stats) = rpc.rpc_trade_statistics(stake_currency,
|
rpc = RPC(freqtradebot)
|
||||||
fiat_display_currency)
|
|
||||||
|
(error, stats) = rpc.rpc_trade_statistics(stake_currency, fiat_display_currency)
|
||||||
assert error
|
assert error
|
||||||
assert stats.find('no closed trade') >= 0
|
assert stats.find('no closed trade') >= 0
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
main.create_trade(0.001, 5)
|
freqtradebot.create_trade()
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
# Simulate fulfilled LIMIT_BUY order for trade
|
# Simulate fulfilled LIMIT_BUY order for trade
|
||||||
trade.update(limit_buy_order)
|
trade.update(limit_buy_order)
|
||||||
|
|
||||||
# Update the ticker with a market going up
|
# Update the ticker with a market going up
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple(
|
||||||
validate_pairs=MagicMock(),
|
'freqtrade.freqtradebot.exchange',
|
||||||
get_ticker=ticker_sell_up)
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=ticker_sell_up
|
||||||
|
)
|
||||||
trade.update(limit_sell_order)
|
trade.update(limit_sell_order)
|
||||||
trade.close_date = datetime.utcnow()
|
trade.close_date = datetime.utcnow()
|
||||||
trade.is_open = False
|
trade.is_open = False
|
||||||
|
|
||||||
(error, stats) = rpc.rpc_trade_statistics(stake_currency,
|
(error, stats) = rpc.rpc_trade_statistics(stake_currency, fiat_display_currency)
|
||||||
fiat_display_currency)
|
|
||||||
assert not error
|
assert not error
|
||||||
assert prec_satoshi(stats['profit_closed_coin'], 6.217e-05)
|
assert prec_satoshi(stats['profit_closed_coin'], 6.217e-05)
|
||||||
assert prec_satoshi(stats['profit_closed_percent'], 6.2)
|
assert prec_satoshi(stats['profit_closed_percent'], 6.2)
|
||||||
@ -285,33 +220,41 @@ def test_rpc_trade_statistics(
|
|||||||
|
|
||||||
# Test that rpc_trade_statistics can handle trades that lacks
|
# Test that rpc_trade_statistics can handle trades that lacks
|
||||||
# trade.open_rate (it is set to None)
|
# trade.open_rate (it is set to None)
|
||||||
def test_rpc_trade_statistics_closed(
|
def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, ticker_sell_up, limit_buy_order,
|
||||||
default_conf, update, ticker, ticker_sell_up, limit_buy_order, limit_sell_order, mocker):
|
limit_sell_order):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
"""
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
Test rpc_trade_statistics() method
|
||||||
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
"""
|
||||||
mocker.patch.multiple('freqtrade.rpc.telegram',
|
patch_get_signal(mocker, (True, False))
|
||||||
_CONF=default_conf,
|
mocker.patch.multiple(
|
||||||
init=MagicMock())
|
'freqtrade.fiat_convert.Market',
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||||
validate_pairs=MagicMock(),
|
)
|
||||||
get_ticker=ticker)
|
|
||||||
mocker.patch.multiple('freqtrade.fiat_convert.Market',
|
|
||||||
ticker=MagicMock(return_value={'price_usd': 15000.0}))
|
|
||||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||||
main.init(default_conf, create_engine('sqlite://'))
|
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.freqtradebot.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=ticker
|
||||||
|
)
|
||||||
|
|
||||||
|
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||||
stake_currency = default_conf['stake_currency']
|
stake_currency = default_conf['stake_currency']
|
||||||
fiat_display_currency = default_conf['fiat_display_currency']
|
fiat_display_currency = default_conf['fiat_display_currency']
|
||||||
|
|
||||||
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
main.create_trade(0.001, 5)
|
freqtradebot.create_trade()
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
# Simulate fulfilled LIMIT_BUY order for trade
|
# Simulate fulfilled LIMIT_BUY order for trade
|
||||||
trade.update(limit_buy_order)
|
trade.update(limit_buy_order)
|
||||||
# Update the ticker with a market going up
|
# Update the ticker with a market going up
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple(
|
||||||
validate_pairs=MagicMock(),
|
'freqtrade.freqtradebot.exchange',
|
||||||
get_ticker=ticker_sell_up)
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=ticker_sell_up
|
||||||
|
)
|
||||||
trade.update(limit_sell_order)
|
trade.update(limit_sell_order)
|
||||||
trade.close_date = datetime.utcnow()
|
trade.close_date = datetime.utcnow()
|
||||||
trade.is_open = False
|
trade.is_open = False
|
||||||
@ -319,8 +262,7 @@ def test_rpc_trade_statistics_closed(
|
|||||||
for trade in Trade.query.order_by(Trade.id).all():
|
for trade in Trade.query.order_by(Trade.id).all():
|
||||||
trade.open_rate = None
|
trade.open_rate = None
|
||||||
|
|
||||||
(error, stats) = rpc.rpc_trade_statistics(stake_currency,
|
(error, stats) = rpc.rpc_trade_statistics(stake_currency, fiat_display_currency)
|
||||||
fiat_display_currency)
|
|
||||||
assert not error
|
assert not error
|
||||||
assert prec_satoshi(stats['profit_closed_coin'], 0)
|
assert prec_satoshi(stats['profit_closed_coin'], 0)
|
||||||
assert prec_satoshi(stats['profit_closed_percent'], 0)
|
assert prec_satoshi(stats['profit_closed_percent'], 0)
|
||||||
@ -336,57 +278,223 @@ def test_rpc_trade_statistics_closed(
|
|||||||
assert prec_satoshi(stats['best_rate'], 6.2)
|
assert prec_satoshi(stats['best_rate'], 6.2)
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_balance_handle(default_conf, update, mocker):
|
def test_rpc_balance_handle(default_conf, mocker):
|
||||||
mock_balance = [{
|
"""
|
||||||
'Currency': 'BTC',
|
Test rpc_balance() method
|
||||||
'Balance': 10.0,
|
"""
|
||||||
'Available': 12.0,
|
mock_balance = [
|
||||||
'Pending': 0.0,
|
{
|
||||||
'CryptoAddress': 'XXXX',
|
'Currency': 'BTC',
|
||||||
}, {
|
'Balance': 10.0,
|
||||||
'Currency': 'ETH',
|
'Available': 12.0,
|
||||||
'Balance': 0.0,
|
'Pending': 0.0,
|
||||||
'Available': 0.0,
|
'CryptoAddress': 'XXXX',
|
||||||
'Pending': 0.0,
|
},
|
||||||
'CryptoAddress': 'XXXX',
|
{
|
||||||
}]
|
'Currency': 'ETH',
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
'Balance': 0.0,
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
'Available': 0.0,
|
||||||
get_balances=MagicMock(return_value=mock_balance))
|
'Pending': 0.0,
|
||||||
mocker.patch.multiple('freqtrade.fiat_convert.Market',
|
'CryptoAddress': 'XXXX',
|
||||||
ticker=MagicMock(return_value={'price_usd': 15000.0}))
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
patch_get_signal(mocker, (True, False))
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.fiat_convert.Market',
|
||||||
|
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||||
|
)
|
||||||
|
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||||
|
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.freqtradebot.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_balances=MagicMock(return_value=mock_balance)
|
||||||
|
)
|
||||||
|
|
||||||
|
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||||
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
(error, res) = rpc.rpc_balance(default_conf['fiat_display_currency'])
|
(error, res) = rpc.rpc_balance(default_conf['fiat_display_currency'])
|
||||||
assert not error
|
assert not error
|
||||||
(trade, x, y, z) = res
|
(trade, x, y, z) = res
|
||||||
assert prec_satoshi(x, 10)
|
assert prec_satoshi(x, 10)
|
||||||
assert prec_satoshi(z, 150000)
|
assert prec_satoshi(z, 150000)
|
||||||
assert y == 'USD'
|
assert 'USD' in y
|
||||||
assert len(trade) == 1
|
assert len(trade) == 1
|
||||||
assert trade[0]['currency'] == 'BTC'
|
assert 'BTC' in trade[0]['currency']
|
||||||
assert prec_satoshi(trade[0]['available'], 12)
|
assert prec_satoshi(trade[0]['available'], 12)
|
||||||
assert prec_satoshi(trade[0]['balance'], 10)
|
assert prec_satoshi(trade[0]['balance'], 10)
|
||||||
assert prec_satoshi(trade[0]['pending'], 0)
|
assert prec_satoshi(trade[0]['pending'], 0)
|
||||||
assert prec_satoshi(trade[0]['est_btc'], 10)
|
assert prec_satoshi(trade[0]['est_btc'], 10)
|
||||||
|
|
||||||
|
|
||||||
def test_performance_handle(
|
def test_rpc_start(mocker, default_conf) -> None:
|
||||||
default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker):
|
"""
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
Test rpc_start() method
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
"""
|
||||||
msg_mock = MagicMock()
|
patch_get_signal(mocker, (True, False))
|
||||||
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
patch_coinmarketcap(mocker)
|
||||||
mocker.patch.multiple('freqtrade.rpc.telegram',
|
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
||||||
_CONF=default_conf,
|
mocker.patch.multiple(
|
||||||
init=MagicMock(),
|
'freqtrade.freqtradebot.exchange',
|
||||||
send_msg=msg_mock)
|
validate_pairs=MagicMock(),
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
get_ticker=MagicMock()
|
||||||
validate_pairs=MagicMock(),
|
)
|
||||||
get_ticker=ticker)
|
|
||||||
main.init(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||||
|
rpc = RPC(freqtradebot)
|
||||||
|
freqtradebot.update_state(State.STOPPED)
|
||||||
|
|
||||||
|
(error, result) = rpc.rpc_start()
|
||||||
|
assert not error
|
||||||
|
assert '`Starting trader ...`' in result
|
||||||
|
assert freqtradebot.get_state() == State.RUNNING
|
||||||
|
|
||||||
|
(error, result) = rpc.rpc_start()
|
||||||
|
assert error
|
||||||
|
assert '*Status:* `already running`' in result
|
||||||
|
assert freqtradebot.get_state() == State.RUNNING
|
||||||
|
|
||||||
|
|
||||||
|
def test_rpc_stop(mocker, default_conf) -> None:
|
||||||
|
"""
|
||||||
|
Test rpc_stop() method
|
||||||
|
"""
|
||||||
|
patch_get_signal(mocker, (True, False))
|
||||||
|
patch_coinmarketcap(mocker)
|
||||||
|
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.freqtradebot.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=MagicMock()
|
||||||
|
)
|
||||||
|
|
||||||
|
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||||
|
rpc = RPC(freqtradebot)
|
||||||
|
freqtradebot.update_state(State.RUNNING)
|
||||||
|
|
||||||
|
(error, result) = rpc.rpc_stop()
|
||||||
|
assert not error
|
||||||
|
assert '`Stopping trader ...`' in result
|
||||||
|
assert freqtradebot.get_state() == State.STOPPED
|
||||||
|
|
||||||
|
(error, result) = rpc.rpc_stop()
|
||||||
|
assert error
|
||||||
|
assert '*Status:* `already stopped`' in result
|
||||||
|
assert freqtradebot.get_state() == State.STOPPED
|
||||||
|
|
||||||
|
|
||||||
|
def test_rpc_forcesell(default_conf, ticker, mocker) -> None:
|
||||||
|
"""
|
||||||
|
Test rpc_forcesell() method
|
||||||
|
"""
|
||||||
|
patch_get_signal(mocker, (True, False))
|
||||||
|
patch_coinmarketcap(mocker)
|
||||||
|
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
||||||
|
|
||||||
|
cancel_order_mock = MagicMock()
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.freqtradebot.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=ticker,
|
||||||
|
cancel_order=cancel_order_mock,
|
||||||
|
get_order=MagicMock(
|
||||||
|
return_value={
|
||||||
|
'closed': True,
|
||||||
|
'type': 'LIMIT_BUY',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||||
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
|
freqtradebot.update_state(State.STOPPED)
|
||||||
|
(error, res) = rpc.rpc_forcesell(None)
|
||||||
|
assert error
|
||||||
|
assert res == '`trader is not running`'
|
||||||
|
|
||||||
|
freqtradebot.update_state(State.RUNNING)
|
||||||
|
(error, res) = rpc.rpc_forcesell(None)
|
||||||
|
assert error
|
||||||
|
assert res == 'Invalid argument.'
|
||||||
|
|
||||||
|
(error, res) = rpc.rpc_forcesell('all')
|
||||||
|
assert not error
|
||||||
|
assert res == ''
|
||||||
|
|
||||||
|
freqtradebot.create_trade()
|
||||||
|
(error, res) = rpc.rpc_forcesell('all')
|
||||||
|
assert not error
|
||||||
|
assert res == ''
|
||||||
|
|
||||||
|
(error, res) = rpc.rpc_forcesell('1')
|
||||||
|
assert not error
|
||||||
|
assert res == ''
|
||||||
|
|
||||||
|
freqtradebot.update_state(State.STOPPED)
|
||||||
|
(error, res) = rpc.rpc_forcesell(None)
|
||||||
|
assert error
|
||||||
|
assert res == '`trader is not running`'
|
||||||
|
|
||||||
|
(error, res) = rpc.rpc_forcesell('all')
|
||||||
|
assert error
|
||||||
|
assert res == '`trader is not running`'
|
||||||
|
|
||||||
|
freqtradebot.update_state(State.RUNNING)
|
||||||
|
assert cancel_order_mock.call_count == 0
|
||||||
|
# make an limit-buy open trade
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.freqtradebot.exchange.get_order',
|
||||||
|
return_value={
|
||||||
|
'closed': None,
|
||||||
|
'type': 'LIMIT_BUY'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
# check that the trade is called, which is done
|
||||||
|
# by ensuring exchange.cancel_order is called
|
||||||
|
(error, res) = rpc.rpc_forcesell('1')
|
||||||
|
assert not error
|
||||||
|
assert res == ''
|
||||||
|
assert cancel_order_mock.call_count == 1
|
||||||
|
|
||||||
|
freqtradebot.create_trade()
|
||||||
|
# make an limit-sell open trade
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.freqtradebot.exchange.get_order',
|
||||||
|
return_value={
|
||||||
|
'closed': None,
|
||||||
|
'type': 'LIMIT_SELL'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
(error, res) = rpc.rpc_forcesell('2')
|
||||||
|
assert not error
|
||||||
|
assert res == ''
|
||||||
|
# status quo, no exchange calls
|
||||||
|
assert cancel_order_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_performance_handle(default_conf, ticker, limit_buy_order,
|
||||||
|
limit_sell_order, mocker) -> None:
|
||||||
|
"""
|
||||||
|
Test rpc_performance() method
|
||||||
|
"""
|
||||||
|
patch_get_signal(mocker, (True, False))
|
||||||
|
patch_coinmarketcap(mocker)
|
||||||
|
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.freqtradebot.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_balances=MagicMock(return_value=ticker),
|
||||||
|
get_ticker=ticker
|
||||||
|
)
|
||||||
|
|
||||||
|
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||||
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
main.create_trade(0.001, int(default_conf['ticker_interval']))
|
freqtradebot.create_trade()
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
assert trade
|
assert trade
|
||||||
|
|
||||||
@ -404,3 +512,33 @@ def test_performance_handle(
|
|||||||
assert res[0]['pair'] == 'BTC_ETH'
|
assert res[0]['pair'] == 'BTC_ETH'
|
||||||
assert res[0]['count'] == 1
|
assert res[0]['count'] == 1
|
||||||
assert prec_satoshi(res[0]['profit'], 6.2)
|
assert prec_satoshi(res[0]['profit'], 6.2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_rpc_count(mocker, default_conf, ticker) -> None:
|
||||||
|
"""
|
||||||
|
Test rpc_count() method
|
||||||
|
"""
|
||||||
|
patch_get_signal(mocker, (True, False))
|
||||||
|
patch_coinmarketcap(mocker)
|
||||||
|
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.freqtradebot.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_balances=MagicMock(return_value=ticker),
|
||||||
|
get_ticker=ticker
|
||||||
|
)
|
||||||
|
|
||||||
|
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||||
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
|
(error, trades) = rpc.rpc_count()
|
||||||
|
nb_trades = len(trades)
|
||||||
|
assert not error
|
||||||
|
assert nb_trades == 0
|
||||||
|
|
||||||
|
# Create some test data
|
||||||
|
freqtradebot.create_trade()
|
||||||
|
(error, trades) = rpc.rpc_count()
|
||||||
|
nb_trades = len(trades)
|
||||||
|
assert not error
|
||||||
|
assert nb_trades == 1
|
||||||
|
139
freqtrade/tests/rpc/test_rpc_manager.py
Normal file
139
freqtrade/tests/rpc/test_rpc_manager.py
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
"""
|
||||||
|
Unit test file for rpc/rpc_manager.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from copy import deepcopy
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from freqtrade.rpc.rpc_manager import RPCManager
|
||||||
|
from freqtrade.rpc.telegram import Telegram
|
||||||
|
from freqtrade.tests.conftest import log_has, get_patched_freqtradebot
|
||||||
|
|
||||||
|
|
||||||
|
def test_rpc_manager_object() -> None:
|
||||||
|
"""
|
||||||
|
Test the Arguments object has the mandatory methods
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
assert hasattr(RPCManager, '_init')
|
||||||
|
assert hasattr(RPCManager, 'send_msg')
|
||||||
|
assert hasattr(RPCManager, 'cleanup')
|
||||||
|
|
||||||
|
|
||||||
|
def test__init__(mocker, default_conf) -> None:
|
||||||
|
"""
|
||||||
|
Test __init__() method
|
||||||
|
"""
|
||||||
|
init_mock = mocker.patch('freqtrade.rpc.rpc_manager.RPCManager._init', MagicMock())
|
||||||
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
|
||||||
|
rpc_manager = RPCManager(freqtradebot)
|
||||||
|
assert rpc_manager.freqtrade == freqtradebot
|
||||||
|
assert rpc_manager.registered_modules == []
|
||||||
|
assert rpc_manager.telegram is None
|
||||||
|
assert init_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_telegram_disabled(mocker, default_conf, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test _init() method with Telegram disabled
|
||||||
|
"""
|
||||||
|
caplog.set_level(logging.DEBUG)
|
||||||
|
|
||||||
|
conf = deepcopy(default_conf)
|
||||||
|
conf['telegram']['enabled'] = False
|
||||||
|
|
||||||
|
freqtradebot = get_patched_freqtradebot(mocker, conf)
|
||||||
|
rpc_manager = RPCManager(freqtradebot)
|
||||||
|
|
||||||
|
assert not log_has('Enabling rpc.telegram ...', caplog.record_tuples)
|
||||||
|
assert rpc_manager.registered_modules == []
|
||||||
|
assert rpc_manager.telegram is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_telegram_enabled(mocker, default_conf, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test _init() method with Telegram enabled
|
||||||
|
"""
|
||||||
|
caplog.set_level(logging.DEBUG)
|
||||||
|
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||||
|
|
||||||
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
rpc_manager = RPCManager(freqtradebot)
|
||||||
|
|
||||||
|
assert log_has('Enabling rpc.telegram ...', caplog.record_tuples)
|
||||||
|
len_modules = len(rpc_manager.registered_modules)
|
||||||
|
assert len_modules == 1
|
||||||
|
assert 'telegram' in rpc_manager.registered_modules
|
||||||
|
assert isinstance(rpc_manager.telegram, Telegram)
|
||||||
|
|
||||||
|
|
||||||
|
def test_cleanup_telegram_disabled(mocker, default_conf, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test cleanup() method with Telegram disabled
|
||||||
|
"""
|
||||||
|
caplog.set_level(logging.DEBUG)
|
||||||
|
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.cleanup', MagicMock())
|
||||||
|
|
||||||
|
conf = deepcopy(default_conf)
|
||||||
|
conf['telegram']['enabled'] = False
|
||||||
|
|
||||||
|
freqtradebot = get_patched_freqtradebot(mocker, conf)
|
||||||
|
rpc_manager = RPCManager(freqtradebot)
|
||||||
|
rpc_manager.cleanup()
|
||||||
|
|
||||||
|
assert not log_has('Cleaning up rpc.telegram ...', caplog.record_tuples)
|
||||||
|
assert telegram_mock.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_cleanup_telegram_enabled(mocker, default_conf, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test cleanup() method with Telegram enabled
|
||||||
|
"""
|
||||||
|
caplog.set_level(logging.DEBUG)
|
||||||
|
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||||
|
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.cleanup', MagicMock())
|
||||||
|
|
||||||
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
rpc_manager = RPCManager(freqtradebot)
|
||||||
|
|
||||||
|
# Check we have Telegram as a registered modules
|
||||||
|
assert 'telegram' in rpc_manager.registered_modules
|
||||||
|
|
||||||
|
rpc_manager.cleanup()
|
||||||
|
assert log_has('Cleaning up rpc.telegram ...', caplog.record_tuples)
|
||||||
|
assert 'telegram' not in rpc_manager.registered_modules
|
||||||
|
assert telegram_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_send_msg_telegram_disabled(mocker, default_conf, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test send_msg() method with Telegram disabled
|
||||||
|
"""
|
||||||
|
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
|
||||||
|
|
||||||
|
conf = deepcopy(default_conf)
|
||||||
|
conf['telegram']['enabled'] = False
|
||||||
|
|
||||||
|
freqtradebot = get_patched_freqtradebot(mocker, conf)
|
||||||
|
rpc_manager = RPCManager(freqtradebot)
|
||||||
|
rpc_manager.send_msg('test')
|
||||||
|
|
||||||
|
assert log_has('test', caplog.record_tuples)
|
||||||
|
assert telegram_mock.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test send_msg() method with Telegram disabled
|
||||||
|
"""
|
||||||
|
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
|
||||||
|
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||||
|
|
||||||
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
rpc_manager = RPCManager(freqtradebot)
|
||||||
|
rpc_manager.send_msg('test')
|
||||||
|
|
||||||
|
assert log_has('test', caplog.record_tuples)
|
||||||
|
assert telegram_mock.call_count == 1
|
File diff suppressed because it is too large
Load Diff
@ -1,14 +1,16 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade.analyze import Analyze
|
||||||
from freqtrade.strategy.default_strategy import DefaultStrategy, class_name
|
from freqtrade.strategy.default_strategy import DefaultStrategy, class_name
|
||||||
from freqtrade.analyze import parse_ticker_dataframe
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def result():
|
def result():
|
||||||
with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file:
|
with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file:
|
||||||
return parse_ticker_dataframe(json.load(data_file))
|
return Analyze.parse_ticker_dataframe(json.load(data_file))
|
||||||
|
|
||||||
|
|
||||||
def test_default_strategy_class_name():
|
def test_default_strategy_class_name():
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# pragma pylint: disable=missing-docstring, protected-access, C0103
|
# pragma pylint: disable=missing-docstring, protected-access, C0103
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from freqtrade.strategy.strategy import Strategy
|
from freqtrade.strategy.strategy import Strategy
|
||||||
|
|
||||||
|
|
||||||
@ -21,7 +22,6 @@ def test_search_strategy():
|
|||||||
|
|
||||||
|
|
||||||
def test_strategy_structure():
|
def test_strategy_structure():
|
||||||
assert hasattr(Strategy, 'init')
|
|
||||||
assert hasattr(Strategy, 'populate_indicators')
|
assert hasattr(Strategy, 'populate_indicators')
|
||||||
assert hasattr(Strategy, 'populate_buy_trend')
|
assert hasattr(Strategy, 'populate_buy_trend')
|
||||||
assert hasattr(Strategy, 'populate_sell_trend')
|
assert hasattr(Strategy, 'populate_sell_trend')
|
||||||
@ -53,8 +53,7 @@ def test_load_not_found_strategy(caplog):
|
|||||||
|
|
||||||
|
|
||||||
def test_strategy(result):
|
def test_strategy(result):
|
||||||
strategy = Strategy()
|
strategy = Strategy({'strategy': 'default_strategy'})
|
||||||
strategy.init({'strategy': 'default_strategy'})
|
|
||||||
|
|
||||||
assert hasattr(strategy.custom_strategy, 'minimal_roi')
|
assert hasattr(strategy.custom_strategy, 'minimal_roi')
|
||||||
assert strategy.minimal_roi[0] == 0.04
|
assert strategy.minimal_roi[0] == 0.04
|
||||||
@ -82,8 +81,7 @@ def test_strategy_override_minimal_roi(caplog):
|
|||||||
"0": 0.5
|
"0": 0.5
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
strategy = Strategy()
|
strategy = Strategy(config)
|
||||||
strategy.init(config)
|
|
||||||
|
|
||||||
assert hasattr(strategy.custom_strategy, 'minimal_roi')
|
assert hasattr(strategy.custom_strategy, 'minimal_roi')
|
||||||
assert strategy.minimal_roi[0] == 0.5
|
assert strategy.minimal_roi[0] == 0.5
|
||||||
@ -99,8 +97,7 @@ def test_strategy_override_stoploss(caplog):
|
|||||||
'strategy': 'default_strategy',
|
'strategy': 'default_strategy',
|
||||||
'stoploss': -0.5
|
'stoploss': -0.5
|
||||||
}
|
}
|
||||||
strategy = Strategy()
|
strategy = Strategy(config)
|
||||||
strategy.init(config)
|
|
||||||
|
|
||||||
assert hasattr(strategy.custom_strategy, 'stoploss')
|
assert hasattr(strategy.custom_strategy, 'stoploss')
|
||||||
assert strategy.stoploss == -0.5
|
assert strategy.stoploss == -0.5
|
||||||
@ -117,8 +114,7 @@ def test_strategy_override_ticker_interval(caplog):
|
|||||||
'strategy': 'default_strategy',
|
'strategy': 'default_strategy',
|
||||||
'ticker_interval': 60
|
'ticker_interval': 60
|
||||||
}
|
}
|
||||||
strategy = Strategy()
|
strategy = Strategy(config)
|
||||||
strategy.init(config)
|
|
||||||
|
|
||||||
assert hasattr(strategy.custom_strategy, 'ticker_interval')
|
assert hasattr(strategy.custom_strategy, 'ticker_interval')
|
||||||
assert strategy.ticker_interval == 60
|
assert strategy.ticker_interval == 60
|
||||||
@ -138,8 +134,7 @@ def test_strategy_fallback_default_strategy():
|
|||||||
|
|
||||||
|
|
||||||
def test_strategy_singleton():
|
def test_strategy_singleton():
|
||||||
strategy1 = Strategy()
|
strategy1 = Strategy({'strategy': 'default_strategy'})
|
||||||
strategy1.init({'strategy': 'default_strategy'})
|
|
||||||
|
|
||||||
assert hasattr(strategy1.custom_strategy, 'minimal_roi')
|
assert hasattr(strategy1.custom_strategy, 'minimal_roi')
|
||||||
assert strategy1.minimal_roi[0] == 0.04
|
assert strategy1.minimal_roi[0] == 0.04
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# pragma pylint: disable=missing-docstring,C0103
|
# pragma pylint: disable=missing-docstring,C0103,protected-access
|
||||||
|
|
||||||
from freqtrade.main import refresh_whitelist, gen_pair_whitelist
|
import freqtrade.tests.conftest as tt # test tools
|
||||||
|
|
||||||
# whitelist, blacklist, filtering, all of that will
|
# whitelist, blacklist, filtering, all of that will
|
||||||
# eventually become some rules to run on a generic ACL engine
|
# eventually become some rules to run on a generic ACL engine
|
||||||
@ -8,21 +8,22 @@ from freqtrade.main import refresh_whitelist, gen_pair_whitelist
|
|||||||
|
|
||||||
|
|
||||||
def whitelist_conf():
|
def whitelist_conf():
|
||||||
return {
|
config = tt.default_conf()
|
||||||
'stake_currency': 'BTC',
|
|
||||||
'exchange': {
|
config['stake_currency'] = 'BTC'
|
||||||
'pair_whitelist': [
|
config['exchange']['pair_whitelist'] = [
|
||||||
'BTC_ETH',
|
'BTC_ETH',
|
||||||
'BTC_TKN',
|
'BTC_TKN',
|
||||||
'BTC_TRST',
|
'BTC_TRST',
|
||||||
'BTC_SWT',
|
'BTC_SWT',
|
||||||
'BTC_BCC'
|
'BTC_BCC'
|
||||||
],
|
]
|
||||||
'pair_blacklist': [
|
|
||||||
'BTC_BLK'
|
config['exchange']['pair_blacklist'] = [
|
||||||
],
|
'BTC_BLK'
|
||||||
},
|
]
|
||||||
}
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
def get_market_summaries():
|
def get_market_summaries():
|
||||||
@ -86,11 +87,13 @@ def get_health_empty():
|
|||||||
|
|
||||||
def test_refresh_market_pair_not_in_whitelist(mocker):
|
def test_refresh_market_pair_not_in_whitelist(mocker):
|
||||||
conf = whitelist_conf()
|
conf = whitelist_conf()
|
||||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
|
||||||
get_wallet_health=get_health)
|
|
||||||
refreshedwhitelist = refresh_whitelist(
|
mocker.patch('freqtrade.freqtradebot.exchange.get_wallet_health', get_health)
|
||||||
conf['exchange']['pair_whitelist'] + ['BTC_XXX'])
|
refreshedwhitelist = freqtradebot._refresh_whitelist(
|
||||||
|
conf['exchange']['pair_whitelist'] + ['BTC_XXX']
|
||||||
|
)
|
||||||
# List ordered by BaseVolume
|
# List ordered by BaseVolume
|
||||||
whitelist = ['BTC_ETH', 'BTC_TKN']
|
whitelist = ['BTC_ETH', 'BTC_TKN']
|
||||||
# Ensure all except those in whitelist are removed
|
# Ensure all except those in whitelist are removed
|
||||||
@ -99,10 +102,11 @@ def test_refresh_market_pair_not_in_whitelist(mocker):
|
|||||||
|
|
||||||
def test_refresh_whitelist(mocker):
|
def test_refresh_whitelist(mocker):
|
||||||
conf = whitelist_conf()
|
conf = whitelist_conf()
|
||||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
get_wallet_health=get_health)
|
mocker.patch('freqtrade.freqtradebot.exchange.get_wallet_health', get_health)
|
||||||
refreshedwhitelist = refresh_whitelist(conf['exchange']['pair_whitelist'])
|
refreshedwhitelist = freqtradebot._refresh_whitelist(conf['exchange']['pair_whitelist'])
|
||||||
|
|
||||||
# List ordered by BaseVolume
|
# List ordered by BaseVolume
|
||||||
whitelist = ['BTC_ETH', 'BTC_TKN']
|
whitelist = ['BTC_ETH', 'BTC_TKN']
|
||||||
# Ensure all except those in whitelist are removed
|
# Ensure all except those in whitelist are removed
|
||||||
@ -111,26 +115,32 @@ def test_refresh_whitelist(mocker):
|
|||||||
|
|
||||||
def test_refresh_whitelist_dynamic(mocker):
|
def test_refresh_whitelist_dynamic(mocker):
|
||||||
conf = whitelist_conf()
|
conf = whitelist_conf()
|
||||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple(
|
||||||
get_wallet_health=get_health)
|
'freqtrade.freqtradebot.exchange',
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
get_wallet_health=get_health,
|
||||||
get_market_summaries=get_market_summaries)
|
get_market_summaries=get_market_summaries
|
||||||
|
)
|
||||||
|
|
||||||
# argument: use the whitelist dynamically by exchange-volume
|
# argument: use the whitelist dynamically by exchange-volume
|
||||||
whitelist = ['BTC_TKN', 'BTC_ETH']
|
whitelist = ['BTC_TKN', 'BTC_ETH']
|
||||||
refreshedwhitelist = refresh_whitelist(
|
|
||||||
gen_pair_whitelist(conf['stake_currency']))
|
refreshedwhitelist = freqtradebot._refresh_whitelist(
|
||||||
|
freqtradebot._gen_pair_whitelist(conf['stake_currency'])
|
||||||
|
)
|
||||||
|
|
||||||
assert whitelist == refreshedwhitelist
|
assert whitelist == refreshedwhitelist
|
||||||
|
|
||||||
|
|
||||||
def test_refresh_whitelist_dynamic_empty(mocker):
|
def test_refresh_whitelist_dynamic_empty(mocker):
|
||||||
conf = whitelist_conf()
|
conf = whitelist_conf()
|
||||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch('freqtrade.freqtradebot.exchange.get_wallet_health', get_health_empty)
|
||||||
get_wallet_health=get_health_empty)
|
|
||||||
# argument: use the whitelist dynamically by exchange-volume
|
# argument: use the whitelist dynamically by exchange-volume
|
||||||
whitelist = []
|
whitelist = []
|
||||||
conf['exchange']['pair_whitelist'] = []
|
conf['exchange']['pair_whitelist'] = []
|
||||||
refresh_whitelist(whitelist)
|
freqtradebot._refresh_whitelist(whitelist)
|
||||||
pairslist = conf['exchange']['pair_whitelist']
|
pairslist = conf['exchange']['pair_whitelist']
|
||||||
|
|
||||||
assert set(whitelist) == set(pairslist)
|
assert set(whitelist) == set(pairslist)
|
||||||
|
@ -1,16 +1,51 @@
|
|||||||
# pragma pylint: disable=missing-docstring, C0103
|
# pragma pylint: disable=missing-docstring, C0103
|
||||||
|
|
||||||
|
"""
|
||||||
|
Unit test file for analyse.py
|
||||||
|
"""
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
import logging
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import logging
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade.analyze import Analyze, SignalType
|
||||||
|
from freqtrade.optimize.__init__ import load_tickerdata_file
|
||||||
from freqtrade.tests.conftest import log_has
|
from freqtrade.tests.conftest import log_has
|
||||||
from freqtrade.analyze import (get_signal, parse_ticker_dataframe,
|
|
||||||
populate_buy_trend, populate_indicators,
|
# Avoid to reinit the same object again and again
|
||||||
populate_sell_trend)
|
_ANALYZE = Analyze({'strategy': 'default_strategy'})
|
||||||
from freqtrade.strategy.strategy import Strategy
|
|
||||||
|
|
||||||
|
def test_signaltype_object() -> None:
|
||||||
|
"""
|
||||||
|
Test the SignalType object has the mandatory Constants
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
assert hasattr(SignalType, 'BUY')
|
||||||
|
assert hasattr(SignalType, 'SELL')
|
||||||
|
|
||||||
|
|
||||||
|
def test_analyze_object() -> None:
|
||||||
|
"""
|
||||||
|
Test the Analyze object has the mandatory methods
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
assert hasattr(Analyze, 'parse_ticker_dataframe')
|
||||||
|
assert hasattr(Analyze, 'populate_indicators')
|
||||||
|
assert hasattr(Analyze, 'populate_buy_trend')
|
||||||
|
assert hasattr(Analyze, 'populate_sell_trend')
|
||||||
|
assert hasattr(Analyze, 'analyze_ticker')
|
||||||
|
assert hasattr(Analyze, 'get_signal')
|
||||||
|
assert hasattr(Analyze, 'should_sell')
|
||||||
|
assert hasattr(Analyze, 'min_roi_reached')
|
||||||
|
|
||||||
|
|
||||||
|
def test_dataframe_correct_length(result):
|
||||||
|
dataframe = Analyze.parse_ticker_dataframe(result)
|
||||||
|
assert len(result.index) == len(dataframe.index)
|
||||||
|
|
||||||
|
|
||||||
def test_dataframe_correct_columns(result):
|
def test_dataframe_correct_columns(result):
|
||||||
@ -18,78 +53,88 @@ def test_dataframe_correct_columns(result):
|
|||||||
['close', 'high', 'low', 'open', 'date', 'volume']
|
['close', 'high', 'low', 'open', 'date', 'volume']
|
||||||
|
|
||||||
|
|
||||||
def test_dataframe_correct_length(result):
|
|
||||||
dataframe = parse_ticker_dataframe(result)
|
|
||||||
assert len(result.index) == len(dataframe.index)
|
|
||||||
|
|
||||||
|
|
||||||
def test_populates_buy_trend(result):
|
def test_populates_buy_trend(result):
|
||||||
# Load the default strategy for the unit test, because this logic is done in main.py
|
# Load the default strategy for the unit test, because this logic is done in main.py
|
||||||
Strategy().init({'strategy': 'default_strategy'})
|
dataframe = _ANALYZE.populate_buy_trend(_ANALYZE.populate_indicators(result))
|
||||||
|
|
||||||
dataframe = populate_buy_trend(populate_indicators(result))
|
|
||||||
assert 'buy' in dataframe.columns
|
assert 'buy' in dataframe.columns
|
||||||
|
|
||||||
|
|
||||||
def test_populates_sell_trend(result):
|
def test_populates_sell_trend(result):
|
||||||
# Load the default strategy for the unit test, because this logic is done in main.py
|
# Load the default strategy for the unit test, because this logic is done in main.py
|
||||||
Strategy().init({'strategy': 'default_strategy'})
|
dataframe = _ANALYZE.populate_sell_trend(_ANALYZE.populate_indicators(result))
|
||||||
|
|
||||||
dataframe = populate_sell_trend(populate_indicators(result))
|
|
||||||
assert 'sell' in dataframe.columns
|
assert 'sell' in dataframe.columns
|
||||||
|
|
||||||
|
|
||||||
def test_returns_latest_buy_signal(mocker):
|
def test_returns_latest_buy_signal(mocker):
|
||||||
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock())
|
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock())
|
||||||
mocker.patch(
|
|
||||||
'freqtrade.analyze.analyze_ticker',
|
|
||||||
return_value=DataFrame([{'buy': 1, 'sell': 0, 'date': arrow.utcnow()}])
|
|
||||||
)
|
|
||||||
assert get_signal('BTC-ETH', 5) == (True, False)
|
|
||||||
|
|
||||||
mocker.patch(
|
mocker.patch.multiple(
|
||||||
'freqtrade.analyze.analyze_ticker',
|
'freqtrade.analyze.Analyze',
|
||||||
return_value=DataFrame([{'buy': 0, 'sell': 1, 'date': arrow.utcnow()}])
|
analyze_ticker=MagicMock(
|
||||||
|
return_value=DataFrame([{'buy': 1, 'sell': 0, 'date': arrow.utcnow()}])
|
||||||
|
)
|
||||||
)
|
)
|
||||||
assert get_signal('BTC-ETH', 5) == (False, True)
|
assert _ANALYZE.get_signal('BTC-ETH', 5) == (True, False)
|
||||||
|
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.analyze.Analyze',
|
||||||
|
analyze_ticker=MagicMock(
|
||||||
|
return_value=DataFrame([{'buy': 0, 'sell': 1, 'date': arrow.utcnow()}])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert _ANALYZE.get_signal('BTC-ETH', 5) == (False, True)
|
||||||
|
|
||||||
|
|
||||||
def test_returns_latest_sell_signal(mocker):
|
def test_returns_latest_sell_signal(mocker):
|
||||||
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock())
|
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock())
|
||||||
mocker.patch(
|
mocker.patch.multiple(
|
||||||
'freqtrade.analyze.analyze_ticker',
|
'freqtrade.analyze.Analyze',
|
||||||
return_value=DataFrame([{'sell': 1, 'buy': 0, 'date': arrow.utcnow()}])
|
analyze_ticker=MagicMock(
|
||||||
|
return_value=DataFrame([{'sell': 1, 'buy': 0, 'date': arrow.utcnow()}])
|
||||||
|
)
|
||||||
)
|
)
|
||||||
assert get_signal('BTC-ETH', 5) == (False, True)
|
|
||||||
|
|
||||||
mocker.patch(
|
assert _ANALYZE.get_signal('BTC-ETH', 5) == (False, True)
|
||||||
'freqtrade.analyze.analyze_ticker',
|
|
||||||
return_value=DataFrame([{'sell': 0, 'buy': 1, 'date': arrow.utcnow()}])
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.analyze.Analyze',
|
||||||
|
analyze_ticker=MagicMock(
|
||||||
|
return_value=DataFrame([{'sell': 0, 'buy': 1, 'date': arrow.utcnow()}])
|
||||||
|
)
|
||||||
)
|
)
|
||||||
assert get_signal('BTC-ETH', 5) == (True, False)
|
assert _ANALYZE.get_signal('BTC-ETH', 5) == (True, False)
|
||||||
|
|
||||||
|
|
||||||
def test_get_signal_empty(default_conf, mocker, caplog):
|
def test_get_signal_empty(default_conf, mocker, caplog):
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=None)
|
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=None)
|
||||||
assert (False, False) == get_signal('foo', int(default_conf['ticker_interval']))
|
assert (False, False) == _ANALYZE.get_signal('foo', int(default_conf['ticker_interval']))
|
||||||
assert log_has('Empty ticker history for pair foo', caplog.record_tuples)
|
assert log_has('Empty ticker history for pair foo', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
def test_get_signal_exception_valueerror(default_conf, mocker, caplog):
|
def test_get_signal_exception_valueerror(default_conf, mocker, caplog):
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=1)
|
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=1)
|
||||||
mocker.patch('freqtrade.analyze.analyze_ticker',
|
mocker.patch.multiple(
|
||||||
side_effect=ValueError('xyz'))
|
'freqtrade.analyze.Analyze',
|
||||||
assert (False, False) == get_signal('foo', int(default_conf['ticker_interval']))
|
analyze_ticker=MagicMock(
|
||||||
|
side_effect=ValueError('xyz')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert (False, False) == _ANALYZE.get_signal('foo', int(default_conf['ticker_interval']))
|
||||||
assert log_has('Unable to analyze ticker for pair foo: xyz', caplog.record_tuples)
|
assert log_has('Unable to analyze ticker for pair foo: xyz', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
def test_get_signal_empty_dataframe(default_conf, mocker, caplog):
|
def test_get_signal_empty_dataframe(default_conf, mocker, caplog):
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=1)
|
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=1)
|
||||||
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=DataFrame([]))
|
mocker.patch.multiple(
|
||||||
assert (False, False) == get_signal('xyz', int(default_conf['ticker_interval']))
|
'freqtrade.analyze.Analyze',
|
||||||
|
analyze_ticker=MagicMock(
|
||||||
|
return_value=DataFrame([])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert (False, False) == _ANALYZE.get_signal('xyz', int(default_conf['ticker_interval']))
|
||||||
assert log_has('Empty dataframe for pair xyz', caplog.record_tuples)
|
assert log_has('Empty dataframe for pair xyz', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
@ -99,27 +144,51 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog):
|
|||||||
# FIX: The get_signal function has hardcoded 10, which we must inturn hardcode
|
# FIX: The get_signal function has hardcoded 10, which we must inturn hardcode
|
||||||
oldtime = arrow.utcnow() - datetime.timedelta(minutes=11)
|
oldtime = arrow.utcnow() - datetime.timedelta(minutes=11)
|
||||||
ticks = DataFrame([{'buy': 1, 'date': oldtime}])
|
ticks = DataFrame([{'buy': 1, 'date': oldtime}])
|
||||||
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=DataFrame(ticks))
|
mocker.patch.multiple(
|
||||||
assert (False, False) == get_signal('xyz', int(default_conf['ticker_interval']))
|
'freqtrade.analyze.Analyze',
|
||||||
assert log_has('Outdated history for pair xyz. Last tick is 11 minutes old',
|
analyze_ticker=MagicMock(
|
||||||
caplog.record_tuples)
|
return_value=DataFrame(ticks)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert (False, False) == _ANALYZE.get_signal('xyz', int(default_conf['ticker_interval']))
|
||||||
|
assert log_has(
|
||||||
|
'Outdated history for pair xyz. Last tick is 11 minutes old',
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_get_signal_handles_exceptions(mocker):
|
def test_get_signal_handles_exceptions(mocker):
|
||||||
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock())
|
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock())
|
||||||
mocker.patch('freqtrade.analyze.analyze_ticker',
|
mocker.patch.multiple(
|
||||||
side_effect=Exception('invalid ticker history '))
|
'freqtrade.analyze.Analyze',
|
||||||
|
analyze_ticker=MagicMock(
|
||||||
|
side_effect=Exception('invalid ticker history ')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
assert get_signal('BTC-ETH', 5) == (False, False)
|
assert _ANALYZE.get_signal('BTC-ETH', 5) == (False, False)
|
||||||
|
|
||||||
|
|
||||||
def test_parse_ticker_dataframe(ticker_history, ticker_history_without_bv):
|
def test_parse_ticker_dataframe(ticker_history, ticker_history_without_bv):
|
||||||
columns = ['close', 'high', 'low', 'open', 'date', 'volume']
|
columns = ['close', 'high', 'low', 'open', 'date', 'volume']
|
||||||
|
|
||||||
# Test file with BV data
|
# Test file with BV data
|
||||||
dataframe = parse_ticker_dataframe(ticker_history)
|
dataframe = Analyze.parse_ticker_dataframe(ticker_history)
|
||||||
assert dataframe.columns.tolist() == columns
|
assert dataframe.columns.tolist() == columns
|
||||||
|
|
||||||
# Test file without BV data
|
# Test file without BV data
|
||||||
dataframe = parse_ticker_dataframe(ticker_history_without_bv)
|
dataframe = Analyze.parse_ticker_dataframe(ticker_history_without_bv)
|
||||||
assert dataframe.columns.tolist() == columns
|
assert dataframe.columns.tolist() == columns
|
||||||
|
|
||||||
|
|
||||||
|
def test_tickerdata_to_dataframe(default_conf) -> None:
|
||||||
|
"""
|
||||||
|
Test Analyze.tickerdata_to_dataframe() method
|
||||||
|
"""
|
||||||
|
analyze = Analyze(default_conf)
|
||||||
|
|
||||||
|
timerange = ((None, 'line'), None, -100)
|
||||||
|
tick = load_tickerdata_file(None, 'BTC_UNITEST', 1, timerange=timerange)
|
||||||
|
tickerlist = {'BTC_UNITEST': tick}
|
||||||
|
data = analyze.tickerdata_to_dataframe(tickerlist)
|
||||||
|
assert len(data['BTC_UNITEST']) == 100
|
||||||
|
134
freqtrade/tests/test_arguments.py
Normal file
134
freqtrade/tests/test_arguments.py
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
# pragma pylint: disable=missing-docstring, C0103
|
||||||
|
|
||||||
|
"""
|
||||||
|
Unit test file for arguments.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from freqtrade.arguments import Arguments
|
||||||
|
|
||||||
|
|
||||||
|
def test_arguments_object() -> None:
|
||||||
|
"""
|
||||||
|
Test the Arguments object has the mandatory methods
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
assert hasattr(Arguments, 'get_parsed_arg')
|
||||||
|
assert hasattr(Arguments, 'parse_args')
|
||||||
|
assert hasattr(Arguments, 'parse_timerange')
|
||||||
|
assert hasattr(Arguments, 'scripts_options')
|
||||||
|
|
||||||
|
|
||||||
|
# Parse common command-line-arguments. Used for all tools
|
||||||
|
def test_parse_args_none() -> None:
|
||||||
|
arguments = Arguments([], '')
|
||||||
|
assert isinstance(arguments, Arguments)
|
||||||
|
assert isinstance(arguments.parser, argparse.ArgumentParser)
|
||||||
|
assert isinstance(arguments.parser, argparse.ArgumentParser)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_defaults() -> None:
|
||||||
|
args = Arguments([], '').get_parsed_arg()
|
||||||
|
assert args.config == 'config.json'
|
||||||
|
assert args.dynamic_whitelist is None
|
||||||
|
assert args.loglevel == logging.INFO
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_config() -> None:
|
||||||
|
args = Arguments(['-c', '/dev/null'], '').get_parsed_arg()
|
||||||
|
assert args.config == '/dev/null'
|
||||||
|
|
||||||
|
args = Arguments(['--config', '/dev/null'], '').get_parsed_arg()
|
||||||
|
assert args.config == '/dev/null'
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_verbose() -> None:
|
||||||
|
args = Arguments(['-v'], '').get_parsed_arg()
|
||||||
|
assert args.loglevel == logging.DEBUG
|
||||||
|
|
||||||
|
args = Arguments(['--verbose'], '').get_parsed_arg()
|
||||||
|
assert args.loglevel == logging.DEBUG
|
||||||
|
|
||||||
|
|
||||||
|
def test_scripts_options() -> None:
|
||||||
|
arguments = Arguments(['-p', 'BTC_ETH'], '')
|
||||||
|
arguments.scripts_options()
|
||||||
|
args = arguments.get_parsed_arg()
|
||||||
|
assert args.pair == 'BTC_ETH'
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_version() -> None:
|
||||||
|
with pytest.raises(SystemExit, match=r'0'):
|
||||||
|
Arguments(['--version'], '').get_parsed_arg()
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_invalid() -> None:
|
||||||
|
with pytest.raises(SystemExit, match=r'2'):
|
||||||
|
Arguments(['-c'], '').get_parsed_arg()
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_dynamic_whitelist() -> None:
|
||||||
|
args = Arguments(['--dynamic-whitelist'], '').get_parsed_arg()
|
||||||
|
assert args.dynamic_whitelist == 20
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_dynamic_whitelist_10() -> None:
|
||||||
|
args = Arguments(['--dynamic-whitelist', '10'], '').get_parsed_arg()
|
||||||
|
assert args.dynamic_whitelist == 10
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_dynamic_whitelist_invalid_values() -> None:
|
||||||
|
with pytest.raises(SystemExit, match=r'2'):
|
||||||
|
Arguments(['--dynamic-whitelist', 'abc'], '').get_parsed_arg()
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_timerange_incorrect() -> None:
|
||||||
|
assert ((None, 'line'), None, -200) == Arguments.parse_timerange('-200')
|
||||||
|
assert (('line', None), 200, None) == Arguments.parse_timerange('200-')
|
||||||
|
with pytest.raises(Exception, match=r'Incorrect syntax.*'):
|
||||||
|
Arguments.parse_timerange('-')
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_backtesting_invalid() -> None:
|
||||||
|
with pytest.raises(SystemExit, match=r'2'):
|
||||||
|
Arguments(['backtesting --ticker-interval'], '').get_parsed_arg()
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit, match=r'2'):
|
||||||
|
Arguments(['backtesting --ticker-interval', 'abc'], '').get_parsed_arg()
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_backtesting_custom() -> None:
|
||||||
|
args = [
|
||||||
|
'-c', 'test_conf.json',
|
||||||
|
'backtesting',
|
||||||
|
'--live',
|
||||||
|
'--ticker-interval', '1',
|
||||||
|
'--refresh-pairs-cached']
|
||||||
|
call_args = Arguments(args, '').get_parsed_arg()
|
||||||
|
assert call_args.config == 'test_conf.json'
|
||||||
|
assert call_args.live is True
|
||||||
|
assert call_args.loglevel == logging.INFO
|
||||||
|
assert call_args.subparser == 'backtesting'
|
||||||
|
assert call_args.func is not None
|
||||||
|
assert call_args.ticker_interval == 1
|
||||||
|
assert call_args.refresh_pairs is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_hyperopt_custom() -> None:
|
||||||
|
args = [
|
||||||
|
'-c', 'test_conf.json',
|
||||||
|
'hyperopt',
|
||||||
|
'--epochs', '20',
|
||||||
|
'--spaces', 'buy'
|
||||||
|
]
|
||||||
|
call_args = Arguments(args, '').get_parsed_arg()
|
||||||
|
assert call_args.config == 'test_conf.json'
|
||||||
|
assert call_args.epochs == 20
|
||||||
|
assert call_args.loglevel == logging.INFO
|
||||||
|
assert call_args.subparser == 'hyperopt'
|
||||||
|
assert call_args.spaces == ['buy']
|
||||||
|
assert call_args.func is not None
|
316
freqtrade/tests/test_configuration.py
Normal file
316
freqtrade/tests/test_configuration.py
Normal file
@ -0,0 +1,316 @@
|
|||||||
|
# pragma pylint: disable=protected-access, invalid-name
|
||||||
|
|
||||||
|
"""
|
||||||
|
Unit test file for configuration.py
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
from copy import deepcopy
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from jsonschema import ValidationError
|
||||||
|
|
||||||
|
from freqtrade.arguments import Arguments
|
||||||
|
from freqtrade.configuration import Configuration
|
||||||
|
from freqtrade.tests.conftest import log_has
|
||||||
|
|
||||||
|
|
||||||
|
def test_configuration_object() -> None:
|
||||||
|
"""
|
||||||
|
Test the Constants object has the mandatory Constants
|
||||||
|
"""
|
||||||
|
assert hasattr(Configuration, 'load_config')
|
||||||
|
assert hasattr(Configuration, '_load_config_file')
|
||||||
|
assert hasattr(Configuration, '_validate_config')
|
||||||
|
assert hasattr(Configuration, '_load_common_config')
|
||||||
|
assert hasattr(Configuration, '_load_backtesting_config')
|
||||||
|
assert hasattr(Configuration, '_load_hyperopt_config')
|
||||||
|
assert hasattr(Configuration, 'get_config')
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_config_invalid_pair(default_conf, mocker) -> None:
|
||||||
|
"""
|
||||||
|
Test the configuration validator with an invalid PAIR format
|
||||||
|
"""
|
||||||
|
conf = deepcopy(default_conf)
|
||||||
|
conf['exchange']['pair_whitelist'].append('BTC-ETH')
|
||||||
|
|
||||||
|
with pytest.raises(ValidationError, match=r'.*does not match.*'):
|
||||||
|
configuration = Configuration([])
|
||||||
|
configuration._validate_config(conf)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_config_missing_attributes(default_conf, mocker) -> None:
|
||||||
|
"""
|
||||||
|
Test the configuration validator with a missing attribute
|
||||||
|
"""
|
||||||
|
conf = deepcopy(default_conf)
|
||||||
|
conf.pop('exchange')
|
||||||
|
|
||||||
|
with pytest.raises(ValidationError, match=r'.*\'exchange\' is a required property.*'):
|
||||||
|
configuration = Configuration([])
|
||||||
|
configuration._validate_config(conf)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_config_file(default_conf, mocker, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test Configuration._load_config_file() method
|
||||||
|
"""
|
||||||
|
file_mock = mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(default_conf)
|
||||||
|
))
|
||||||
|
|
||||||
|
configuration = Configuration([])
|
||||||
|
validated_conf = configuration._load_config_file('somefile')
|
||||||
|
assert file_mock.call_count == 1
|
||||||
|
assert validated_conf.items() >= default_conf.items()
|
||||||
|
assert 'internals' in validated_conf
|
||||||
|
assert log_has('Validating configuration ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_config_file_exception(mocker, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test Configuration._load_config_file() method
|
||||||
|
"""
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.configuration.open',
|
||||||
|
MagicMock(side_effect=FileNotFoundError('File not found'))
|
||||||
|
)
|
||||||
|
configuration = Configuration([])
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
configuration._load_config_file('somefile')
|
||||||
|
assert log_has(
|
||||||
|
'Config file "somefile" not found. Please create your config file',
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_config(default_conf, mocker) -> None:
|
||||||
|
"""
|
||||||
|
Test Configuration.load_config() without any cli params
|
||||||
|
"""
|
||||||
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(default_conf)
|
||||||
|
))
|
||||||
|
|
||||||
|
args = Arguments([], '').get_parsed_arg()
|
||||||
|
configuration = Configuration(args)
|
||||||
|
validated_conf = configuration.load_config()
|
||||||
|
|
||||||
|
assert 'strategy' in validated_conf
|
||||||
|
assert validated_conf['strategy'] == 'default_strategy'
|
||||||
|
assert 'dynamic_whitelist' not in validated_conf
|
||||||
|
assert 'dry_run_db' not in validated_conf
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_config_with_params(default_conf, mocker) -> None:
|
||||||
|
"""
|
||||||
|
Test Configuration.load_config() with cli params used
|
||||||
|
"""
|
||||||
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(default_conf)
|
||||||
|
))
|
||||||
|
|
||||||
|
args = [
|
||||||
|
'--dynamic-whitelist', '10',
|
||||||
|
'--strategy', 'test_strategy',
|
||||||
|
'--dry-run-db'
|
||||||
|
]
|
||||||
|
args = Arguments(args, '').get_parsed_arg()
|
||||||
|
|
||||||
|
configuration = Configuration(args)
|
||||||
|
validated_conf = configuration.load_config()
|
||||||
|
|
||||||
|
assert 'dynamic_whitelist' in validated_conf
|
||||||
|
assert validated_conf['dynamic_whitelist'] == 10
|
||||||
|
assert 'strategy' in validated_conf
|
||||||
|
assert validated_conf['strategy'] == 'test_strategy'
|
||||||
|
assert 'dry_run_db' in validated_conf
|
||||||
|
assert validated_conf['dry_run_db'] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_show_info(default_conf, mocker, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test Configuration.show_info()
|
||||||
|
"""
|
||||||
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(default_conf)
|
||||||
|
))
|
||||||
|
|
||||||
|
args = [
|
||||||
|
'--dynamic-whitelist', '10',
|
||||||
|
'--strategy', 'test_strategy',
|
||||||
|
'--dry-run-db'
|
||||||
|
]
|
||||||
|
args = Arguments(args, '').get_parsed_arg()
|
||||||
|
|
||||||
|
configuration = Configuration(args)
|
||||||
|
configuration.get_config()
|
||||||
|
|
||||||
|
assert log_has(
|
||||||
|
'Parameter --dynamic-whitelist detected. '
|
||||||
|
'Using dynamically generated whitelist. '
|
||||||
|
'(not applicable with Backtesting and Hyperopt)',
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
|
||||||
|
assert log_has(
|
||||||
|
'Parameter --dry-run-db detected ...',
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
|
||||||
|
assert log_has(
|
||||||
|
'Dry_run will use the DB file: "tradesv3.dry_run.sqlite"',
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test the Dry run condition
|
||||||
|
configuration.config.update({'dry_run': False})
|
||||||
|
configuration._load_common_config(configuration.config)
|
||||||
|
assert log_has(
|
||||||
|
'Dry run is disabled. (--dry_run_db ignored)',
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test setup_configuration() function
|
||||||
|
"""
|
||||||
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(default_conf)
|
||||||
|
))
|
||||||
|
|
||||||
|
args = [
|
||||||
|
'--config', 'config.json',
|
||||||
|
'--strategy', 'default_strategy',
|
||||||
|
'backtesting'
|
||||||
|
]
|
||||||
|
|
||||||
|
args = Arguments(args, '').get_parsed_arg()
|
||||||
|
|
||||||
|
configuration = Configuration(args)
|
||||||
|
config = configuration.get_config()
|
||||||
|
assert 'max_open_trades' in config
|
||||||
|
assert 'stake_currency' in config
|
||||||
|
assert 'stake_amount' in config
|
||||||
|
assert 'exchange' in config
|
||||||
|
assert 'pair_whitelist' in config['exchange']
|
||||||
|
assert 'datadir' in config
|
||||||
|
assert log_has(
|
||||||
|
'Parameter --datadir detected: {} ...'.format(config['datadir']),
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
assert 'ticker_interval' in config
|
||||||
|
assert not log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
assert 'live' not in config
|
||||||
|
assert not log_has('Parameter -l/--live detected ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
assert 'realistic_simulation' not in config
|
||||||
|
assert not log_has('Parameter --realistic-simulation detected ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
assert 'refresh_pairs' not in config
|
||||||
|
assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
assert 'timerange' not in config
|
||||||
|
assert 'export' not in config
|
||||||
|
|
||||||
|
|
||||||
|
def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test setup_configuration() function
|
||||||
|
"""
|
||||||
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(default_conf)
|
||||||
|
))
|
||||||
|
|
||||||
|
args = [
|
||||||
|
'--config', 'config.json',
|
||||||
|
'--strategy', 'default_strategy',
|
||||||
|
'--datadir', '/foo/bar',
|
||||||
|
'backtesting',
|
||||||
|
'--ticker-interval', '1',
|
||||||
|
'--live',
|
||||||
|
'--realistic-simulation',
|
||||||
|
'--refresh-pairs-cached',
|
||||||
|
'--timerange', ':100',
|
||||||
|
'--export', '/bar/foo'
|
||||||
|
]
|
||||||
|
|
||||||
|
args = Arguments(args, '').get_parsed_arg()
|
||||||
|
|
||||||
|
configuration = Configuration(args)
|
||||||
|
config = configuration.get_config()
|
||||||
|
assert 'max_open_trades' in config
|
||||||
|
assert 'stake_currency' in config
|
||||||
|
assert 'stake_amount' in config
|
||||||
|
assert 'exchange' in config
|
||||||
|
assert 'pair_whitelist' in config['exchange']
|
||||||
|
assert 'datadir' in config
|
||||||
|
assert log_has(
|
||||||
|
'Parameter --datadir detected: {} ...'.format(config['datadir']),
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
assert 'ticker_interval' in config
|
||||||
|
assert log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples)
|
||||||
|
assert log_has(
|
||||||
|
'Using ticker_interval: 1 ...',
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
|
||||||
|
assert 'live' in config
|
||||||
|
assert log_has('Parameter -l/--live detected ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
assert 'realistic_simulation'in config
|
||||||
|
assert log_has('Parameter --realistic-simulation detected ...', caplog.record_tuples)
|
||||||
|
assert log_has('Using max_open_trades: 1 ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
assert 'refresh_pairs'in config
|
||||||
|
assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
|
||||||
|
assert 'timerange' in config
|
||||||
|
assert log_has(
|
||||||
|
'Parameter --timerange detected: {} ...'.format(config['timerange']),
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
|
||||||
|
assert 'export' in config
|
||||||
|
assert log_has(
|
||||||
|
'Parameter --export detected: {} ...'.format(config['export']),
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test setup_configuration() function
|
||||||
|
"""
|
||||||
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(default_conf)
|
||||||
|
))
|
||||||
|
|
||||||
|
args = [
|
||||||
|
'hyperopt',
|
||||||
|
'--epochs', '10',
|
||||||
|
'--use-mongodb',
|
||||||
|
'--spaces', 'all',
|
||||||
|
]
|
||||||
|
|
||||||
|
args = Arguments(args, '').get_parsed_arg()
|
||||||
|
|
||||||
|
configuration = Configuration(args)
|
||||||
|
config = configuration.get_config()
|
||||||
|
|
||||||
|
assert 'epochs' in config
|
||||||
|
assert int(config['epochs']) == 10
|
||||||
|
assert log_has('Parameter --epochs detected ...', caplog.record_tuples)
|
||||||
|
assert log_has('Will run Hyperopt with for 10 epochs ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
assert 'mongodb' in config
|
||||||
|
assert config['mongodb'] is True
|
||||||
|
assert log_has('Parameter --use-mongodb detected ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
assert 'spaces' in config
|
||||||
|
assert config['spaces'] == ['all']
|
||||||
|
assert log_has('Parameter -s/--spaces detected: [\'all\']', caplog.record_tuples)
|
26
freqtrade/tests/test_constants.py
Normal file
26
freqtrade/tests/test_constants.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
"""
|
||||||
|
Unit test file for constants.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
from freqtrade.constants import Constants
|
||||||
|
|
||||||
|
|
||||||
|
def test_constant_object() -> None:
|
||||||
|
"""
|
||||||
|
Test the Constants object has the mandatory Constants
|
||||||
|
"""
|
||||||
|
assert hasattr(Constants, 'CONF_SCHEMA')
|
||||||
|
assert hasattr(Constants, 'DYNAMIC_WHITELIST')
|
||||||
|
assert hasattr(Constants, 'PROCESS_THROTTLE_SECS')
|
||||||
|
assert hasattr(Constants, 'TICKER_INTERVAL')
|
||||||
|
assert hasattr(Constants, 'HYPEROPT_EPOCH')
|
||||||
|
assert hasattr(Constants, 'RETRY_TIMEOUT')
|
||||||
|
assert hasattr(Constants, 'DEFAULT_STRATEGY')
|
||||||
|
|
||||||
|
|
||||||
|
def test_conf_schema() -> None:
|
||||||
|
"""
|
||||||
|
Test the CONF_SCHEMA is from the right type
|
||||||
|
"""
|
||||||
|
constant = Constants()
|
||||||
|
assert isinstance(constant.CONF_SCHEMA, dict)
|
@ -1,30 +1,33 @@
|
|||||||
# pragma pylint: disable=missing-docstring, C0103
|
# pragma pylint: disable=missing-docstring, C0103
|
||||||
|
|
||||||
import pandas
|
import pandas
|
||||||
import freqtrade.optimize
|
|
||||||
from freqtrade import analyze
|
from freqtrade.analyze import Analyze
|
||||||
|
from freqtrade.optimize import load_data
|
||||||
from freqtrade.strategy.strategy import Strategy
|
from freqtrade.strategy.strategy import Strategy
|
||||||
|
|
||||||
_pairs = ['BTC_ETH']
|
_pairs = ['BTC_ETH']
|
||||||
|
|
||||||
|
|
||||||
def load_dataframe_pair(pairs):
|
def load_dataframe_pair(pairs):
|
||||||
ld = freqtrade.optimize.load_data(None, ticker_interval=5, pairs=pairs)
|
ld = load_data(None, ticker_interval=5, pairs=pairs)
|
||||||
assert isinstance(ld, dict)
|
assert isinstance(ld, dict)
|
||||||
assert isinstance(pairs[0], str)
|
assert isinstance(pairs[0], str)
|
||||||
dataframe = ld[pairs[0]]
|
dataframe = ld[pairs[0]]
|
||||||
|
|
||||||
|
analyze = Analyze({'strategy': 'default_strategy'})
|
||||||
dataframe = analyze.analyze_ticker(dataframe)
|
dataframe = analyze.analyze_ticker(dataframe)
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
|
|
||||||
def test_dataframe_load():
|
def test_dataframe_load():
|
||||||
Strategy().init({'strategy': 'default_strategy'})
|
Strategy({'strategy': 'default_strategy'})
|
||||||
dataframe = load_dataframe_pair(_pairs)
|
dataframe = load_dataframe_pair(_pairs)
|
||||||
assert isinstance(dataframe, pandas.core.frame.DataFrame)
|
assert isinstance(dataframe, pandas.core.frame.DataFrame)
|
||||||
|
|
||||||
|
|
||||||
def test_dataframe_columns_exists():
|
def test_dataframe_columns_exists():
|
||||||
Strategy().init({'strategy': 'default_strategy'})
|
Strategy({'strategy': 'default_strategy'})
|
||||||
dataframe = load_dataframe_pair(_pairs)
|
dataframe = load_dataframe_pair(_pairs)
|
||||||
assert 'high' in dataframe.columns
|
assert 'high' in dataframe.columns
|
||||||
assert 'low' in dataframe.columns
|
assert 'low' in dataframe.columns
|
||||||
|
1278
freqtrade/tests/test_freqtradebot.py
Normal file
1278
freqtrade/tests/test_freqtradebot.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,5 @@
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from freqtrade.indicator_helpers import went_up, went_down
|
from freqtrade.indicator_helpers import went_up, went_down
|
||||||
|
|
||||||
|
|
||||||
|
97
freqtrade/tests/test_logger.py
Normal file
97
freqtrade/tests/test_logger.py
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
"""
|
||||||
|
Unit test file for logger.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from freqtrade.logger import Logger
|
||||||
|
|
||||||
|
|
||||||
|
def test_logger_object() -> None:
|
||||||
|
"""
|
||||||
|
Test the Constants object has the mandatory Constants
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
logger = Logger()
|
||||||
|
assert logger.name == ''
|
||||||
|
assert logger.level == 20
|
||||||
|
assert logger.level is logging.INFO
|
||||||
|
assert hasattr(logger, 'get_logger')
|
||||||
|
|
||||||
|
logger = Logger(name='Foo', level=logging.WARNING)
|
||||||
|
assert logger.name == 'Foo'
|
||||||
|
assert logger.name != ''
|
||||||
|
assert logger.level == 30
|
||||||
|
assert logger.level is logging.WARNING
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_logger() -> None:
|
||||||
|
"""
|
||||||
|
Test Logger.get_logger() and Logger._init_logger()
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
logger = Logger(name='get_logger', level=logging.WARNING)
|
||||||
|
get_logger = logger.get_logger()
|
||||||
|
assert logger.logger is get_logger
|
||||||
|
assert get_logger is not None
|
||||||
|
assert hasattr(get_logger, 'debug')
|
||||||
|
assert hasattr(get_logger, 'info')
|
||||||
|
assert hasattr(get_logger, 'warning')
|
||||||
|
assert hasattr(get_logger, 'critical')
|
||||||
|
assert hasattr(get_logger, 'exception')
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_name() -> None:
|
||||||
|
"""
|
||||||
|
Test Logger.set_name()
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
logger = Logger(name='set_name')
|
||||||
|
assert logger.name == 'set_name'
|
||||||
|
|
||||||
|
logger.set_name('set_name_new')
|
||||||
|
assert logger.name == 'set_name_new'
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_level() -> None:
|
||||||
|
"""
|
||||||
|
Test Logger.set_name()
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
logger = Logger(name='Foo', level=logging.WARNING)
|
||||||
|
assert logger.level == logging.WARNING
|
||||||
|
assert logger.get_logger().level == logging.WARNING
|
||||||
|
|
||||||
|
logger.set_level(logging.INFO)
|
||||||
|
assert logger.level == logging.INFO
|
||||||
|
assert logger.get_logger().level == logging.INFO
|
||||||
|
|
||||||
|
|
||||||
|
def test_sending_msg(caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test send a logging message
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
logger = Logger(name='sending_msg', level=logging.WARNING).get_logger()
|
||||||
|
|
||||||
|
logger.info('I am an INFO message')
|
||||||
|
assert('sending_msg', logging.INFO, 'I am an INFO message') not in caplog.record_tuples
|
||||||
|
|
||||||
|
logger.warning('I am an WARNING message')
|
||||||
|
assert ('sending_msg', logging.WARNING, 'I am an WARNING message') in caplog.record_tuples
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_format(caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test Logger.set_format()
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
log = Logger(name='set_format')
|
||||||
|
logger = log.get_logger()
|
||||||
|
|
||||||
|
logger.info('I am the first message')
|
||||||
|
assert ('set_format', logging.INFO, 'I am the first message') in caplog.record_tuples
|
||||||
|
|
||||||
|
log.set_format(log_format='%(message)s', propagate=True)
|
||||||
|
logger.info('I am the second message')
|
||||||
|
assert ('set_format', logging.INFO, 'I am the second message') in caplog.record_tuples
|
@ -1,30 +1,23 @@
|
|||||||
# pragma pylint: disable=missing-docstring, C0103
|
"""
|
||||||
import copy
|
Unit test file for main.py
|
||||||
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import arrow
|
|
||||||
import pytest
|
import pytest
|
||||||
import requests
|
|
||||||
from sqlalchemy import create_engine
|
|
||||||
|
|
||||||
import freqtrade.main as main
|
from freqtrade.main import main, set_loggers
|
||||||
from freqtrade import DependencyException, OperationalException
|
|
||||||
from freqtrade.exchange import Exchanges
|
|
||||||
from freqtrade.main import (_process, check_handle_timedout, create_trade,
|
|
||||||
execute_sell, get_target_bid, handle_trade, init)
|
|
||||||
from freqtrade.misc import State, get_state
|
|
||||||
from freqtrade.persistence import Trade
|
|
||||||
from freqtrade.tests.conftest import log_has
|
from freqtrade.tests.conftest import log_has
|
||||||
|
|
||||||
|
|
||||||
def test_parse_args_backtesting(mocker):
|
def test_parse_args_backtesting(mocker) -> None:
|
||||||
""" Test that main() can start backtesting or hyperopt.
|
"""
|
||||||
and also ensure we can pass some specific arguments
|
Test that main() can start backtesting and also ensure we can pass some specific arguments
|
||||||
further argument parsing is done in test_misc.py """
|
further argument parsing is done in test_arguments.py
|
||||||
backtesting_mock = mocker.patch(
|
"""
|
||||||
'freqtrade.optimize.backtesting.start', MagicMock())
|
backtesting_mock = mocker.patch('freqtrade.optimize.backtesting.start', MagicMock())
|
||||||
main.main(['backtesting'])
|
main(['backtesting'])
|
||||||
assert backtesting_mock.call_count == 1
|
assert backtesting_mock.call_count == 1
|
||||||
call_args = backtesting_mock.call_args[0][0]
|
call_args = backtesting_mock.call_args[0][0]
|
||||||
assert call_args.config == 'config.json'
|
assert call_args.config == 'config.json'
|
||||||
@ -35,10 +28,12 @@ def test_parse_args_backtesting(mocker):
|
|||||||
assert call_args.ticker_interval is None
|
assert call_args.ticker_interval is None
|
||||||
|
|
||||||
|
|
||||||
def test_main_start_hyperopt(mocker):
|
def test_main_start_hyperopt(mocker) -> None:
|
||||||
hyperopt_mock = mocker.patch(
|
"""
|
||||||
'freqtrade.optimize.hyperopt.start', MagicMock())
|
Test that main() can start hyperopt
|
||||||
main.main(['hyperopt'])
|
"""
|
||||||
|
hyperopt_mock = mocker.patch('freqtrade.optimize.hyperopt.start', MagicMock())
|
||||||
|
main(['hyperopt'])
|
||||||
assert hyperopt_mock.call_count == 1
|
assert hyperopt_mock.call_count == 1
|
||||||
call_args = hyperopt_mock.call_args[0][0]
|
call_args = hyperopt_mock.call_args[0][0]
|
||||||
assert call_args.config == 'config.json'
|
assert call_args.config == 'config.json'
|
||||||
@ -47,793 +42,52 @@ def test_main_start_hyperopt(mocker):
|
|||||||
assert call_args.func is not None
|
assert call_args.func is not None
|
||||||
|
|
||||||
|
|
||||||
def test_process_maybe_execute_buy(default_conf, mocker):
|
def test_set_loggers() -> None:
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
"""
|
||||||
mocker.patch('freqtrade.main.create_trade', return_value=True)
|
Test set_loggers() update the logger level for third-party libraries
|
||||||
assert main.process_maybe_execute_buy(int(default_conf['ticker_interval']))
|
"""
|
||||||
mocker.patch('freqtrade.main.create_trade', return_value=False)
|
previous_value1 = logging.getLogger('requests.packages.urllib3').level
|
||||||
assert not main.process_maybe_execute_buy(int(default_conf['ticker_interval']))
|
previous_value2 = logging.getLogger('telegram').level
|
||||||
|
|
||||||
|
set_loggers()
|
||||||
|
|
||||||
|
value1 = logging.getLogger('requests.packages.urllib3').level
|
||||||
|
assert previous_value1 is not value1
|
||||||
|
assert value1 is logging.INFO
|
||||||
|
|
||||||
|
value2 = logging.getLogger('telegram').level
|
||||||
|
assert previous_value2 is not value2
|
||||||
|
assert value2 is logging.INFO
|
||||||
|
|
||||||
|
|
||||||
def test_process_maybe_execute_sell(default_conf, mocker):
|
def test_main(mocker, caplog) -> None:
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
"""
|
||||||
mocker.patch('freqtrade.main.handle_trade', return_value=True)
|
Test main() function
|
||||||
mocker.patch('freqtrade.exchange.get_order', return_value=1)
|
In this test we are skipping the while True loop by throwing an exception.
|
||||||
trade = MagicMock()
|
"""
|
||||||
trade.open_order_id = '123'
|
mocker.patch.multiple(
|
||||||
assert not main.process_maybe_execute_sell(trade, int(default_conf['ticker_interval']))
|
'freqtrade.freqtradebot.FreqtradeBot',
|
||||||
trade.is_open = True
|
_init_modules=MagicMock(),
|
||||||
trade.open_order_id = None
|
worker=MagicMock(
|
||||||
# Assert we call handle_trade() if trade is feasible for execution
|
side_effect=KeyboardInterrupt
|
||||||
assert main.process_maybe_execute_sell(trade, int(default_conf['ticker_interval']))
|
),
|
||||||
|
clean=MagicMock(),
|
||||||
|
|
||||||
def test_process_maybe_execute_buy_exception(default_conf, mocker, caplog):
|
|
||||||
caplog.set_level(logging.INFO)
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
mocker.patch('freqtrade.main.create_trade', MagicMock(side_effect=DependencyException))
|
|
||||||
main.process_maybe_execute_buy(int(default_conf['ticker_interval']))
|
|
||||||
assert log_has('Unable to create trade: ', caplog.record_tuples)
|
|
||||||
|
|
||||||
|
|
||||||
def test_process_trade_creation(default_conf, ticker, limit_buy_order, health, mocker):
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
|
||||||
get_wallet_health=health,
|
|
||||||
buy=MagicMock(return_value='mocked_limit_buy'),
|
|
||||||
get_order=MagicMock(return_value=limit_buy_order))
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
|
||||||
|
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
|
||||||
assert not trades
|
|
||||||
|
|
||||||
result = _process(interval=int(default_conf['ticker_interval']))
|
|
||||||
assert result is True
|
|
||||||
|
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
|
||||||
assert len(trades) == 1
|
|
||||||
trade = trades[0]
|
|
||||||
assert trade is not None
|
|
||||||
assert trade.stake_amount == default_conf['stake_amount']
|
|
||||||
assert trade.is_open
|
|
||||||
assert trade.open_date is not None
|
|
||||||
assert trade.exchange == Exchanges.BITTREX.name
|
|
||||||
assert trade.open_rate == 0.00001099
|
|
||||||
assert trade.amount == 90.99181073703367
|
|
||||||
|
|
||||||
|
|
||||||
def test_process_exchange_failures(default_conf, ticker, health, mocker):
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
|
||||||
sleep_mock = mocker.patch('time.sleep', side_effect=lambda _: None)
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
|
||||||
get_wallet_health=health,
|
|
||||||
buy=MagicMock(side_effect=requests.exceptions.RequestException))
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
|
||||||
result = _process(interval=int(default_conf['ticker_interval']))
|
|
||||||
assert result is False
|
|
||||||
assert sleep_mock.has_calls()
|
|
||||||
|
|
||||||
|
|
||||||
def test_process_operational_exception(default_conf, ticker, health, mocker):
|
|
||||||
msg_mock = MagicMock()
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=msg_mock)
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
|
||||||
get_wallet_health=health,
|
|
||||||
buy=MagicMock(side_effect=OperationalException))
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
|
||||||
assert get_state() == State.RUNNING
|
|
||||||
|
|
||||||
result = _process(interval=int(default_conf['ticker_interval']))
|
|
||||||
assert result is False
|
|
||||||
assert get_state() == State.STOPPED
|
|
||||||
assert 'OperationalException' in msg_mock.call_args_list[-1][0][0]
|
|
||||||
|
|
||||||
|
|
||||||
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.rpc', init=MagicMock(), send_msg=MagicMock())
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
|
||||||
get_wallet_health=health,
|
|
||||||
buy=MagicMock(return_value='mocked_limit_buy'),
|
|
||||||
get_order=MagicMock(return_value=limit_buy_order))
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
|
||||||
|
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
|
||||||
assert not trades
|
|
||||||
result = _process(interval=int(default_conf['ticker_interval']))
|
|
||||||
assert result is True
|
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
|
||||||
assert len(trades) == 1
|
|
||||||
|
|
||||||
result = _process(interval=int(default_conf['ticker_interval']))
|
|
||||||
assert result is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_create_trade(default_conf, ticker, limit_buy_order, mocker):
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
|
||||||
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'))
|
|
||||||
# Save state of current whitelist
|
|
||||||
whitelist = copy.deepcopy(default_conf['exchange']['pair_whitelist'])
|
|
||||||
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
|
||||||
create_trade(0.001, int(default_conf['ticker_interval']))
|
|
||||||
|
|
||||||
trade = Trade.query.first()
|
|
||||||
assert trade is not None
|
|
||||||
assert trade.stake_amount == 0.001
|
|
||||||
assert trade.is_open
|
|
||||||
assert trade.open_date is not None
|
|
||||||
assert trade.exchange == Exchanges.BITTREX.name
|
|
||||||
|
|
||||||
# Simulate fulfilled LIMIT_BUY order for trade
|
|
||||||
trade.update(limit_buy_order)
|
|
||||||
|
|
||||||
assert trade.open_rate == 0.00001099
|
|
||||||
assert trade.amount == 90.99181073
|
|
||||||
|
|
||||||
assert whitelist == default_conf['exchange']['pair_whitelist']
|
|
||||||
|
|
||||||
|
|
||||||
def test_create_trade_minimal_amount(default_conf, ticker, mocker):
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
|
||||||
buy_mock = mocker.patch(
|
|
||||||
'freqtrade.main.exchange.buy', MagicMock(return_value='mocked_limit_buy')
|
|
||||||
)
|
)
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
args = ['-c', 'config.json.example']
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker)
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
|
||||||
min_stake_amount = 0.0005
|
|
||||||
create_trade(min_stake_amount, int(default_conf['ticker_interval']))
|
|
||||||
rate, amount = buy_mock.call_args[0][1], buy_mock.call_args[0][2]
|
|
||||||
assert rate * amount >= min_stake_amount
|
|
||||||
|
|
||||||
|
# Test Main + the KeyboardInterrupt exception
|
||||||
|
with pytest.raises(SystemExit) as pytest_wrapped_e:
|
||||||
|
main(args)
|
||||||
|
log_has('Starting freqtrade', caplog.record_tuples)
|
||||||
|
log_has('Got SIGINT, aborting ...', caplog.record_tuples)
|
||||||
|
assert pytest_wrapped_e.type == SystemExit
|
||||||
|
assert pytest_wrapped_e.value.code == 42
|
||||||
|
|
||||||
def test_create_trade_no_stake_amount(default_conf, ticker, mocker):
|
# Test the BaseException case
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch(
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
'freqtrade.freqtradebot.FreqtradeBot.worker',
|
||||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
MagicMock(side_effect=BaseException)
|
||||||
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(DependencyException, match=r'.*stake amount.*'):
|
|
||||||
create_trade(default_conf['stake_amount'], int(default_conf['ticker_interval']))
|
|
||||||
|
|
||||||
|
|
||||||
def test_create_trade_no_pairs(default_conf, ticker, mocker):
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
|
||||||
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(DependencyException, match=r'.*No currency pairs in whitelist.*'):
|
|
||||||
conf = copy.deepcopy(default_conf)
|
|
||||||
conf['exchange']['pair_whitelist'] = []
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
|
||||||
create_trade(default_conf['stake_amount'], int(default_conf['ticker_interval']))
|
|
||||||
|
|
||||||
|
|
||||||
def test_create_trade_no_pairs_after_blacklist(default_conf, ticker, mocker):
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
|
||||||
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(DependencyException, match=r'.*No currency pairs in whitelist.*'):
|
|
||||||
conf = copy.deepcopy(default_conf)
|
|
||||||
conf['exchange']['pair_whitelist'] = ["BTC_ETH"]
|
|
||||||
conf['exchange']['pair_blacklist'] = ["BTC_ETH"]
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
|
||||||
create_trade(default_conf['stake_amount'], int(default_conf['ticker_interval']))
|
|
||||||
|
|
||||||
|
|
||||||
def test_create_trade_no_signal(default_conf, mocker):
|
|
||||||
default_conf['dry_run'] = True
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
mocker.patch('freqtrade.main.get_signal', MagicMock(return_value=(False, False)))
|
|
||||||
mocker.patch.multiple('freqtrade.exchange',
|
|
||||||
get_ticker_history=MagicMock(return_value=20))
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
get_balance=MagicMock(return_value=20))
|
|
||||||
stake_amount = 10
|
|
||||||
Trade.query = MagicMock()
|
|
||||||
Trade.query.filter = MagicMock()
|
|
||||||
assert not create_trade(stake_amount, int(default_conf['ticker_interval']))
|
|
||||||
|
|
||||||
|
|
||||||
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_signal', side_effect=lambda s, t: (True, False))
|
|
||||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=MagicMock(return_value={
|
|
||||||
'bid': 0.00001172,
|
|
||||||
'ask': 0.00001173,
|
|
||||||
'last': 0.00001172
|
|
||||||
}),
|
|
||||||
buy=MagicMock(return_value='mocked_limit_buy'),
|
|
||||||
sell=MagicMock(return_value='mocked_limit_sell'))
|
|
||||||
mocker.patch.multiple('freqtrade.fiat_convert.Market',
|
|
||||||
ticker=MagicMock(return_value={'price_usd': 15000.0}))
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
|
||||||
create_trade(0.001, int(default_conf['ticker_interval']))
|
|
||||||
|
|
||||||
trade = Trade.query.first()
|
|
||||||
assert trade
|
|
||||||
|
|
||||||
trade.update(limit_buy_order)
|
|
||||||
assert trade.is_open is True
|
|
||||||
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (False, True))
|
|
||||||
assert handle_trade(trade, int(default_conf['ticker_interval'])) is True
|
|
||||||
assert trade.open_order_id == 'mocked_limit_sell'
|
|
||||||
|
|
||||||
# Simulate fulfilled LIMIT_SELL order for trade
|
|
||||||
trade.update(limit_sell_order)
|
|
||||||
|
|
||||||
assert trade.close_rate == 0.00001173
|
|
||||||
assert trade.close_profit == 0.06201057
|
|
||||||
assert trade.calc_profit() == 0.00006217
|
|
||||||
assert trade.close_date is not None
|
|
||||||
|
|
||||||
|
|
||||||
def test_handle_overlpapping_signals(default_conf, ticker, mocker):
|
|
||||||
default_conf.update({'experimental': {'use_sell_signal': True}})
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, 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'))
|
|
||||||
mocker.patch('freqtrade.main.min_roi_reached', return_value=False)
|
|
||||||
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
|
||||||
create_trade(0.001, int(default_conf['ticker_interval']))
|
|
||||||
|
|
||||||
# Buy and Sell triggering, so doing nothing ...
|
|
||||||
trades = Trade.query.all()
|
|
||||||
nb_trades = len(trades)
|
|
||||||
assert nb_trades == 0
|
|
||||||
|
|
||||||
# Buy is triggering, so buying ...
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
|
||||||
create_trade(0.001, int(default_conf['ticker_interval']))
|
|
||||||
trades = Trade.query.all()
|
|
||||||
nb_trades = len(trades)
|
|
||||||
assert nb_trades == 1
|
|
||||||
assert trades[0].is_open is True
|
|
||||||
|
|
||||||
# Buy and Sell are not triggering, so doing nothing ...
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (False, False))
|
|
||||||
assert handle_trade(trades[0], int(default_conf['ticker_interval'])) is False
|
|
||||||
trades = Trade.query.all()
|
|
||||||
nb_trades = len(trades)
|
|
||||||
assert nb_trades == 1
|
|
||||||
assert trades[0].is_open is True
|
|
||||||
|
|
||||||
# Buy and Sell are triggering, so doing nothing ...
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, True))
|
|
||||||
assert handle_trade(trades[0], int(default_conf['ticker_interval'])) is False
|
|
||||||
trades = Trade.query.all()
|
|
||||||
nb_trades = len(trades)
|
|
||||||
assert nb_trades == 1
|
|
||||||
assert trades[0].is_open is True
|
|
||||||
|
|
||||||
# Sell is triggering, guess what : we are Selling!
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (False, True))
|
|
||||||
trades = Trade.query.all()
|
|
||||||
assert handle_trade(trades[0], int(default_conf['ticker_interval'])) is True
|
|
||||||
|
|
||||||
|
|
||||||
def test_handle_trade_roi(default_conf, ticker, mocker, caplog):
|
|
||||||
caplog.set_level(logging.DEBUG)
|
|
||||||
default_conf.update({'experimental': {'use_sell_signal': True}})
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
|
||||||
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'))
|
|
||||||
mocker.patch('freqtrade.main.min_roi_reached', return_value=True)
|
|
||||||
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
|
||||||
create_trade(0.001, int(default_conf['ticker_interval']))
|
|
||||||
|
|
||||||
trade = Trade.query.first()
|
|
||||||
trade.is_open = True
|
|
||||||
|
|
||||||
# FIX: sniffing logs, suggest handle_trade should not execute_sell
|
|
||||||
# instead that responsibility should be moved out of handle_trade(),
|
|
||||||
# we might just want to check if we are in a sell condition without
|
|
||||||
# executing
|
|
||||||
# if ROI is reached we must sell
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (False, True))
|
|
||||||
assert handle_trade(trade, interval=int(default_conf['ticker_interval']))
|
|
||||||
assert log_has('Required profit reached. Selling..', caplog.record_tuples)
|
|
||||||
# if ROI is reached we must sell even if sell-signal is not signalled
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (False, True))
|
|
||||||
assert handle_trade(trade, interval=int(default_conf['ticker_interval']))
|
|
||||||
assert log_has('Required profit reached. Selling..', caplog.record_tuples)
|
|
||||||
|
|
||||||
|
|
||||||
def test_handle_trade_experimental(default_conf, ticker, mocker, caplog):
|
|
||||||
caplog.set_level(logging.DEBUG)
|
|
||||||
default_conf.update({'experimental': {'use_sell_signal': True}})
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
|
||||||
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'))
|
|
||||||
mocker.patch('freqtrade.main.min_roi_reached', return_value=False)
|
|
||||||
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
|
||||||
create_trade(0.001, int(default_conf['ticker_interval']))
|
|
||||||
|
|
||||||
trade = Trade.query.first()
|
|
||||||
trade.is_open = True
|
|
||||||
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (False, False))
|
|
||||||
value_returned = handle_trade(trade, int(default_conf['ticker_interval']))
|
|
||||||
assert value_returned is False
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (False, True))
|
|
||||||
assert handle_trade(trade, int(default_conf['ticker_interval']))
|
|
||||||
assert log_has('Sell signal received. Selling..', caplog.record_tuples)
|
|
||||||
|
|
||||||
|
|
||||||
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_signal', side_effect=lambda s, t: (True, False))
|
|
||||||
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'))
|
|
||||||
|
|
||||||
# Create trade and sell it
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
|
||||||
create_trade(0.001, int(default_conf['ticker_interval']))
|
|
||||||
|
|
||||||
trade = Trade.query.first()
|
|
||||||
assert trade
|
|
||||||
|
|
||||||
trade.update(limit_buy_order)
|
|
||||||
trade.update(limit_sell_order)
|
|
||||||
assert trade.is_open is False
|
|
||||||
|
|
||||||
with pytest.raises(ValueError, match=r'.*closed trade.*'):
|
|
||||||
handle_trade(trade, int(default_conf['ticker_interval']))
|
|
||||||
|
|
||||||
|
|
||||||
def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, mocker):
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
cancel_order_mock = MagicMock()
|
|
||||||
mocker.patch('freqtrade.rpc.init', MagicMock())
|
|
||||||
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
|
||||||
get_order=MagicMock(return_value=limit_buy_order_old),
|
|
||||||
cancel_order=cancel_order_mock)
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
|
||||||
|
|
||||||
trade_buy = Trade(
|
|
||||||
pair='BTC_ETH',
|
|
||||||
open_rate=0.00001099,
|
|
||||||
exchange='BITTREX',
|
|
||||||
open_order_id='123456789',
|
|
||||||
amount=90.99181073,
|
|
||||||
fee=0.0,
|
|
||||||
stake_amount=1,
|
|
||||||
open_date=arrow.utcnow().shift(minutes=-601).datetime,
|
|
||||||
is_open=True
|
|
||||||
)
|
)
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
Trade.session.add(trade_buy)
|
main(args)
|
||||||
|
log_has('Got fatal exception!', caplog.record_tuples)
|
||||||
# check it does cancel buy orders over the time limit
|
|
||||||
check_handle_timedout(600)
|
|
||||||
assert cancel_order_mock.call_count == 1
|
|
||||||
assert rpc_mock.call_count == 1
|
|
||||||
trades = Trade.query.filter(Trade.open_order_id.is_(trade_buy.open_order_id)).all()
|
|
||||||
nb_trades = len(trades)
|
|
||||||
assert nb_trades == 0
|
|
||||||
|
|
||||||
|
|
||||||
def test_handle_timedout_limit_buy(mocker):
|
|
||||||
cancel_order = MagicMock()
|
|
||||||
mocker.patch('freqtrade.exchange.cancel_order', cancel_order)
|
|
||||||
Trade.session = MagicMock()
|
|
||||||
trade = MagicMock()
|
|
||||||
order = {'remaining': 1,
|
|
||||||
'amount': 1}
|
|
||||||
assert main.handle_timedout_limit_buy(trade, order)
|
|
||||||
assert cancel_order.call_count == 1
|
|
||||||
order['amount'] = 2
|
|
||||||
assert not main.handle_timedout_limit_buy(trade, order)
|
|
||||||
assert cancel_order.call_count == 2
|
|
||||||
|
|
||||||
|
|
||||||
def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, mocker):
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
cancel_order_mock = MagicMock()
|
|
||||||
mocker.patch('freqtrade.rpc.init', MagicMock())
|
|
||||||
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
|
||||||
get_order=MagicMock(return_value=limit_sell_order_old),
|
|
||||||
cancel_order=cancel_order_mock)
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
|
||||||
|
|
||||||
trade_sell = Trade(
|
|
||||||
pair='BTC_ETH',
|
|
||||||
open_rate=0.00001099,
|
|
||||||
exchange='BITTREX',
|
|
||||||
open_order_id='123456789',
|
|
||||||
amount=90.99181073,
|
|
||||||
fee=0.0,
|
|
||||||
stake_amount=1,
|
|
||||||
open_date=arrow.utcnow().shift(hours=-5).datetime,
|
|
||||||
close_date=arrow.utcnow().shift(minutes=-601).datetime,
|
|
||||||
is_open=False
|
|
||||||
)
|
|
||||||
|
|
||||||
Trade.session.add(trade_sell)
|
|
||||||
|
|
||||||
# check it does cancel sell orders over the time limit
|
|
||||||
check_handle_timedout(600)
|
|
||||||
assert cancel_order_mock.call_count == 1
|
|
||||||
assert rpc_mock.call_count == 1
|
|
||||||
assert trade_sell.is_open is True
|
|
||||||
|
|
||||||
|
|
||||||
def test_handle_timedout_limit_sell(mocker):
|
|
||||||
cancel_order = MagicMock()
|
|
||||||
mocker.patch('freqtrade.exchange.cancel_order', cancel_order)
|
|
||||||
trade = MagicMock()
|
|
||||||
order = {'remaining': 1,
|
|
||||||
'amount': 1}
|
|
||||||
assert main.handle_timedout_limit_sell(trade, order)
|
|
||||||
assert cancel_order.call_count == 1
|
|
||||||
order['amount'] = 2
|
|
||||||
assert not main.handle_timedout_limit_sell(trade, order)
|
|
||||||
# Assert cancel_order was not called (callcount remains unchanged)
|
|
||||||
assert cancel_order.call_count == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old_partial,
|
|
||||||
mocker):
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
cancel_order_mock = MagicMock()
|
|
||||||
mocker.patch('freqtrade.rpc.init', MagicMock())
|
|
||||||
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
|
||||||
get_order=MagicMock(return_value=limit_buy_order_old_partial),
|
|
||||||
cancel_order=cancel_order_mock)
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
|
||||||
|
|
||||||
trade_buy = Trade(
|
|
||||||
pair='BTC_ETH',
|
|
||||||
open_rate=0.00001099,
|
|
||||||
exchange='BITTREX',
|
|
||||||
open_order_id='123456789',
|
|
||||||
amount=90.99181073,
|
|
||||||
fee=0.0,
|
|
||||||
stake_amount=1,
|
|
||||||
open_date=arrow.utcnow().shift(minutes=-601).datetime,
|
|
||||||
is_open=True
|
|
||||||
)
|
|
||||||
|
|
||||||
Trade.session.add(trade_buy)
|
|
||||||
|
|
||||||
# check it does cancel buy orders over the time limit
|
|
||||||
# note this is for a partially-complete buy order
|
|
||||||
check_handle_timedout(600)
|
|
||||||
assert cancel_order_mock.call_count == 1
|
|
||||||
assert rpc_mock.call_count == 1
|
|
||||||
trades = Trade.query.filter(Trade.open_order_id.is_(trade_buy.open_order_id)).all()
|
|
||||||
assert len(trades) == 1
|
|
||||||
assert trades[0].amount == 23.0
|
|
||||||
assert trades[0].stake_amount == trade_buy.open_rate * trades[0].amount
|
|
||||||
|
|
||||||
|
|
||||||
def test_balance_fully_ask_side(mocker):
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 0.0}})
|
|
||||||
assert get_target_bid({'ask': 20, 'last': 10}) == 20
|
|
||||||
|
|
||||||
|
|
||||||
def test_balance_fully_last_side(mocker):
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}})
|
|
||||||
assert get_target_bid({'ask': 20, 'last': 10}) == 10
|
|
||||||
|
|
||||||
|
|
||||||
def test_balance_bigger_last_ask(mocker):
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}})
|
|
||||||
assert get_target_bid({'ask': 5, 'last': 10}) == 5
|
|
||||||
|
|
||||||
|
|
||||||
def test_execute_sell_up(default_conf, ticker, ticker_sell_up, mocker):
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
|
||||||
mocker.patch('freqtrade.rpc.init', MagicMock())
|
|
||||||
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker)
|
|
||||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
|
||||||
|
|
||||||
# Create some test data
|
|
||||||
create_trade(0.001, int(default_conf['ticker_interval']))
|
|
||||||
|
|
||||||
trade = Trade.query.first()
|
|
||||||
assert trade
|
|
||||||
|
|
||||||
# Increase the price and sell it
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker_sell_up)
|
|
||||||
|
|
||||||
execute_sell(trade=trade, limit=ticker_sell_up()['bid'])
|
|
||||||
|
|
||||||
assert rpc_mock.call_count == 2
|
|
||||||
assert 'Selling' in rpc_mock.call_args_list[-1][0][0]
|
|
||||||
assert '[BTC_ETH]' in rpc_mock.call_args_list[-1][0][0]
|
|
||||||
assert 'Amount' in rpc_mock.call_args_list[-1][0][0]
|
|
||||||
assert 'Profit' in rpc_mock.call_args_list[-1][0][0]
|
|
||||||
assert '0.00001172' in rpc_mock.call_args_list[-1][0][0]
|
|
||||||
assert 'profit: 6.11%, 0.00006126' in rpc_mock.call_args_list[-1][0][0]
|
|
||||||
assert '0.919 USD' in rpc_mock.call_args_list[-1][0][0]
|
|
||||||
|
|
||||||
|
|
||||||
def test_execute_sell_down(default_conf, ticker, ticker_sell_down, mocker):
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
|
||||||
mocker.patch('freqtrade.rpc.init', MagicMock())
|
|
||||||
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
|
||||||
mocker.patch.multiple('freqtrade.rpc.telegram',
|
|
||||||
_CONF=default_conf,
|
|
||||||
init=MagicMock(),
|
|
||||||
send_msg=MagicMock())
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker)
|
|
||||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
|
||||||
|
|
||||||
# Create some test data
|
|
||||||
create_trade(0.001, int(default_conf['ticker_interval']))
|
|
||||||
|
|
||||||
trade = Trade.query.first()
|
|
||||||
assert trade
|
|
||||||
|
|
||||||
# Decrease the price and sell it
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker_sell_down)
|
|
||||||
|
|
||||||
execute_sell(trade=trade, limit=ticker_sell_down()['bid'])
|
|
||||||
|
|
||||||
assert rpc_mock.call_count == 2
|
|
||||||
assert 'Selling' in rpc_mock.call_args_list[-1][0][0]
|
|
||||||
assert '[BTC_ETH]' in rpc_mock.call_args_list[-1][0][0]
|
|
||||||
assert 'Amount' in rpc_mock.call_args_list[-1][0][0]
|
|
||||||
assert '0.00001044' in rpc_mock.call_args_list[-1][0][0]
|
|
||||||
assert 'loss: -5.48%, -0.00005492' in rpc_mock.call_args_list[-1][0][0]
|
|
||||||
assert '-0.824 USD' in rpc_mock.call_args_list[-1][0][0]
|
|
||||||
|
|
||||||
|
|
||||||
def test_execute_sell_without_conf_sell_down(default_conf, ticker, ticker_sell_down, mocker):
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
|
||||||
mocker.patch('freqtrade.rpc.init', MagicMock())
|
|
||||||
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker)
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
|
||||||
|
|
||||||
# Create some test data
|
|
||||||
create_trade(0.001, int(default_conf['ticker_interval']))
|
|
||||||
|
|
||||||
trade = Trade.query.first()
|
|
||||||
assert trade
|
|
||||||
|
|
||||||
# Decrease the price and sell it
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker_sell_down)
|
|
||||||
mocker.patch('freqtrade.main._CONF', {})
|
|
||||||
|
|
||||||
execute_sell(trade=trade, limit=ticker_sell_down()['bid'])
|
|
||||||
|
|
||||||
assert rpc_mock.call_count == 2
|
|
||||||
assert 'Selling' in rpc_mock.call_args_list[-1][0][0]
|
|
||||||
assert '[BTC_ETH]' in rpc_mock.call_args_list[-1][0][0]
|
|
||||||
assert '0.00001044' in rpc_mock.call_args_list[-1][0][0]
|
|
||||||
assert 'loss: -5.48%, -0.00005492' in rpc_mock.call_args_list[-1][0][0]
|
|
||||||
|
|
||||||
|
|
||||||
def test_execute_sell_without_conf_sell_up(default_conf, ticker, ticker_sell_up, mocker):
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
|
||||||
mocker.patch('freqtrade.rpc.init', MagicMock())
|
|
||||||
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker)
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
|
||||||
|
|
||||||
# Create some test data
|
|
||||||
create_trade(0.001, int(default_conf['ticker_interval']))
|
|
||||||
|
|
||||||
trade = Trade.query.first()
|
|
||||||
assert trade
|
|
||||||
|
|
||||||
# Increase the price and sell it
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker_sell_up)
|
|
||||||
mocker.patch('freqtrade.main._CONF', {})
|
|
||||||
|
|
||||||
execute_sell(trade=trade, limit=ticker_sell_up()['bid'])
|
|
||||||
|
|
||||||
assert rpc_mock.call_count == 2
|
|
||||||
assert 'Selling' in rpc_mock.call_args_list[-1][0][0]
|
|
||||||
assert '[BTC_ETH]' in rpc_mock.call_args_list[-1][0][0]
|
|
||||||
assert 'Amount' in rpc_mock.call_args_list[-1][0][0]
|
|
||||||
assert '0.00001172' in rpc_mock.call_args_list[-1][0][0]
|
|
||||||
assert '(profit: 6.11%, 0.00006126)' in rpc_mock.call_args_list[-1][0][0]
|
|
||||||
assert 'USD' not in rpc_mock.call_args_list[-1][0][0]
|
|
||||||
|
|
||||||
|
|
||||||
def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, mocker):
|
|
||||||
default_conf['experimental'] = {
|
|
||||||
'use_sell_signal': True,
|
|
||||||
'sell_profit_only': True,
|
|
||||||
}
|
|
||||||
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
mocker.patch('freqtrade.main.min_roi_reached', return_value=False)
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
|
||||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=MagicMock(return_value={
|
|
||||||
'bid': 0.00002172,
|
|
||||||
'ask': 0.00002173,
|
|
||||||
'last': 0.00002172
|
|
||||||
}),
|
|
||||||
buy=MagicMock(return_value='mocked_limit_buy'))
|
|
||||||
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
|
||||||
create_trade(0.001, int(default_conf['ticker_interval']))
|
|
||||||
|
|
||||||
trade = Trade.query.first()
|
|
||||||
trade.update(limit_buy_order)
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (False, True))
|
|
||||||
assert handle_trade(trade, int(default_conf['ticker_interval'])) is True
|
|
||||||
|
|
||||||
|
|
||||||
def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, mocker):
|
|
||||||
default_conf['experimental'] = {
|
|
||||||
'use_sell_signal': True,
|
|
||||||
'sell_profit_only': False,
|
|
||||||
}
|
|
||||||
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
mocker.patch('freqtrade.main.min_roi_reached', return_value=False)
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
|
||||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=MagicMock(return_value={
|
|
||||||
'bid': 0.00002172,
|
|
||||||
'ask': 0.00002173,
|
|
||||||
'last': 0.00002172
|
|
||||||
}),
|
|
||||||
buy=MagicMock(return_value='mocked_limit_buy'))
|
|
||||||
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
|
||||||
create_trade(0.001, int(default_conf['ticker_interval']))
|
|
||||||
|
|
||||||
trade = Trade.query.first()
|
|
||||||
trade.update(limit_buy_order)
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (False, True))
|
|
||||||
assert handle_trade(trade, int(default_conf['ticker_interval'])) is True
|
|
||||||
|
|
||||||
|
|
||||||
def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, mocker):
|
|
||||||
default_conf['experimental'] = {
|
|
||||||
'use_sell_signal': True,
|
|
||||||
'sell_profit_only': True,
|
|
||||||
}
|
|
||||||
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
mocker.patch('freqtrade.main.min_roi_reached', return_value=False)
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
|
||||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=MagicMock(return_value={
|
|
||||||
'bid': 0.00000172,
|
|
||||||
'ask': 0.00000173,
|
|
||||||
'last': 0.00000172
|
|
||||||
}),
|
|
||||||
buy=MagicMock(return_value='mocked_limit_buy'))
|
|
||||||
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
|
||||||
create_trade(0.001, int(default_conf['ticker_interval']))
|
|
||||||
|
|
||||||
trade = Trade.query.first()
|
|
||||||
trade.update(limit_buy_order)
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (False, True))
|
|
||||||
assert handle_trade(trade, int(default_conf['ticker_interval'])) is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, mocker):
|
|
||||||
default_conf['experimental'] = {
|
|
||||||
'use_sell_signal': True,
|
|
||||||
'sell_profit_only': False,
|
|
||||||
}
|
|
||||||
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
mocker.patch('freqtrade.main.min_roi_reached', return_value=False)
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
|
||||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=MagicMock(return_value={
|
|
||||||
'bid': 0.00000172,
|
|
||||||
'ask': 0.00000173,
|
|
||||||
'last': 0.00000172
|
|
||||||
}),
|
|
||||||
buy=MagicMock(return_value='mocked_limit_buy'))
|
|
||||||
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
|
||||||
create_trade(0.001, int(default_conf['ticker_interval']))
|
|
||||||
|
|
||||||
trade = Trade.query.first()
|
|
||||||
trade.update(limit_buy_order)
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (False, True))
|
|
||||||
assert handle_trade(trade, int(default_conf['ticker_interval'])) is True
|
|
||||||
|
@ -1,188 +1,34 @@
|
|||||||
# pragma pylint: disable=missing-docstring,C0103
|
# pragma pylint: disable=missing-docstring,C0103
|
||||||
import argparse
|
|
||||||
import json
|
"""
|
||||||
import time
|
Unit test file for misc.py
|
||||||
from copy import deepcopy
|
"""
|
||||||
from unittest.mock import MagicMock
|
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import pytest
|
from unittest.mock import MagicMock
|
||||||
from jsonschema import ValidationError
|
|
||||||
from freqtrade.analyze import parse_ticker_dataframe
|
from freqtrade.analyze import Analyze
|
||||||
from freqtrade.misc import (common_args_parser, file_dump_json, load_config,
|
from freqtrade.misc import (shorten_date, datesarray_to_datetimearray,
|
||||||
parse_args, parse_timerange, throttle, datesarray_to_datetimearray)
|
common_datearray, file_dump_json)
|
||||||
|
from freqtrade.optimize.__init__ import load_tickerdata_file
|
||||||
|
|
||||||
|
|
||||||
def test_throttle():
|
def test_shorten_date() -> None:
|
||||||
|
"""
|
||||||
def func():
|
Test shorten_date() function
|
||||||
return 42
|
:return: None
|
||||||
|
"""
|
||||||
start = time.time()
|
str_data = '1 day, 2 hours, 3 minutes, 4 seconds ago'
|
||||||
result = throttle(func, min_secs=0.1)
|
str_shorten_data = '1 d, 2 h, 3 min, 4 sec ago'
|
||||||
end = time.time()
|
assert shorten_date(str_data) == str_shorten_data
|
||||||
|
|
||||||
assert result == 42
|
|
||||||
assert end - start > 0.1
|
|
||||||
|
|
||||||
result = throttle(func, min_secs=-1)
|
|
||||||
assert result == 42
|
|
||||||
|
|
||||||
|
|
||||||
def test_throttle_with_assets():
|
|
||||||
|
|
||||||
def func(nb_assets=-1):
|
|
||||||
return nb_assets
|
|
||||||
|
|
||||||
result = throttle(func, min_secs=0.1, nb_assets=666)
|
|
||||||
assert result == 666
|
|
||||||
|
|
||||||
result = throttle(func, min_secs=0.1)
|
|
||||||
assert result == -1
|
|
||||||
|
|
||||||
|
|
||||||
# Parse common command-line-arguments. Used for all tools
|
|
||||||
|
|
||||||
def test_parse_args_none():
|
|
||||||
args = common_args_parser('')
|
|
||||||
assert isinstance(args, argparse.ArgumentParser)
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_args_defaults():
|
|
||||||
args = parse_args([], '')
|
|
||||||
assert args.config == 'config.json'
|
|
||||||
assert args.dynamic_whitelist is None
|
|
||||||
assert args.loglevel == 20
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_args_config():
|
|
||||||
args = parse_args(['-c', '/dev/null'], '')
|
|
||||||
assert args.config == '/dev/null'
|
|
||||||
|
|
||||||
args = parse_args(['--config', '/dev/null'], '')
|
|
||||||
assert args.config == '/dev/null'
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_args_verbose():
|
|
||||||
args = parse_args(['-v'], '')
|
|
||||||
assert args.loglevel == 10
|
|
||||||
|
|
||||||
args = parse_args(['--verbose'], '')
|
|
||||||
assert args.loglevel == 10
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_args_version():
|
|
||||||
with pytest.raises(SystemExit, match=r'0'):
|
|
||||||
parse_args(['--version'], '')
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_args_invalid():
|
|
||||||
with pytest.raises(SystemExit, match=r'2'):
|
|
||||||
parse_args(['-c'], '')
|
|
||||||
|
|
||||||
|
|
||||||
# Parse command-line-arguments
|
|
||||||
# used for main, backtesting and hyperopt
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_args_dynamic_whitelist():
|
|
||||||
args = parse_args(['--dynamic-whitelist'], '')
|
|
||||||
assert args.dynamic_whitelist == 20
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_args_dynamic_whitelist_10():
|
|
||||||
args = parse_args(['--dynamic-whitelist', '10'], '')
|
|
||||||
assert args.dynamic_whitelist == 10
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_args_dynamic_whitelist_invalid_values():
|
|
||||||
with pytest.raises(SystemExit, match=r'2'):
|
|
||||||
parse_args(['--dynamic-whitelist', 'abc'], '')
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_args_backtesting_invalid():
|
|
||||||
with pytest.raises(SystemExit, match=r'2'):
|
|
||||||
parse_args(['backtesting --ticker-interval'], '')
|
|
||||||
|
|
||||||
with pytest.raises(SystemExit, match=r'2'):
|
|
||||||
parse_args(['backtesting --ticker-interval', 'abc'], '')
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_args_backtesting_custom():
|
|
||||||
args = [
|
|
||||||
'-c', 'test_conf.json',
|
|
||||||
'backtesting',
|
|
||||||
'--live',
|
|
||||||
'--ticker-interval', '1',
|
|
||||||
'--refresh-pairs-cached']
|
|
||||||
call_args = parse_args(args, '')
|
|
||||||
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
|
|
||||||
assert call_args.refresh_pairs is True
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_args_hyperopt_custom():
|
|
||||||
args = ['-c', 'test_conf.json', 'hyperopt', '--epochs', '20']
|
|
||||||
call_args = parse_args(args, '')
|
|
||||||
assert call_args.config == 'test_conf.json'
|
|
||||||
assert call_args.epochs == 20
|
|
||||||
assert call_args.loglevel == 20
|
|
||||||
assert call_args.subparser == 'hyperopt'
|
|
||||||
assert call_args.func is not None
|
|
||||||
|
|
||||||
|
|
||||||
def test_file_dump_json(mocker):
|
|
||||||
file_open = mocker.patch('freqtrade.misc.open', MagicMock())
|
|
||||||
json_dump = mocker.patch('json.dump', MagicMock())
|
|
||||||
file_dump_json('somefile', [1, 2, 3])
|
|
||||||
assert file_open.call_count == 1
|
|
||||||
assert json_dump.call_count == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_timerange_incorrect():
|
|
||||||
assert ((None, 'line'), None, -200) == parse_timerange('-200')
|
|
||||||
assert (('line', None), 200, None) == parse_timerange('200-')
|
|
||||||
with pytest.raises(Exception, match=r'Incorrect syntax.*'):
|
|
||||||
parse_timerange('-')
|
|
||||||
|
|
||||||
|
|
||||||
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')
|
|
||||||
|
|
||||||
|
|
||||||
def test_datesarray_to_datetimearray(ticker_history):
|
def test_datesarray_to_datetimearray(ticker_history):
|
||||||
dataframes = parse_ticker_dataframe(ticker_history)
|
"""
|
||||||
|
Test datesarray_to_datetimearray() function
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
dataframes = Analyze.parse_ticker_dataframe(ticker_history)
|
||||||
dates = datesarray_to_datetimearray(dataframes['date'])
|
dates = datesarray_to_datetimearray(dataframes['date'])
|
||||||
|
|
||||||
assert isinstance(dates[0], datetime.datetime)
|
assert isinstance(dates[0], datetime.datetime)
|
||||||
@ -194,3 +40,34 @@ def test_datesarray_to_datetimearray(ticker_history):
|
|||||||
|
|
||||||
date_len = len(dates)
|
date_len = len(dates)
|
||||||
assert date_len == 3
|
assert date_len == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_common_datearray(default_conf, mocker) -> None:
|
||||||
|
"""
|
||||||
|
Test common_datearray()
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
mocker.patch('freqtrade.strategy.strategy.Strategy', MagicMock())
|
||||||
|
|
||||||
|
analyze = Analyze(default_conf)
|
||||||
|
tick = load_tickerdata_file(None, 'BTC_UNITEST', 1)
|
||||||
|
tickerlist = {'BTC_UNITEST': tick}
|
||||||
|
dataframes = analyze.tickerdata_to_dataframe(tickerlist)
|
||||||
|
|
||||||
|
dates = common_datearray(dataframes)
|
||||||
|
|
||||||
|
assert dates.size == dataframes['BTC_UNITEST']['date'].size
|
||||||
|
assert dates[0] == dataframes['BTC_UNITEST']['date'][0]
|
||||||
|
assert dates[-1] == dataframes['BTC_UNITEST']['date'][-1]
|
||||||
|
|
||||||
|
|
||||||
|
def test_file_dump_json(mocker) -> None:
|
||||||
|
"""
|
||||||
|
Test file_dump_json()
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
file_open = mocker.patch('freqtrade.misc.open', MagicMock())
|
||||||
|
json_dump = mocker.patch('json.dump', MagicMock())
|
||||||
|
file_dump_json('somefile', [1, 2, 3])
|
||||||
|
assert file_open.call_count == 1
|
||||||
|
assert json_dump.call_count == 1
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
# pragma pylint: disable=missing-docstring, C0103
|
# pragma pylint: disable=missing-docstring, C0103
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
|
|
||||||
from freqtrade.exchange import Exchanges
|
from freqtrade.exchange import Exchanges
|
||||||
from freqtrade.persistence import Trade, init, clean_dry_run_db
|
from freqtrade.persistence import Trade, init, clean_dry_run_db
|
||||||
|
|
||||||
|
14
freqtrade/tests/test_state.py
Normal file
14
freqtrade/tests/test_state.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
"""
|
||||||
|
Unit test file for constants.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
from freqtrade.state import State
|
||||||
|
|
||||||
|
|
||||||
|
def test_state_object() -> None:
|
||||||
|
"""
|
||||||
|
Test the State object has the mandatory states
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
assert hasattr(State, 'RUNNING')
|
||||||
|
assert hasattr(State, 'STOPPED')
|
@ -1,12 +1,12 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
"""This script generate json data from bittrex"""
|
"""This script generate json data from bittrex"""
|
||||||
import sys
|
|
||||||
import json
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
from freqtrade import exchange
|
from freqtrade import exchange
|
||||||
from freqtrade.exchange import Bittrex
|
|
||||||
from freqtrade import misc
|
from freqtrade import misc
|
||||||
|
from freqtrade.exchange import Bittrex
|
||||||
|
|
||||||
parser = misc.common_args_parser('download utility')
|
parser = misc.common_args_parser('download utility')
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
|
@ -1,41 +1,55 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Script to display when the bot will buy a specific pair
|
||||||
|
|
||||||
|
Mandatory Cli parameters:
|
||||||
|
-p / --pair: pair to examine
|
||||||
|
|
||||||
|
Optional Cli parameters
|
||||||
|
-s / --strategy: strategy to use
|
||||||
|
-d / --datadir: path to pair backtest data
|
||||||
|
--timerange: specify what timerange of data to use.
|
||||||
|
-l / --live: Live, to download the latest ticker for the pair
|
||||||
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import logging
|
from argparse import Namespace
|
||||||
|
|
||||||
|
from typing import List
|
||||||
|
|
||||||
from plotly import tools
|
from plotly import tools
|
||||||
from plotly.offline import plot
|
from plotly.offline import plot
|
||||||
import plotly.graph_objs as go
|
import plotly.graph_objs as go
|
||||||
|
|
||||||
from freqtrade import exchange, analyze
|
from freqtrade.arguments import Arguments
|
||||||
from freqtrade.strategy.strategy import Strategy
|
from freqtrade.analyze import Analyze
|
||||||
import freqtrade.misc as misc
|
from freqtrade import exchange
|
||||||
|
from freqtrade.logger import Logger
|
||||||
import freqtrade.optimize as optimize
|
import freqtrade.optimize as optimize
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = Logger(name="Graph dataframe").get_logger()
|
||||||
|
|
||||||
|
|
||||||
def plot_parse_args(args):
|
def plot_analyzed_dataframe(args: Namespace) -> None:
|
||||||
parser = misc.common_args_parser('Graph dataframe')
|
|
||||||
misc.backtesting_options(parser)
|
|
||||||
misc.scripts_options(parser)
|
|
||||||
return parser.parse_args(args)
|
|
||||||
|
|
||||||
|
|
||||||
def plot_analyzed_dataframe(args) -> None:
|
|
||||||
"""
|
"""
|
||||||
Calls analyze() and plots the returned dataframe
|
Calls analyze() and plots the returned dataframe
|
||||||
:param pair: pair as str
|
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
pair = args.pair.replace('-', '_')
|
pair = args.pair.replace('-', '_')
|
||||||
timerange = misc.parse_timerange(args.timerange)
|
timerange = Arguments.parse_timerange(args.timerange)
|
||||||
|
|
||||||
# Init strategy
|
# Init strategy
|
||||||
strategy = Strategy()
|
try:
|
||||||
strategy.init({'strategy': args.strategy})
|
analyze = Analyze({'strategy': args.strategy})
|
||||||
tick_interval = strategy.ticker_interval
|
except AttributeError:
|
||||||
|
logger.critical(
|
||||||
|
'Impossible to load the strategy. Please check the file "user_data/strategies/%s.py"',
|
||||||
|
args.strategy
|
||||||
|
)
|
||||||
|
exit()
|
||||||
|
|
||||||
|
tick_interval = analyze.strategy.ticker_interval
|
||||||
|
|
||||||
tickers = {}
|
tickers = {}
|
||||||
if args.live:
|
if args.live:
|
||||||
@ -44,27 +58,32 @@ def plot_analyzed_dataframe(args) -> None:
|
|||||||
exchange._API = exchange.Bittrex({'key': '', 'secret': ''})
|
exchange._API = exchange.Bittrex({'key': '', 'secret': ''})
|
||||||
tickers[pair] = exchange.get_ticker_history(pair, tick_interval)
|
tickers[pair] = exchange.get_ticker_history(pair, tick_interval)
|
||||||
else:
|
else:
|
||||||
tickers = optimize.load_data(args.datadir, pairs=[pair],
|
tickers = optimize.load_data(
|
||||||
ticker_interval=tick_interval,
|
datadir=args.datadir,
|
||||||
refresh_pairs=False,
|
pairs=[pair],
|
||||||
timerange=timerange)
|
ticker_interval=tick_interval,
|
||||||
dataframes = optimize.tickerdata_to_dataframe(tickers)
|
refresh_pairs=False,
|
||||||
|
timerange=timerange
|
||||||
|
)
|
||||||
|
dataframes = analyze.tickerdata_to_dataframe(tickers)
|
||||||
dataframe = dataframes[pair]
|
dataframe = dataframes[pair]
|
||||||
dataframe = analyze.populate_buy_trend(dataframe)
|
dataframe = analyze.populate_buy_trend(dataframe)
|
||||||
dataframe = analyze.populate_sell_trend(dataframe)
|
dataframe = analyze.populate_sell_trend(dataframe)
|
||||||
|
|
||||||
if (len(dataframe.index) > 750):
|
if len(dataframe.index) > 750:
|
||||||
logger.warn('Ticker contained more than 750 candles, clipping.')
|
logger.warning('Ticker contained more than 750 candles, clipping.')
|
||||||
df = dataframe.tail(750)
|
data = dataframe.tail(750)
|
||||||
|
|
||||||
candles = go.Candlestick(x=df.date,
|
candles = go.Candlestick(
|
||||||
open=df.open,
|
x=data.date,
|
||||||
high=df.high,
|
open=data.open,
|
||||||
low=df.low,
|
high=data.high,
|
||||||
close=df.close,
|
low=data.low,
|
||||||
name='Price')
|
close=data.close,
|
||||||
|
name='Price'
|
||||||
|
)
|
||||||
|
|
||||||
df_buy = df[df['buy'] == 1]
|
df_buy = data[data['buy'] == 1]
|
||||||
buys = go.Scattergl(
|
buys = go.Scattergl(
|
||||||
x=df_buy.date,
|
x=df_buy.date,
|
||||||
y=df_buy.close,
|
y=df_buy.close,
|
||||||
@ -73,13 +92,11 @@ def plot_analyzed_dataframe(args) -> None:
|
|||||||
marker=dict(
|
marker=dict(
|
||||||
symbol='triangle-up-dot',
|
symbol='triangle-up-dot',
|
||||||
size=9,
|
size=9,
|
||||||
line=dict(
|
line=dict(width=1),
|
||||||
width=1,
|
|
||||||
),
|
|
||||||
color='green',
|
color='green',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
df_sell = df[df['sell'] == 1]
|
df_sell = data[data['sell'] == 1]
|
||||||
sells = go.Scattergl(
|
sells = go.Scattergl(
|
||||||
x=df_sell.date,
|
x=df_sell.date,
|
||||||
y=df_sell.close,
|
y=df_sell.close,
|
||||||
@ -88,30 +105,28 @@ def plot_analyzed_dataframe(args) -> None:
|
|||||||
marker=dict(
|
marker=dict(
|
||||||
symbol='triangle-down-dot',
|
symbol='triangle-down-dot',
|
||||||
size=9,
|
size=9,
|
||||||
line=dict(
|
line=dict(width=1),
|
||||||
width=1,
|
|
||||||
),
|
|
||||||
color='red',
|
color='red',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
bb_lower = go.Scatter(
|
bb_lower = go.Scatter(
|
||||||
x=df.date,
|
x=data.date,
|
||||||
y=df.bb_lowerband,
|
y=data.bb_lowerband,
|
||||||
name='BB lower',
|
name='BB lower',
|
||||||
line={'color': "transparent"},
|
line={'color': "transparent"},
|
||||||
)
|
)
|
||||||
bb_upper = go.Scatter(
|
bb_upper = go.Scatter(
|
||||||
x=df.date,
|
x=data.date,
|
||||||
y=df.bb_upperband,
|
y=data.bb_upperband,
|
||||||
name='BB upper',
|
name='BB upper',
|
||||||
fill="tonexty",
|
fill="tonexty",
|
||||||
fillcolor="rgba(0,176,246,0.2)",
|
fillcolor="rgba(0,176,246,0.2)",
|
||||||
line={'color': "transparent"},
|
line={'color': "transparent"},
|
||||||
)
|
)
|
||||||
macd = go.Scattergl(x=df['date'], y=df['macd'], name='MACD')
|
macd = go.Scattergl(x=data['date'], y=data['macd'], name='MACD')
|
||||||
macdsignal = go.Scattergl(x=df['date'], y=df['macdsignal'], name='MACD signal')
|
macdsignal = go.Scattergl(x=data['date'], y=data['macdsignal'], name='MACD signal')
|
||||||
volume = go.Bar(x=df['date'], y=df['volume'], name='Volume')
|
volume = go.Bar(x=data['date'], y=data['volume'], name='Volume')
|
||||||
|
|
||||||
fig = tools.make_subplots(
|
fig = tools.make_subplots(
|
||||||
rows=3,
|
rows=3,
|
||||||
@ -138,6 +153,31 @@ def plot_analyzed_dataframe(args) -> None:
|
|||||||
plot(fig, filename='freqtrade-plot.html')
|
plot(fig, filename='freqtrade-plot.html')
|
||||||
|
|
||||||
|
|
||||||
|
def plot_parse_args(args: List[str]) -> Namespace:
|
||||||
|
"""
|
||||||
|
Parse args passed to the script
|
||||||
|
:param args: Cli arguments
|
||||||
|
:return: args: Array with all arguments
|
||||||
|
"""
|
||||||
|
arguments = Arguments(args, 'Graph dataframe')
|
||||||
|
arguments.scripts_options()
|
||||||
|
arguments.common_args_parser()
|
||||||
|
arguments.optimizer_shared_options(arguments.parser)
|
||||||
|
arguments.backtesting_options(arguments.parser)
|
||||||
|
|
||||||
|
return arguments.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def main(sysargv: List[str]) -> None:
|
||||||
|
"""
|
||||||
|
This function will initiate the bot and start the trading loop.
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
logger.info('Starting Plot Dataframe')
|
||||||
|
plot_analyzed_dataframe(
|
||||||
|
plot_parse_args(sysargv)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
args = plot_parse_args(sys.argv[1:])
|
main(sys.argv[1:])
|
||||||
plot_analyzed_dataframe(args)
|
|
||||||
|
@ -1,32 +1,45 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Script to display profits
|
||||||
|
|
||||||
|
Mandatory Cli parameters:
|
||||||
|
-p / --pair: pair to examine
|
||||||
|
|
||||||
|
Optional Cli parameters
|
||||||
|
-c / --config: specify configuration file
|
||||||
|
-s / --strategy: strategy to use
|
||||||
|
--timerange: specify what timerange of data to use.
|
||||||
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
|
from argparse import Namespace
|
||||||
|
from typing import List, Optional
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from plotly import tools
|
from plotly import tools
|
||||||
from plotly.offline import plot
|
from plotly.offline import plot
|
||||||
import plotly.graph_objs as go
|
import plotly.graph_objs as go
|
||||||
|
|
||||||
|
from freqtrade.arguments import Arguments
|
||||||
|
from freqtrade.configuration import Configuration
|
||||||
|
from freqtrade.analyze import Analyze
|
||||||
|
from freqtrade.logger import Logger
|
||||||
|
|
||||||
import freqtrade.optimize as optimize
|
import freqtrade.optimize as optimize
|
||||||
import freqtrade.misc as misc
|
import freqtrade.misc as misc
|
||||||
from freqtrade.strategy.strategy import Strategy
|
|
||||||
|
|
||||||
|
|
||||||
def plot_parse_args(args):
|
logger = Logger(name="Graph profits").get_logger()
|
||||||
parser = misc.common_args_parser('Graph profits')
|
|
||||||
# FIX: perhaps delete those backtesting options that are not feasible (shows up in -h)
|
|
||||||
misc.backtesting_options(parser)
|
|
||||||
misc.scripts_options(parser)
|
|
||||||
return parser.parse_args(args)
|
|
||||||
|
|
||||||
|
|
||||||
# data:: [ pair, profit-%, enter, exit, time, duration]
|
# data:: [ pair, profit-%, enter, exit, time, duration]
|
||||||
# data:: ['BTC_XMR', 0.00537847, '1511176800', '1511178000', 5057, 1]
|
# data:: ["BTC_ETH", 0.0023975, "1515598200", "1515602100", "2018-01-10 07:30:00+00:00", 65]
|
||||||
# FIX: make use of the enter/exit dates to insert the
|
def make_profit_array(
|
||||||
# profit more precisely into the pg array
|
data: List, px: int, min_date: int,
|
||||||
def make_profit_array(data, px, filter_pairs=[]):
|
interval: int, filter_pairs: Optional[List] = None) -> np.ndarray:
|
||||||
pg = np.zeros(px)
|
pg = np.zeros(px)
|
||||||
|
filter_pairs = filter_pairs or []
|
||||||
# Go through the trades
|
# Go through the trades
|
||||||
# and make an total profit
|
# and make an total profit
|
||||||
# array
|
# array
|
||||||
@ -35,10 +48,11 @@ def make_profit_array(data, px, filter_pairs=[]):
|
|||||||
if filter_pairs and pair not in filter_pairs:
|
if filter_pairs and pair not in filter_pairs:
|
||||||
continue
|
continue
|
||||||
profit = trade[1]
|
profit = trade[1]
|
||||||
tim = trade[4]
|
trade_sell_time = int(trade[3])
|
||||||
dur = trade[5]
|
|
||||||
ix = tim + dur - 1
|
ix = define_index(min_date, trade_sell_time, interval)
|
||||||
if ix < px:
|
if ix < px:
|
||||||
|
logger.debug('[%s]: Add profit %s on %s', pair, profit, trade[4])
|
||||||
pg[ix] += profit
|
pg[ix] += profit
|
||||||
|
|
||||||
# rewrite the pg array to go from
|
# rewrite the pg array to go from
|
||||||
@ -53,7 +67,7 @@ def make_profit_array(data, px, filter_pairs=[]):
|
|||||||
return pg
|
return pg
|
||||||
|
|
||||||
|
|
||||||
def plot_profit(args) -> None:
|
def plot_profit(args: Namespace) -> None:
|
||||||
"""
|
"""
|
||||||
Plots the total profit for all pairs.
|
Plots the total profit for all pairs.
|
||||||
Note, the profit calculation isn't realistic.
|
Note, the profit calculation isn't realistic.
|
||||||
@ -64,47 +78,62 @@ def plot_profit(args) -> None:
|
|||||||
# We need to use the same pairs, same tick_interval
|
# We need to use the same pairs, same tick_interval
|
||||||
# and same timeperiod as used in backtesting
|
# and same timeperiod as used in backtesting
|
||||||
# to match the tickerdata against the profits-results
|
# to match the tickerdata against the profits-results
|
||||||
|
timerange = Arguments.parse_timerange(args.timerange)
|
||||||
|
|
||||||
filter_pairs = args.pair
|
config = Configuration(args).get_config()
|
||||||
|
|
||||||
config = misc.load_config(args.config)
|
|
||||||
config.update({'strategy': args.strategy})
|
|
||||||
|
|
||||||
# Init strategy
|
# Init strategy
|
||||||
strategy = Strategy()
|
try:
|
||||||
strategy.init(config)
|
analyze = Analyze({'strategy': config.get('strategy')})
|
||||||
|
except AttributeError:
|
||||||
|
logger.critical(
|
||||||
|
'Impossible to load the strategy. Please check the file "user_data/strategies/%s.py"',
|
||||||
|
config.get('strategy')
|
||||||
|
)
|
||||||
|
exit()
|
||||||
|
|
||||||
|
# Take pairs from the cli otherwise switch to the pair in the config file
|
||||||
|
if args.pair:
|
||||||
|
filter_pairs = args.pair
|
||||||
|
filter_pairs = filter_pairs.split(',')
|
||||||
|
else:
|
||||||
|
filter_pairs = config['exchange']['pair_whitelist']
|
||||||
|
|
||||||
|
tick_interval = analyze.strategy.ticker_interval
|
||||||
pairs = config['exchange']['pair_whitelist']
|
pairs = config['exchange']['pair_whitelist']
|
||||||
|
|
||||||
if filter_pairs:
|
if filter_pairs:
|
||||||
filter_pairs = filter_pairs.split(',')
|
|
||||||
pairs = list(set(pairs) & set(filter_pairs))
|
pairs = list(set(pairs) & set(filter_pairs))
|
||||||
print('Filter, keep pairs %s' % pairs)
|
logger.info('Filter, keep pairs %s' % pairs)
|
||||||
|
|
||||||
timerange = misc.parse_timerange(args.timerange)
|
tickers = optimize.load_data(
|
||||||
tickers = optimize.load_data(args.datadir, pairs=pairs,
|
datadir=args.datadir,
|
||||||
ticker_interval=strategy.ticker_interval,
|
pairs=pairs,
|
||||||
refresh_pairs=False,
|
ticker_interval=tick_interval,
|
||||||
timerange=timerange)
|
refresh_pairs=False,
|
||||||
dataframes = optimize.preprocess(tickers)
|
timerange=timerange
|
||||||
|
)
|
||||||
|
dataframes = analyze.tickerdata_to_dataframe(tickers)
|
||||||
|
|
||||||
# NOTE: the dataframes are of unequal length,
|
# NOTE: the dataframes are of unequal length,
|
||||||
# 'dates' is an merged date array of them all.
|
# 'dates' is an merged date array of them all.
|
||||||
|
|
||||||
dates = misc.common_datearray(dataframes)
|
dates = misc.common_datearray(dataframes)
|
||||||
max_x = dates.size
|
min_date = int(min(dates).timestamp())
|
||||||
|
max_date = int(max(dates).timestamp())
|
||||||
|
num_iterations = define_index(min_date, max_date, tick_interval) + 1
|
||||||
|
|
||||||
# Make an average close price of all the pairs that was involved.
|
# Make an average close price of all the pairs that was involved.
|
||||||
# this could be useful to gauge the overall market trend
|
# this could be useful to gauge the overall market trend
|
||||||
# We are essentially saying:
|
# We are essentially saying:
|
||||||
# array <- sum dataframes[*]['close'] / num_items dataframes
|
# array <- sum dataframes[*]['close'] / num_items dataframes
|
||||||
# FIX: there should be some onliner numpy/panda for this
|
# FIX: there should be some onliner numpy/panda for this
|
||||||
avgclose = np.zeros(max_x)
|
avgclose = np.zeros(num_iterations)
|
||||||
num = 0
|
num = 0
|
||||||
for pair, pair_data in dataframes.items():
|
for pair, pair_data in dataframes.items():
|
||||||
close = pair_data['close']
|
close = pair_data['close']
|
||||||
maxprice = max(close) # Normalize price to [0,1]
|
maxprice = max(close) # Normalize price to [0,1]
|
||||||
print('Pair %s has length %s' % (pair, len(close)))
|
logger.info('Pair %s has length %s' % (pair, len(close)))
|
||||||
for x in range(0, len(close)):
|
for x in range(0, len(close)):
|
||||||
avgclose[x] += close[x] / maxprice
|
avgclose[x] += close[x] / maxprice
|
||||||
# avgclose += close
|
# avgclose += close
|
||||||
@ -114,10 +143,16 @@ def plot_profit(args) -> None:
|
|||||||
# Load the profits results
|
# Load the profits results
|
||||||
# And make an profits-growth array
|
# And make an profits-growth array
|
||||||
|
|
||||||
filename = 'backtest-result.json'
|
try:
|
||||||
with open(filename) as file:
|
filename = 'backtest-result.json'
|
||||||
data = json.load(file)
|
with open(filename) as file:
|
||||||
pg = make_profit_array(data, max_x, filter_pairs)
|
data = json.load(file)
|
||||||
|
except FileNotFoundError:
|
||||||
|
logger.critical('File "backtest-result.json" not found. This script require backtesting '
|
||||||
|
'results to run.\nPlease run a backtesting with the parameter --export.')
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
pg = make_profit_array(data, num_iterations, min_date, tick_interval, filter_pairs)
|
||||||
|
|
||||||
#
|
#
|
||||||
# Plot the pairs average close prices, and total profit growth
|
# Plot the pairs average close prices, and total profit growth
|
||||||
@ -128,6 +163,7 @@ def plot_profit(args) -> None:
|
|||||||
y=avgclose,
|
y=avgclose,
|
||||||
name='Avg close price',
|
name='Avg close price',
|
||||||
)
|
)
|
||||||
|
|
||||||
profit = go.Scattergl(
|
profit = go.Scattergl(
|
||||||
x=dates,
|
x=dates,
|
||||||
y=pg,
|
y=pg,
|
||||||
@ -140,7 +176,7 @@ def plot_profit(args) -> None:
|
|||||||
fig.append_trace(profit, 2, 1)
|
fig.append_trace(profit, 2, 1)
|
||||||
|
|
||||||
for pair in pairs:
|
for pair in pairs:
|
||||||
pg = make_profit_array(data, max_x, pair)
|
pg = make_profit_array(data, num_iterations, min_date, tick_interval, pair)
|
||||||
pair_profit = go.Scattergl(
|
pair_profit = go.Scattergl(
|
||||||
x=dates,
|
x=dates,
|
||||||
y=pg,
|
y=pg,
|
||||||
@ -151,6 +187,38 @@ def plot_profit(args) -> None:
|
|||||||
plot(fig, filename='freqtrade-profit-plot.html')
|
plot(fig, filename='freqtrade-profit-plot.html')
|
||||||
|
|
||||||
|
|
||||||
|
def define_index(min_date: int, max_date: int, interval: int) -> int:
|
||||||
|
"""
|
||||||
|
Return the index of a specific date
|
||||||
|
"""
|
||||||
|
return int((max_date - min_date) / (interval * 60))
|
||||||
|
|
||||||
|
|
||||||
|
def plot_parse_args(args: List[str]) -> Namespace:
|
||||||
|
"""
|
||||||
|
Parse args passed to the script
|
||||||
|
:param args: Cli arguments
|
||||||
|
:return: args: Array with all arguments
|
||||||
|
"""
|
||||||
|
arguments = Arguments(args, 'Graph profits')
|
||||||
|
arguments.scripts_options()
|
||||||
|
arguments.common_args_parser()
|
||||||
|
arguments.optimizer_shared_options(arguments.parser)
|
||||||
|
arguments.backtesting_options(arguments.parser)
|
||||||
|
|
||||||
|
return arguments.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def main(sysargv: List[str]) -> None:
|
||||||
|
"""
|
||||||
|
This function will initiate the bot and start the trading loop.
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
logger.info('Starting Plot Dataframe')
|
||||||
|
plot_profit(
|
||||||
|
plot_parse_args(sysargv)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
args = plot_parse_args(sys.argv[1:])
|
main(sys.argv[1:])
|
||||||
plot_profit(args)
|
|
||||||
|
Loading…
Reference in New Issue
Block a user