Merge branch 'develop' into datadir

This commit is contained in:
kryofly 2018-01-07 10:00:35 +01:00
commit 890083ce7f
13 changed files with 453 additions and 177 deletions

View File

@ -204,19 +204,39 @@ signal. Given following result from hyperopt:
``` ```
Best parameters: Best parameters:
{ {
"adx": 1, "adx": {
"adx-value": 15.0, "enabled": true,
"fastd": 1, "value": 15.0
"fastd-value": 40.0, },
"green_candle": 1, "fastd": {
"mfi": 0, "enabled": true,
"over_sar": 0, "value": 40.0
"rsi": 1, },
"rsi-value": 37.0, "green_candle": {
"trigger": 0, "enabled": true
"uptrend_long_ema": 1, },
"uptrend_short_ema": 0, "mfi": {
"uptrend_sma": 0 "enabled": false
},
"over_sar": {
"enabled": false
},
"rsi": {
"enabled": true,
"value": 37.0
},
"trigger": {
"type": "lower_bb"
},
"uptrend_long_ema": {
"enabled": true
},
"uptrend_short_ema": {
"enabled": false
},
"uptrend_sma": {
"enabled": false
}
} }
Best Result: Best Result:
@ -224,14 +244,14 @@ Best Result:
``` ```
You should understand this result like: You should understand this result like:
- You should **consider** the guard "adx" (`"adx": 1,` = `adx` is true) - You should **consider** the guard "adx" (`"adx"` is `"enabled": true`)
and the best value is `15.0` (`"adx-value": 15.0,`) and the best value is `15.0` (`"value": 15.0,`)
- You should **consider** the guard "fastd" (`"fastd": 1,` = `fastd` - You should **consider** the guard "fastd" (`"fastd"` is `"enabled":
is true) and the best value is `40.0` (`"fastd-value": 40.0,`) true`) and the best value is `40.0` (`"value": 40.0,`)
- You should **consider** to enable the guard "green_candle" - You should **consider** to enable the guard "green_candle"
(`"green_candle": 1,` = `candle` is true) but this guards as no (`"green_candle"` is `"enabled": true`) but this guards as no
customizable value. customizable value.
- You should **ignore** the guard "mfi" (`"mfi": 0,` = `mfi` is false) - You should **ignore** the guard "mfi" (`"mfi"` is `"enabled": false`)
- and so on... - and so on...
@ -239,8 +259,8 @@ You have to look from
[freqtrade/optimize/hyperopt.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L170-L200) [freqtrade/optimize/hyperopt.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L170-L200)
what those values match to. what those values match to.
So for example you had `adx-value: 15.0` (and `adx: 1` was true) so we So for example you had `adx:` with the `value: 15.0` so we would look
would look at `adx`-block from at `adx`-block from
[freqtrade/optimize/hyperopt.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L178-L179). [freqtrade/optimize/hyperopt.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L178-L179).
That translates to the following code block to That translates to the following code block to
[analyze.populate_buy_trend()](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/analyze.py#L73) [analyze.populate_buy_trend()](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/analyze.py#L73)

View File

@ -11,7 +11,7 @@ import talib.abstract as ta
from pandas import DataFrame, to_datetime from pandas import DataFrame, to_datetime
from freqtrade.exchange import get_ticker_history from freqtrade.exchange import get_ticker_history
from freqtrade.vendor.qtpylib.indicators import awesome_oscillator, crossed_above from freqtrade.vendor.qtpylib.indicators import awesome_oscillator, PandasObject as qtpylib
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -40,34 +40,185 @@ def parse_ticker_dataframe(ticker: list) -> DataFrame:
def populate_indicators(dataframe: DataFrame) -> DataFrame: def populate_indicators(dataframe: DataFrame) -> DataFrame:
""" """
Adds several different TA indicators to the given DataFrame Adds several different TA indicators to the given 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.
""" """
dataframe['sar'] = ta.SAR(dataframe)
# Momentum Indicator
# ------------------------------------
# ADX
dataframe['adx'] = ta.ADX(dataframe) dataframe['adx'] = ta.ADX(dataframe)
stoch = ta.STOCHF(dataframe)
dataframe['fastd'] = stoch['fastd'] # Awesome oscillator
dataframe['fastk'] = stoch['fastk']
dataframe['blower'] = ta.BBANDS(dataframe, nbdevup=2, nbdevdn=2)['lowerband']
dataframe['sma'] = ta.SMA(dataframe, timeperiod=40)
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
dataframe['mfi'] = ta.MFI(dataframe)
dataframe['rsi'] = ta.RSI(dataframe)
dataframe['cci'] = ta.CCI(dataframe)
dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
dataframe['ao'] = awesome_oscillator(dataframe) dataframe['ao'] = awesome_oscillator(dataframe)
"""
# Commodity Channel Index: values Oversold:<-100, Overbought:>100
dataframe['cci'] = ta.CCI(dataframe)
"""
# MACD
macd = ta.MACD(dataframe) macd = ta.MACD(dataframe)
dataframe['macd'] = macd['macd'] dataframe['macd'] = macd['macd']
dataframe['macdsignal'] = macd['macdsignal'] dataframe['macdsignal'] = macd['macdsignal']
dataframe['macdhist'] = macd['macdhist'] dataframe['macdhist'] = macd['macdhist']
# MFI
dataframe['mfi'] = ta.MFI(dataframe)
# Minus Directional Indicator / Movement
dataframe['minus_dm'] = ta.MINUS_DM(dataframe)
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
# Plus Directional Indicator / Movement
dataframe['plus_dm'] = ta.PLUS_DM(dataframe)
dataframe['plus_di'] = ta.PLUS_DI(dataframe)
"""
# ROC
dataframe['roc'] = ta.ROC(dataframe)
"""
# RSI
dataframe['rsi'] = ta.RSI(dataframe)
"""
# Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy)
rsi = 0.1 * (dataframe['rsi'] - 50)
dataframe['fisher_rsi'] = (numpy.exp(2 * rsi) - 1) / (numpy.exp(2 * rsi) + 1)
# Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy)
dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1)
# Stoch
stoch = ta.STOCH(dataframe)
dataframe['slowd'] = stoch['slowd']
dataframe['slowk'] = stoch['slowk']
"""
# Stoch fast
stoch_fast = ta.STOCHF(dataframe)
dataframe['fastd'] = stoch_fast['fastd']
dataframe['fastk'] = stoch_fast['fastk']
"""
# Stoch RSI
stoch_rsi = ta.STOCHRSI(dataframe)
dataframe['fastd_rsi'] = stoch_rsi['fastd']
dataframe['fastk_rsi'] = stoch_rsi['fastk']
"""
# Overlap Studies
# ------------------------------------
# Previous Bollinger bands
# Because ta.BBANDS implementation is broken with small numbers, it actually
# returns middle band for all the three bands. Switch to qtpylib.bollinger_bands
# and use middle band instead.
dataframe['blower'] = ta.BBANDS(dataframe, nbdevup=2, nbdevdn=2)['lowerband']
"""
# Bollinger bands
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
dataframe['bb_lowerband'] = bollinger['lower']
dataframe['bb_middleband'] = bollinger['mid']
dataframe['bb_upperband'] = bollinger['upper']
"""
# EMA - Exponential Moving Average
dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
# SAR Parabol
dataframe['sar'] = ta.SAR(dataframe)
# SMA - Simple Moving Average
dataframe['sma'] = ta.SMA(dataframe, timeperiod=40)
# TEMA - Triple Exponential Moving Average
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
# Cycle Indicator
# ------------------------------------
# Hilbert Transform Indicator - SineWave
hilbert = ta.HT_SINE(dataframe) hilbert = ta.HT_SINE(dataframe)
dataframe['htsine'] = hilbert['sine'] dataframe['htsine'] = hilbert['sine']
dataframe['htleadsine'] = hilbert['leadsine'] dataframe['htleadsine'] = hilbert['leadsine']
dataframe['plus_dm'] = ta.PLUS_DM(dataframe)
dataframe['plus_di'] = ta.PLUS_DI(dataframe) # Pattern Recognition - Bullish candlestick patterns
dataframe['minus_dm'] = ta.MINUS_DM(dataframe) # ------------------------------------
dataframe['minus_di'] = ta.MINUS_DI(dataframe) """
# Hammer: values [0, 100]
dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe)
# Inverted Hammer: values [0, 100]
dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe)
# Dragonfly Doji: values [0, 100]
dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe)
# Piercing Line: values [0, 100]
dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100]
# Morningstar: values [0, 100]
dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100]
# Three White Soldiers: values [0, 100]
dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100]
"""
# Pattern Recognition - Bearish candlestick patterns
# ------------------------------------
"""
# Hanging Man: values [0, 100]
dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe)
# Shooting Star: values [0, 100]
dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe)
# Gravestone Doji: values [0, 100]
dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe)
# Dark Cloud Cover: values [0, 100]
dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe)
# Evening Doji Star: values [0, 100]
dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe)
# Evening Star: values [0, 100]
dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe)
"""
# Pattern Recognition - Bullish/Bearish candlestick patterns
# ------------------------------------
"""
# Three Line Strike: values [0, -100, 100]
dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe)
# Spinning Top: values [0, -100, 100]
dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100]
# Engulfing: values [0, -100, 100]
dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100]
# Harami: values [0, -100, 100]
dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100]
# Three Outside Up/Down: values [0, -100, 100]
dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100]
# Three Inside Up/Down: values [0, -100, 100]
dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100]
"""
# Chart type
# ------------------------------------
"""
# Heikinashi stategy
heikinashi = qtpylib.heikinashi(dataframe)
dataframe['ha_open'] = heikinashi['open']
dataframe['ha_close'] = heikinashi['close']
dataframe['ha_high'] = heikinashi['high']
dataframe['ha_low'] = heikinashi['low']
"""
return dataframe return dataframe
@ -102,8 +253,8 @@ def populate_sell_trend(dataframe: DataFrame) -> DataFrame:
dataframe.loc[ dataframe.loc[
( (
( (
(crossed_above(dataframe['rsi'], 70)) | (qtpylib.crossed_above(dataframe['rsi'], 70)) |
(crossed_above(dataframe['fastd'], 70)) (qtpylib.crossed_above(dataframe['fastd'], 70))
) & ) &
(dataframe['adx'] > 10) & (dataframe['adx'] > 10) &
(dataframe['minus_di'] > 0) (dataframe['minus_di'] > 0)

View File

@ -57,7 +57,11 @@ class CryptoToFiatConverter():
] ]
def __init__(self) -> None: def __init__(self) -> None:
try:
self._coinmarketcap = Pymarketcap() self._coinmarketcap = Pymarketcap()
except BaseException:
self._coinmarketcap = None
self._pairs = [] self._pairs = []
def convert_amount(self, crypto_amount: float, crypto_symbol: str, fiat_symbol: str) -> float: def convert_amount(self, crypto_amount: float, crypto_symbol: str, fiat_symbol: str) -> float:
@ -147,10 +151,12 @@ class CryptoToFiatConverter():
# Check if the fiat convertion you want is supported # Check if the fiat convertion you want is supported
if not self._is_supported_fiat(fiat=fiat_symbol): if not self._is_supported_fiat(fiat=fiat_symbol):
raise ValueError('The fiat {} is not supported.'.format(fiat_symbol)) raise ValueError('The fiat {} is not supported.'.format(fiat_symbol))
try:
return float( return float(
self._coinmarketcap.ticker( self._coinmarketcap.ticker(
currency=crypto_symbol, currency=crypto_symbol,
convert=fiat_symbol convert=fiat_symbol
)['price_' + fiat_symbol.lower()] )['price_' + fiat_symbol.lower()]
) )
except BaseException:
return 0.0

View File

@ -247,12 +247,6 @@ def handle_trade(trade: Trade) -> bool:
logger.debug('Handling %s ...', trade) logger.debug('Handling %s ...', trade)
current_rate = exchange.get_ticker(trade.pair)['bid'] current_rate = exchange.get_ticker(trade.pair)['bid']
# Experimental: Check if the trade is profitable before selling it (avoid selling at loss)
if _CONF.get('experimental', {}).get('sell_profit_only'):
logger.debug('Checking if trade is profitable ...')
if trade.calc_profit(rate=current_rate) <= 0:
return False
# Check if minimal roi has been reached # Check if minimal roi has been reached
if min_roi_reached(trade, current_rate, datetime.utcnow()): if min_roi_reached(trade, current_rate, datetime.utcnow()):
logger.debug('Executing sell due to ROI ...') logger.debug('Executing sell due to ROI ...')
@ -261,6 +255,11 @@ def handle_trade(trade: Trade) -> bool:
# Experimental: Check if sell signal has been enabled and triggered # Experimental: Check if sell signal has been enabled and triggered
if _CONF.get('experimental', {}).get('use_sell_signal'): if _CONF.get('experimental', {}).get('use_sell_signal'):
# Experimental: Check if the trade is profitable before selling it (avoid selling at loss)
if _CONF.get('experimental', {}).get('sell_profit_only'):
logger.debug('Checking if trade is profitable ...')
if trade.calc_profit(rate=current_rate) <= 0:
return False
logger.debug('Checking sell_signal ...') logger.debug('Checking sell_signal ...')
if get_signal(trade.pair, SignalType.SELL): if get_signal(trade.pair, SignalType.SELL):
logger.debug('Executing sell due to sell signal ...') logger.debug('Executing sell due to sell signal ...')
@ -399,14 +398,18 @@ def cleanup() -> None:
exit(0) exit(0)
def main() -> None: def main(sysargv=sys.argv[1:]) -> None:
""" """
Loads and validates the config and handles the main loop Loads and validates the config and handles the main loop
:return: None :return: None
""" """
global _CONF global _CONF
args = parse_args(sys.argv[1:]) args = parse_args(sysargv,
if not args: 'Simple High Frequency Trading Bot for crypto currencies')
# A subcommand has been issued
if hasattr(args, 'func'):
args.func(args)
exit(0) exit(0)
# Initialize logger # Initialize logger

View File

@ -81,21 +81,12 @@ def throttle(func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any:
return result return result
def parse_args(args: List[str]): def parse_args_common(args: List[str], description: str):
""" """
Parses given arguments and returns an argparse Namespace instance. Parses given common arguments and returns them as a parsed object.
Returns None if a sub command has been selected and executed.
""" """
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description='Simple High Frequency Trading Bot for crypto currencies' description=description
)
parser.add_argument(
'-c', '--config',
help='specify configuration file (default: config.json)',
dest='config',
default='config.json',
type=str,
metavar='PATH',
) )
parser.add_argument( parser.add_argument(
'-v', '--verbose', '-v', '--verbose',
@ -111,14 +102,22 @@ def parse_args(args: List[str]):
version='%(prog)s {}'.format(__version__), version='%(prog)s {}'.format(__version__),
) )
parser.add_argument( parser.add_argument(
'--dynamic-whitelist', '-c', '--config',
help='dynamically generate and update whitelist based on 24h BaseVolume (Default 20 currencies)', # noqa help='specify configuration file (default: config.json)',
dest='dynamic_whitelist', dest='config',
const=20, default='config.json',
type=int, type=str,
metavar='INT', metavar='PATH',
nargs='?',
) )
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 = parse_args_common(args, description)
parser.add_argument( parser.add_argument(
'--dry-run-db', '--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 \ help='Force dry run to use a local DB "tradesv3.dry_run.sqlite" instead of memory DB. Work only if dry_run is \
@ -134,16 +133,18 @@ def parse_args(args: List[str]):
type=str, type=str,
metavar='PATH', metavar='PATH',
) )
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) build_subcommands(parser)
parsed_args = parser.parse_args(args) return parser.parse_args(args)
# No subcommand as been selected
if not hasattr(parsed_args, 'func'):
return parsed_args
parsed_args.func(parsed_args)
return None
def build_subcommands(parser: argparse.ArgumentParser) -> None: def build_subcommands(parser: argparse.ArgumentParser) -> None:

View File

@ -8,7 +8,7 @@ from functools import reduce
from math import exp from math import exp
from operator import itemgetter from operator import itemgetter
from hyperopt import fmin, tpe, hp, Trials, STATUS_OK, STATUS_FAIL from hyperopt import fmin, tpe, hp, Trials, STATUS_OK, STATUS_FAIL, space_eval
from hyperopt.mongoexp import MongoTrials from hyperopt.mongoexp import MongoTrials
from pandas import DataFrame from pandas import DataFrame
@ -209,7 +209,7 @@ def buy_strategy_generator(params):
def start(args): def start(args):
global TOTAL_TRIES, PROCESSED global TOTAL_TRIES, PROCESSED, SPACE
TOTAL_TRIES = args.epochs TOTAL_TRIES = args.epochs
exchange._API = Bittrex({'key': '', 'secret': ''}) exchange._API = Bittrex({'key': '', 'secret': ''})
@ -236,6 +236,11 @@ def start(args):
trials = Trials() trials = Trials()
best = fmin(fn=optimizer, space=SPACE, algo=tpe.suggest, max_evals=TOTAL_TRIES, trials=trials) best = fmin(fn=optimizer, space=SPACE, algo=tpe.suggest, max_evals=TOTAL_TRIES, trials=trials)
# Improve best parameter logging display
if best:
best = space_eval(SPACE, best)
logger.info('Best parameters:\n%s', json.dumps(best, indent=4)) logger.info('Best parameters:\n%s', json.dumps(best, indent=4))
results = sorted(trials.results, key=itemgetter('loss')) results = sorted(trials.results, key=itemgetter('loss'))

View File

@ -77,3 +77,40 @@ def test_no_log_if_loss_does_not_improve(mocker):
}) })
assert not logger.called assert not logger.called
def test_fmin_best_results(mocker, caplog):
fmin_result = {
"adx": 1,
"adx-value": 15.0,
"fastd": 1,
"fastd-value": 40.0,
"green_candle": 1,
"mfi": 0,
"over_sar": 0,
"rsi": 1,
"rsi-value": 37.0,
"trigger": 2,
"uptrend_long_ema": 1,
"uptrend_short_ema": 0,
"uptrend_sma": 0
}
mocker.patch('freqtrade.optimize.hyperopt.MongoTrials', return_value=create_trials(mocker))
mocker.patch('freqtrade.optimize.preprocess')
mocker.patch('freqtrade.optimize.load_data')
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value=fmin_result)
args = mocker.Mock(epochs=1, config='config.json.example')
start(args)
exists = [
'Best parameters',
'"adx": {\n "enabled": true,\n "value": 15.0\n },',
'"green_candle": {\n "enabled": true\n },',
'"mfi": {\n "enabled": false\n },',
'"trigger": {\n "type": "ao_cross_zero"\n },'
]
for line in exists:
assert line in caplog.text

View File

@ -167,6 +167,7 @@ def test_profit_handle(
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap', mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
ticker=MagicMock(return_value={'price_usd': 15000.0}), ticker=MagicMock(return_value={'price_usd': 15000.0}),
_cache_symbols=MagicMock(return_value={'BTC': 1})) _cache_symbols=MagicMock(return_value={'BTC': 1}))
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
_profit(bot=MagicMock(), update=update) _profit(bot=MagicMock(), update=update)
@ -422,6 +423,7 @@ def test_daily_handle(
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap', mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
ticker=MagicMock(return_value={'price_usd': 15000.0}), ticker=MagicMock(return_value={'price_usd': 15000.0}),
_cache_symbols=MagicMock(return_value={'BTC': 1})) _cache_symbols=MagicMock(return_value={'BTC': 1}))
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
# Create some test data # Create some test data

View File

@ -72,8 +72,11 @@ def test_fiat_convert_find_price(mocker):
with pytest.raises(ValueError, match=r'The fiat ABC is not supported.'): with pytest.raises(ValueError, match=r'The fiat ABC is not supported.'):
fiat_convert._find_price(crypto_symbol='BTC', fiat_symbol='ABC') fiat_convert._find_price(crypto_symbol='BTC', fiat_symbol='ABC')
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=12345.0)
assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='USD') == 12345.0 assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='USD') == 12345.0
assert fiat_convert.get_price(crypto_symbol='btc', fiat_symbol='usd') == 12345.0 assert fiat_convert.get_price(crypto_symbol='btc', fiat_symbol='usd') == 12345.0
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=13000.2)
assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='EUR') == 13000.2 assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='EUR') == 13000.2
@ -83,6 +86,7 @@ def test_fiat_convert_get_price(mocker):
'price_eur': 15000.0 'price_eur': 15000.0
}) })
mocker.patch('freqtrade.fiat_convert.Pymarketcap.ticker', api_mock) mocker.patch('freqtrade.fiat_convert.Pymarketcap.ticker', api_mock)
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=28000.0)
fiat_convert = CryptoToFiatConverter() fiat_convert = CryptoToFiatConverter()
@ -109,3 +113,12 @@ def test_fiat_convert_get_price(mocker):
fiat_convert._pairs[0]._expiration = expiration fiat_convert._pairs[0]._expiration = expiration
assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='USD') == 28000.0 assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='USD') == 28000.0
assert fiat_convert._pairs[0]._expiration is not expiration assert fiat_convert._pairs[0]._expiration is not expiration
def test_fiat_convert_without_network(mocker):
Pymarketcap = MagicMock(side_effect=ImportError('Oh boy, you have no network!'))
mocker.patch('freqtrade.fiat_convert.Pymarketcap', Pymarketcap)
fiat_convert = CryptoToFiatConverter()
assert fiat_convert._coinmarketcap is None
assert fiat_convert._find_price(crypto_symbol='BTC', fiat_symbol='USD') == 0.0

View File

@ -15,6 +15,39 @@ from freqtrade.main import create_trade, handle_trade, init, \
get_target_bid, _process, execute_sell, check_handle_timedout get_target_bid, _process, execute_sell, check_handle_timedout
from freqtrade.misc import get_state, State from freqtrade.misc import get_state, State
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
import freqtrade.main as main
# Test that main() can start backtesting or hyperopt.
# and also ensure we can pass some specific arguments
# argument parsing is done in test_misc.py
def test_parse_args_backtesting(mocker):
backtesting_mock = mocker.patch(
'freqtrade.optimize.backtesting.start', MagicMock())
with pytest.raises(SystemExit, match=r'0'):
main.main(['backtesting'])
assert backtesting_mock.call_count == 1
call_args = backtesting_mock.call_args[0][0]
assert call_args.config == 'config.json'
assert call_args.live is False
assert call_args.loglevel == 20
assert call_args.subparser == 'backtesting'
assert call_args.func is not None
assert call_args.ticker_interval == 5
def test_main_start_hyperopt(mocker):
hyperopt_mock = mocker.patch(
'freqtrade.optimize.hyperopt.start', MagicMock())
with pytest.raises(SystemExit, match=r'0'):
main.main(['hyperopt'])
assert hyperopt_mock.call_count == 1
call_args = hyperopt_mock.call_args[0][0]
assert call_args.config == 'config.json'
assert call_args.loglevel == 20
assert call_args.subparser == 'hyperopt'
assert call_args.func is not None
def test_process_trade_creation(default_conf, ticker, limit_buy_order, health, mocker): def test_process_trade_creation(default_conf, ticker, limit_buy_order, health, mocker):
@ -331,7 +364,7 @@ def test_check_handle_timedout_buy(default_conf, ticker, health, limit_buy_order
cancel_order=cancel_order_mock) cancel_order=cancel_order_mock)
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
tradeBuy = Trade( trade_buy = Trade(
pair='BTC_ETH', pair='BTC_ETH',
open_rate=0.00001099, open_rate=0.00001099,
exchange='BITTREX', exchange='BITTREX',
@ -343,12 +376,12 @@ def test_check_handle_timedout_buy(default_conf, ticker, health, limit_buy_order
is_open=True is_open=True
) )
Trade.session.add(tradeBuy) Trade.session.add(trade_buy)
# check it does cancel buy orders over the time limit # check it does cancel buy orders over the time limit
check_handle_timedout(600) check_handle_timedout(600)
assert cancel_order_mock.call_count == 1 assert cancel_order_mock.call_count == 1
trades = Trade.query.filter(Trade.open_order_id.is_(tradeBuy.open_order_id)).all() trades = Trade.query.filter(Trade.open_order_id.is_(trade_buy.open_order_id)).all()
assert len(trades) == 0 assert len(trades) == 0
@ -363,7 +396,7 @@ def test_check_handle_timedout_sell(default_conf, ticker, health, limit_sell_ord
cancel_order=cancel_order_mock) cancel_order=cancel_order_mock)
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
tradeSell = Trade( trade_sell = Trade(
pair='BTC_ETH', pair='BTC_ETH',
open_rate=0.00001099, open_rate=0.00001099,
exchange='BITTREX', exchange='BITTREX',
@ -376,12 +409,12 @@ def test_check_handle_timedout_sell(default_conf, ticker, health, limit_sell_ord
is_open=False is_open=False
) )
Trade.session.add(tradeSell) Trade.session.add(trade_sell)
# check it does cancel sell orders over the time limit # check it does cancel sell orders over the time limit
check_handle_timedout(600) check_handle_timedout(600)
assert cancel_order_mock.call_count == 1 assert cancel_order_mock.call_count == 1
assert tradeSell.is_open is True assert trade_sell.is_open is True
def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old_partial, def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old_partial,
@ -396,7 +429,7 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old
cancel_order=cancel_order_mock) cancel_order=cancel_order_mock)
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
tradeBuy = Trade( trade_buy = Trade(
pair='BTC_ETH', pair='BTC_ETH',
open_rate=0.00001099, open_rate=0.00001099,
exchange='BITTREX', exchange='BITTREX',
@ -408,16 +441,16 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old
is_open=True is_open=True
) )
Trade.session.add(tradeBuy) Trade.session.add(trade_buy)
# check it does cancel buy orders over the time limit # check it does cancel buy orders over the time limit
# note this is for a partially-complete buy order # note this is for a partially-complete buy order
check_handle_timedout(600) check_handle_timedout(600)
assert cancel_order_mock.call_count == 1 assert cancel_order_mock.call_count == 1
trades = Trade.query.filter(Trade.open_order_id.is_(tradeBuy.open_order_id)).all() trades = Trade.query.filter(Trade.open_order_id.is_(trade_buy.open_order_id)).all()
assert len(trades) == 1 assert len(trades) == 1
assert trades[0].amount == 23.0 assert trades[0].amount == 23.0
assert trades[0].stake_amount == tradeBuy.open_rate * trades[0].amount assert trades[0].stake_amount == trade_buy.open_rate * trades[0].amount
def test_balance_fully_ask_side(mocker): def test_balance_fully_ask_side(mocker):
@ -537,10 +570,13 @@ def test_execute_sell_without_conf(default_conf, ticker, ticker_sell_up, mocker)
def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, mocker): def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, mocker):
default_conf['experimental'] = {} default_conf['experimental'] = {
default_conf['experimental']['sell_profit_only'] = True 'use_sell_signal': True,
'sell_profit_only': True,
}
mocker.patch.dict('freqtrade.main._CONF', default_conf) 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) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange', mocker.patch.multiple('freqtrade.main.exchange',
@ -561,10 +597,13 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, mocker):
def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, mocker): def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, mocker):
default_conf['experimental'] = {} default_conf['experimental'] = {
default_conf['experimental']['sell_profit_only'] = False 'use_sell_signal': True,
'sell_profit_only': False,
}
mocker.patch.dict('freqtrade.main._CONF', default_conf) 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) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange', mocker.patch.multiple('freqtrade.main.exchange',
@ -585,10 +624,13 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, mocker):
def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, mocker): def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, mocker):
default_conf['experimental'] = {} default_conf['experimental'] = {
default_conf['experimental']['sell_profit_only'] = True 'use_sell_signal': True,
'sell_profit_only': True,
}
mocker.patch.dict('freqtrade.main._CONF', default_conf) 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) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange', mocker.patch.multiple('freqtrade.main.exchange',
@ -609,10 +651,13 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, mocker):
def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, mocker): def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, mocker):
default_conf['experimental'] = {} default_conf['experimental'] = {
default_conf['experimental']['sell_profit_only'] = False 'use_sell_signal': True,
'sell_profit_only': False,
}
mocker.patch.dict('freqtrade.main._CONF', default_conf) 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) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange', mocker.patch.multiple('freqtrade.main.exchange',

View File

@ -1,13 +1,14 @@
# pragma pylint: disable=missing-docstring,C0103 # pragma pylint: disable=missing-docstring,C0103
import json import json
import time import time
import argparse
from copy import deepcopy from copy import deepcopy
from unittest.mock import MagicMock
import pytest import pytest
from jsonschema import ValidationError from jsonschema import ValidationError
from freqtrade.misc import throttle, parse_args, load_config from freqtrade.misc import throttle, parse_args, load_config,\
parse_args_common
def test_throttle(): def test_throttle():
@ -38,89 +39,83 @@ def test_throttle_with_assets():
assert result == -1 assert result == -1
# Parse common command-line-arguments
# used for all tools
def test_parse_args_none():
args = parse_args_common([], '')
assert isinstance(args, argparse.ArgumentParser)
def test_parse_args_defaults(): def test_parse_args_defaults():
args = parse_args([]) args = parse_args([], '')
assert args is not None
assert args.config == 'config.json' assert args.config == 'config.json'
assert args.dynamic_whitelist is None assert args.dynamic_whitelist is None
assert args.loglevel == 20 assert args.loglevel == 20
def test_parse_args_invalid():
with pytest.raises(SystemExit, match=r'2'):
parse_args(['-c'])
def test_parse_args_config(): def test_parse_args_config():
args = parse_args(['-c', '/dev/null']) args = parse_args(['-c', '/dev/null'], '')
assert args is not None
assert args.config == '/dev/null' assert args.config == '/dev/null'
args = parse_args(['--config', '/dev/null']) args = parse_args(['--config', '/dev/null'], '')
assert args is not None
assert args.config == '/dev/null' assert args.config == '/dev/null'
def test_parse_args_verbose(): def test_parse_args_verbose():
args = parse_args(['-v']) args = parse_args(['-v'], '')
assert args is not None assert args.loglevel == 10
args = parse_args(['--verbose'], '')
assert args.loglevel == 10 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(): def test_parse_args_dynamic_whitelist():
args = parse_args(['--dynamic-whitelist']) args = parse_args(['--dynamic-whitelist'], '')
assert args is not None
assert args.dynamic_whitelist is 20 assert args.dynamic_whitelist is 20
def test_parse_args_dynamic_whitelist_10(): def test_parse_args_dynamic_whitelist_10():
args = parse_args(['--dynamic-whitelist', '10']) args = parse_args(['--dynamic-whitelist', '10'], '')
assert args is not None
assert args.dynamic_whitelist is 10 assert args.dynamic_whitelist is 10
def test_parse_args_dynamic_whitelist_invalid_values(): def test_parse_args_dynamic_whitelist_invalid_values():
with pytest.raises(SystemExit, match=r'2'): with pytest.raises(SystemExit, match=r'2'):
parse_args(['--dynamic-whitelist', 'abc']) parse_args(['--dynamic-whitelist', 'abc'], '')
def test_parse_args_backtesting(mocker):
backtesting_mock = mocker.patch(
'freqtrade.optimize.backtesting.start', MagicMock())
args = parse_args(['backtesting'])
assert args is None
assert backtesting_mock.call_count == 1
call_args = backtesting_mock.call_args[0][0]
assert call_args.config == 'config.json'
assert call_args.live is False
assert call_args.loglevel == 20
assert call_args.subparser == 'backtesting'
assert call_args.func is not None
assert call_args.ticker_interval == 5
def test_parse_args_backtesting_invalid(): def test_parse_args_backtesting_invalid():
with pytest.raises(SystemExit, match=r'2'): with pytest.raises(SystemExit, match=r'2'):
parse_args(['backtesting --ticker-interval']) parse_args(['backtesting --ticker-interval'], '')
with pytest.raises(SystemExit, match=r'2'): with pytest.raises(SystemExit, match=r'2'):
parse_args(['backtesting --ticker-interval', 'abc']) parse_args(['backtesting --ticker-interval', 'abc'], '')
def test_parse_args_backtesting_custom(mocker): def test_parse_args_backtesting_custom():
backtesting_mock = mocker.patch( args = [
'freqtrade.optimize.backtesting.start', MagicMock())
args = parse_args([
'-c', 'test_conf.json', '-c', 'test_conf.json',
'backtesting', 'backtesting',
'--live', '--live',
'--ticker-interval', '1', '--ticker-interval', '1',
'--refresh-pairs-cached']) '--refresh-pairs-cached']
assert args is None call_args = parse_args(args, '')
assert backtesting_mock.call_count == 1
call_args = backtesting_mock.call_args[0][0]
assert call_args.config == 'test_conf.json' assert call_args.config == 'test_conf.json'
assert call_args.live is True assert call_args.live is True
assert call_args.loglevel == 20 assert call_args.loglevel == 20
@ -130,28 +125,9 @@ def test_parse_args_backtesting_custom(mocker):
assert call_args.refresh_pairs is True assert call_args.refresh_pairs is True
def test_parse_args_hyperopt(mocker):
hyperopt_mock = mocker.patch(
'freqtrade.optimize.hyperopt.start', MagicMock())
args = parse_args(['hyperopt'])
assert args is None
assert hyperopt_mock.call_count == 1
call_args = hyperopt_mock.call_args[0][0]
assert call_args.config == 'config.json'
assert call_args.loglevel == 20
assert call_args.subparser == 'hyperopt'
assert call_args.func is not None
def test_parse_args_hyperopt_custom(mocker): def test_parse_args_hyperopt_custom(mocker):
hyperopt_mock = mocker.patch( args = ['-c', 'test_conf.json', 'hyperopt', '--epochs', '20']
'freqtrade.optimize.hyperopt.start', MagicMock()) call_args = parse_args(args, '')
args = parse_args(['-c', 'test_conf.json', 'hyperopt', '--epochs', '20'])
assert args is None
assert hyperopt_mock.call_count == 1
call_args = hyperopt_mock.call_args[0][0]
assert call_args.config == 'test_conf.json' assert call_args.config == 'test_conf.json'
assert call_args.epochs == 20 assert call_args.epochs == 20
assert call_args.loglevel == 20 assert call_args.loglevel == 20

View File

@ -10,7 +10,7 @@ pandas==0.22.0
scikit-learn==0.19.1 scikit-learn==0.19.1
scipy==1.0.0 scipy==1.0.0
jsonschema==2.6.0 jsonschema==2.6.0
numpy==1.13.3 numpy==1.14.0
TA-Lib==0.4.10 TA-Lib==0.4.10
pytest==3.3.2 pytest==3.3.2
pytest-mock==1.6.3 pytest-mock==1.6.3

View File

@ -1,17 +1,33 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import sys
import argparse
import matplotlib # Install PYQT5 manually if you want to test this helper function import matplotlib # Install PYQT5 manually if you want to test this helper function
matplotlib.use("Qt5Agg") matplotlib.use("Qt5Agg")
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
from freqtrade import exchange, analyze from freqtrade import exchange, analyze
from freqtrade.misc import parse_args_common
def plot_analyzed_dataframe(pair: str) -> None: def plot_parse_args(args ):
parser = parse_args_common(args, 'Graph utility')
parser.add_argument(
'-p', '--pair',
help = 'What currency pair',
dest = 'pair',
default = 'BTC_ETH',
type = str,
)
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 :param pair: pair as str
:return: None :return: None
""" """
pair = args.pair
# Init Bittrex to use public API # Init Bittrex to use public API
exchange._API = exchange.Bittrex({'key': '', 'secret': ''}) exchange._API = exchange.Bittrex({'key': '', 'secret': ''})
@ -50,4 +66,5 @@ def plot_analyzed_dataframe(pair: str) -> None:
if __name__ == '__main__': if __name__ == '__main__':
plot_analyzed_dataframe('BTC_ETH') args = plot_parse_args(sys.argv[1:])
plot_analyzed_dataframe(args)