Merge branch 'release/0.14.3'
This commit is contained in:
commit
e9dbdc9247
|
@ -1,3 +1,10 @@
|
||||||
|
[MASTER]
|
||||||
|
extension-pkg-whitelist=numpy,talib
|
||||||
|
|
||||||
[BASIC]
|
[BASIC]
|
||||||
good-names=logger
|
good-names=logger
|
||||||
ignore=vendor
|
ignore=vendor
|
||||||
|
|
||||||
|
[TYPECHECK]
|
||||||
|
ignored-modules=numpy,talib
|
||||||
|
|
||||||
|
|
37
README.md
37
README.md
|
@ -137,6 +137,43 @@ $ docker start freqtrade
|
||||||
You do not need to rebuild the image for configuration
|
You do not need to rebuild the image for configuration
|
||||||
changes, it will suffice to edit `config.json` and restart the container.
|
changes, it will suffice to edit `config.json` and restart the container.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
```
|
||||||
|
usage: freqtrade [-h] [-c PATH] [-v] [--version] [--dynamic-whitelist]
|
||||||
|
{backtesting} ...
|
||||||
|
|
||||||
|
Simple High Frequency Trading Bot for crypto currencies
|
||||||
|
|
||||||
|
positional arguments:
|
||||||
|
{backtesting}
|
||||||
|
backtesting backtesting module
|
||||||
|
|
||||||
|
optional arguments:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
-c PATH, --config PATH
|
||||||
|
specify configuration file (default: config.json)
|
||||||
|
-v, --verbose be verbose
|
||||||
|
--version show program's version number and exit
|
||||||
|
--dynamic-whitelist dynamically generate and update whitelist based on 24h
|
||||||
|
BaseVolume
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backtesting
|
||||||
|
|
||||||
|
Backtesting also uses the config specified via `-c/--config`.
|
||||||
|
|
||||||
|
```
|
||||||
|
usage: freqtrade backtesting [-h] [-l] [-i INT]
|
||||||
|
|
||||||
|
optional arguments:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
-l, --live using live data
|
||||||
|
-i INT, --ticker-interval INT
|
||||||
|
specify ticker interval in minutes (default: 5)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
### Execute tests
|
### Execute tests
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
__version__ = '0.14.2'
|
""" FreqTrade bot """
|
||||||
|
__version__ = '0.14.3'
|
||||||
|
|
||||||
from . import main
|
from . import main
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
"""
|
||||||
|
Functions to analyze ticker data with indicators and produce buy and sell signals
|
||||||
|
"""
|
||||||
|
from enum import Enum
|
||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
|
@ -6,10 +10,15 @@ import talib.abstract as ta
|
||||||
from pandas import DataFrame, to_datetime
|
from pandas import DataFrame, to_datetime
|
||||||
|
|
||||||
from freqtrade.exchange import get_ticker_history
|
from freqtrade.exchange import get_ticker_history
|
||||||
from freqtrade.vendor.qtpylib.indicators import awesome_oscillator
|
from freqtrade.vendor.qtpylib.indicators import awesome_oscillator, crossed_above
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class SignalType(Enum):
|
||||||
|
""" Enum to distinguish between buy and sell signals """
|
||||||
|
BUY = "buy"
|
||||||
|
SELL = "sell"
|
||||||
|
|
||||||
|
|
||||||
def parse_ticker_dataframe(ticker: list) -> DataFrame:
|
def parse_ticker_dataframe(ticker: list) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
|
@ -57,18 +66,28 @@ def populate_indicators(dataframe: DataFrame) -> DataFrame:
|
||||||
|
|
||||||
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Based on TA indicators, populates the buy trend for the given dataframe
|
Based on TA indicators, populates the buy signal for the given dataframe
|
||||||
:param dataframe: DataFrame
|
:param dataframe: DataFrame
|
||||||
:return: DataFrame with buy column
|
:return: DataFrame with buy column
|
||||||
"""
|
"""
|
||||||
dataframe.ix[
|
dataframe.loc[
|
||||||
(dataframe['close'] < dataframe['sma']) &
|
|
||||||
(dataframe['tema'] <= dataframe['blower']) &
|
(dataframe['tema'] <= dataframe['blower']) &
|
||||||
(dataframe['mfi'] < 25) &
|
(dataframe['rsi'] < 37) &
|
||||||
(dataframe['fastd'] < 25) &
|
(dataframe['fastd'] < 48) &
|
||||||
(dataframe['adx'] > 30),
|
(dataframe['adx'] > 31),
|
||||||
'buy'] = 1
|
'buy'] = 1
|
||||||
dataframe.ix[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_sell_trend(dataframe: DataFrame) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Based on TA indicators, populates the sell signal for the given dataframe
|
||||||
|
:param dataframe: DataFrame
|
||||||
|
:return: DataFrame with buy column
|
||||||
|
"""
|
||||||
|
dataframe.loc[
|
||||||
|
(crossed_above(dataframe['rsi'], 70)),
|
||||||
|
'sell'] = 1
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
|
@ -87,12 +106,16 @@ def analyze_ticker(pair: str) -> DataFrame:
|
||||||
dataframe = parse_ticker_dataframe(ticker_hist)
|
dataframe = parse_ticker_dataframe(ticker_hist)
|
||||||
dataframe = populate_indicators(dataframe)
|
dataframe = populate_indicators(dataframe)
|
||||||
dataframe = populate_buy_trend(dataframe)
|
dataframe = populate_buy_trend(dataframe)
|
||||||
|
dataframe = populate_sell_trend(dataframe)
|
||||||
|
# TODO: buy_price and sell_price are only used by the plotter, should probably be moved there
|
||||||
|
dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
|
||||||
|
dataframe.loc[dataframe['sell'] == 1, 'sell_price'] = dataframe['close']
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
|
|
||||||
def get_buy_signal(pair: str) -> bool:
|
def get_signal(pair: str, signal: SignalType) -> bool:
|
||||||
"""
|
"""
|
||||||
Calculates a buy signal based several technical analysis indicators
|
Calculates current signal based several technical analysis indicators
|
||||||
:param pair: pair in format BTC_ANT or BTC-ANT
|
:param pair: pair in format BTC_ANT or BTC-ANT
|
||||||
:return: True if pair is good for buying, False otherwise
|
:return: True if pair is good for buying, False otherwise
|
||||||
"""
|
"""
|
||||||
|
@ -107,6 +130,6 @@ def get_buy_signal(pair: str) -> bool:
|
||||||
if signal_date < arrow.now() - timedelta(minutes=10):
|
if signal_date < arrow.now() - timedelta(minutes=10):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
signal = latest['buy'] == 1
|
result = latest[signal.value] == 1
|
||||||
logger.debug('buy_trigger: %s (pair=%s, signal=%s)', latest['date'], pair, signal)
|
logger.debug('%s_trigger: %s (pair=%s, signal=%s)', signal.value, latest['date'], pair, result)
|
||||||
return signal
|
return result
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
|
# pragma pylint: disable=W0603
|
||||||
|
""" Cryptocurrency Exchanges support """
|
||||||
import enum
|
import enum
|
||||||
import logging
|
import logging
|
||||||
from random import randint
|
from random import randint
|
||||||
from typing import List, Dict, Any, Optional
|
from typing import List, Dict, Any, Optional
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
|
import requests
|
||||||
from cachetools import cached, TTLCache
|
from cachetools import cached, TTLCache
|
||||||
|
|
||||||
from freqtrade.exchange.bittrex import Bittrex
|
from freqtrade.exchange.bittrex import Bittrex
|
||||||
|
@ -63,7 +66,12 @@ def validate_pairs(pairs: List[str]) -> None:
|
||||||
:param pairs: list of pairs
|
:param pairs: list of pairs
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
markets = _API.get_markets()
|
try:
|
||||||
|
markets = _API.get_markets()
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logger.warning('Unable to validate pairs (assuming they are correct). Reason: %s', e)
|
||||||
|
return
|
||||||
|
|
||||||
stake_cur = _CONF['stake_currency']
|
stake_cur = _CONF['stake_currency']
|
||||||
for pair in pairs:
|
for pair in pairs:
|
||||||
if not pair.startswith(stake_cur):
|
if not pair.startswith(stake_cur):
|
||||||
|
@ -77,7 +85,7 @@ def validate_pairs(pairs: List[str]) -> None:
|
||||||
def buy(pair: str, rate: float, amount: float) -> str:
|
def buy(pair: str, rate: float, amount: float) -> str:
|
||||||
if _CONF['dry_run']:
|
if _CONF['dry_run']:
|
||||||
global _DRY_RUN_OPEN_ORDERS
|
global _DRY_RUN_OPEN_ORDERS
|
||||||
order_id = 'dry_run_buy_{}'.format(randint(0, 1e6))
|
order_id = 'dry_run_buy_{}'.format(randint(0, 10**6))
|
||||||
_DRY_RUN_OPEN_ORDERS[order_id] = {
|
_DRY_RUN_OPEN_ORDERS[order_id] = {
|
||||||
'pair': pair,
|
'pair': pair,
|
||||||
'rate': rate,
|
'rate': rate,
|
||||||
|
@ -95,7 +103,7 @@ def buy(pair: str, rate: float, amount: float) -> str:
|
||||||
def sell(pair: str, rate: float, amount: float) -> str:
|
def sell(pair: str, rate: float, amount: float) -> str:
|
||||||
if _CONF['dry_run']:
|
if _CONF['dry_run']:
|
||||||
global _DRY_RUN_OPEN_ORDERS
|
global _DRY_RUN_OPEN_ORDERS
|
||||||
order_id = 'dry_run_sell_{}'.format(randint(0, 1e6))
|
order_id = 'dry_run_sell_{}'.format(randint(0, 10**6))
|
||||||
_DRY_RUN_OPEN_ORDERS[order_id] = {
|
_DRY_RUN_OPEN_ORDERS[order_id] = {
|
||||||
'pair': pair,
|
'pair': pair,
|
||||||
'rate': rate,
|
'rate': rate,
|
||||||
|
|
|
@ -2,6 +2,7 @@ import logging
|
||||||
from typing import List, Dict
|
from typing import List, Dict
|
||||||
|
|
||||||
from bittrex.bittrex import Bittrex as _Bittrex, API_V2_0, API_V1_1
|
from bittrex.bittrex import Bittrex as _Bittrex, API_V2_0, API_V1_1
|
||||||
|
from requests.exceptions import ContentDecodingError
|
||||||
|
|
||||||
from freqtrade.exchange.interface import Exchange
|
from freqtrade.exchange.interface import Exchange
|
||||||
|
|
||||||
|
@ -82,9 +83,13 @@ class Bittrex(Exchange):
|
||||||
raise RuntimeError('{message} params=({pair})'.format(
|
raise RuntimeError('{message} params=({pair})'.format(
|
||||||
message=data['message'],
|
message=data['message'],
|
||||||
pair=pair))
|
pair=pair))
|
||||||
if not data['result']['Bid'] or not data['result']['Ask'] or not data['result']['Last']:
|
|
||||||
raise RuntimeError('{message} params=({pair})'.format(
|
if not data.get('result') \
|
||||||
message=data['message'],
|
or not data['result'].get('Bid') \
|
||||||
|
or not data['result'].get('Ask') \
|
||||||
|
or not data['result'].get('Last'):
|
||||||
|
raise ContentDecodingError('{message} params=({pair})'.format(
|
||||||
|
message='Got invalid response from bittrex',
|
||||||
pair=pair))
|
pair=pair))
|
||||||
return {
|
return {
|
||||||
'bid': float(data['result']['Bid']),
|
'bid': float(data['result']['Bid']),
|
||||||
|
@ -104,13 +109,16 @@ class Bittrex(Exchange):
|
||||||
|
|
||||||
# These sanity check are necessary because bittrex cannot keep their API stable.
|
# These sanity check are necessary because bittrex cannot keep their API stable.
|
||||||
if not data.get('result'):
|
if not data.get('result'):
|
||||||
return []
|
raise ContentDecodingError('{message} params=({pair})'.format(
|
||||||
|
message='Got invalid response from bittrex',
|
||||||
|
pair=pair))
|
||||||
|
|
||||||
for prop in ['C', 'V', 'O', 'H', 'L', 'T']:
|
for prop in ['C', 'V', 'O', 'H', 'L', 'T']:
|
||||||
for tick in data['result']:
|
for tick in data['result']:
|
||||||
if prop not in tick.keys():
|
if prop not in tick.keys():
|
||||||
logger.warning('Required property %s not present in response', prop)
|
raise ContentDecodingError('{message} params=({pair})'.format(
|
||||||
return []
|
message='Required property {} not present in response'.format(prop),
|
||||||
|
pair=pair))
|
||||||
|
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
raise RuntimeError('{message} params=({pair})'.format(
|
raise RuntimeError('{message} params=({pair})'.format(
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import copy
|
import copy
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
@ -10,13 +11,12 @@ from typing import Dict, Optional, List
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from cachetools import cached, TTLCache
|
from cachetools import cached, TTLCache
|
||||||
from jsonschema import validate
|
|
||||||
|
|
||||||
from freqtrade import __version__, exchange, persistence
|
from freqtrade import __version__, exchange, persistence, rpc
|
||||||
from freqtrade.analyze import get_buy_signal
|
from freqtrade.analyze import get_signal, SignalType
|
||||||
from freqtrade.misc import CONF_SCHEMA, State, get_state, update_state, build_arg_parser, throttle
|
from freqtrade.misc import State, get_state, update_state, parse_args, throttle, \
|
||||||
|
load_config, FreqtradeException
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.rpc import telegram
|
|
||||||
|
|
||||||
logger = logging.getLogger('freqtrade')
|
logger = logging.getLogger('freqtrade')
|
||||||
|
|
||||||
|
@ -76,8 +76,8 @@ def _process(dynamic_whitelist: Optional[bool] = False) -> bool:
|
||||||
'Checked all whitelisted currencies. '
|
'Checked all whitelisted currencies. '
|
||||||
'Found no suitable entry positions for buying. Will keep looking ...'
|
'Found no suitable entry positions for buying. Will keep looking ...'
|
||||||
)
|
)
|
||||||
except ValueError:
|
except FreqtradeException as e:
|
||||||
logger.exception('Unable to create trade')
|
logger.warning('Unable to create trade: %s', e)
|
||||||
|
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
# Get order details for actual price per unit
|
# Get order details for actual price per unit
|
||||||
|
@ -86,17 +86,19 @@ def _process(dynamic_whitelist: Optional[bool] = False) -> bool:
|
||||||
logger.info('Got open order for %s', trade)
|
logger.info('Got open order for %s', trade)
|
||||||
trade.update(exchange.get_order(trade.open_order_id))
|
trade.update(exchange.get_order(trade.open_order_id))
|
||||||
|
|
||||||
if not close_trade_if_fulfilled(trade):
|
if trade.is_open and trade.open_order_id is None:
|
||||||
# Check if we can sell our current pair
|
# Check if we can sell our current pair
|
||||||
state_changed = handle_trade(trade) or state_changed
|
state_changed = handle_trade(trade) or state_changed
|
||||||
|
|
||||||
Trade.session.flush()
|
Trade.session.flush()
|
||||||
except (requests.exceptions.RequestException, json.JSONDecodeError) as error:
|
except (requests.exceptions.RequestException, json.JSONDecodeError) as error:
|
||||||
msg = 'Got {} in _process(), retrying in 30 seconds...'.format(error.__class__.__name__)
|
logger.warning(
|
||||||
logger.exception(msg)
|
'Got %s in _process(), retrying in 30 seconds...',
|
||||||
|
error
|
||||||
|
)
|
||||||
time.sleep(30)
|
time.sleep(30)
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
telegram.send_msg('*Status:* Got RuntimeError:\n```\n{traceback}```{hint}'.format(
|
rpc.send_msg('*Status:* Got RuntimeError:\n```\n{traceback}```{hint}'.format(
|
||||||
traceback=traceback.format_exc(),
|
traceback=traceback.format_exc(),
|
||||||
hint='Issue `/start` if you think it is safe to restart.'
|
hint='Issue `/start` if you think it is safe to restart.'
|
||||||
))
|
))
|
||||||
|
@ -105,27 +107,6 @@ def _process(dynamic_whitelist: Optional[bool] = False) -> bool:
|
||||||
return state_changed
|
return state_changed
|
||||||
|
|
||||||
|
|
||||||
def close_trade_if_fulfilled(trade: Trade) -> bool:
|
|
||||||
"""
|
|
||||||
Checks if the trade is closable, and if so it is being closed.
|
|
||||||
:param trade: Trade
|
|
||||||
:return: True if trade has been closed else False
|
|
||||||
"""
|
|
||||||
# If we don't have an open order and the close rate is already set,
|
|
||||||
# we can close this trade.
|
|
||||||
if trade.close_profit is not None \
|
|
||||||
and trade.close_date is not None \
|
|
||||||
and trade.close_rate is not None \
|
|
||||||
and trade.open_order_id is None:
|
|
||||||
trade.is_open = False
|
|
||||||
logger.info(
|
|
||||||
'Marking %s as closed as the trade is fulfilled and found no open orders for it.',
|
|
||||||
trade
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def execute_sell(trade: Trade, limit: float) -> None:
|
def execute_sell(trade: Trade, limit: float) -> None:
|
||||||
"""
|
"""
|
||||||
Executes a limit sell for the given trade and limit
|
Executes a limit sell for the given trade and limit
|
||||||
|
@ -138,20 +119,18 @@ def execute_sell(trade: Trade, limit: float) -> None:
|
||||||
trade.open_order_id = order_id
|
trade.open_order_id = order_id
|
||||||
|
|
||||||
fmt_exp_profit = round(trade.calc_profit(limit) * 100, 2)
|
fmt_exp_profit = round(trade.calc_profit(limit) * 100, 2)
|
||||||
message = '*{}:* Selling [{}]({}) with limit `{:.8f} (profit: ~{:.2f}%)`'.format(
|
rpc.send_msg('*{}:* Selling [{}]({}) with limit `{:.8f} (profit: ~{:.2f}%)`'.format(
|
||||||
trade.exchange,
|
trade.exchange,
|
||||||
trade.pair.replace('_', '/'),
|
trade.pair.replace('_', '/'),
|
||||||
exchange.get_pair_detail_url(trade.pair),
|
exchange.get_pair_detail_url(trade.pair),
|
||||||
limit,
|
limit,
|
||||||
fmt_exp_profit
|
fmt_exp_profit
|
||||||
)
|
))
|
||||||
logger.info(message)
|
|
||||||
telegram.send_msg(message)
|
|
||||||
|
|
||||||
|
|
||||||
def should_sell(trade: Trade, current_rate: float, current_time: datetime) -> bool:
|
def min_roi_reached(trade: Trade, current_rate: float, current_time: datetime) -> bool:
|
||||||
"""
|
"""
|
||||||
Based an earlier trade and current price and configuration, decides whether bot should sell
|
Based an earlier trade and current price and ROI configuration, decides whether bot should sell
|
||||||
:return True if bot should sell at current rate
|
:return True if bot should sell at current rate
|
||||||
"""
|
"""
|
||||||
current_profit = trade.calc_profit(current_rate)
|
current_profit = trade.calc_profit(current_rate)
|
||||||
|
@ -159,9 +138,9 @@ def should_sell(trade: Trade, current_rate: float, current_time: datetime) -> bo
|
||||||
logger.debug('Stop loss hit.')
|
logger.debug('Stop loss hit.')
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
# Check if time matches and current rate is above threshold
|
||||||
|
time_diff = (current_time - trade.open_date).total_seconds() / 60
|
||||||
for duration, threshold in sorted(_CONF['minimal_roi'].items()):
|
for duration, threshold in sorted(_CONF['minimal_roi'].items()):
|
||||||
# Check if time matches and current rate is above threshold
|
|
||||||
time_diff = (current_time - trade.open_date).total_seconds() / 60
|
|
||||||
if time_diff > float(duration) and current_profit > threshold:
|
if time_diff > float(duration) and current_profit > threshold:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -179,7 +158,7 @@ def handle_trade(trade: Trade) -> bool:
|
||||||
|
|
||||||
logger.debug('Handling %s ...', trade)
|
logger.debug('Handling %s ...', trade)
|
||||||
current_rate = exchange.get_ticker(trade.pair)['bid']
|
current_rate = exchange.get_ticker(trade.pair)['bid']
|
||||||
if should_sell(trade, current_rate, datetime.utcnow()):
|
if min_roi_reached(trade, current_rate, datetime.utcnow()) or get_signal(trade.pair, SignalType.SELL):
|
||||||
execute_sell(trade, current_rate)
|
execute_sell(trade, current_rate)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
@ -206,7 +185,7 @@ def create_trade(stake_amount: float) -> Optional[Trade]:
|
||||||
whitelist = copy.deepcopy(_CONF['exchange']['pair_whitelist'])
|
whitelist = copy.deepcopy(_CONF['exchange']['pair_whitelist'])
|
||||||
# Check if stake_amount is fulfilled
|
# Check if stake_amount is fulfilled
|
||||||
if exchange.get_balance(_CONF['stake_currency']) < stake_amount:
|
if exchange.get_balance(_CONF['stake_currency']) < stake_amount:
|
||||||
raise ValueError(
|
raise FreqtradeException(
|
||||||
'stake amount is not fulfilled (currency={})'.format(_CONF['stake_currency'])
|
'stake amount is not fulfilled (currency={})'.format(_CONF['stake_currency'])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -216,11 +195,11 @@ def create_trade(stake_amount: float) -> Optional[Trade]:
|
||||||
whitelist.remove(trade.pair)
|
whitelist.remove(trade.pair)
|
||||||
logger.debug('Ignoring %s in pair whitelist', trade.pair)
|
logger.debug('Ignoring %s in pair whitelist', trade.pair)
|
||||||
if not whitelist:
|
if not whitelist:
|
||||||
raise ValueError('No pair in whitelist')
|
raise FreqtradeException('No pair in whitelist')
|
||||||
|
|
||||||
# Pick pair based on StochRSI buy signals
|
# Pick pair based on StochRSI buy signals
|
||||||
for _pair in whitelist:
|
for _pair in whitelist:
|
||||||
if get_buy_signal(_pair):
|
if get_signal(_pair, SignalType.BUY):
|
||||||
pair = _pair
|
pair = _pair
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
|
@ -233,14 +212,12 @@ def create_trade(stake_amount: float) -> Optional[Trade]:
|
||||||
|
|
||||||
order_id = exchange.buy(pair, buy_limit, amount)
|
order_id = exchange.buy(pair, buy_limit, amount)
|
||||||
# Create trade entity and return
|
# Create trade entity and return
|
||||||
message = '*{}:* Buying [{}]({}) with limit `{:.8f}`'.format(
|
rpc.send_msg('*{}:* Buying [{}]({}) with limit `{:.8f}`'.format(
|
||||||
exchange.get_name().upper(),
|
exchange.get_name().upper(),
|
||||||
pair.replace('_', '/'),
|
pair.replace('_', '/'),
|
||||||
exchange.get_pair_detail_url(pair),
|
exchange.get_pair_detail_url(pair),
|
||||||
buy_limit
|
buy_limit
|
||||||
)
|
))
|
||||||
logger.info(message)
|
|
||||||
telegram.send_msg(message)
|
|
||||||
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
||||||
return Trade(pair=pair,
|
return Trade(pair=pair,
|
||||||
stake_amount=stake_amount,
|
stake_amount=stake_amount,
|
||||||
|
@ -260,7 +237,7 @@ def init(config: dict, db_url: Optional[str] = None) -> None:
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
# Initialize all modules
|
# Initialize all modules
|
||||||
telegram.init(config)
|
rpc.init(config)
|
||||||
persistence.init(config, db_url)
|
persistence.init(config, db_url)
|
||||||
exchange.init(config)
|
exchange.init(config)
|
||||||
|
|
||||||
|
@ -298,11 +275,11 @@ def cleanup(*args, **kwargs) -> None:
|
||||||
Cleanup the application state und finish all pending tasks
|
Cleanup the application state und finish all pending tasks
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
telegram.send_msg('*Status:* `Stopping trader...`')
|
rpc.send_msg('*Status:* `Stopping trader...`')
|
||||||
logger.info('Stopping trader and cleaning up modules...')
|
logger.info('Stopping trader and cleaning up modules...')
|
||||||
update_state(State.STOPPED)
|
update_state(State.STOPPED)
|
||||||
persistence.cleanup()
|
persistence.cleanup()
|
||||||
telegram.cleanup()
|
rpc.cleanup()
|
||||||
exit(0)
|
exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
@ -312,7 +289,9 @@ def main():
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
global _CONF
|
global _CONF
|
||||||
args = build_arg_parser().parse_args()
|
args = parse_args(sys.argv[1:])
|
||||||
|
if not args:
|
||||||
|
exit(0)
|
||||||
|
|
||||||
# Initialize logger
|
# Initialize logger
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
|
@ -327,12 +306,7 @@ def main():
|
||||||
)
|
)
|
||||||
|
|
||||||
# Load and validate configuration
|
# Load and validate configuration
|
||||||
with open(args.config) as file:
|
_CONF = load_config(args.config)
|
||||||
_CONF = json.load(file)
|
|
||||||
if 'internals' not in _CONF:
|
|
||||||
_CONF['internals'] = {}
|
|
||||||
logger.info('Validating configuration ...')
|
|
||||||
validate(_CONF, CONF_SCHEMA)
|
|
||||||
|
|
||||||
# Initialize all modules and start main loop
|
# Initialize all modules and start main loop
|
||||||
if args.dynamic_whitelist:
|
if args.dynamic_whitelist:
|
||||||
|
@ -343,7 +317,7 @@ def main():
|
||||||
new_state = get_state()
|
new_state = get_state()
|
||||||
# Log state transition
|
# Log state transition
|
||||||
if new_state != old_state:
|
if new_state != old_state:
|
||||||
telegram.send_msg('*Status:* `{}`'.format(new_state.name.lower()))
|
rpc.send_msg('*Status:* `{}`'.format(new_state.name.lower()))
|
||||||
logger.info('Changing state to: %s', new_state.name)
|
logger.info('Changing state to: %s', new_state.name)
|
||||||
|
|
||||||
if new_state == State.STOPPED:
|
if new_state == State.STOPPED:
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
import argparse
|
import argparse
|
||||||
import enum
|
import enum
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Callable
|
import os
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
from typing import Any, Callable, List, Dict
|
||||||
|
|
||||||
|
from jsonschema import validate, Draft4Validator
|
||||||
|
from jsonschema.exceptions import best_match, ValidationError
|
||||||
from wrapt import synchronized
|
from wrapt import synchronized
|
||||||
|
|
||||||
from freqtrade import __version__
|
from freqtrade import __version__
|
||||||
|
@ -11,6 +15,10 @@ from freqtrade import __version__
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class FreqtradeException(BaseException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class State(enum.Enum):
|
class State(enum.Enum):
|
||||||
RUNNING = 0
|
RUNNING = 0
|
||||||
STOPPED = 1
|
STOPPED = 1
|
||||||
|
@ -40,6 +48,27 @@ def get_state() -> State:
|
||||||
return _STATE
|
return _STATE
|
||||||
|
|
||||||
|
|
||||||
|
def load_config(path: str) -> Dict:
|
||||||
|
"""
|
||||||
|
Loads a config file from the given path
|
||||||
|
:param path: path as str
|
||||||
|
:return: configuration as dictionary
|
||||||
|
"""
|
||||||
|
with open(path) as file:
|
||||||
|
conf = json.load(file)
|
||||||
|
if 'internals' not in conf:
|
||||||
|
conf['internals'] = {}
|
||||||
|
logger.info('Validating configuration ...')
|
||||||
|
try:
|
||||||
|
validate(conf, CONF_SCHEMA)
|
||||||
|
return conf
|
||||||
|
except ValidationError:
|
||||||
|
logger.fatal('Configuration is not valid! See config.json.example')
|
||||||
|
raise ValidationError(
|
||||||
|
best_match(Draft4Validator(CONF_SCHEMA).iter_errors(conf)).message
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def throttle(func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any:
|
def throttle(func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any:
|
||||||
"""
|
"""
|
||||||
Throttles the given callable that it
|
Throttles the given callable that it
|
||||||
|
@ -57,8 +86,11 @@ def throttle(func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def build_arg_parser() -> argparse.ArgumentParser:
|
def parse_args(args: List[str]):
|
||||||
""" Builds and returns an ArgumentParser instance """
|
"""
|
||||||
|
Parses given arguments and returns an argparse Namespace instance.
|
||||||
|
Returns None if a sub command has been selected and executed.
|
||||||
|
"""
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description='Simple High Frequency Trading Bot for crypto currencies'
|
description='Simple High Frequency Trading Bot for crypto currencies'
|
||||||
)
|
)
|
||||||
|
@ -88,7 +120,54 @@ def build_arg_parser() -> argparse.ArgumentParser:
|
||||||
help='dynamically generate and update whitelist based on 24h BaseVolume',
|
help='dynamically generate and update whitelist based on 24h BaseVolume',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
)
|
)
|
||||||
return parser
|
build_subcommands(parser)
|
||||||
|
parsed_args = parser.parse_args(args)
|
||||||
|
|
||||||
|
# No subcommand as been selected
|
||||||
|
if not hasattr(parsed_args, 'func'):
|
||||||
|
return parsed_args
|
||||||
|
|
||||||
|
parsed_args.func(parsed_args)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def build_subcommands(parser: argparse.ArgumentParser) -> None:
|
||||||
|
""" Builds and attaches all subcommands """
|
||||||
|
subparsers = parser.add_subparsers(dest='subparser')
|
||||||
|
backtest = subparsers.add_parser('backtesting', help='backtesting module')
|
||||||
|
backtest.set_defaults(func=start_backtesting)
|
||||||
|
backtest.add_argument(
|
||||||
|
'-l', '--live',
|
||||||
|
action='store_true',
|
||||||
|
dest='live',
|
||||||
|
help='using live data',
|
||||||
|
)
|
||||||
|
backtest.add_argument(
|
||||||
|
'-i', '--ticker-interval',
|
||||||
|
help='specify ticker interval in minutes (default: 5)',
|
||||||
|
dest='ticker_interval',
|
||||||
|
default=5,
|
||||||
|
type=int,
|
||||||
|
metavar='INT',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def start_backtesting(args) -> None:
|
||||||
|
"""
|
||||||
|
Exports all args as environment variables and starts backtesting via pytest.
|
||||||
|
:param args: arguments namespace
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
os.environ.update({
|
||||||
|
'BACKTEST': 'true',
|
||||||
|
'BACKTEST_LIVE': 'true' if args.live else '',
|
||||||
|
'BACKTEST_CONFIG': args.config,
|
||||||
|
'BACKTEST_TICKER_INTERVAL': str(args.ticker_interval),
|
||||||
|
})
|
||||||
|
path = os.path.join(os.path.dirname(__file__), 'tests', 'test_backtesting.py')
|
||||||
|
pytest.main(['-s', path])
|
||||||
|
|
||||||
|
|
||||||
# Required json-schema for user specified config
|
# Required json-schema for user specified config
|
||||||
|
@ -146,7 +225,10 @@ CONF_SCHEMA = {
|
||||||
'secret': {'type': 'string'},
|
'secret': {'type': 'string'},
|
||||||
'pair_whitelist': {
|
'pair_whitelist': {
|
||||||
'type': 'array',
|
'type': 'array',
|
||||||
'items': {'type': 'string'},
|
'items': {
|
||||||
|
'type': 'string',
|
||||||
|
'pattern': '^[0-9A-Z]+_[0-9A-Z]+$'
|
||||||
|
},
|
||||||
'uniqueItems': True
|
'uniqueItems': True
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -85,20 +85,27 @@ class Trade(_DECL_BASE):
|
||||||
if not order['closed']:
|
if not order['closed']:
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.debug('Updating trade (id=%d) ...', self.id)
|
logger.info('Updating trade (id=%d) ...', self.id)
|
||||||
if order['type'] == 'LIMIT_BUY':
|
if order['type'] == 'LIMIT_BUY':
|
||||||
# Update open rate and actual amount
|
# Update open rate and actual amount
|
||||||
self.open_rate = order['rate']
|
self.open_rate = order['rate']
|
||||||
self.amount = order['amount']
|
self.amount = order['amount']
|
||||||
|
logger.info('LIMIT_BUY has been fulfilled for %s.', self)
|
||||||
elif order['type'] == 'LIMIT_SELL':
|
elif order['type'] == 'LIMIT_SELL':
|
||||||
# Set close rate and set actual profit
|
# Set close rate and set actual profit
|
||||||
self.close_rate = order['rate']
|
self.close_rate = order['rate']
|
||||||
self.close_profit = self.calc_profit()
|
self.close_profit = self.calc_profit()
|
||||||
self.close_date = datetime.utcnow()
|
self.close_date = datetime.utcnow()
|
||||||
|
self.is_open = False
|
||||||
|
logger.info(
|
||||||
|
'Marking %s as closed as the trade is fulfilled and found no open orders for it.',
|
||||||
|
self
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise ValueError('Unknown order type: {}'.format(order['type']))
|
raise ValueError('Unknown order type: {}'.format(order['type']))
|
||||||
|
|
||||||
self.open_order_id = None
|
self.open_order_id = None
|
||||||
|
Trade.session.flush()
|
||||||
|
|
||||||
def calc_profit(self, rate: Optional[float] = None) -> float:
|
def calc_profit(self, rate: Optional[float] = None) -> float:
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1 +1,42 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
from . import telegram
|
from . import telegram
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
REGISTERED_MODULES = []
|
||||||
|
|
||||||
|
|
||||||
|
def init(config: dict) -> None:
|
||||||
|
"""
|
||||||
|
Initializes all enabled rpc modules
|
||||||
|
:param config: config to use
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
|
||||||
|
if config['telegram'].get('enabled', False):
|
||||||
|
logger.info('Enabling rpc.telegram ...')
|
||||||
|
REGISTERED_MODULES.append('telegram')
|
||||||
|
telegram.init(config)
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup() -> None:
|
||||||
|
"""
|
||||||
|
Stops all enabled rpc modules
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
if 'telegram' in REGISTERED_MODULES:
|
||||||
|
logger.debug('Cleaning up rpc.telegram ...')
|
||||||
|
telegram.cleanup()
|
||||||
|
|
||||||
|
|
||||||
|
def send_msg(msg: str) -> None:
|
||||||
|
"""
|
||||||
|
Send given markdown message to all registered rpc modules
|
||||||
|
:param msg: message
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
logger.info(msg)
|
||||||
|
if 'telegram' in REGISTERED_MODULES:
|
||||||
|
telegram.send_msg(msg)
|
||||||
|
|
|
@ -8,7 +8,7 @@ from tabulate import tabulate
|
||||||
import arrow
|
import arrow
|
||||||
from sqlalchemy import and_, func, text
|
from sqlalchemy import and_, func, text
|
||||||
from telegram import ParseMode, Bot, Update
|
from telegram import ParseMode, Bot, Update
|
||||||
from telegram.error import NetworkError
|
from telegram.error import NetworkError, TelegramError
|
||||||
from telegram.ext import CommandHandler, Updater
|
from telegram.ext import CommandHandler, Updater
|
||||||
|
|
||||||
from freqtrade import exchange, __version__
|
from freqtrade import exchange, __version__
|
||||||
|
@ -57,7 +57,7 @@ def init(config: dict) -> None:
|
||||||
_UPDATER.dispatcher.add_handler(handle)
|
_UPDATER.dispatcher.add_handler(handle)
|
||||||
_UPDATER.start_polling(
|
_UPDATER.start_polling(
|
||||||
clean=True,
|
clean=True,
|
||||||
bootstrap_retries=3,
|
bootstrap_retries=-1,
|
||||||
timeout=30,
|
timeout=30,
|
||||||
read_latency=60,
|
read_latency=60,
|
||||||
)
|
)
|
||||||
|
@ -475,13 +475,17 @@ def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDO
|
||||||
return
|
return
|
||||||
|
|
||||||
bot = bot or _UPDATER.bot
|
bot = bot or _UPDATER.bot
|
||||||
|
|
||||||
try:
|
try:
|
||||||
bot.send_message(_CONF['telegram']['chat_id'], msg, parse_mode=parse_mode)
|
try:
|
||||||
except NetworkError as error:
|
bot.send_message(_CONF['telegram']['chat_id'], msg, parse_mode=parse_mode)
|
||||||
# Sometimes the telegram server resets the current connection,
|
except NetworkError as network_err:
|
||||||
# if this is the case we send the message again.
|
# Sometimes the telegram server resets the current connection,
|
||||||
logger.warning(
|
# if this is the case we send the message again.
|
||||||
'Got Telegram NetworkError: %s! Trying one more time.',
|
logger.warning(
|
||||||
error.message
|
'Got Telegram NetworkError: %s! Trying one more time.',
|
||||||
)
|
network_err.message
|
||||||
bot.send_message(_CONF['telegram']['chat_id'], msg, parse_mode=parse_mode)
|
)
|
||||||
|
bot.send_message(_CONF['telegram']['chat_id'], msg, parse_mode=parse_mode)
|
||||||
|
except TelegramError as telegram_err:
|
||||||
|
logger.warning('Got TelegramError: %s! Giving up on that message.', telegram_err.message)
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
# pragma pylint: disable=missing-docstring
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def load_backtesting_data(ticker_interval: int = 5):
|
||||||
|
path = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
result = {}
|
||||||
|
pairs = [
|
||||||
|
'BTC_BCC', 'BTC_ETH', 'BTC_DASH', 'BTC_POWR', 'BTC_ETC',
|
||||||
|
'BTC_VTC', 'BTC_WAVES', 'BTC_LSK', 'BTC_XLM', 'BTC_OK',
|
||||||
|
]
|
||||||
|
for pair in pairs:
|
||||||
|
with open('{abspath}/testdata/{pair}-{ticker_interval}.json'.format(
|
||||||
|
abspath=path,
|
||||||
|
pair=pair,
|
||||||
|
ticker_interval=ticker_interval,
|
||||||
|
)) as tickerdata:
|
||||||
|
result[pair] = json.load(tickerdata)
|
||||||
|
return result
|
|
@ -1,5 +1,4 @@
|
||||||
# pragma pylint: disable=missing-docstring
|
# pragma pylint: disable=missing-docstring
|
||||||
import json
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
@ -55,6 +54,8 @@ def default_conf():
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def backtest_conf():
|
def backtest_conf():
|
||||||
return {
|
return {
|
||||||
|
"stake_currency": "BTC",
|
||||||
|
"stake_amount": 0.01,
|
||||||
"minimal_roi": {
|
"minimal_roi": {
|
||||||
"40": 0.0,
|
"40": 0.0,
|
||||||
"30": 0.01,
|
"30": 0.01,
|
||||||
|
@ -65,16 +66,6 @@ def backtest_conf():
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
|
||||||
def backdata():
|
|
||||||
result = {}
|
|
||||||
for pair in ['btc-neo', 'btc-eth', 'btc-omg', 'btc-edg', 'btc-pay',
|
|
||||||
'btc-pivx', 'btc-qtum', 'btc-mtl', 'btc-etc', 'btc-ltc']:
|
|
||||||
with open('freqtrade/tests/testdata/' + pair + '.json') as data_file:
|
|
||||||
result[pair] = json.load(data_file)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def update():
|
def update():
|
||||||
_update = Update(0)
|
_update = Update(0)
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
# pragma pylint: disable=missing-docstring
|
# pragma pylint: disable=missing-docstring,W0621
|
||||||
from datetime import datetime
|
|
||||||
import json
|
import json
|
||||||
|
import arrow
|
||||||
import pytest
|
import pytest
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.analyze import parse_ticker_dataframe, populate_buy_trend, populate_indicators, \
|
from freqtrade.analyze import parse_ticker_dataframe, populate_buy_trend, populate_indicators, \
|
||||||
get_buy_signal
|
get_signal, SignalType, populate_sell_trend
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def result():
|
def result():
|
||||||
with open('freqtrade/tests/testdata/btc-eth.json') as data_file:
|
with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file:
|
||||||
return parse_ticker_dataframe(json.load(data_file))
|
return parse_ticker_dataframe(json.load(data_file))
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,20 +20,34 @@ def test_dataframe_correct_columns(result):
|
||||||
|
|
||||||
|
|
||||||
def test_dataframe_correct_length(result):
|
def test_dataframe_correct_length(result):
|
||||||
assert len(result.index) == 5751
|
assert len(result.index) == 14382
|
||||||
|
|
||||||
|
|
||||||
def test_populates_buy_trend(result):
|
def test_populates_buy_trend(result):
|
||||||
dataframe = populate_buy_trend(populate_indicators(result))
|
dataframe = populate_buy_trend(populate_indicators(result))
|
||||||
assert 'buy' in dataframe.columns
|
assert 'buy' in dataframe.columns
|
||||||
assert 'buy_price' in dataframe.columns
|
|
||||||
|
|
||||||
|
def test_populates_sell_trend(result):
|
||||||
|
dataframe = populate_sell_trend(populate_indicators(result))
|
||||||
|
assert 'sell' in dataframe.columns
|
||||||
|
|
||||||
|
|
||||||
def test_returns_latest_buy_signal(mocker):
|
def test_returns_latest_buy_signal(mocker):
|
||||||
buydf = DataFrame([{'buy': 1, 'date': datetime.today()}])
|
buydf = DataFrame([{'buy': 1, 'date': arrow.utcnow()}])
|
||||||
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf)
|
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf)
|
||||||
assert get_buy_signal('BTC-ETH')
|
assert get_signal('BTC-ETH', SignalType.BUY)
|
||||||
|
|
||||||
buydf = DataFrame([{'buy': 0, 'date': datetime.today()}])
|
buydf = DataFrame([{'buy': 0, 'date': arrow.utcnow()}])
|
||||||
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf)
|
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf)
|
||||||
assert not get_buy_signal('BTC-ETH')
|
assert not get_signal('BTC-ETH', SignalType.BUY)
|
||||||
|
|
||||||
|
|
||||||
|
def test_returns_latest_sell_signal(mocker):
|
||||||
|
selldf = DataFrame([{'sell': 1, 'date': arrow.utcnow()}])
|
||||||
|
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=selldf)
|
||||||
|
assert get_signal('BTC-ETH', SignalType.SELL)
|
||||||
|
|
||||||
|
selldf = DataFrame([{'sell': 0, 'date': arrow.utcnow()}])
|
||||||
|
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=selldf)
|
||||||
|
assert not get_signal('BTC-ETH', SignalType.SELL)
|
||||||
|
|
|
@ -1,65 +1,154 @@
|
||||||
# pragma pylint: disable=missing-docstring
|
# pragma pylint: disable=missing-docstring,W0212
|
||||||
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from typing import Tuple, Dict
|
||||||
|
|
||||||
import pytest
|
|
||||||
import arrow
|
import arrow
|
||||||
|
import pytest
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
from tabulate import tabulate
|
||||||
|
|
||||||
from freqtrade import exchange
|
from freqtrade import exchange
|
||||||
from freqtrade.analyze import analyze_ticker
|
from freqtrade.analyze import parse_ticker_dataframe, populate_indicators, \
|
||||||
|
populate_buy_trend, populate_sell_trend
|
||||||
from freqtrade.exchange import Bittrex
|
from freqtrade.exchange import Bittrex
|
||||||
from freqtrade.main import should_sell
|
from freqtrade.main import min_roi_reached
|
||||||
|
from freqtrade.misc import load_config
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
|
from freqtrade.tests import load_backtesting_data
|
||||||
|
|
||||||
logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def format_results(results):
|
def format_results(results: DataFrame):
|
||||||
return 'Made {} buys. Average profit {:.2f}%. Total profit was {:.3f}. Average duration {:.1f} mins.'.format(
|
return ('Made {:6d} buys. Average profit {: 5.2f}%. '
|
||||||
len(results.index), results.profit.mean() * 100.0, results.profit.sum(), results.duration.mean() * 5)
|
'Total profit was {: 7.3f}. Average duration {:5.1f} mins.').format(
|
||||||
|
len(results.index),
|
||||||
|
results.profit.mean() * 100.0,
|
||||||
|
results.profit.sum(),
|
||||||
|
results.duration.mean() * 5,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def print_pair_results(pair, results):
|
def preprocess(backdata) -> Dict[str, DataFrame]:
|
||||||
print('For currency {}:'.format(pair))
|
processed = {}
|
||||||
print(format_results(results[results.currency == pair]))
|
for pair, pair_data in backdata.items():
|
||||||
|
processed[pair] = populate_indicators(parse_ticker_dataframe(pair_data))
|
||||||
|
return processed
|
||||||
|
|
||||||
|
|
||||||
def backtest(backtest_conf, backdata, mocker):
|
def get_timeframe(data: Dict[str, Dict]) -> Tuple[arrow.Arrow, arrow.Arrow]:
|
||||||
|
"""
|
||||||
|
Get the maximum timeframe for the given backtest data
|
||||||
|
:param data: dictionary with backtesting data
|
||||||
|
:return: tuple containing min_date, max_date
|
||||||
|
"""
|
||||||
|
min_date, max_date = None, None
|
||||||
|
for values in data.values():
|
||||||
|
sorted_values = sorted(values, key=lambda d: arrow.get(d['T']))
|
||||||
|
if not min_date or sorted_values[0]['T'] < min_date:
|
||||||
|
min_date = sorted_values[0]['T']
|
||||||
|
if not max_date or sorted_values[-1]['T'] > max_date:
|
||||||
|
max_date = sorted_values[-1]['T']
|
||||||
|
return arrow.get(min_date), arrow.get(max_date)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_text_table(data: Dict[str, Dict], results: DataFrame, stake_currency) -> str:
|
||||||
|
"""
|
||||||
|
Generates and returns a text table for the given backtest data and the results dataframe
|
||||||
|
:return: pretty printed table with tabulate as str
|
||||||
|
"""
|
||||||
|
tabular_data = []
|
||||||
|
headers = ['pair', 'buy count', 'avg profit', 'total profit', 'avg duration']
|
||||||
|
for pair in data:
|
||||||
|
result = results[results.currency == pair]
|
||||||
|
tabular_data.append([
|
||||||
|
pair,
|
||||||
|
len(result.index),
|
||||||
|
'{:.2f}%'.format(result.profit.mean() * 100.0),
|
||||||
|
'{:.08f} {}'.format(result.profit.sum(), stake_currency),
|
||||||
|
'{:.2f}'.format(result.duration.mean() * 5),
|
||||||
|
])
|
||||||
|
|
||||||
|
# Append Total
|
||||||
|
tabular_data.append([
|
||||||
|
'TOTAL',
|
||||||
|
len(results.index),
|
||||||
|
'{:.2f}%'.format(results.profit.mean() * 100.0),
|
||||||
|
'{:.08f} {}'.format(results.profit.sum(), stake_currency),
|
||||||
|
'{:.2f}'.format(results.duration.mean() * 5),
|
||||||
|
])
|
||||||
|
return tabulate(tabular_data, headers=headers)
|
||||||
|
|
||||||
|
|
||||||
|
def backtest(backtest_conf, processed, mocker):
|
||||||
trades = []
|
trades = []
|
||||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
exchange._API = Bittrex({'key': '', 'secret': ''})
|
||||||
mocked_history = mocker.patch('freqtrade.analyze.get_ticker_history')
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', backtest_conf)
|
mocker.patch.dict('freqtrade.main._CONF', backtest_conf)
|
||||||
mocker.patch('arrow.utcnow', return_value=arrow.get('2017-08-20T14:50:00'))
|
for pair, pair_data in processed.items():
|
||||||
for pair, pair_data in backdata.items():
|
pair_data['buy'] = 0
|
||||||
mocked_history.return_value = pair_data
|
pair_data['sell'] = 0
|
||||||
ticker = analyze_ticker(pair)[['close', 'date', 'buy']].copy()
|
ticker = populate_sell_trend(populate_buy_trend(pair_data))
|
||||||
# for each buy point
|
# for each buy point
|
||||||
for row in ticker[ticker.buy == 1].itertuples(index=True):
|
for row in ticker[ticker.buy == 1].itertuples(index=True):
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
open_rate=row.close,
|
open_rate=row.close,
|
||||||
open_date=row.date,
|
open_date=row.date,
|
||||||
amount=1,
|
amount=backtest_conf['stake_amount'],
|
||||||
fee=exchange.get_fee() * 2
|
fee=exchange.get_fee() * 2
|
||||||
)
|
)
|
||||||
# calculate win/lose forwards from buy point
|
# calculate win/lose forwards from buy point
|
||||||
for row2 in ticker[row.Index:].itertuples(index=True):
|
for row2 in ticker[row.Index:].itertuples(index=True):
|
||||||
if should_sell(trade, row2.close, row2.date):
|
if min_roi_reached(trade, row2.close, row2.date) or row2.sell == 1:
|
||||||
current_profit = trade.calc_profit(row2.close)
|
current_profit = trade.calc_profit(row2.close)
|
||||||
|
|
||||||
trades.append((pair, current_profit, row2.Index - row.Index))
|
trades.append((pair, current_profit, row2.Index - row.Index))
|
||||||
break
|
break
|
||||||
labels = ['currency', 'profit', 'duration']
|
labels = ['currency', 'profit', 'duration']
|
||||||
results = DataFrame.from_records(trades, columns=labels)
|
return DataFrame.from_records(trades, columns=labels)
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set")
|
@pytest.mark.skipif(not os.environ.get('BACKTEST'), reason="BACKTEST not set")
|
||||||
def test_backtest(backtest_conf, backdata, mocker, report=True):
|
def test_backtest(backtest_conf, mocker):
|
||||||
results = backtest(backtest_conf, backdata, mocker)
|
print('')
|
||||||
|
exchange._API = Bittrex({'key': '', 'secret': ''})
|
||||||
|
|
||||||
print('====================== BACKTESTING REPORT ================================')
|
# Load configuration file based on env variable
|
||||||
for pair in backdata:
|
conf_path = os.environ.get('BACKTEST_CONFIG')
|
||||||
print_pair_results(pair, results)
|
if conf_path:
|
||||||
print('TOTAL OVER ALL TRADES:')
|
print('Using config: {} ...'.format(conf_path))
|
||||||
print(format_results(results))
|
config = load_config(conf_path)
|
||||||
|
else:
|
||||||
|
config = backtest_conf
|
||||||
|
|
||||||
|
# Parse ticker interval
|
||||||
|
ticker_interval = int(os.environ.get('BACKTEST_TICKER_INTERVAL') or 5)
|
||||||
|
print('Using ticker_interval: {} ...'.format(ticker_interval))
|
||||||
|
|
||||||
|
data = {}
|
||||||
|
if os.environ.get('BACKTEST_LIVE'):
|
||||||
|
print('Downloading data for all pairs in whitelist ...')
|
||||||
|
for pair in config['exchange']['pair_whitelist']:
|
||||||
|
data[pair] = exchange.get_ticker_history(pair, ticker_interval)
|
||||||
|
else:
|
||||||
|
print('Using local backtesting data (ignoring whitelist in given config)...')
|
||||||
|
data = load_backtesting_data(ticker_interval)
|
||||||
|
|
||||||
|
print('Using stake_currency: {} ...\nUsing stake_amount: {} ...'.format(
|
||||||
|
config['stake_currency'], config['stake_amount']
|
||||||
|
))
|
||||||
|
|
||||||
|
# Print timeframe
|
||||||
|
min_date, max_date = get_timeframe(data)
|
||||||
|
print('Measuring data from {} up to {} ...'.format(
|
||||||
|
min_date.isoformat(), max_date.isoformat()
|
||||||
|
))
|
||||||
|
|
||||||
|
# Execute backtest and print results
|
||||||
|
results = backtest(config, preprocess(data), mocker)
|
||||||
|
print('====================== BACKTESTING REPORT ======================================\n\n'
|
||||||
|
'NOTE: This Report doesn\'t respect the limits of max_open_trades, \n'
|
||||||
|
' so the projected values should be taken with a grain of salt.\n')
|
||||||
|
print(generate_text_table(data, results, config['stake_currency']))
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# pragma pylint: disable=missing-docstring
|
# pragma pylint: disable=missing-docstring,C0103
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -33,4 +33,3 @@ def test_validate_pairs_not_compatible(default_conf, mocker):
|
||||||
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
||||||
with pytest.raises(RuntimeError, match=r'not compatible'):
|
with pytest.raises(RuntimeError, match=r'not compatible'):
|
||||||
validate_pairs(default_conf['exchange']['pair_whitelist'])
|
validate_pairs(default_conf['exchange']['pair_whitelist'])
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# pragma pylint: disable=missing-docstring
|
# pragma pylint: disable=missing-docstring,W0212
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
@ -9,16 +9,22 @@ import pytest
|
||||||
from hyperopt import fmin, tpe, hp, Trials, STATUS_OK
|
from hyperopt import fmin, tpe, hp, Trials, STATUS_OK
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade import exchange
|
||||||
|
from freqtrade.exchange import Bittrex
|
||||||
|
from freqtrade.tests import load_backtesting_data
|
||||||
from freqtrade.tests.test_backtesting import backtest, format_results
|
from freqtrade.tests.test_backtesting import backtest, format_results
|
||||||
|
from freqtrade.tests.test_backtesting import preprocess
|
||||||
from freqtrade.vendor.qtpylib.indicators import crossed_above
|
from freqtrade.vendor.qtpylib.indicators import crossed_above
|
||||||
|
|
||||||
logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot
|
logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot
|
||||||
|
|
||||||
# set TARGET_TRADES to suit your number concurrent trades so its realistic to 20days of data
|
# set TARGET_TRADES to suit your number concurrent trades so its realistic to 20days of data
|
||||||
TARGET_TRADES = 1300
|
TARGET_TRADES = 1100
|
||||||
TOTAL_TRIES = 4
|
TOTAL_TRIES = 4
|
||||||
|
# pylint: disable=C0103
|
||||||
current_tries = 0
|
current_tries = 0
|
||||||
|
|
||||||
|
|
||||||
def buy_strategy_generator(params):
|
def buy_strategy_generator(params):
|
||||||
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
||||||
conditions = []
|
conditions = []
|
||||||
|
@ -59,32 +65,36 @@ def buy_strategy_generator(params):
|
||||||
dataframe.loc[
|
dataframe.loc[
|
||||||
reduce(lambda x, y: x & y, conditions),
|
reduce(lambda x, y: x & y, conditions),
|
||||||
'buy'] = 1
|
'buy'] = 1
|
||||||
dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
|
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
return populate_buy_trend
|
return populate_buy_trend
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set")
|
@pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set")
|
||||||
def test_hyperopt(backtest_conf, backdata, mocker):
|
def test_hyperopt(backtest_conf, mocker):
|
||||||
mocked_buy_trend = mocker.patch('freqtrade.analyze.populate_buy_trend')
|
mocked_buy_trend = mocker.patch('freqtrade.tests.test_backtesting.populate_buy_trend')
|
||||||
|
|
||||||
|
backdata = load_backtesting_data()
|
||||||
|
processed = preprocess(backdata)
|
||||||
|
exchange._API = Bittrex({'key': '', 'secret': ''})
|
||||||
|
|
||||||
def optimizer(params):
|
def optimizer(params):
|
||||||
mocked_buy_trend.side_effect = buy_strategy_generator(params)
|
mocked_buy_trend.side_effect = buy_strategy_generator(params)
|
||||||
|
|
||||||
results = backtest(backtest_conf, backdata, mocker)
|
results = backtest(backtest_conf, processed, mocker)
|
||||||
|
|
||||||
result = format_results(results)
|
result = format_results(results)
|
||||||
|
|
||||||
total_profit = results.profit.sum() * 1000
|
total_profit = results.profit.sum() * 1000
|
||||||
trade_count = len(results.index)
|
trade_count = len(results.index)
|
||||||
|
|
||||||
trade_loss = 1 - 0.4 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.2)
|
trade_loss = 1 - 0.35 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.2)
|
||||||
profit_loss = max(0, 1 - total_profit / 15000) # max profit 15000
|
profit_loss = max(0, 1 - total_profit / 10000) # max profit 10000
|
||||||
|
|
||||||
|
# pylint: disable=W0603
|
||||||
global current_tries
|
global current_tries
|
||||||
current_tries += 1
|
current_tries += 1
|
||||||
print('{}/{}: {}'.format(current_tries, TOTAL_TRIES, result))
|
print('{:5d}/{}: {}'.format(current_tries, TOTAL_TRIES, result))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'loss': trade_loss + profit_loss,
|
'loss': trade_loss + profit_loss,
|
||||||
|
@ -146,3 +156,8 @@ def test_hyperopt(backtest_conf, backdata, mocker):
|
||||||
print('Best parameters {}'.format(best))
|
print('Best parameters {}'.format(best))
|
||||||
newlist = sorted(trials.results, key=itemgetter('loss'))
|
newlist = sorted(trials.results, key=itemgetter('loss'))
|
||||||
print('Result: {}'.format(newlist[0]['result']))
|
print('Result: {}'.format(newlist[0]['result']))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# for profiling with cProfile and line_profiler
|
||||||
|
pytest.main([__file__, '-s'])
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# pragma pylint: disable=missing-docstring
|
# pragma pylint: disable=missing-docstring,C0103
|
||||||
import copy
|
import copy
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
@ -7,16 +7,17 @@ import requests
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
|
|
||||||
from freqtrade.exchange import Exchanges
|
from freqtrade.exchange import Exchanges
|
||||||
from freqtrade.main import create_trade, handle_trade, close_trade_if_fulfilled, init, \
|
from freqtrade.analyze import SignalType
|
||||||
|
from freqtrade.main import create_trade, handle_trade, init, \
|
||||||
get_target_bid, _process
|
get_target_bid, _process
|
||||||
from freqtrade.misc import get_state, State
|
from freqtrade.misc import get_state, State, FreqtradeException
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
|
||||||
def test_process_trade_creation(default_conf, ticker, health, mocker):
|
def test_process_trade_creation(default_conf, ticker, health, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
|
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
|
@ -25,7 +26,7 @@ def test_process_trade_creation(default_conf, ticker, health, mocker):
|
||||||
init(default_conf, create_engine('sqlite://'))
|
init(default_conf, create_engine('sqlite://'))
|
||||||
|
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||||
assert len(trades) == 0
|
assert not trades
|
||||||
|
|
||||||
result = _process()
|
result = _process()
|
||||||
assert result is True
|
assert result is True
|
||||||
|
@ -44,8 +45,8 @@ def test_process_trade_creation(default_conf, ticker, health, mocker):
|
||||||
|
|
||||||
def test_process_exchange_failures(default_conf, ticker, health, mocker):
|
def test_process_exchange_failures(default_conf, ticker, health, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
|
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
sleep_mock = mocker.patch('time.sleep', side_effect=lambda _: None)
|
sleep_mock = mocker.patch('time.sleep', side_effect=lambda _: None)
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
|
@ -61,8 +62,8 @@ def test_process_exchange_failures(default_conf, ticker, health, mocker):
|
||||||
def test_process_runtime_error(default_conf, ticker, health, mocker):
|
def test_process_runtime_error(default_conf, ticker, health, mocker):
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=msg_mock)
|
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=msg_mock)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
|
@ -79,8 +80,9 @@ def test_process_runtime_error(default_conf, ticker, health, mocker):
|
||||||
|
|
||||||
def test_process_trade_handling(default_conf, ticker, limit_buy_order, health, mocker):
|
def test_process_trade_handling(default_conf, ticker, limit_buy_order, health, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
|
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal',
|
||||||
|
side_effect=lambda *args: False if args[1] == SignalType.SELL else True)
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
|
@ -90,7 +92,7 @@ def test_process_trade_handling(default_conf, ticker, limit_buy_order, health, m
|
||||||
init(default_conf, create_engine('sqlite://'))
|
init(default_conf, create_engine('sqlite://'))
|
||||||
|
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||||
assert len(trades) == 0
|
assert not trades
|
||||||
result = _process()
|
result = _process()
|
||||||
assert result is True
|
assert result is True
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||||
|
@ -102,8 +104,8 @@ def test_process_trade_handling(default_conf, ticker, limit_buy_order, health, m
|
||||||
|
|
||||||
def test_create_trade(default_conf, ticker, limit_buy_order, mocker):
|
def test_create_trade(default_conf, ticker, limit_buy_order, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
|
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
|
@ -132,27 +134,27 @@ def test_create_trade(default_conf, ticker, limit_buy_order, mocker):
|
||||||
|
|
||||||
def test_create_trade_no_stake_amount(default_conf, ticker, mocker):
|
def test_create_trade_no_stake_amount(default_conf, ticker, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
|
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
buy=MagicMock(return_value='mocked_limit_buy'),
|
buy=MagicMock(return_value='mocked_limit_buy'),
|
||||||
get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5))
|
get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5))
|
||||||
with pytest.raises(ValueError, match=r'.*stake amount.*'):
|
with pytest.raises(FreqtradeException, match=r'.*stake amount.*'):
|
||||||
create_trade(default_conf['stake_amount'])
|
create_trade(default_conf['stake_amount'])
|
||||||
|
|
||||||
|
|
||||||
def test_create_trade_no_pairs(default_conf, ticker, mocker):
|
def test_create_trade_no_pairs(default_conf, ticker, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
|
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
buy=MagicMock(return_value='mocked_limit_buy'))
|
buy=MagicMock(return_value='mocked_limit_buy'))
|
||||||
|
|
||||||
with pytest.raises(ValueError, match=r'.*No pair in whitelist.*'):
|
with pytest.raises(FreqtradeException, match=r'.*No pair in whitelist.*'):
|
||||||
conf = copy.deepcopy(default_conf)
|
conf = copy.deepcopy(default_conf)
|
||||||
conf['exchange']['pair_whitelist'] = []
|
conf['exchange']['pair_whitelist'] = []
|
||||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||||
|
@ -161,8 +163,8 @@ def test_create_trade_no_pairs(default_conf, ticker, mocker):
|
||||||
|
|
||||||
def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker):
|
def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
|
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=MagicMock(return_value={
|
get_ticker=MagicMock(return_value={
|
||||||
|
@ -182,7 +184,6 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker):
|
||||||
|
|
||||||
handle_trade(trade)
|
handle_trade(trade)
|
||||||
assert trade.open_order_id == 'mocked_limit_sell'
|
assert trade.open_order_id == 'mocked_limit_sell'
|
||||||
assert close_trade_if_fulfilled(trade) is False
|
|
||||||
|
|
||||||
# Simulate fulfilled LIMIT_SELL order for trade
|
# Simulate fulfilled LIMIT_SELL order for trade
|
||||||
trade.update(limit_sell_order)
|
trade.update(limit_sell_order)
|
||||||
|
@ -194,8 +195,8 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker):
|
||||||
|
|
||||||
def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mocker):
|
def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
|
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
|
@ -204,20 +205,15 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mo
|
||||||
# Create trade and sell it
|
# Create trade and sell it
|
||||||
init(default_conf, create_engine('sqlite://'))
|
init(default_conf, create_engine('sqlite://'))
|
||||||
trade = create_trade(15.0)
|
trade = create_trade(15.0)
|
||||||
trade.update(limit_buy_order)
|
|
||||||
trade.update(limit_sell_order)
|
|
||||||
|
|
||||||
Trade.session.add(trade)
|
Trade.session.add(trade)
|
||||||
Trade.session.flush()
|
trade.update(limit_buy_order)
|
||||||
trade = Trade.query.filter(Trade.is_open.is_(True)).first()
|
trade = Trade.query.filter(Trade.is_open.is_(True)).first()
|
||||||
assert trade
|
assert trade
|
||||||
|
|
||||||
# Simulate that there is no open order
|
trade.update(limit_sell_order)
|
||||||
trade.open_order_id = None
|
trade = Trade.query.filter(Trade.is_open.is_(False)).first()
|
||||||
|
assert trade
|
||||||
|
|
||||||
closed = close_trade_if_fulfilled(trade)
|
|
||||||
assert closed
|
|
||||||
assert not trade.is_open
|
|
||||||
with pytest.raises(ValueError, match=r'.*closed trade.*'):
|
with pytest.raises(ValueError, match=r'.*closed trade.*'):
|
||||||
handle_trade(trade)
|
handle_trade(trade)
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,15 @@
|
||||||
# pragma pylint: disable=missing-docstring
|
# pragma pylint: disable=missing-docstring,C0103
|
||||||
|
import json
|
||||||
|
import os
|
||||||
import time
|
import time
|
||||||
|
from argparse import Namespace
|
||||||
|
from copy import deepcopy
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
from freqtrade.misc import throttle
|
import pytest
|
||||||
|
from jsonschema import ValidationError
|
||||||
|
|
||||||
|
from freqtrade.misc import throttle, parse_args, start_backtesting, load_config
|
||||||
|
|
||||||
|
|
||||||
def test_throttle():
|
def test_throttle():
|
||||||
|
@ -18,3 +26,124 @@ def test_throttle():
|
||||||
|
|
||||||
result = throttle(func, -1)
|
result = throttle(func, -1)
|
||||||
assert result == 42
|
assert result == 42
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_defaults():
|
||||||
|
args = parse_args([])
|
||||||
|
assert args is not None
|
||||||
|
assert args.config == 'config.json'
|
||||||
|
assert args.dynamic_whitelist is False
|
||||||
|
assert args.loglevel == 20
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_invalid():
|
||||||
|
with pytest.raises(SystemExit, match=r'2'):
|
||||||
|
parse_args(['-c'])
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_config():
|
||||||
|
args = parse_args(['-c', '/dev/null'])
|
||||||
|
assert args is not None
|
||||||
|
assert args.config == '/dev/null'
|
||||||
|
|
||||||
|
args = parse_args(['--config', '/dev/null'])
|
||||||
|
assert args is not None
|
||||||
|
assert args.config == '/dev/null'
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_verbose():
|
||||||
|
args = parse_args(['-v'])
|
||||||
|
assert args is not None
|
||||||
|
assert args.loglevel == 10
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_dynamic_whitelist():
|
||||||
|
args = parse_args(['--dynamic-whitelist'])
|
||||||
|
assert args is not None
|
||||||
|
assert args.dynamic_whitelist is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_backtesting(mocker):
|
||||||
|
backtesting_mock = mocker.patch('freqtrade.misc.start_backtesting', MagicMock())
|
||||||
|
args = parse_args(['backtesting'])
|
||||||
|
assert args is None
|
||||||
|
assert backtesting_mock.call_count == 1
|
||||||
|
|
||||||
|
call_args = backtesting_mock.call_args[0][0]
|
||||||
|
assert call_args.config == 'config.json'
|
||||||
|
assert call_args.live is False
|
||||||
|
assert call_args.loglevel == 20
|
||||||
|
assert call_args.subparser == 'backtesting'
|
||||||
|
assert call_args.func is not None
|
||||||
|
assert call_args.ticker_interval == 5
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_backtesting_invalid():
|
||||||
|
with pytest.raises(SystemExit, match=r'2'):
|
||||||
|
parse_args(['--ticker-interval'])
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit, match=r'2'):
|
||||||
|
parse_args(['--ticker-interval', 'abc'])
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_backtesting_custom(mocker):
|
||||||
|
backtesting_mock = mocker.patch('freqtrade.misc.start_backtesting', MagicMock())
|
||||||
|
args = parse_args(['-c', 'test_conf.json', 'backtesting', '--live', '--ticker-interval', '1'])
|
||||||
|
assert args is None
|
||||||
|
assert backtesting_mock.call_count == 1
|
||||||
|
|
||||||
|
call_args = backtesting_mock.call_args[0][0]
|
||||||
|
assert call_args.config == 'test_conf.json'
|
||||||
|
assert call_args.live is True
|
||||||
|
assert call_args.loglevel == 20
|
||||||
|
assert call_args.subparser == 'backtesting'
|
||||||
|
assert call_args.func is not None
|
||||||
|
assert call_args.ticker_interval == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_start_backtesting(mocker):
|
||||||
|
pytest_mock = mocker.patch('pytest.main', MagicMock())
|
||||||
|
env_mock = mocker.patch('os.environ', {})
|
||||||
|
args = Namespace(
|
||||||
|
config='config.json',
|
||||||
|
live=True,
|
||||||
|
loglevel=20,
|
||||||
|
ticker_interval=1,
|
||||||
|
)
|
||||||
|
start_backtesting(args)
|
||||||
|
assert env_mock == {
|
||||||
|
'BACKTEST': 'true',
|
||||||
|
'BACKTEST_LIVE': 'true',
|
||||||
|
'BACKTEST_CONFIG': 'config.json',
|
||||||
|
'BACKTEST_TICKER_INTERVAL': '1',
|
||||||
|
}
|
||||||
|
assert pytest_mock.call_count == 1
|
||||||
|
|
||||||
|
main_call_args = pytest_mock.call_args[0][0]
|
||||||
|
assert main_call_args[0] == '-s'
|
||||||
|
assert main_call_args[1].endswith(os.path.join('freqtrade', 'tests', 'test_backtesting.py'))
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_config(default_conf, mocker):
|
||||||
|
file_mock = mocker.patch('freqtrade.misc.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(default_conf)
|
||||||
|
))
|
||||||
|
validated_conf = load_config('somefile')
|
||||||
|
assert file_mock.call_count == 1
|
||||||
|
assert validated_conf.items() >= default_conf.items()
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_config_invalid_pair(default_conf, mocker):
|
||||||
|
conf = deepcopy(default_conf)
|
||||||
|
conf['exchange']['pair_whitelist'].append('BTC-ETH')
|
||||||
|
mocker.patch('freqtrade.misc.open', mocker.mock_open(read_data=json.dumps(conf)))
|
||||||
|
with pytest.raises(ValidationError, match=r'.*does not match.*'):
|
||||||
|
load_config('somefile')
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_config_missing_attributes(default_conf, mocker):
|
||||||
|
conf = deepcopy(default_conf)
|
||||||
|
conf.pop('exchange')
|
||||||
|
mocker.patch('freqtrade.misc.open', mocker.mock_open(read_data=json.dumps(conf)))
|
||||||
|
with pytest.raises(ValidationError, match=r'.*\'exchange\' is a required property.*'):
|
||||||
|
load_config('somefile')
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
# pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, C0103
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
|
from freqtrade.rpc import init, cleanup, send_msg
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_telegram_enabled(default_conf, mocker):
|
||||||
|
module_list = []
|
||||||
|
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', module_list)
|
||||||
|
telegram_mock = mocker.patch('freqtrade.rpc.telegram.init', MagicMock())
|
||||||
|
|
||||||
|
init(default_conf)
|
||||||
|
|
||||||
|
assert telegram_mock.call_count == 1
|
||||||
|
assert 'telegram' in module_list
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_telegram_disabled(default_conf, mocker):
|
||||||
|
module_list = []
|
||||||
|
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', module_list)
|
||||||
|
telegram_mock = mocker.patch('freqtrade.rpc.telegram.init', MagicMock())
|
||||||
|
|
||||||
|
conf = deepcopy(default_conf)
|
||||||
|
conf['telegram']['enabled'] = False
|
||||||
|
init(conf)
|
||||||
|
|
||||||
|
assert telegram_mock.call_count == 0
|
||||||
|
assert 'telegram' not in module_list
|
||||||
|
|
||||||
|
|
||||||
|
def test_cleanup_telegram_enabled(mocker):
|
||||||
|
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', ['telegram'])
|
||||||
|
telegram_mock = mocker.patch('freqtrade.rpc.telegram.cleanup', MagicMock())
|
||||||
|
cleanup()
|
||||||
|
assert telegram_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_cleanup_telegram_disabled(mocker):
|
||||||
|
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', [])
|
||||||
|
telegram_mock = mocker.patch('freqtrade.rpc.telegram.cleanup', MagicMock())
|
||||||
|
cleanup()
|
||||||
|
assert telegram_mock.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_send_msg_telegram_enabled(mocker):
|
||||||
|
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', ['telegram'])
|
||||||
|
telegram_mock = mocker.patch('freqtrade.rpc.telegram.send_msg', MagicMock())
|
||||||
|
send_msg('test')
|
||||||
|
assert telegram_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_send_msg_telegram_disabled(mocker):
|
||||||
|
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', [])
|
||||||
|
telegram_mock = mocker.patch('freqtrade.rpc.telegram.send_msg', MagicMock())
|
||||||
|
send_msg('test')
|
||||||
|
assert telegram_mock.call_count == 0
|
|
@ -1,10 +1,9 @@
|
||||||
# pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors
|
# pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, C0103
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from random import randint
|
from random import randint
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from telegram import Update, Message, Chat
|
from telegram import Update, Message, Chat
|
||||||
from telegram.error import NetworkError
|
from telegram.error import NetworkError
|
||||||
|
@ -14,10 +13,8 @@ from freqtrade.main import init, create_trade
|
||||||
from freqtrade.misc import update_state, State, get_state
|
from freqtrade.misc import update_state, State, get_state
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.rpc import telegram
|
from freqtrade.rpc import telegram
|
||||||
from freqtrade.rpc.telegram import (
|
from freqtrade.rpc.telegram import authorized_only, is_enabled, send_msg, _status, _status_table, \
|
||||||
_status, _status_table, _profit, _forcesell, _performance, _count, _start, _stop, _balance,
|
_profit, _forcesell, _performance, _count, _start, _stop, _balance, _version, _help
|
||||||
authorized_only, _help, is_enabled, send_msg,
|
|
||||||
_version)
|
|
||||||
|
|
||||||
|
|
||||||
def test_is_enabled(default_conf, mocker):
|
def test_is_enabled(default_conf, mocker):
|
||||||
|
@ -79,9 +76,10 @@ def test_authorized_only_exception(default_conf, mocker):
|
||||||
|
|
||||||
def test_status_handle(default_conf, update, ticker, mocker):
|
def test_status_handle(default_conf, update, ticker, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||||
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
|
@ -111,16 +109,17 @@ def test_status_handle(default_conf, update, ticker, mocker):
|
||||||
# Trigger status while we have a fulfilled order for the open trade
|
# Trigger status while we have a fulfilled order for the open trade
|
||||||
_status(bot=MagicMock(), update=update)
|
_status(bot=MagicMock(), update=update)
|
||||||
|
|
||||||
assert msg_mock.call_count == 2
|
assert msg_mock.call_count == 1
|
||||||
assert '[BTC_ETH]' in msg_mock.call_args_list[-1][0][0]
|
assert '[BTC_ETH]' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_status_table_handle(default_conf, update, ticker, mocker):
|
def test_status_table_handle(default_conf, update, ticker, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
|
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.main.telegram',
|
'freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
|
@ -155,14 +154,15 @@ def test_status_table_handle(default_conf, update, ticker, mocker):
|
||||||
|
|
||||||
assert int(fields[0]) == 1
|
assert int(fields[0]) == 1
|
||||||
assert fields[1] == 'BTC_ETH'
|
assert fields[1] == 'BTC_ETH'
|
||||||
assert msg_mock.call_count == 2
|
assert msg_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker):
|
def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||||
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
|
@ -184,7 +184,7 @@ def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell
|
||||||
trade.update(limit_buy_order)
|
trade.update(limit_buy_order)
|
||||||
|
|
||||||
_profit(bot=MagicMock(), update=update)
|
_profit(bot=MagicMock(), update=update)
|
||||||
assert msg_mock.call_count == 2
|
assert msg_mock.call_count == 1
|
||||||
assert 'no closed trade' in msg_mock.call_args_list[-1][0][0]
|
assert 'no closed trade' in msg_mock.call_args_list[-1][0][0]
|
||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
|
|
||||||
|
@ -204,12 +204,12 @@ def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell
|
||||||
|
|
||||||
def test_forcesell_handle(default_conf, update, ticker, mocker):
|
def test_forcesell_handle(default_conf, update, ticker, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
msg_mock = MagicMock()
|
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=MagicMock())
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker)
|
get_ticker=ticker)
|
||||||
|
@ -225,19 +225,19 @@ def test_forcesell_handle(default_conf, update, ticker, mocker):
|
||||||
update.message.text = '/forcesell 1'
|
update.message.text = '/forcesell 1'
|
||||||
_forcesell(bot=MagicMock(), update=update)
|
_forcesell(bot=MagicMock(), update=update)
|
||||||
|
|
||||||
assert msg_mock.call_count == 2
|
assert rpc_mock.call_count == 2
|
||||||
assert 'Selling [BTC/ETH]' in msg_mock.call_args_list[-1][0][0]
|
assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0]
|
||||||
assert '0.07256061 (profit: ~-0.64%)' in msg_mock.call_args_list[-1][0][0]
|
assert '0.07256061 (profit: ~-0.64%)' in rpc_mock.call_args_list[-1][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_forcesell_all_handle(default_conf, update, ticker, mocker):
|
def test_forcesell_all_handle(default_conf, update, ticker, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
msg_mock = MagicMock()
|
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=MagicMock())
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker)
|
get_ticker=ticker)
|
||||||
|
@ -247,22 +247,21 @@ def test_forcesell_all_handle(default_conf, update, ticker, mocker):
|
||||||
for _ in range(4):
|
for _ in range(4):
|
||||||
Trade.session.add(create_trade(15.0))
|
Trade.session.add(create_trade(15.0))
|
||||||
Trade.session.flush()
|
Trade.session.flush()
|
||||||
|
rpc_mock.reset_mock()
|
||||||
msg_mock.reset_mock()
|
|
||||||
|
|
||||||
update.message.text = '/forcesell all'
|
update.message.text = '/forcesell all'
|
||||||
_forcesell(bot=MagicMock(), update=update)
|
_forcesell(bot=MagicMock(), update=update)
|
||||||
|
|
||||||
assert msg_mock.call_count == 4
|
assert rpc_mock.call_count == 4
|
||||||
for args in msg_mock.call_args_list:
|
for args in rpc_mock.call_args_list:
|
||||||
assert '0.07256061 (profit: ~-0.64%)' in args[0][0]
|
assert '0.07256061 (profit: ~-0.64%)' in args[0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_forcesell_handle_invalid(default_conf, update, mocker):
|
def test_forcesell_handle_invalid(default_conf, update, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
|
@ -297,9 +296,10 @@ def test_forcesell_handle_invalid(default_conf, update, mocker):
|
||||||
def test_performance_handle(
|
def test_performance_handle(
|
||||||
default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker):
|
default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||||
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
|
@ -324,17 +324,17 @@ def test_performance_handle(
|
||||||
Trade.session.flush()
|
Trade.session.flush()
|
||||||
|
|
||||||
_performance(bot=MagicMock(), update=update)
|
_performance(bot=MagicMock(), update=update)
|
||||||
assert msg_mock.call_count == 2
|
assert msg_mock.call_count == 1
|
||||||
assert 'Performance' in msg_mock.call_args_list[-1][0][0]
|
assert 'Performance' in msg_mock.call_args_list[0][0][0]
|
||||||
assert '<code>BTC_ETH\t10.05%</code>' in msg_mock.call_args_list[-1][0][0]
|
assert '<code>BTC_ETH\t10.05%</code>' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_count_handle(default_conf, update, ticker, mocker):
|
def test_count_handle(default_conf, update, ticker, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.main.telegram',
|
'freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
|
@ -365,9 +365,9 @@ def test_count_handle(default_conf, update, ticker, mocker):
|
||||||
|
|
||||||
def test_performance_handle_invalid(default_conf, update, mocker):
|
def test_performance_handle_invalid(default_conf, update, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
|
@ -385,7 +385,7 @@ def test_performance_handle_invalid(default_conf, update, mocker):
|
||||||
def test_start_handle(default_conf, update, mocker):
|
def test_start_handle(default_conf, update, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
|
@ -403,7 +403,7 @@ def test_start_handle(default_conf, update, mocker):
|
||||||
def test_start_handle_already_running(default_conf, update, mocker):
|
def test_start_handle_already_running(default_conf, update, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
|
@ -422,7 +422,7 @@ def test_start_handle_already_running(default_conf, update, mocker):
|
||||||
def test_stop_handle(default_conf, update, mocker):
|
def test_stop_handle(default_conf, update, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
|
@ -441,7 +441,7 @@ def test_stop_handle(default_conf, update, mocker):
|
||||||
def test_stop_handle_already_stopped(default_conf, update, mocker):
|
def test_stop_handle_already_stopped(default_conf, update, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
|
@ -473,7 +473,7 @@ def test_balance_handle(default_conf, update, mocker):
|
||||||
}]
|
}]
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
|
@ -489,7 +489,7 @@ def test_balance_handle(default_conf, update, mocker):
|
||||||
def test_help_handle(default_conf, update, mocker):
|
def test_help_handle(default_conf, update, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
|
@ -502,7 +502,7 @@ def test_help_handle(default_conf, update, mocker):
|
||||||
def test_version_handle(default_conf, update, mocker):
|
def test_version_handle(default_conf, update, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
|
@ -514,12 +514,12 @@ def test_version_handle(default_conf, update, mocker):
|
||||||
|
|
||||||
def test_send_msg(default_conf, mocker):
|
def test_send_msg(default_conf, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock())
|
init=MagicMock())
|
||||||
bot = MagicMock()
|
bot = MagicMock()
|
||||||
send_msg('test', bot)
|
send_msg('test', bot)
|
||||||
assert len(bot.method_calls) == 0
|
assert not bot.method_calls
|
||||||
bot.reset_mock()
|
bot.reset_mock()
|
||||||
|
|
||||||
default_conf['telegram']['enabled'] = True
|
default_conf['telegram']['enabled'] = True
|
||||||
|
@ -529,14 +529,13 @@ def test_send_msg(default_conf, mocker):
|
||||||
|
|
||||||
def test_send_msg_network_error(default_conf, mocker):
|
def test_send_msg_network_error(default_conf, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock())
|
init=MagicMock())
|
||||||
default_conf['telegram']['enabled'] = True
|
default_conf['telegram']['enabled'] = True
|
||||||
bot = MagicMock()
|
bot = MagicMock()
|
||||||
bot.send_message = MagicMock(side_effect=NetworkError('Oh snap'))
|
bot.send_message = MagicMock(side_effect=NetworkError('Oh snap'))
|
||||||
with pytest.raises(NetworkError, match=r'Oh snap'):
|
send_msg('test', bot)
|
||||||
send_msg('test', bot)
|
|
||||||
|
|
||||||
# Bot should've tried to send it twice
|
# Bot should've tried to send it twice
|
||||||
assert len(bot.method_calls) == 2
|
assert len(bot.method_calls) == 2
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -7,8 +7,13 @@ from os import path
|
||||||
from freqtrade import exchange
|
from freqtrade import exchange
|
||||||
from freqtrade.exchange import Bittrex
|
from freqtrade.exchange import Bittrex
|
||||||
|
|
||||||
PAIRS = ['BTC-OK', 'BTC-NEO', 'BTC-DASH', 'BTC-ETC', 'BTC-ETH', 'BTC-SNT']
|
PAIRS = [
|
||||||
TICKER_INTERVAL = 1 # ticker interval in minutes (currently implemented: 1 and 5)
|
'BTC_BCC', 'BTC_ETH', 'BTC_MER', 'BTC_POWR', 'BTC_ETC',
|
||||||
|
'BTC_OK', 'BTC_NEO', 'BTC_EMC2', 'BTC_DASH', 'BTC_LSK',
|
||||||
|
'BTC_LTC', 'BTC_XZC', 'BTC_OMG', 'BTC_STRAT', 'BTC_XRP',
|
||||||
|
'BTC_QTUM', 'BTC_WAVES', 'BTC_VTC', 'BTC_XLM', 'BTC_MCO'
|
||||||
|
]
|
||||||
|
TICKER_INTERVAL = 5 # ticker interval in minutes (currently implemented: 1 and 5)
|
||||||
OUTPUT_DIR = path.dirname(path.realpath(__file__))
|
OUTPUT_DIR = path.dirname(path.realpath(__file__))
|
||||||
|
|
||||||
# Init Bittrex exchange
|
# Init Bittrex exchange
|
||||||
|
@ -16,8 +21,8 @@ exchange._API = Bittrex({'key': '', 'secret': ''})
|
||||||
|
|
||||||
for pair in PAIRS:
|
for pair in PAIRS:
|
||||||
data = exchange.get_ticker_history(pair, TICKER_INTERVAL)
|
data = exchange.get_ticker_history(pair, TICKER_INTERVAL)
|
||||||
filename = path.join(OUTPUT_DIR, '{}-{}m.json'.format(
|
filename = path.join(OUTPUT_DIR, '{}-{}.json'.format(
|
||||||
pair.lower(),
|
pair,
|
||||||
TICKER_INTERVAL,
|
TICKER_INTERVAL,
|
||||||
))
|
))
|
||||||
with open(filename, 'w') as fp:
|
with open(filename, 'w') as fp:
|
||||||
|
|
Loading…
Reference in New Issue